From b20a341f0c2cdb12a306937adec6a400185e866c Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Thu, 6 Jan 2022 18:24:28 -0800 Subject: [PATCH] chore: Typescript database models (#2886) closes #2798 --- app/.jestconfig.json | 3 +- app/typings/index.d.ts | 53 - package.json | 12 +- server/.babelrc | 20 +- server/collaboration/authentication.ts | 6 +- server/collaboration/persistence.ts | 4 +- server/commands/accountProvisioner.test.ts | 57 +- server/commands/accountProvisioner.ts | 16 +- server/commands/attachmentCreator.ts | 1 - server/commands/collectionExporter.ts | 9 +- server/commands/collectionImporter.test.ts | 1 + server/commands/collectionImporter.ts | 7 +- server/commands/documentCreator.ts | 11 +- server/commands/documentImporter.test.ts | 3 +- server/commands/documentImporter.ts | 1 - server/commands/documentMover.test.ts | 24 +- server/commands/documentMover.ts | 94 +- .../commands/documentPermanentDeleter.test.ts | 12 +- server/commands/documentPermanentDeleter.ts | 12 +- server/commands/documentUpdater.ts | 4 + server/commands/fileOperationDeleter.test.ts | 12 +- server/commands/fileOperationDeleter.ts | 5 +- server/commands/pinCreator.test.ts | 10 +- server/commands/pinCreator.ts | 15 +- server/commands/pinDestroyer.test.ts | 4 +- server/commands/pinDestroyer.ts | 10 +- server/commands/pinUpdater.ts | 12 +- server/commands/revisionCreator.test.ts | 7 +- server/commands/revisionCreator.ts | 6 +- server/commands/teamCreator.test.ts | 14 +- server/commands/teamCreator.ts | 35 +- server/commands/teamPermanentDeleter.test.ts | 12 +- server/commands/teamPermanentDeleter.ts | 9 +- server/commands/userCreator.test.ts | 17 +- server/commands/userCreator.ts | 57 +- server/commands/userDestroyer.test.ts | 1 + server/commands/userDestroyer.ts | 5 +- server/commands/userInviter.test.ts | 15 +- server/commands/userInviter.ts | 8 +- server/commands/userSuspender.test.ts | 5 +- server/commands/userSuspender.ts | 15 +- server/{ => database}/sequelize.ts | 18 +- server/database/vaults.ts | 9 + server/emails/CollectionNotificationEmail.tsx | 2 - server/emails/DocumentNotificationEmail.tsx | 7 +- server/errors.ts | 6 +- server/index.ts | 10 +- server/mailer.test.ts | 8 +- server/mailer.tsx | 4 +- server/middlewares/authentication.test.ts | 2 +- server/middlewares/authentication.ts | 2 +- server/middlewares/errorHandling.ts | 4 +- server/models/ApiKey.ts | 68 +- server/models/Attachment.ts | 175 +- server/models/AuthenticationProvider.ts | 143 +- server/models/Backlink.ts | 57 +- server/models/Collection.test.ts | 86 +- server/models/Collection.ts | 1086 ++++++----- server/models/CollectionGroup.ts | 74 +- server/models/CollectionUser.ts | 74 +- server/models/Document.test.ts | 43 +- server/models/Document.ts | 1341 +++++++------- server/models/Event.ts | 272 +-- server/models/FileOperation.ts | 152 +- server/models/Group.test.ts | 12 +- server/models/Group.ts | 174 +- server/models/GroupUser.ts | 75 +- server/models/Integration.ts | 88 +- server/models/IntegrationAuthentication.ts | 71 +- server/models/Notification.ts | 83 +- server/models/NotificationSetting.ts | 129 +- server/models/Pin.ts | 94 +- server/models/Revision.test.ts | 7 +- server/models/Revision.ts | 198 +- server/models/SearchQuery.ts | 99 +- server/models/Share.ts | 152 +- server/models/Star.ts | 40 +- server/models/Team.test.ts | 2 + server/models/Team.ts | 440 +++-- server/models/User.test.ts | 4 +- server/models/User.ts | 827 +++++---- server/models/UserAuthentication.ts | 81 +- server/models/View.ts | 164 +- server/models/base/BaseModel.ts | 26 + server/models/base/ParanoidModel.ts | 9 + server/models/decorators/Encrypted.ts | 30 + server/models/decorators/Fix.ts | 50 + server/models/index.ts | 131 +- server/policies/apiKey.ts | 5 +- server/policies/attachment.ts | 4 +- server/policies/authenticationProvider.ts | 8 +- server/policies/cancan.ts | 13 + server/policies/collection.test.ts | 21 +- server/policies/collection.ts | 38 +- server/policies/document.test.ts | 3 + server/policies/document.ts | 38 +- server/policies/group.ts | 4 +- server/policies/index.test.ts | 2 + server/policies/index.ts | 19 +- server/policies/integration.ts | 6 +- server/policies/notificationSetting.ts | 6 +- server/policies/pins.ts | 14 +- server/policies/policy.ts | 3 - server/policies/searchQuery.ts | 13 +- server/policies/share.ts | 12 +- server/policies/team.ts | 6 +- server/policies/user.ts | 4 +- .../__snapshots__/user.test.ts.snap | 20 +- server/presenters/apiKey.ts | 3 +- server/presenters/authenticationProvider.ts | 1 - server/presenters/collection.ts | 3 +- .../presenters/collectionGroupMembership.ts | 1 - server/presenters/document.ts | 10 +- server/presenters/event.ts | 12 +- server/presenters/fileOperation.ts | 1 - server/presenters/group.ts | 3 +- server/presenters/groupMembership.ts | 5 +- server/presenters/integration.ts | 1 - server/presenters/membership.ts | 1 - server/presenters/notificationSetting.ts | 1 - server/presenters/pin.ts | 4 +- server/presenters/policy.ts | 1 - server/presenters/revision.ts | 1 - server/presenters/searchQuery.ts | 4 +- server/presenters/share.ts | 3 +- server/presenters/slackAttachment.ts | 8 +- server/presenters/team.ts | 1 - server/presenters/user.test.ts | 26 +- server/presenters/user.ts | 1 - server/presenters/view.ts | 1 - server/queues/processors/backlinks.test.ts | 7 +- server/queues/processors/backlinks.ts | 3 +- server/queues/processors/debouncer.ts | 4 +- server/queues/processors/exports.ts | 15 +- server/queues/processors/imports.ts | 9 +- .../queues/processors/notifications.test.ts | 29 +- server/queues/processors/notifications.ts | 14 +- server/queues/processors/slack.ts | 2 +- server/queues/processors/websockets.ts | 39 +- server/routes/api/apiKeys.ts | 15 +- server/routes/api/attachments.test.ts | 24 +- server/routes/api/attachments.ts | 9 +- server/routes/api/auth.test.ts | 2 + server/routes/api/auth.ts | 10 +- .../api/authenticationProviders.test.ts | 18 +- server/routes/api/authenticationProviders.ts | 20 +- server/routes/api/collections.test.ts | 42 +- server/routes/api/collections.ts | 98 +- server/routes/api/documents.test.ts | 108 +- server/routes/api/documents.ts | 207 ++- server/routes/api/events.test.ts | 1 + server/routes/api/events.ts | 42 +- server/routes/api/fileOperations.test.ts | 17 +- server/routes/api/fileOperations.ts | 20 +- server/routes/api/groups.test.ts | 30 +- server/routes/api/groups.ts | 47 +- server/routes/api/hooks.test.ts | 36 +- server/routes/api/hooks.ts | 3 + server/routes/api/index.test.ts | 2 + server/routes/api/integrations.ts | 6 +- .../routes/api/middlewares/pagination.test.ts | 1 + server/routes/api/notificationSettings.ts | 13 +- server/routes/api/pins.ts | 18 +- server/routes/api/revisions.test.ts | 2 + server/routes/api/revisions.ts | 10 +- server/routes/api/searches.ts | 2 +- server/routes/api/shares.test.ts | 9 +- server/routes/api/shares.ts | 45 +- server/routes/api/team.test.ts | 1 + server/routes/api/team.ts | 5 +- server/routes/api/users.test.ts | 10 + server/routes/api/users.ts | 32 +- server/routes/api/utils.test.ts | 22 +- server/routes/api/utils.ts | 3 +- server/routes/api/views.test.ts | 6 +- server/routes/api/views.ts | 13 +- server/routes/auth/index.ts | 9 +- server/routes/auth/providers/email.test.ts | 40 +- server/routes/auth/providers/email.ts | 2 +- server/routes/auth/providers/index.ts | 12 +- server/routes/auth/providers/slack.ts | 14 +- server/routes/index.test.ts | 1 + server/routes/index.ts | 1 + ...10226232041-migrate-authentication.test.ts | 136 -- .../20210226232041-migrate-authentication.ts | 104 -- .../20210716000000-backfill-revisions.test.ts | 21 +- server/services/collaboration.ts | 1 - server/services/websockets.ts | 5 +- server/test/factories.ts | 54 +- server/test/setup.ts | 15 + server/test/support.ts | 8 +- server/types.ts | 1 - server/typings/cancan.d.ts | 36 +- server/typings/index.d.ts | 33 + server/typings/socketio-auth.d.ts | 28 + server/utils/authentication.ts | 2 - server/utils/collectionIndexing.ts | 13 +- server/utils/fs.test.ts | 1 + server/utils/jwt.ts | 46 +- server/utils/removeIndexCollision.ts | 6 +- server/utils/s3.ts | 24 +- server/utils/startup.ts | 3 +- server/utils/updates.ts | 5 +- server/utils/zip.ts | 6 +- shared/theme.ts | 5 +- shared/typings/index.d.ts | 4 + yarn.lock | 1648 +++++++---------- 207 files changed, 5624 insertions(+), 5315 deletions(-) rename server/{ => database}/sequelize.ts (54%) create mode 100644 server/database/vaults.ts create mode 100644 server/models/base/BaseModel.ts create mode 100644 server/models/base/ParanoidModel.ts create mode 100644 server/models/decorators/Encrypted.ts create mode 100644 server/models/decorators/Fix.ts create mode 100644 server/policies/cancan.ts delete mode 100644 server/policies/policy.ts delete mode 100644 server/scripts/20210226232041-migrate-authentication.test.ts delete mode 100644 server/scripts/20210226232041-migrate-authentication.ts create mode 100644 server/typings/index.d.ts create mode 100644 server/typings/socketio-auth.d.ts create mode 100644 shared/typings/index.d.ts diff --git a/app/.jestconfig.json b/app/.jestconfig.json index 515b464d3..ad8cf47c9 100644 --- a/app/.jestconfig.json +++ b/app/.jestconfig.json @@ -22,5 +22,6 @@ ], "setupFilesAfterEnv": [ "./app/test/setup.ts" - ] + ], + "testEnvironment": "jsdom" } \ No newline at end of file diff --git a/app/typings/index.d.ts b/app/typings/index.d.ts index 173f34caf..e7e8d4437 100644 --- a/app/typings/index.d.ts +++ b/app/typings/index.d.ts @@ -8,59 +8,6 @@ declare module "sequelize-encrypted"; declare module "styled-components-breakpoint"; -declare module "formidable/lib/file"; - -declare module "socket.io-client"; - -declare module "socket.io-redis" { - import { Redis } from "ioredis"; - - type Config = { - pubClient: Redis; - subClient: Redis; - }; - - const socketRedisAdapter: (config: Config) => void; - - export = socketRedisAdapter; -} - -declare module "socketio-auth" { - import IO from "socket.io"; - - type AuthenticatedSocket = IO.Socket & { - client: IO.Client & { - user: any; - }; - }; - - type AuthenticateCallback = ( - socket: AuthenticatedSocket, - data: { token: string }, - callback: (err: Error | null, allow: boolean) => void - ) => Promise; - - type PostAuthenticateCallback = ( - socket: AuthenticatedSocket - ) => Promise; - - type AuthenticationConfig = { - authenticate: AuthenticateCallback; - postAuthenticate: PostAuthenticateCallback; - }; - - const SocketAuth: (io: IO.Server, config: AuthenticationConfig) => void; - - export = SocketAuth; -} - -declare module "oy-vey"; - -declare module "emoji-regex" { - const RegExpFactory: () => RegExp; - export = RegExpFactory; -} - declare module "*.png" { const value: any; export = value; diff --git a/package.json b/package.json index b56d0639b..227cdad16 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "url": "https://github.com/sponsors/outline" }, "engines": { - "node": ">= 12 <=16" + "node": ">= 14 <=16" }, "repository": { "type": "git", @@ -149,12 +149,14 @@ "react-waypoint": "^10.1.0", "react-window": "^1.8.6", "reakit": "^1.3.10", + "reflect-metadata": "^0.1.13", "regenerator-runtime": "^0.13.7", "rich-markdown-editor": "^11.21.3", "semver": "^7.3.2", "sequelize": "^6.9.0", "sequelize-cli": "^6.3.0", "sequelize-encrypted": "^1.0.0", + "sequelize-typescript": "^2.1.1", "slate": "0.45.0", "slate-md-serializer": "5.5.4", "slug": "^4.0.4", @@ -209,7 +211,7 @@ "@types/koa-static": "^4.0.2", "@types/markdown-it": "^12.2.3", "@types/natural-sort": "^0.0.21", - "@types/node": "^15.12.0", + "@types/node": "15.12.2", "@types/nodemailer": "^6.4.4", "@types/passport-oauth2": "^1.4.11", "@types/prosemirror-inputrules": "^1.0.4", @@ -242,12 +244,14 @@ "@types/tmp": "^0.2.2", "@types/turndown": "^5.0.1", "@types/utf8": "^3.0.0", + "@types/validator": "^13.7.1", "@typescript-eslint/eslint-plugin": "^5.3.1", "@typescript-eslint/parser": "^5.3.1", "babel-eslint": "^10.1.0", - "babel-jest": "^26.2.2", + "babel-jest": "^27.4.5", "babel-loader": "^8.1.0", "babel-plugin-transform-inline-environment-variables": "^0.4.3", + "babel-plugin-transform-typescript-metadata": "^0.3.2", "babel-plugin-tsconfig-paths-module-resolver": "^1.0.3", "concurrently": "^6.2.1", "enzyme": "^3.11.0", @@ -264,7 +268,7 @@ "fetch-test-server": "^1.1.0", "html-webpack-plugin": "4.5.1", "i18next-parser": "^4.7.0", - "jest-cli": "^26.0.0", + "jest-cli": "^27.4.5", "jest-fetch-mock": "^3.0.3", "koa-webpack-dev-middleware": "^1.4.5", "koa-webpack-hot-middleware": "^1.0.3", diff --git a/server/.babelrc b/server/.babelrc index 202742c0a..e7401f653 100644 --- a/server/.babelrc +++ b/server/.babelrc @@ -1,24 +1,19 @@ { "presets": [ "@babel/preset-react", - "@babel/preset-typescript", [ "@babel/preset-env", { - "corejs": { - "version": "3", - "proposals": true - }, "targets": { - "node": "12" - }, - "useBuiltIns": "usage" + "node": "14" + } } - ] + ], + "@babel/preset-typescript" ], "plugins": [ - "transform-class-properties", - "tsconfig-paths-module-resolver", + "babel-plugin-transform-typescript-metadata", + ["@babel/plugin-proposal-decorators", { "legacy": true }], [ "transform-inline-environment-variables", { @@ -27,6 +22,7 @@ "SOURCE_VERSION" ] } - ] + ], + "tsconfig-paths-module-resolver" ] } \ No newline at end of file diff --git a/server/collaboration/authentication.ts b/server/collaboration/authentication.ts index a9a063bc7..09a7a69e0 100644 --- a/server/collaboration/authentication.ts +++ b/server/collaboration/authentication.ts @@ -1,10 +1,8 @@ import { onAuthenticatePayload } from "@hocuspocus/server"; -import { Document } from "@server/models"; +import Document from "@server/models/Document"; +import { can } from "@server/policies"; import { getUserForJWT } from "@server/utils/jwt"; import { AuthenticationError } from "../errors"; -import policy from "../policies"; - -const { can } = policy; export default class Authentication { async onAuthenticate({ diff --git a/server/collaboration/persistence.ts b/server/collaboration/persistence.ts index f71a4f9b9..83b1f49cc 100644 --- a/server/collaboration/persistence.ts +++ b/server/collaboration/persistence.ts @@ -1,8 +1,9 @@ import { onChangePayload, onLoadDocumentPayload } from "@hocuspocus/server"; +import invariant from "invariant"; import { debounce } from "lodash"; import * as Y from "yjs"; import Logger from "@server/logging/logger"; -import { Document } from "@server/models"; +import Document from "@server/models/Document"; import documentUpdater from "../commands/documentUpdater"; import markdownToYDoc from "./utils/markdownToYDoc"; @@ -20,6 +21,7 @@ export default class Persistence { } const document = await Document.findByPk(documentId); + invariant(document, "Document not found"); if (document.state) { const ydoc = new Y.Doc(); diff --git a/server/commands/accountProvisioner.test.ts b/server/commands/accountProvisioner.test.ts index 3c2460406..b3a591ba1 100644 --- a/server/commands/accountProvisioner.test.ts +++ b/server/commands/accountProvisioner.test.ts @@ -1,30 +1,19 @@ -import { Collection, UserAuthentication } from "@server/models"; +import mailer from "@server/mailer"; +import Collection from "@server/models/Collection"; +import UserAuthentication from "@server/models/UserAuthentication"; import { buildUser, buildTeam } from "@server/test/factories"; import { flushdb } from "@server/test/support"; -import mailer from "../mailer"; import accountProvisioner from "./accountProvisioner"; -jest.mock("../mailer"); -jest.mock("aws-sdk", () => { - const mS3 = { - createPresignedPost: jest.fn(), - putObject: jest.fn().mockReturnThis(), - promise: jest.fn(), - }; - return { - S3: jest.fn(() => mS3), - Endpoint: jest.fn(), - }; -}); beforeEach(() => { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'mockReset' does not exist on type '(type... Remove this comment to see the full error message - mailer.sendTemplate.mockReset(); return flushdb(); }); + describe("accountProvisioner", () => { const ip = "127.0.0.1"; it("should create a new user and team", async () => { + const spy = jest.spyOn(mailer, "sendTemplate"); const { user, team, isNewTeam, isNewUser } = await accountProvisioner({ ip, user: { @@ -48,7 +37,7 @@ describe("accountProvisioner", () => { scopes: ["read"], }, }); - const authentications = await user.getAuthentications(); + const authentications = await user.$get("authentications"); const auth = authentications[0]; expect(auth.accessToken).toEqual("123"); expect(auth.scopes.length).toEqual(1); @@ -58,19 +47,22 @@ describe("accountProvisioner", () => { expect(user.username).toEqual("jtester"); expect(isNewUser).toEqual(true); expect(isNewTeam).toEqual(true); - expect(mailer.sendTemplate).toHaveBeenCalled(); + expect(spy).toHaveBeenCalled(); const collectionCount = await Collection.count(); expect(collectionCount).toEqual(1); + + spy.mockRestore(); }); it("should update exising user and authentication", async () => { + const spy = jest.spyOn(mailer, "sendTemplate"); const existingTeam = await buildTeam(); - const providers = await existingTeam.getAuthenticationProviders(); + const providers = await existingTeam.$get("authenticationProviders"); const authenticationProvider = providers[0]; const existing = await buildUser({ teamId: existingTeam.id, }); - const authentications = await existing.getAuthentications(); + const authentications = await existing.$get("authentications"); const authentication = authentications[0]; const newEmail = "test@example.com"; const newUsername = "tname"; @@ -98,21 +90,23 @@ describe("accountProvisioner", () => { }, }); const auth = await UserAuthentication.findByPk(authentication.id); - expect(auth.accessToken).toEqual("123"); - expect(auth.scopes.length).toEqual(1); - expect(auth.scopes[0]).toEqual("read"); + expect(auth?.accessToken).toEqual("123"); + expect(auth?.scopes.length).toEqual(1); + expect(auth?.scopes[0]).toEqual("read"); expect(user.email).toEqual(newEmail); expect(user.username).toEqual(newUsername); expect(isNewTeam).toEqual(false); expect(isNewUser).toEqual(false); - expect(mailer.sendTemplate).not.toHaveBeenCalled(); + expect(spy).not.toHaveBeenCalled(); const collectionCount = await Collection.count(); expect(collectionCount).toEqual(0); + + spy.mockRestore(); }); it("should throw an error when authentication provider is disabled", async () => { const existingTeam = await buildTeam(); - const providers = await existingTeam.getAuthenticationProviders(); + const providers = await existingTeam.$get("authenticationProviders"); const authenticationProvider = providers[0]; await authenticationProvider.update({ enabled: false, @@ -120,7 +114,7 @@ describe("accountProvisioner", () => { const existing = await buildUser({ teamId: existingTeam.id, }); - const authentications = await existing.getAuthentications(); + const authentications = await existing.$get("authentications"); const authentication = authentications[0]; let error; @@ -129,7 +123,7 @@ describe("accountProvisioner", () => { ip, user: { name: existing.name, - email: existing.email, + email: existing.email!, avatarUrl: existing.avatarUrl, }, team: { @@ -155,8 +149,9 @@ describe("accountProvisioner", () => { }); it("should create a new user in an existing team", async () => { + const spy = jest.spyOn(mailer, "sendTemplate"); const team = await buildTeam(); - const authenticationProviders = await team.getAuthenticationProviders(); + const authenticationProviders = await team.$get("authenticationProviders"); const authenticationProvider = authenticationProviders[0]; const { user, isNewUser } = await accountProvisioner({ ip, @@ -181,7 +176,7 @@ describe("accountProvisioner", () => { scopes: ["read"], }, }); - const authentications = await user.getAuthentications(); + const authentications = await user.$get("authentications"); const auth = authentications[0]; expect(auth.accessToken).toEqual("123"); expect(auth.scopes.length).toEqual(1); @@ -189,9 +184,11 @@ describe("accountProvisioner", () => { expect(user.email).toEqual("jenny@example.com"); expect(user.username).toEqual("jtester"); expect(isNewUser).toEqual(true); - expect(mailer.sendTemplate).toHaveBeenCalled(); + expect(spy).toHaveBeenCalled(); // should provision welcome collection const collectionCount = await Collection.count(); expect(collectionCount).toEqual(1); + + spy.mockRestore(); }); }); diff --git a/server/commands/accountProvisioner.ts b/server/commands/accountProvisioner.ts index 3c4eafe97..6b94a1e6c 100644 --- a/server/commands/accountProvisioner.ts +++ b/server/commands/accountProvisioner.ts @@ -1,12 +1,12 @@ import invariant from "invariant"; -import Sequelize from "sequelize"; -import { Collection, Team, User } from "@server/models"; +import { UniqueConstraintError } from "sequelize"; import { AuthenticationError, EmailAuthenticationRequiredError, AuthenticationProviderDisabledError, -} from "../errors"; -import mailer from "../mailer"; +} from "@server/errors"; +import mailer from "@server/mailer"; +import { Collection, Team, User } from "@server/models"; import teamCreator from "./teamCreator"; import userCreator from "./userCreator"; @@ -15,14 +15,14 @@ type Props = { user: { name: string; email: string; - avatarUrl?: string; + avatarUrl?: string | null; username?: string; }; team: { name: string; domain?: string; subdomain: string; - avatarUrl?: string; + avatarUrl?: string | null; }; authenticationProvider: { name: string; @@ -37,9 +37,7 @@ type Props = { }; export type AccountProvisionerResult = { - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message user: User; - // @ts-expect-error ts-migrate(2749) FIXME: 'Team' refers to a value, but is being used as a t... Remove this comment to see the full error message team: Team; isNewTeam: boolean; isNewUser: boolean; @@ -123,7 +121,7 @@ export default async function accountProvisioner({ isNewTeam, }; } catch (err) { - if (err instanceof Sequelize.UniqueConstraintError) { + if (err instanceof UniqueConstraintError) { const exists = await User.findOne({ where: { email: userParams.email, diff --git a/server/commands/attachmentCreator.ts b/server/commands/attachmentCreator.ts index ed50a7680..917ef9d03 100644 --- a/server/commands/attachmentCreator.ts +++ b/server/commands/attachmentCreator.ts @@ -13,7 +13,6 @@ export default async function attachmentCreator({ name: string; type: string; buffer: Buffer; - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message user: User; source?: "import"; ip: string; diff --git a/server/commands/collectionExporter.ts b/server/commands/collectionExporter.ts index 6e3864462..f474ea12c 100644 --- a/server/commands/collectionExporter.ts +++ b/server/commands/collectionExporter.ts @@ -7,11 +7,8 @@ export default async function collectionExporter({ user, ip, }: { - // @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message collection?: Collection; - // @ts-expect-error ts-migrate(2749) FIXME: 'Team' refers to a value, but is being used as a t... Remove this comment to see the full error message team: Team; - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message user: User; ip: string; }) { @@ -39,6 +36,10 @@ export default async function collectionExporter({ }); fileOperation.user = user; - fileOperation.collection = collection; + + if (collection) { + fileOperation.collection = collection; + } + return fileOperation; } diff --git a/server/commands/collectionImporter.test.ts b/server/commands/collectionImporter.test.ts index f685cb3c9..ee24333f6 100644 --- a/server/commands/collectionImporter.test.ts +++ b/server/commands/collectionImporter.test.ts @@ -7,6 +7,7 @@ import collectionImporter from "./collectionImporter"; jest.mock("../utils/s3"); beforeEach(() => flushdb()); + describe("collectionImporter", () => { const ip = "127.0.0.1"; diff --git a/server/commands/collectionImporter.ts b/server/commands/collectionImporter.ts index e611505ad..3d5c4d641 100644 --- a/server/commands/collectionImporter.ts +++ b/server/commands/collectionImporter.ts @@ -24,7 +24,6 @@ export default async function collectionImporter({ ip, }: { file: FileWithPath; - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message user: User; type: "outline"; ip: string; @@ -48,7 +47,6 @@ export default async function collectionImporter({ // store progress and pointers // @ts-expect-error ts-migrate(2741) FIXME: Property 'string' is missing in type '{}' but requ... Remove this comment to see the full error message const collections: { - // @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message string: Collection; } = {}; // @ts-expect-error ts-migrate(2741) FIXME: Property 'string' is missing in type '{}' but requ... Remove this comment to see the full error message @@ -57,7 +55,6 @@ export default async function collectionImporter({ } = {}; // @ts-expect-error ts-migrate(2741) FIXME: Property 'string' is missing in type '{}' but requ... Remove this comment to see the full error message const attachments: { - // @ts-expect-error ts-migrate(2749) FIXME: 'Attachment' refers to a value, but is being used ... Remove this comment to see the full error message string: Attachment; } = {}; @@ -186,13 +183,13 @@ export default async function collectionImporter({ /(.*)uploads\//, "uploads/" ); - // @ts-expect-error ts-migrate(2339) FIXME: Property 'text' does not exist on type 'Document'. + document.text = document.text .replace(attachmentPath, attachment.redirectUrl) .replace(normalizedAttachmentPath, attachment.redirectUrl) .replace(`/${normalizedAttachmentPath}`, attachment.redirectUrl); + // does nothing if the document text is unchanged - // @ts-expect-error ts-migrate(2339) FIXME: Property 'save' does not exist on type 'Document'. await document.save({ fields: ["text"], }); diff --git a/server/commands/documentCreator.ts b/server/commands/documentCreator.ts index 8a7ae0954..1ca74e5b8 100644 --- a/server/commands/documentCreator.ts +++ b/server/commands/documentCreator.ts @@ -1,3 +1,4 @@ +import invariant from "invariant"; import { Document, Event, User } from "@server/models"; export default async function documentCreator({ @@ -21,18 +22,16 @@ export default async function documentCreator({ publish?: boolean; collectionId: string; parentDocumentId?: string; - templateDocument?: Document; + templateDocument?: Document | null; template?: boolean; createdAt?: Date; updatedAt?: Date; index?: number; - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message user: User; editorVersion?: string; source?: "import"; ip: string; }): Promise { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'. const templateId = templateDocument ? templateDocument.id : undefined; const document = await Document.create({ parentDocumentId, @@ -47,7 +46,6 @@ export default async function documentCreator({ template, templateId, title: templateDocument ? templateDocument.title : title, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'text' does not exist on type 'Document'. text: templateDocument ? templateDocument.text : text, }); await Event.create({ @@ -83,10 +81,13 @@ export default async function documentCreator({ // reload to get all of the data needed to present (user, collection etc) // we need to specify publishedAt to bypass default scope that only returns // published documents - return Document.findOne({ + const doc = await Document.findOne({ where: { id: document.id, publishedAt: document.publishedAt, }, }); + invariant(doc, "Document must exist"); + + return doc; } diff --git a/server/commands/documentImporter.test.ts b/server/commands/documentImporter.test.ts index 253e048c8..3b854bc2b 100644 --- a/server/commands/documentImporter.test.ts +++ b/server/commands/documentImporter.test.ts @@ -1,12 +1,13 @@ import path from "path"; import File from "formidable/lib/file"; -import { Attachment } from "@server/models"; +import Attachment from "@server/models/Attachment"; import { buildUser } from "@server/test/factories"; import { flushdb } from "@server/test/support"; import documentImporter from "./documentImporter"; jest.mock("../utils/s3"); beforeEach(() => flushdb()); + describe("documentImporter", () => { const ip = "127.0.0.1"; diff --git a/server/commands/documentImporter.ts b/server/commands/documentImporter.ts index b5ce33540..7677ac5aa 100644 --- a/server/commands/documentImporter.ts +++ b/server/commands/documentImporter.ts @@ -144,7 +144,6 @@ export default async function documentImporter({ user, ip, }: { - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message user: User; file: File; ip: string; diff --git a/server/commands/documentMover.test.ts b/server/commands/documentMover.test.ts index 4c9954419..9d1a102a2 100644 --- a/server/commands/documentMover.test.ts +++ b/server/commands/documentMover.test.ts @@ -1,4 +1,4 @@ -import { Attachment } from "@server/models"; +import Attachment from "@server/models/Attachment"; import { buildDocument, buildAttachment, @@ -10,6 +10,7 @@ import parseAttachmentIds from "@server/utils/parseAttachmentIds"; import documentMover from "./documentMover"; beforeEach(() => flushdb()); + describe("documentMover", () => { const ip = "127.0.0.1"; @@ -33,7 +34,7 @@ describe("documentMover", () => { const document = await buildDocument({ collectionId: collection.id, }); - await document.archive(); + await document.archive(user.id); const response = await documentMover({ user, document, @@ -63,13 +64,11 @@ describe("documentMover", () => { index: 0, ip, }); - // @ts-expect-error ts-migrate(2339) FIXME: Property 'documentStructure' does not exist on typ... Remove this comment to see the full error message - expect(response.collections[0].documentStructure[0].children[0].id).toBe( + expect(response.collections[0].documentStructure![0].children[0].id).toBe( newDocument.id ); expect(response.collections.length).toEqual(1); expect(response.documents.length).toEqual(1); - // @ts-expect-error ts-migrate(2339) FIXME: Property 'collection' does not exist on type 'neve... Remove this comment to see the full error message expect(response.documents[0].collection.id).toEqual(collection.id); }); @@ -98,22 +97,17 @@ describe("documentMover", () => { // check document ids where updated await newDocument.reload(); expect(newDocument.collectionId).toBe(newCollection.id); - await document.reload(); - expect(document.collectionId).toBe(newCollection.id); + // check collection structure updated - // @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'never'. expect(response.collections[0].id).toBe(collection.id); - // @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'never'. expect(response.collections[1].id).toBe(newCollection.id); - // @ts-expect-error ts-migrate(2339) FIXME: Property 'documentStructure' does not exist on typ... Remove this comment to see the full error message - expect(response.collections[1].documentStructure[0].children[0].id).toBe( + expect(response.collections[1].documentStructure![0].children[0].id).toBe( newDocument.id ); expect(response.collections.length).toEqual(2); expect(response.documents.length).toEqual(2); - // @ts-expect-error ts-migrate(2339) FIXME: Property 'collection' does not exist on type 'neve... Remove this comment to see the full error message + expect(response.documents[0].collection.id).toEqual(newCollection.id); - // @ts-expect-error ts-migrate(2339) FIXME: Property 'collection' does not exist on type 'neve... Remove this comment to see the full error message expect(response.documents[1].collection.id).toEqual(newCollection.id); }); @@ -152,8 +146,8 @@ describe("documentMover", () => { // check new attachment was created pointint to same key const attachmentIds = parseAttachmentIds(newDocument.text); const newAttachment = await Attachment.findByPk(attachmentIds[0]); - expect(newAttachment.documentId).toBe(newDocument.id); - expect(newAttachment.key).toBe(attachment.key); + expect(newAttachment?.documentId).toBe(newDocument.id); + expect(newAttachment?.key).toBe(attachment.key); await document.reload(); expect(document.collectionId).toBe(newCollection.id); }); diff --git a/server/commands/documentMover.ts b/server/commands/documentMover.ts index 6c6098167..bad89d59d 100644 --- a/server/commands/documentMover.ts +++ b/server/commands/documentMover.ts @@ -1,16 +1,22 @@ +import invariant from "invariant"; import { Transaction } from "sequelize"; -import { Document, Attachment, Collection, Pin, Event } from "@server/models"; +import { sequelize } from "@server/database/sequelize"; +import { + User, + Document, + Attachment, + Collection, + Pin, + Event, +} from "@server/models"; import parseAttachmentIds from "@server/utils/parseAttachmentIds"; -import { sequelize } from "../sequelize"; import pinDestroyer from "./pinDestroyer"; async function copyAttachments( document: Document, options?: { transaction?: Transaction } ) { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'text' does not exist on type 'Document'. let text = document.text; - // @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'. const documentId = document.id; // find any image attachments that are in this documents text const attachmentIds = parseAttachmentIds(text); @@ -18,7 +24,6 @@ async function copyAttachments( for (const id of attachmentIds) { const existing = await Attachment.findOne({ where: { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'teamId' does not exist on type 'Document... Remove this comment to see the full error message teamId: document.teamId, id, }, @@ -29,6 +34,7 @@ async function copyAttachments( // then create a new attachment pointed to this doc and update the reference // in the text so that it gets the moved documents permissions if (existing && existing.documentId !== documentId) { + // @ts-expect-error dataValues exists const { id, ...rest } = existing.dataValues; const attachment = await Attachment.create( { ...rest, documentId }, @@ -41,6 +47,21 @@ async function copyAttachments( return text; } +type Props = { + user: User; + document: Document; + collectionId: string; + parentDocumentId?: string | null; + index?: number; + ip: string; +}; + +type Result = { + collections: Collection[]; + documents: Document[]; + collectionChanged: boolean; +}; + export default async function documentMover({ user, document, @@ -49,19 +70,11 @@ export default async function documentMover({ // convert undefined to null so parentId comparison treats them as equal index, ip, -}: { - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message - user: User; - document: any; - collectionId: string; - parentDocumentId?: string | null; - index?: number; - ip: string; -}) { +}: Props): Promise { let transaction: Transaction | undefined; const collectionChanged = collectionId !== document.collectionId; const previousCollectionId = document.collectionId; - const result = { + const result: Result = { collections: [], documents: [], collectionChanged, @@ -77,7 +90,6 @@ export default async function documentMover({ document.lastModifiedById = user.id; document.updatedBy = user; await document.save(); - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'Document' is not assignable to p... Remove this comment to see the full error message result.documents.push(document); } else { try { @@ -88,12 +100,13 @@ export default async function documentMover({ transaction, paranoid: false, }); - const [ - documentJson, - fromIndex, - ] = (await collection.removeDocumentInStructure(document, { + + const response = await collection?.removeDocumentInStructure(document, { save: false, - })) || [undefined, index]; + }); + + const documentJson = response?.[0]; + const fromIndex = response?.[1] || 0; // if we're reordering from within the same parent // the original and destination collection are the same, @@ -110,7 +123,7 @@ export default async function documentMover({ // if the collection is the same then it will get saved below, this // line prevents a pointless intermediate save from occurring. if (collectionChanged) { - await collection.save({ + await collection?.save({ transaction, }); document.text = await copyAttachments(document, { @@ -124,36 +137,37 @@ export default async function documentMover({ document.lastModifiedById = user.id; document.updatedBy = user; - // @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message - const newCollection: Collection = collectionChanged + const newCollection = collectionChanged ? await Collection.scope({ method: ["withMembership", user.id], }).findByPk(collectionId, { transaction, }) : collection; - await newCollection.addDocumentToStructure(document, toIndex, { + + invariant(newCollection, "collection should exist"); + + await newCollection?.addDocumentToStructure(document, toIndex, { documentJson, }); - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'any' is not assignable to parame... Remove this comment to see the full error message - result.collections.push(collection); + + if (collection) { + result.collections.push(collection); + } // if collection does not remain the same loop through children and change their // collectionId and move any attachments they may have too. This includes // archived children, otherwise their collection would be wrong once restored. if (collectionChanged) { - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'any' is not assignable to parame... Remove this comment to see the full error message result.collections.push(newCollection); - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documentId' implicitly has an 'any' typ... Remove this comment to see the full error message - const loopChildren = async (documentId) => { + const loopChildren = async (documentId: string) => { const childDocuments = await Document.findAll({ where: { parentDocumentId: documentId, }, }); await Promise.all( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'child' implicitly has an 'any' type. childDocuments.map(async (child) => { await loopChildren(child.id); child.text = await copyAttachments(child, { @@ -161,8 +175,10 @@ export default async function documentMover({ }); child.collectionId = collectionId; await child.save(); - child.collection = newCollection; - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'any' is not assignable to parame... Remove this comment to see the full error message + + if (newCollection) { + child.collection = newCollection; + } result.documents.push(child); }) ); @@ -189,13 +205,13 @@ export default async function documentMover({ await document.save({ transaction, }); - document.collection = newCollection; - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'Document' is not assignable to p... Remove this comment to see the full error message + + if (newCollection) { + document.collection = newCollection; + } result.documents.push(document); - if (transaction) { - await transaction.commit(); - } + await transaction.commit(); } catch (err) { if (transaction) { await transaction.rollback(); @@ -213,9 +229,7 @@ export default async function documentMover({ teamId: document.teamId, data: { title: document.title, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'never'. collectionIds: result.collections.map((c) => c.id), - // @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'never'. documentIds: result.documents.map((d) => d.id), }, ip, diff --git a/server/commands/documentPermanentDeleter.test.ts b/server/commands/documentPermanentDeleter.test.ts index f0168ea87..814780733 100644 --- a/server/commands/documentPermanentDeleter.test.ts +++ b/server/commands/documentPermanentDeleter.test.ts @@ -4,18 +4,8 @@ import { buildAttachment, buildDocument } from "@server/test/factories"; import { flushdb } from "@server/test/support"; import documentPermanentDeleter from "./documentPermanentDeleter"; -jest.mock("aws-sdk", () => { - const mS3 = { - createPresignedPost: jest.fn(), - deleteObject: jest.fn().mockReturnThis(), - promise: jest.fn(), - }; - return { - S3: jest.fn(() => mS3), - Endpoint: jest.fn(), - }; -}); beforeEach(() => flushdb()); + describe("documentPermanentDeleter", () => { it("should destroy documents", async () => { const document = await buildDocument({ diff --git a/server/commands/documentPermanentDeleter.ts b/server/commands/documentPermanentDeleter.ts index 378e58d00..cc1a144a5 100644 --- a/server/commands/documentPermanentDeleter.ts +++ b/server/commands/documentPermanentDeleter.ts @@ -1,15 +1,14 @@ +import { QueryTypes } from "sequelize"; +import { sequelize } from "@server/database/sequelize"; import Logger from "@server/logging/logger"; import { Document, Attachment } from "@server/models"; import parseAttachmentIds from "@server/utils/parseAttachmentIds"; -import { sequelize } from "../sequelize"; export default async function documentPermanentDeleter(documents: Document[]) { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'deletedAt' does not exist on type 'Docum... Remove this comment to see the full error message const activeDocument = documents.find((doc) => !doc.deletedAt); if (activeDocument) { throw new Error( - // @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'. `Cannot permanently delete ${activeDocument.id} document. Please delete it and try again.` ); } @@ -23,16 +22,13 @@ export default async function documentPermanentDeleter(documents: Document[]) { `; for (const document of documents) { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'text' does not exist on type 'Document'. const attachmentIds = parseAttachmentIds(document.text); for (const attachmentId of attachmentIds) { const [{ count }] = await sequelize.query(query, { - type: sequelize.QueryTypes.SELECT, + type: QueryTypes.SELECT, replacements: { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'. documentId: document.id, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'teamId' does not exist on type 'Document... Remove this comment to see the full error message teamId: document.teamId, query: attachmentId, }, @@ -41,7 +37,6 @@ export default async function documentPermanentDeleter(documents: Document[]) { if (parseInt(count) === 0) { const attachment = await Attachment.findOne({ where: { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'teamId' does not exist on type 'Document... Remove this comment to see the full error message teamId: document.teamId, id: attachmentId, }, @@ -59,7 +54,6 @@ export default async function documentPermanentDeleter(documents: Document[]) { return Document.scope("withUnpublished").destroy({ where: { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'. id: documents.map((document) => document.id), }, force: true, diff --git a/server/commands/documentUpdater.ts b/server/commands/documentUpdater.ts index e1c05e18c..e9b5ed364 100644 --- a/server/commands/documentUpdater.ts +++ b/server/commands/documentUpdater.ts @@ -1,3 +1,4 @@ +import invariant from "invariant"; import { uniq } from "lodash"; import { Node } from "prosemirror-model"; import { schema, serializer } from "rich-markdown-editor"; @@ -15,6 +16,8 @@ export default async function documentUpdater({ userId?: string; }) { const document = await Document.findByPk(documentId); + invariant(document, "document not found"); + const state = Y.encodeStateAsUpdate(ydoc); const node = Node.fromJSON(schema, yDocToProsemirrorJSON(ydoc, "default")); const text = serializer.serialize(node, undefined); @@ -30,6 +33,7 @@ export default async function documentUpdater({ const pudIds = Array.from(pud.clients.values()); const existingIds = document.collaboratorIds; const collaboratorIds = uniq([...pudIds, ...existingIds]); + await Document.scope("withUnpublished").update( { text, diff --git a/server/commands/fileOperationDeleter.test.ts b/server/commands/fileOperationDeleter.test.ts index 105270d40..f29ba2305 100644 --- a/server/commands/fileOperationDeleter.test.ts +++ b/server/commands/fileOperationDeleter.test.ts @@ -3,18 +3,8 @@ import { buildAdmin, buildFileOperation } from "@server/test/factories"; import { flushdb } from "@server/test/support"; import fileOperationDeleter from "./fileOperationDeleter"; -jest.mock("aws-sdk", () => { - const mS3 = { - createPresignedPost: jest.fn(), - deleteObject: jest.fn().mockReturnThis(), - promise: jest.fn(), - }; - return { - S3: jest.fn(() => mS3), - Endpoint: jest.fn(), - }; -}); beforeEach(() => flushdb()); + describe("fileOperationDeleter", () => { const ip = "127.0.0.1"; diff --git a/server/commands/fileOperationDeleter.ts b/server/commands/fileOperationDeleter.ts index 01a402aaa..95f857be8 100644 --- a/server/commands/fileOperationDeleter.ts +++ b/server/commands/fileOperationDeleter.ts @@ -1,10 +1,8 @@ +import { sequelize } from "@server/database/sequelize"; import { FileOperation, Event, User } from "@server/models"; -import { sequelize } from "../sequelize"; export default async function fileOperationDeleter( - // @ts-expect-error ts-migrate(2749) FIXME: 'FileOperation' refers to a value, but is being us... Remove this comment to see the full error message fileOp: FileOperation, - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message user: User, ip: string ) { @@ -19,6 +17,7 @@ export default async function fileOperationDeleter( name: "fileOperations.delete", teamId: user.teamId, actorId: user.id, + // @ts-expect-error dataValues does exist data: fileOp.dataValues, ip, }, diff --git a/server/commands/pinCreator.test.ts b/server/commands/pinCreator.test.ts index 94d87e44b..6e27bcd51 100644 --- a/server/commands/pinCreator.test.ts +++ b/server/commands/pinCreator.test.ts @@ -25,8 +25,8 @@ describe("pinCreator", () => { expect(pin.collectionId).toEqual(null); expect(pin.createdById).toEqual(user.id); expect(pin.index).toEqual("P"); - expect(event.name).toEqual("pins.create"); - expect(event.modelId).toEqual(pin.id); + expect(event!.name).toEqual("pins.create"); + expect(event!.modelId).toEqual(pin.id); }); it("should create pin to collection", async () => { @@ -48,8 +48,8 @@ describe("pinCreator", () => { expect(pin.collectionId).toEqual(document.collectionId); expect(pin.createdById).toEqual(user.id); expect(pin.index).toEqual("P"); - expect(event.name).toEqual("pins.create"); - expect(event.modelId).toEqual(pin.id); - expect(event.collectionId).toEqual(pin.collectionId); + expect(event!.name).toEqual("pins.create"); + expect(event!.modelId).toEqual(pin.id); + expect(event!.collectionId).toEqual(pin.collectionId); }); }); diff --git a/server/commands/pinCreator.ts b/server/commands/pinCreator.ts index e8308f81f..7ce38427b 100644 --- a/server/commands/pinCreator.ts +++ b/server/commands/pinCreator.ts @@ -1,13 +1,14 @@ import fractionalIndex from "fractional-index"; +import { Sequelize, Op, WhereOptions } from "sequelize"; +import { sequelize } from "@server/database/sequelize"; import { ValidationError } from "@server/errors"; -import { Pin, Event } from "@server/models"; -import { sequelize, Op } from "@server/sequelize"; +import { Pin, User, Event } from "@server/models"; const MAX_PINS = 8; type Props = { /** The user creating the pin */ - user: any; + user: User; /** The document to pin */ documentId: string; /** The collection to pin the document in. If no collection is provided then it will be pinned to home */ @@ -31,11 +32,11 @@ export default async function pinCreator({ collectionId, ip, ...rest -}: Props): Promise { +}: Props): Promise { let { index } = rest; - const where = { + const where: WhereOptions = { teamId: user.teamId, - ...(collectionId ? { collectionId } : { collectionId: { [Op.eq]: null } }), + ...(collectionId ? { collectionId } : { collectionId: { [Op.is]: null } }), }; const count = await Pin.count({ where }); @@ -51,7 +52,7 @@ export default async function pinCreator({ order: [ // using LC_COLLATE:"C" because we need byte order to drive the sorting // find only the last pin so we can create an index after it - sequelize.literal('"pins"."index" collate "C" DESC'), + Sequelize.literal('"pin"."index" collate "C" DESC'), ["updatedAt", "ASC"], ], }); diff --git a/server/commands/pinDestroyer.test.ts b/server/commands/pinDestroyer.test.ts index 38dc23341..a3bf5fd9e 100644 --- a/server/commands/pinDestroyer.test.ts +++ b/server/commands/pinDestroyer.test.ts @@ -32,7 +32,7 @@ describe("pinCreator", () => { expect(count).toEqual(0); const event = await Event.findOne(); - expect(event.name).toEqual("pins.delete"); - expect(event.modelId).toEqual(pin.id); + expect(event!.name).toEqual("pins.delete"); + expect(event!.modelId).toEqual(pin.id); }); }); diff --git a/server/commands/pinDestroyer.ts b/server/commands/pinDestroyer.ts index 0d1c4cd3d..d97543296 100644 --- a/server/commands/pinDestroyer.ts +++ b/server/commands/pinDestroyer.ts @@ -1,12 +1,12 @@ import { Transaction } from "sequelize"; -import { Event } from "@server/models"; -import { sequelize } from "@server/sequelize"; +import { sequelize } from "@server/database/sequelize"; +import { Event, Pin, User } from "@server/models"; type Props = { /** The user destroying the pin */ - user: any; + user: User; /** The pin to destroy */ - pin: any; + pin: Pin; /** The IP address of the user creating the pin */ ip: string; /** Optional existing transaction */ @@ -25,7 +25,7 @@ export default async function pinDestroyer({ pin, ip, transaction: t, -}: Props): Promise { +}: Props): Promise { const transaction = t || (await sequelize.transaction()); try { diff --git a/server/commands/pinUpdater.ts b/server/commands/pinUpdater.ts index 637aa0d2a..9cb61ed39 100644 --- a/server/commands/pinUpdater.ts +++ b/server/commands/pinUpdater.ts @@ -1,13 +1,13 @@ -import { Event } from "@server/models"; -import { sequelize } from "@server/sequelize"; +import { sequelize } from "@server/database/sequelize"; +import { Event, Pin, User } from "@server/models"; type Props = { /** The user updating the pin */ - user: any; + user: User; /** The existing pin */ - pin: any; + pin: Pin; /** The index to pin the document at */ - index?: string; + index: string; /** The IP address of the user creating the pin */ ip: string; }; @@ -24,7 +24,7 @@ export default async function pinUpdater({ pin, index, ip, -}: Props): Promise { +}: Props): Promise { const transaction = await sequelize.transaction(); try { diff --git a/server/commands/revisionCreator.test.ts b/server/commands/revisionCreator.test.ts index f1d7f17b0..3da30ba33 100644 --- a/server/commands/revisionCreator.test.ts +++ b/server/commands/revisionCreator.test.ts @@ -4,6 +4,7 @@ import { flushdb } from "@server/test/support"; import revisionCreator from "./revisionCreator"; beforeEach(() => flushdb()); + describe("revisionCreator", () => { const ip = "127.0.0.1"; @@ -21,8 +22,8 @@ describe("revisionCreator", () => { const event = await Event.findOne(); expect(revision.documentId).toEqual(document.id); expect(revision.userId).toEqual(user.id); - expect(event.name).toEqual("revisions.create"); - expect(event.modelId).toEqual(revision.id); - expect(event.createdAt).toEqual(document.updatedAt); + expect(event!.name).toEqual("revisions.create"); + expect(event!.modelId).toEqual(revision.id); + expect(event!.createdAt).toEqual(document.updatedAt); }); }); diff --git a/server/commands/revisionCreator.ts b/server/commands/revisionCreator.ts index 050b2bd44..408f4013f 100644 --- a/server/commands/revisionCreator.ts +++ b/server/commands/revisionCreator.ts @@ -1,5 +1,5 @@ +import { sequelize } from "@server/database/sequelize"; import { Document, User, Event, Revision } from "@server/models"; -import { sequelize } from "../sequelize"; export default async function revisionCreator({ document, @@ -7,7 +7,6 @@ export default async function revisionCreator({ ip, }: { document: Document; - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message user: User; ip?: string; }) { @@ -21,13 +20,10 @@ export default async function revisionCreator({ await Event.create( { name: "revisions.create", - // @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'. documentId: document.id, modelId: revision.id, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'teamId' does not exist on type 'Document... Remove this comment to see the full error message teamId: document.teamId, actorId: user.id, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'updatedAt' does not exist on type 'Docum... Remove this comment to see the full error message createdAt: document.updatedAt, ip: ip || user.lastActiveIp, }, diff --git a/server/commands/teamCreator.test.ts b/server/commands/teamCreator.test.ts index 9bb0a61a1..41bffa31f 100644 --- a/server/commands/teamCreator.test.ts +++ b/server/commands/teamCreator.test.ts @@ -2,18 +2,8 @@ import { buildTeam } from "@server/test/factories"; import { flushdb } from "@server/test/support"; import teamCreator from "./teamCreator"; -jest.mock("aws-sdk", () => { - const mS3 = { - createPresignedPost: jest.fn(), - putObject: jest.fn().mockReturnThis(), - promise: jest.fn(), - }; - return { - S3: jest.fn(() => mS3), - Endpoint: jest.fn(), - }; -}); beforeEach(() => flushdb()); + describe("teamCreator", () => { it("should create team and authentication provider", async () => { const result = await teamCreator({ @@ -73,7 +63,7 @@ describe("teamCreator", () => { expect(authenticationProvider.name).toEqual("google"); expect(authenticationProvider.providerId).toEqual("allowed-domain.com"); expect(isNewTeam).toEqual(false); - const providers = await team.getAuthenticationProviders(); + const providers = await team.$get("authenticationProviders"); expect(providers.length).toEqual(2); }); diff --git a/server/commands/teamCreator.ts b/server/commands/teamCreator.ts index 50cf955e7..6cbec8286 100644 --- a/server/commands/teamCreator.ts +++ b/server/commands/teamCreator.ts @@ -1,34 +1,34 @@ +import invariant from "invariant"; import Logger from "@server/logging/logger"; import { Team, AuthenticationProvider } from "@server/models"; import { getAllowedDomains } from "@server/utils/authentication"; import { generateAvatarUrl } from "@server/utils/avatars"; import { MaximumTeamsError } from "../errors"; -import { sequelize } from "../sequelize"; type TeamCreatorResult = { - // @ts-expect-error ts-migrate(2749) FIXME: 'Team' refers to a value, but is being used as a t... Remove this comment to see the full error message team: Team; - // @ts-expect-error ts-migrate(2749) FIXME: 'AuthenticationProvider' refers to a value, but is... Remove this comment to see the full error message authenticationProvider: AuthenticationProvider; isNewTeam: boolean; }; +type Props = { + name: string; + domain?: string; + subdomain: string; + avatarUrl?: string | null; + authenticationProvider: { + name: string; + providerId: string; + }; +}; + export default async function teamCreator({ name, domain, subdomain, avatarUrl, authenticationProvider, -}: { - name: string; - domain?: string; - subdomain: string; - avatarUrl?: string; - authenticationProvider: { - name: string; - providerId: string; - }; -}): Promise { +}: Props): Promise { let authP = await AuthenticationProvider.findOne({ where: authenticationProvider, include: [ @@ -61,7 +61,12 @@ export default async function teamCreator({ // authentication provider to the existing team if (teamCount === 1 && domain && getAllowedDomains().includes(domain)) { const team = await Team.findOne(); - authP = await team.createAuthenticationProvider(authenticationProvider); + invariant(team, "Team should exist"); + + authP = await team.$create( + "authenticationProvider", + authenticationProvider + ); return { authenticationProvider: authP, team, @@ -84,7 +89,7 @@ export default async function teamCreator({ }); } - const transaction = await sequelize.transaction(); + const transaction = await Team.sequelize!.transaction(); let team; try { diff --git a/server/commands/teamPermanentDeleter.test.ts b/server/commands/teamPermanentDeleter.test.ts index 87c36955b..7ebf2d98b 100644 --- a/server/commands/teamPermanentDeleter.test.ts +++ b/server/commands/teamPermanentDeleter.test.ts @@ -9,18 +9,8 @@ import { import { flushdb } from "@server/test/support"; import teamPermanentDeleter from "./teamPermanentDeleter"; -jest.mock("aws-sdk", () => { - const mS3 = { - createPresignedPost: jest.fn(), - deleteObject: jest.fn().mockReturnThis(), - promise: jest.fn(), - }; - return { - S3: jest.fn(() => mS3), - Endpoint: jest.fn(), - }; -}); beforeEach(() => flushdb()); + describe("teamPermanentDeleter", () => { it("should destroy related data", async () => { const team = await buildTeam({ diff --git a/server/commands/teamPermanentDeleter.ts b/server/commands/teamPermanentDeleter.ts index 674024846..0d5b19190 100644 --- a/server/commands/teamPermanentDeleter.ts +++ b/server/commands/teamPermanentDeleter.ts @@ -1,3 +1,4 @@ +import { sequelize } from "@server/database/sequelize"; import Logger from "@server/logging/logger"; import { ApiKey, @@ -17,9 +18,7 @@ import { SearchQuery, Share, } from "@server/models"; -import { sequelize } from "../sequelize"; -// @ts-expect-error ts-migrate(2749) FIXME: 'Team' refers to a value, but is being used as a t... Remove this comment to see the full error message export default async function teamPermanentDeleter(team: Team) { if (!team.deletedAt) { throw new Error( @@ -45,16 +44,14 @@ export default async function teamPermanentDeleter(team: Team) { limit: 100, offset: 0, }, - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'attachments' implicitly has an 'any' ty... Remove this comment to see the full error message async (attachments, options) => { Logger.info( "commands", `Deleting attachments ${options.offset} – ${ - options.offset + options.limit + (options.offset || 0) + (options?.limit || 0) }…` ); await Promise.all( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'attachment' implicitly has an 'any' typ... Remove this comment to see the full error message attachments.map((attachment) => attachment.destroy({ // @ts-expect-error ts-migrate(7005) FIXME: Variable 'transaction' implicitly has an 'any' typ... Remove this comment to see the full error message @@ -74,9 +71,7 @@ export default async function teamPermanentDeleter(team: Team) { limit: 100, offset: 0, }, - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'users' implicitly has an 'any' type. async (users) => { - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'user' implicitly has an 'any' type. const userIds = users.map((user) => user.id); await UserAuthentication.destroy({ where: { diff --git a/server/commands/userCreator.test.ts b/server/commands/userCreator.test.ts index 53c98dd25..7e57873da 100644 --- a/server/commands/userCreator.test.ts +++ b/server/commands/userCreator.test.ts @@ -3,12 +3,13 @@ import { flushdb } from "@server/test/support"; import userCreator from "./userCreator"; beforeEach(() => flushdb()); + describe("userCreator", () => { const ip = "127.0.0.1"; it("should update exising user and authentication", async () => { const existing = await buildUser(); - const authentications = await existing.getAuthentications(); + const authentications = await existing.$get("authentications"); const existingAuth = authentications[0]; const newEmail = "test@example.com"; const newUsername = "tname"; @@ -37,7 +38,7 @@ describe("userCreator", () => { it("should create user with deleted user matching providerId", async () => { const existing = await buildUser(); - const authentications = await existing.getAuthentications(); + const authentications = await existing.$get("authentications"); const existingAuth = authentications[0]; const newEmail = "test@example.com"; await existing.destroy(); @@ -63,7 +64,7 @@ describe("userCreator", () => { it("should handle duplicate providerId for different iDP", async () => { const existing = await buildUser(); - const authentications = await existing.getAuthentications(); + const authentications = await existing.$get("authentications"); const existingAuth = authentications[0]; let error; @@ -89,7 +90,7 @@ describe("userCreator", () => { it("should create a new user", async () => { const team = await buildTeam(); - const authenticationProviders = await team.getAuthenticationProviders(); + const authenticationProviders = await team.$get("authenticationProviders"); const authenticationProvider = authenticationProviders[0]; const result = await userCreator({ name: "Test Name", @@ -119,7 +120,7 @@ describe("userCreator", () => { const team = await buildTeam({ defaultUserRole: "viewer", }); - const authenticationProviders = await team.getAuthenticationProviders(); + const authenticationProviders = await team.$get("authenticationProviders"); const authenticationProvider = authenticationProviders[0]; const result = await userCreator({ name: "Test Name", @@ -143,7 +144,7 @@ describe("userCreator", () => { const team = await buildTeam({ defaultUserRole: "viewer", }); - const authenticationProviders = await team.getAuthenticationProviders(); + const authenticationProviders = await team.$get("authenticationProviders"); const authenticationProvider = authenticationProviders[0]; const result = await userCreator({ name: "Test Name", @@ -187,11 +188,11 @@ describe("userCreator", () => { const invite = await buildInvite({ teamId: team.id, }); - const authenticationProviders = await team.getAuthenticationProviders(); + const authenticationProviders = await team.$get("authenticationProviders"); const authenticationProvider = authenticationProviders[0]; const result = await userCreator({ name: invite.name, - email: invite.email, + email: invite.email!, teamId: invite.teamId, ip, authentication: { diff --git a/server/commands/userCreator.ts b/server/commands/userCreator.ts index 116355bca..a5fe3ef05 100644 --- a/server/commands/userCreator.ts +++ b/server/commands/userCreator.ts @@ -1,15 +1,29 @@ import { Op } from "sequelize"; import { Event, Team, User, UserAuthentication } from "@server/models"; -import { sequelize } from "../sequelize"; type UserCreatorResult = { - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message user: User; isNewUser: boolean; - // @ts-expect-error ts-migrate(2749) FIXME: 'UserAuthentication' refers to a value, but is bei... Remove this comment to see the full error message authentication: UserAuthentication; }; +type Props = { + name: string; + email: string; + username?: string; + isAdmin?: boolean; + avatarUrl?: string | null; + teamId: string; + ip: string; + authentication: { + authenticationProviderId: string; + providerId: string; + scopes: string[]; + accessToken?: string; + refreshToken?: string; + }; +}; + export default async function userCreator({ name, email, @@ -19,22 +33,7 @@ export default async function userCreator({ teamId, authentication, ip, -}: { - name: string; - email: string; - username?: string; - isAdmin?: boolean; - avatarUrl?: string; - teamId: string; - ip: string; - authentication: { - authenticationProviderId: string; - providerId: string; - scopes: string[]; - accessToken?: string; - refreshToken?: string; - }; -}): Promise { +}: Props): Promise { const { authenticationProviderId, providerId, ...rest } = authentication; const auth = await UserAuthentication.findOne({ where: { @@ -90,7 +89,7 @@ export default async function userCreator({ email, teamId, lastActiveAt: { - [Op.eq]: null, + [Op.is]: null, }, }, include: [ @@ -105,7 +104,7 @@ export default async function userCreator({ // We have an existing invite for his user, so we need to update it with our // new details and link up the authentication method if (invite && !invite.authentications.length) { - const transaction = await sequelize.transaction(); + const transaction = await User.sequelize!.transaction(); let auth; try { @@ -118,9 +117,13 @@ export default async function userCreator({ transaction, } ); - auth = await invite.createAuthentication(authentication, { - transaction, - }); + auth = await invite.$create( + "authentication", + authentication, + { + transaction, + } + ); await transaction.commit(); } catch (err) { await transaction.rollback(); @@ -135,13 +138,15 @@ export default async function userCreator({ } // No auth, no user – this is an entirely new sign in. - const transaction = await sequelize.transaction(); + const transaction = await User.sequelize!.transaction(); try { - const { defaultUserRole } = await Team.findByPk(teamId, { + const team = await Team.findByPk(teamId, { attributes: ["defaultUserRole"], transaction, }); + const defaultUserRole = team?.defaultUserRole; + const user = await User.create( { name, diff --git a/server/commands/userDestroyer.test.ts b/server/commands/userDestroyer.test.ts index 7dd235af0..4676e229d 100644 --- a/server/commands/userDestroyer.test.ts +++ b/server/commands/userDestroyer.test.ts @@ -3,6 +3,7 @@ import { flushdb } from "@server/test/support"; import userDestroyer from "./userDestroyer"; beforeEach(() => flushdb()); + describe("userDestroyer", () => { const ip = "127.0.0.1"; diff --git a/server/commands/userDestroyer.ts b/server/commands/userDestroyer.ts index f94924399..182ae03e3 100644 --- a/server/commands/userDestroyer.ts +++ b/server/commands/userDestroyer.ts @@ -1,15 +1,14 @@ +import { Op } from "sequelize"; +import { sequelize } from "@server/database/sequelize"; import { Event, User } from "@server/models"; import { ValidationError } from "../errors"; -import { Op, sequelize } from "../sequelize"; export default async function userDestroyer({ user, actor, ip, }: { - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message user: User; - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message actor: User; ip: string; }) { diff --git a/server/commands/userInviter.test.ts b/server/commands/userInviter.test.ts index f8dcedc7b..cd143a4ad 100644 --- a/server/commands/userInviter.test.ts +++ b/server/commands/userInviter.test.ts @@ -3,6 +3,7 @@ import { flushdb } from "@server/test/support"; import userInviter from "./userInviter"; beforeEach(() => flushdb()); + describe("userInviter", () => { const ip = "127.0.0.1"; @@ -10,8 +11,8 @@ describe("userInviter", () => { const user = await buildUser(); const response = await userInviter({ invites: [ - // @ts-expect-error ts-migrate(2741) FIXME: Property 'role' is missing in type '{ email: strin... Remove this comment to see the full error message { + role: "member", email: "test@example.com", name: "Test", }, @@ -26,8 +27,8 @@ describe("userInviter", () => { const user = await buildUser(); const response = await userInviter({ invites: [ - // @ts-expect-error ts-migrate(2741) FIXME: Property 'role' is missing in type '{ email: strin... Remove this comment to see the full error message { + role: "member", email: " ", name: "Test", }, @@ -42,8 +43,8 @@ describe("userInviter", () => { const user = await buildUser(); const response = await userInviter({ invites: [ - // @ts-expect-error ts-migrate(2741) FIXME: Property 'role' is missing in type '{ email: strin... Remove this comment to see the full error message { + role: "member", email: "notanemail", name: "Test", }, @@ -58,13 +59,13 @@ describe("userInviter", () => { const user = await buildUser(); const response = await userInviter({ invites: [ - // @ts-expect-error ts-migrate(2741) FIXME: Property 'role' is missing in type '{ email: strin... Remove this comment to see the full error message { + role: "member", email: "the@same.com", name: "Test", }, - // @ts-expect-error ts-migrate(2741) FIXME: Property 'role' is missing in type '{ email: strin... Remove this comment to see the full error message { + role: "member", email: "the@SAME.COM", name: "Test", }, @@ -79,9 +80,9 @@ describe("userInviter", () => { const user = await buildUser(); const response = await userInviter({ invites: [ - // @ts-expect-error ts-migrate(2741) FIXME: Property 'role' is missing in type '{ email: any; ... Remove this comment to see the full error message { - email: user.email, + role: "member", + email: user.email!, name: user.name, }, ], diff --git a/server/commands/userInviter.ts b/server/commands/userInviter.ts index a0f618d04..f082e26ff 100644 --- a/server/commands/userInviter.ts +++ b/server/commands/userInviter.ts @@ -1,7 +1,8 @@ +import invariant from "invariant"; import { uniqBy } from "lodash"; import { Role } from "@shared/types"; +import mailer from "@server/mailer"; import { User, Event, Team } from "@server/models"; -import mailer from "../mailer"; type Invite = { name: string; @@ -14,16 +15,16 @@ export default async function userInviter({ invites, ip, }: { - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message user: User; invites: Invite[]; ip: string; }): Promise<{ sent: Invite[]; - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message users: User[]; }> { const team = await Team.findByPk(user.teamId); + invariant(team, "team not found"); + // filter out empties and obvious non-emails const compactedInvites = invites.filter( (invite) => !!invite.email.trim() && invite.email.match("@") @@ -44,7 +45,6 @@ export default async function userInviter({ email: emails, }, }); - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'user' implicitly has an 'any' type. const existingEmails = existingUsers.map((user) => user.email); const filteredInvites = normalizedInvites.filter( (invite) => !existingEmails.includes(invite.email) diff --git a/server/commands/userSuspender.test.ts b/server/commands/userSuspender.test.ts index b8a45fc8f..72b63c207 100644 --- a/server/commands/userSuspender.test.ts +++ b/server/commands/userSuspender.test.ts @@ -1,9 +1,10 @@ -import { GroupUser } from "@server/models"; +import GroupUser from "@server/models/GroupUser"; import { buildGroup, buildAdmin, buildUser } from "@server/test/factories"; import { flushdb } from "@server/test/support"; import userSuspender from "./userSuspender"; beforeEach(() => flushdb()); + describe("userSuspender", () => { const ip = "127.0.0.1"; @@ -46,7 +47,7 @@ describe("userSuspender", () => { const group = await buildGroup({ teamId: user.teamId, }); - await group.addUser(user, { + await group.$add("user", user, { through: { createdById: user.id, }, diff --git a/server/commands/userSuspender.ts b/server/commands/userSuspender.ts index 5cf25c670..a42aee487 100644 --- a/server/commands/userSuspender.ts +++ b/server/commands/userSuspender.ts @@ -1,18 +1,19 @@ import { Transaction } from "sequelize"; +import { sequelize } from "@server/database/sequelize"; import { User, Event, GroupUser } from "@server/models"; import { ValidationError } from "../errors"; -import { sequelize } from "../sequelize"; + +type Props = { + user: User; + actorId: string; + ip: string; +}; export default async function userSuspender({ user, actorId, ip, -}: { - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message - user: User; - actorId: string; - ip: string; -}): Promise { +}: Props): Promise { if (user.id === actorId) { throw ValidationError("Unable to suspend the current user"); } diff --git a/server/sequelize.ts b/server/database/sequelize.ts similarity index 54% rename from server/sequelize.ts rename to server/database/sequelize.ts index f2a90f85b..c65ecb6a6 100644 --- a/server/sequelize.ts +++ b/server/database/sequelize.ts @@ -1,22 +1,13 @@ -import Sequelize from "sequelize"; -import EncryptedField from "sequelize-encrypted"; -import Logger from "./logging/logger"; +import { Sequelize } from "sequelize-typescript"; +import Logger from "../logging/logger"; +import * as models from "../models"; const isProduction = process.env.NODE_ENV === "production"; const isSSLDisabled = process.env.PGSSLMODE === "disable"; -export const encryptedFields = () => - EncryptedField(Sequelize, process.env.SECRET_KEY); - -export const DataTypes = Sequelize; - -export const Op = Sequelize.Op; - -// @ts-expect-error ts-migrate(2351) FIXME: This expression is not constructable. export const sequelize = new Sequelize( - process.env.DATABASE_URL || process.env.DATABASE_CONNECTION_POOL_URL, + process.env.DATABASE_URL || process.env.DATABASE_CONNECTION_POOL_URL || "", { - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'msg' implicitly has an 'any' type. logging: (msg) => Logger.debug("database", msg), typeValidation: true, dialectOptions: { @@ -28,5 +19,6 @@ export const sequelize = new Sequelize( } : false, }, + models: Object.values(models), } ); diff --git a/server/database/vaults.ts b/server/database/vaults.ts new file mode 100644 index 000000000..45e55dcfd --- /dev/null +++ b/server/database/vaults.ts @@ -0,0 +1,9 @@ +import SequelizeEncrypted from "sequelize-encrypted"; +import { Sequelize } from "sequelize-typescript"; + +/** + * Encrypted field storage, use via the Encrypted decorator, not directly. + */ +export default function vaults() { + return SequelizeEncrypted(Sequelize, process.env.SECRET_KEY); +} diff --git a/server/emails/CollectionNotificationEmail.tsx b/server/emails/CollectionNotificationEmail.tsx index 96da46a81..bfbbb291e 100644 --- a/server/emails/CollectionNotificationEmail.tsx +++ b/server/emails/CollectionNotificationEmail.tsx @@ -9,9 +9,7 @@ import Header from "./components/Header"; import Heading from "./components/Heading"; export type Props = { - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message actor: User; - // @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message collection: Collection; eventName: string; unsubscribeUrl: string; diff --git a/server/emails/DocumentNotificationEmail.tsx b/server/emails/DocumentNotificationEmail.tsx index dea2a555a..4fc12b8f4 100644 --- a/server/emails/DocumentNotificationEmail.tsx +++ b/server/emails/DocumentNotificationEmail.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { User, Team, Collection } from "@server/models"; +import { Document, User, Team, Collection } from "@server/models"; import Body from "./components/Body"; import Button from "./components/Button"; import EmailTemplate from "./components/EmailLayout"; @@ -9,12 +9,9 @@ import Header from "./components/Header"; import Heading from "./components/Heading"; export type Props = { - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message actor: User; - // @ts-expect-error ts-migrate(2749) FIXME: 'Team' refers to a value, but is being used as a t... Remove this comment to see the full error message team: Team; - document: any; - // @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message + document: Document; collection: Collection; eventName: string; unsubscribeUrl: string; diff --git a/server/errors.ts b/server/errors.ts index e3e3c4300..338efddc2 100644 --- a/server/errors.ts +++ b/server/errors.ts @@ -28,7 +28,11 @@ export function AdminRequiredError( }); } -export function UserSuspendedError({ adminEmail }: { adminEmail: string }) { +export function UserSuspendedError({ + adminEmail, +}: { + adminEmail: string | undefined; +}) { return httpErrors(403, "Your access has been suspended by the team admin", { id: "user_suspended", errorData: { diff --git a/server/index.ts b/server/index.ts index 3772231b6..397440399 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,4 +1,5 @@ -import env from "./env"; // eslint-disable-line import/order +/* eslint-disable import/order */ +import env from "./env"; import "./tracing"; // must come before importing any instrumented module @@ -10,6 +11,7 @@ import logger from "koa-logger"; import onerror from "koa-onerror"; import Router from "koa-router"; import { uniq } from "lodash"; +import { AddressInfo } from "net"; import stoppable from "stoppable"; import throng from "throng"; import Logger from "./logging/logger"; @@ -104,8 +106,10 @@ async function start(id: number, disconnect: () => void) { server.on("listening", () => { const address = server.address(); - // @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'. - Logger.info("lifecycle", `Listening on http://localhost:${address.port}`); + Logger.info( + "lifecycle", + `Listening on http://localhost:${(address as AddressInfo).port}` + ); }); server.listen(normalizedPortFlag || env.PORT || "3000"); process.once("SIGTERM", shutdown); diff --git a/server/mailer.test.ts b/server/mailer.test.ts index 73b08a50e..56ef95a5e 100644 --- a/server/mailer.test.ts +++ b/server/mailer.test.ts @@ -2,15 +2,14 @@ import mailer from "./mailer"; describe("Mailer", () => { const fakeMailer = mailer; - // @ts-expect-error ts-migrate(7034) FIXME: Variable 'sendMailOutput' implicitly has type 'any... Remove this comment to see the full error message - let sendMailOutput; + let sendMailOutput: any; + beforeEach(() => { process.env.URL = "http://localhost:3000"; process.env.SMTP_FROM_EMAIL = "hello@example.com"; jest.resetModules(); fakeMailer.transporter = { - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'output' implicitly has an 'any' type. - sendMail: (output) => (sendMailOutput = output), + sendMail: (output: any) => (sendMailOutput = output), }; }); @@ -19,7 +18,6 @@ describe("Mailer", () => { to: "user@example.com", teamUrl: "http://example.com", }); - // @ts-expect-error ts-migrate(7005) FIXME: Variable 'sendMailOutput' implicitly has an 'any' ... Remove this comment to see the full error message expect(sendMailOutput).toMatchSnapshot(); }); }); diff --git a/server/mailer.tsx b/server/mailer.tsx index 2b95db312..058e43f6d 100644 --- a/server/mailer.tsx +++ b/server/mailer.tsx @@ -251,8 +251,8 @@ export class Mailer { }); }; - sendTemplate = async (type: EmailTypes, opts: Record = {}) => { - await emailsQueue.add( + sendTemplate = (type: EmailTypes, opts: Record = {}) => { + return emailsQueue.add( { type, opts, diff --git a/server/middlewares/authentication.test.ts b/server/middlewares/authentication.test.ts index fa0991eee..d5d46a837 100644 --- a/server/middlewares/authentication.test.ts +++ b/server/middlewares/authentication.test.ts @@ -1,5 +1,5 @@ import randomstring from "randomstring"; -import { ApiKey } from "@server/models"; +import ApiKey from "@server/models/ApiKey"; import { buildUser, buildTeam } from "@server/test/factories"; import { flushdb } from "@server/test/support"; import auth from "./authentication"; diff --git a/server/middlewares/authentication.ts b/server/middlewares/authentication.ts index ab24dea41..4b13828fc 100644 --- a/server/middlewares/authentication.ts +++ b/server/middlewares/authentication.ts @@ -89,7 +89,7 @@ export default function auth( paranoid: false, }); throw UserSuspendedError({ - adminEmail: suspendingAdmin.email, + adminEmail: suspendingAdmin?.email || undefined, }); } diff --git a/server/middlewares/errorHandling.ts b/server/middlewares/errorHandling.ts index f66f3cf15..6ee30234e 100644 --- a/server/middlewares/errorHandling.ts +++ b/server/middlewares/errorHandling.ts @@ -1,6 +1,6 @@ import { Context, Next } from "koa"; import { snakeCase } from "lodash"; -import Sequelize from "sequelize"; +import { ValidationError } from "sequelize"; export default function errorHandling() { return async function errorHandlingMiddleware(ctx: Context, next: Next) { @@ -11,7 +11,7 @@ export default function errorHandling() { let message = err.message || err.name; let error; - if (err instanceof Sequelize.ValidationError) { + if (err instanceof ValidationError) { // super basic form error handling ctx.status = 400; diff --git a/server/models/ApiKey.ts b/server/models/ApiKey.ts index 5328636c5..a2132770f 100644 --- a/server/models/ApiKey.ts +++ b/server/models/ApiKey.ts @@ -1,37 +1,43 @@ import randomstring from "randomstring"; -import { DataTypes, sequelize } from "../sequelize"; +import { + Column, + Table, + Unique, + BeforeValidate, + BelongsTo, + ForeignKey, +} from "sequelize-typescript"; +import User from "./User"; +import ParanoidModel from "./base/ParanoidModel"; +import Fix from "./decorators/Fix"; -const ApiKey = sequelize.define( - "apiKey", - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - name: DataTypes.STRING, - secret: { - type: DataTypes.STRING, - unique: true, - }, - }, - { - paranoid: true, - hooks: { - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'key' implicitly has an 'any' type. - beforeValidate: (key) => { - key.secret = randomstring.generate(38); - }, - }, +@Table({ tableName: "apiKeys", modelName: "apiKey" }) +@Fix +class ApiKey extends ParanoidModel { + @Column + name: string; + + @Unique + @Column + secret: string; + + // hooks + + @BeforeValidate + static async generateSecret(model: ApiKey) { + if (!model.secret) { + model.secret = randomstring.generate(38); + } } -); -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -ApiKey.associate = (models) => { - ApiKey.belongsTo(models.User, { - as: "user", - foreignKey: "userId", - }); -}; + // associations + + @BelongsTo(() => User, "userId") + user: User; + + @ForeignKey(() => User) + @Column + userId: string; +} export default ApiKey; diff --git a/server/models/Attachment.ts b/server/models/Attachment.ts index c68d0d22f..da311f80b 100644 --- a/server/models/Attachment.ts +++ b/server/models/Attachment.ts @@ -1,88 +1,107 @@ import path from "path"; +import { FindOptions } from "sequelize"; +import { + BeforeDestroy, + BelongsTo, + Column, + Default, + ForeignKey, + IsIn, + Table, + DataType, +} from "sequelize-typescript"; import { deleteFromS3, getFileByKey } from "@server/utils/s3"; -import { DataTypes, sequelize } from "../sequelize"; +import Document from "./Document"; +import Team from "./Team"; +import User from "./User"; +import BaseModel from "./base/BaseModel"; +import Fix from "./decorators/Fix"; -const Attachment = sequelize.define( - "attachment", - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - key: { - type: DataTypes.STRING, - allowNull: false, - }, - url: { - type: DataTypes.STRING, - allowNull: false, - }, - contentType: { - type: DataTypes.STRING, - allowNull: false, - }, - size: { - type: DataTypes.BIGINT, - allowNull: false, - }, - acl: { - type: DataTypes.STRING, - allowNull: false, - defaultValue: "public-read", - validate: { - isIn: [["private", "public-read"]], - }, - }, - }, - { - getterMethods: { - name: function () { - return path.parse(this.key).base; - }, - redirectUrl: function () { - return `/api/attachments.redirect?id=${this.id}`; - }, - isPrivate: function () { - return this.acl === "private"; - }, - buffer: function () { - return getFileByKey(this.key); - }, - }, +@Table({ tableName: "attachments", modelName: "attachment" }) +@Fix +class Attachment extends BaseModel { + @Column + key: string; + + @Column + url: string; + + @Column + contentType: string; + + @Column(DataType.BIGINT) + size: number; + + @Default("public-read") + @IsIn([["private", "public-read"]]) + @Column + acl: string; + + // getters + + get name() { + return path.parse(this.key).base; } -); -Attachment.findAllInBatches = async ( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'query' implicitly has an 'any' type. - query, - callback: ( - // @ts-expect-error ts-migrate(2749) FIXME: 'Attachment' refers to a value, but is being used ... Remove this comment to see the full error message - attachments: Array, - query: Record - ) => Promise -) => { - if (!query.offset) query.offset = 0; - if (!query.limit) query.limit = 10; - let results; + get redirectUrl() { + return `/api/attachments.redirect?id=${this.id}`; + } - do { - results = await Attachment.findAll(query); - await callback(results, query); - query.offset += query.limit; - } while (results.length >= query.limit); -}; + get isPrivate() { + return this.acl === "private"; + } -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'model' implicitly has an 'any' type. -Attachment.beforeDestroy(async (model) => { - await deleteFromS3(model.key); -}); + get buffer() { + return getFileByKey(this.key); + } -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -Attachment.associate = (models) => { - Attachment.belongsTo(models.Team); - Attachment.belongsTo(models.Document); - Attachment.belongsTo(models.User); -}; + // hooks + + @BeforeDestroy + static async deleteAttachmentFromS3(model: Attachment) { + await deleteFromS3(model.key); + } + + // associations + + @BelongsTo(() => Team, "teamId") + team: Team; + + @ForeignKey(() => Team) + @Column(DataType.UUID) + teamId: string; + + @BelongsTo(() => Document, "documentId") + document: Document; + + @ForeignKey(() => Document) + @Column(DataType.UUID) + documentId: string | null; + + @BelongsTo(() => User, "userId") + user: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + userId: string; + + static async findAllInBatches( + query: FindOptions, + callback: ( + attachments: Array, + query: FindOptions + ) => Promise + ) { + if (!query.offset) query.offset = 0; + if (!query.limit) query.limit = 10; + let results; + + do { + results = await this.findAll(query); + await callback(results, query); + query.offset += query.limit; + } while (results.length >= query.limit); + } +} export default Attachment; diff --git a/server/models/AuthenticationProvider.ts b/server/models/AuthenticationProvider.ts index 06d5cd3a1..d659c6a90 100644 --- a/server/models/AuthenticationProvider.ts +++ b/server/models/AuthenticationProvider.ts @@ -1,68 +1,89 @@ +import { Op } from "sequelize"; +import { + BelongsTo, + Column, + CreatedAt, + DataType, + Default, + ForeignKey, + HasMany, + Table, + Model, + IsUUID, + PrimaryKey, +} from "sequelize-typescript"; import { ValidationError } from "../errors"; -// @ts-expect-error ts-migrate(7034) FIXME: Variable 'providers' implicitly has type 'any[]' i... Remove this comment to see the full error message -import providers from "../routes/auth/providers"; -import { DataTypes, Op, sequelize } from "../sequelize"; +import Team from "./Team"; +import UserAuthentication from "./UserAuthentication"; +import Fix from "./decorators/Fix"; -const AuthenticationProvider = sequelize.define( - "authentication_providers", - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - name: { - type: DataTypes.STRING, - validate: { - // @ts-expect-error ts-migrate(7005) FIXME: Variable 'providers' implicitly has an 'any[]' typ... Remove this comment to see the full error message - isIn: [providers.map((p) => p.id)], +@Table({ + tableName: "authentication_providers", + modelName: "authentication_provider", + updatedAt: false, +}) +@Fix +class AuthenticationProvider extends Model { + @IsUUID(4) + @PrimaryKey + @Default(DataType.UUIDV4) + @Column(DataType.UUID) + id: string; + + @Column + name: string; + + @Default(true) + @Column + enabled: boolean; + + @Column + providerId: string; + + @CreatedAt + createdAt: Date; + + // associations + + @BelongsTo(() => Team, "teamId") + team: Team; + + @ForeignKey(() => Team) + @Column(DataType.UUID) + teamId: string; + + @HasMany(() => UserAuthentication, "providerId") + userAuthentications: UserAuthentication[]; + + // instance methods + + disable = async () => { + const res = await (this + .constructor as typeof AuthenticationProvider).findAndCountAll({ + where: { + teamId: this.teamId, + enabled: true, + id: { + [Op.ne]: this.id, + }, }, - }, - enabled: { - type: DataTypes.BOOLEAN, - defaultValue: true, - }, - providerId: { - type: DataTypes.STRING, - }, - }, - { - timestamps: true, - updatedAt: false, - } -); - -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -AuthenticationProvider.associate = (models) => { - AuthenticationProvider.belongsTo(models.Team); - AuthenticationProvider.hasMany(models.UserAuthentication); -}; - -AuthenticationProvider.prototype.disable = async function () { - const res = await AuthenticationProvider.findAndCountAll({ - where: { - teamId: this.teamId, - enabled: true, - id: { - [Op.ne]: this.id, - }, - }, - limit: 1, - }); - - if (res.count >= 1) { - return this.update({ - enabled: false, + limit: 1, }); - } else { - throw ValidationError("At least one authentication provider is required"); - } -}; -AuthenticationProvider.prototype.enable = async function () { - return this.update({ - enabled: true, - }); -}; + if (res.count >= 1) { + return this.update({ + enabled: false, + }); + } else { + throw ValidationError("At least one authentication provider is required"); + } + }; + + enable = () => { + return this.update({ + enabled: true, + }); + }; +} export default AuthenticationProvider; diff --git a/server/models/Backlink.ts b/server/models/Backlink.ts index b6012343a..3e2a06b45 100644 --- a/server/models/Backlink.ts +++ b/server/models/Backlink.ts @@ -1,27 +1,38 @@ -import { DataTypes, sequelize } from "../sequelize"; +import { + DataType, + BelongsTo, + ForeignKey, + Column, + Table, +} from "sequelize-typescript"; +import Document from "./Document"; +import User from "./User"; +import BaseModel from "./base/BaseModel"; +import Fix from "./decorators/Fix"; -const Backlink = sequelize.define("backlink", { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, -}); +@Table({ tableName: "backlinks", modelName: "backlink" }) +@Fix +class Backlink extends BaseModel { + @BelongsTo(() => User, "userId") + user: User; -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -Backlink.associate = (models) => { - Backlink.belongsTo(models.Document, { - as: "document", - foreignKey: "documentId", - }); - Backlink.belongsTo(models.Document, { - as: "reverseDocument", - foreignKey: "reverseDocumentId", - }); - Backlink.belongsTo(models.User, { - as: "user", - foreignKey: "userId", - }); -}; + @ForeignKey(() => User) + @Column(DataType.UUID) + userId: string; + + @BelongsTo(() => Document, "documentId") + document: Document; + + @ForeignKey(() => Document) + @Column(DataType.UUID) + documentId: string; + + @BelongsTo(() => Document, "reverseDocumentId") + reverseDocument: Document; + + @ForeignKey(() => Document) + @Column(DataType.UUID) + reverseDocumentId: string; +} export default Backlink; diff --git a/server/models/Collection.test.ts b/server/models/Collection.test.ts index 5d9671258..3fccd7ea6 100644 --- a/server/models/Collection.test.ts +++ b/server/models/Collection.test.ts @@ -1,6 +1,5 @@ import randomstring from "randomstring"; import { v4 as uuidv4 } from "uuid"; -import { Collection, Document } from "@server/models"; import { buildUser, buildGroup, @@ -10,9 +9,12 @@ import { } from "@server/test/factories"; import { flushdb, seed } from "@server/test/support"; import slugify from "@server/utils/slugify"; +import Collection from "./Collection"; +import Document from "./Document"; beforeEach(() => flushdb()); beforeEach(jest.resetAllMocks); + describe("#url", () => { test("should return correct url for the collection", () => { const collection = new Collection({ @@ -21,6 +23,7 @@ describe("#url", () => { expect(collection.url).toBe(`/collection/untitled-${collection.urlId}`); }); }); + describe("getDocumentParents", () => { test("should return array of parent document ids", async () => { const parent = await buildDocument(); @@ -31,7 +34,7 @@ describe("getDocumentParents", () => { ], }); const result = collection.getDocumentParents(document.id); - expect(result.length).toBe(1); + expect(result?.length).toBe(1); expect(result[0]).toBe(parent.id); }); @@ -44,7 +47,7 @@ describe("getDocumentParents", () => { ], }); const result = collection.getDocumentParents(parent.id); - expect(result.length).toBe(0); + expect(result?.length).toBe(0); }); test("should not error if documentStructure is empty", async () => { @@ -55,6 +58,7 @@ describe("getDocumentParents", () => { expect(result).toBe(undefined); }); }); + describe("getDocumentTree", () => { test("should return document tree", async () => { const document = await buildDocument(); @@ -79,6 +83,7 @@ describe("getDocumentTree", () => { expect(collection.getDocumentTree(document.id)).toEqual(document.toJSON()); }); }); + describe("isChildDocument", () => { test("should return false with unexpected data", async () => { const document = await buildDocument(); @@ -128,6 +133,7 @@ describe("isChildDocument", () => { expect(collection.isChildDocument(document.id, parent.id)).toEqual(false); }); }); + describe("#addDocumentToStructure", () => { test("should add as last element without index", async () => { const { collection } = await seed(); @@ -138,8 +144,8 @@ describe("#addDocumentToStructure", () => { parentDocumentId: null, }); await collection.addDocumentToStructure(newDocument); - expect(collection.documentStructure.length).toBe(2); - expect(collection.documentStructure[1].id).toBe(id); + expect(collection.documentStructure!.length).toBe(2); + expect(collection.documentStructure![1].id).toBe(id); }); test("should add with an index", async () => { @@ -151,8 +157,8 @@ describe("#addDocumentToStructure", () => { parentDocumentId: null, }); await collection.addDocumentToStructure(newDocument, 1); - expect(collection.documentStructure.length).toBe(2); - expect(collection.documentStructure[1].id).toBe(id); + expect(collection.documentStructure!.length).toBe(2); + expect(collection.documentStructure![1].id).toBe(id); }); test("should add as a child if with parent", async () => { @@ -164,10 +170,10 @@ describe("#addDocumentToStructure", () => { parentDocumentId: document.id, }); await collection.addDocumentToStructure(newDocument, 1); - expect(collection.documentStructure.length).toBe(1); - expect(collection.documentStructure[0].id).toBe(document.id); - expect(collection.documentStructure[0].children.length).toBe(1); - expect(collection.documentStructure[0].children[0].id).toBe(id); + expect(collection.documentStructure!.length).toBe(1); + expect(collection.documentStructure![0].id).toBe(document.id); + expect(collection.documentStructure![0].children.length).toBe(1); + expect(collection.documentStructure![0].children[0].id).toBe(id); }); test("should add as a child if with parent with index", async () => { @@ -185,10 +191,10 @@ describe("#addDocumentToStructure", () => { }); await collection.addDocumentToStructure(newDocument); await collection.addDocumentToStructure(secondDocument, 0); - expect(collection.documentStructure.length).toBe(1); - expect(collection.documentStructure[0].id).toBe(document.id); - expect(collection.documentStructure[0].children.length).toBe(2); - expect(collection.documentStructure[0].children[0].id).toBe(id); + expect(collection.documentStructure!.length).toBe(1); + expect(collection.documentStructure![0].id).toBe(document.id); + expect(collection.documentStructure![0].children.length).toBe(2); + expect(collection.documentStructure![0].children[0].id).toBe(id); }); describe("options: documentJson", () => { test("should append supplied json over document's own", async () => { @@ -201,27 +207,32 @@ describe("#addDocumentToStructure", () => { }); await collection.addDocumentToStructure(newDocument, undefined, { documentJson: { + id, + title: "Parent", + url: "parent", children: [ { id, title: "Totally fake", children: [], + url: "totally-fake", }, ], }, }); - expect(collection.documentStructure[1].children.length).toBe(1); - expect(collection.documentStructure[1].children[0].id).toBe(id); + expect(collection.documentStructure![1].children.length).toBe(1); + expect(collection.documentStructure![1].children[0].id).toBe(id); }); }); }); + describe("#updateDocument", () => { test("should update root document's data", async () => { const { collection, document } = await seed(); document.title = "Updated title"; await document.save(); await collection.updateDocument(document); - expect(collection.documentStructure[0].title).toBe("Updated title"); + expect(collection.documentStructure![0].title).toBe("Updated title"); }); test("should update child document's data", async () => { @@ -241,11 +252,12 @@ describe("#updateDocument", () => { await newDocument.save(); await collection.updateDocument(newDocument); const reloaded = await Collection.findByPk(collection.id); - expect(reloaded.documentStructure[0].children[0].title).toBe( + expect(reloaded!.documentStructure![0].children[0].title).toBe( "Updated title" ); }); }); + describe("#removeDocument", () => { test("should save if removing", async () => { const { collection, document } = await seed(); @@ -257,7 +269,7 @@ describe("#removeDocument", () => { test("should remove documents from root", async () => { const { collection, document } = await seed(); await collection.deleteDocument(document); - expect(collection.documentStructure.length).toBe(0); + expect(collection.documentStructure!.length).toBe(0); // Verify that the document was removed const collectionDocuments = await Document.findAndCountAll({ where: { @@ -281,10 +293,10 @@ describe("#removeDocument", () => { text: "content", }); await collection.addDocumentToStructure(newDocument); - expect(collection.documentStructure[0].children.length).toBe(1); + expect(collection.documentStructure![0].children.length).toBe(1); // Remove the document await collection.deleteDocument(document); - expect(collection.documentStructure.length).toBe(0); + expect(collection.documentStructure!.length).toBe(0); const collectionDocuments = await Document.findAndCountAll({ where: { collectionId: collection.id, @@ -308,13 +320,13 @@ describe("#removeDocument", () => { text: "content", }); await collection.addDocumentToStructure(newDocument); - expect(collection.documentStructure.length).toBe(1); - expect(collection.documentStructure[0].children.length).toBe(1); + expect(collection.documentStructure!.length).toBe(1); + expect(collection.documentStructure![0].children.length).toBe(1); // Remove the document await collection.deleteDocument(newDocument); const reloaded = await Collection.findByPk(collection.id); - expect(reloaded.documentStructure.length).toBe(1); - expect(reloaded.documentStructure[0].children.length).toBe(0); + expect(reloaded!.documentStructure!.length).toBe(1); + expect(reloaded!.documentStructure![0].children.length).toBe(0); const collectionDocuments = await Document.findAndCountAll({ where: { collectionId: collection.id, @@ -323,6 +335,7 @@ describe("#removeDocument", () => { expect(collectionDocuments.count).toBe(1); }); }); + describe("#membershipUserIds", () => { test("should return collection and group memberships", async () => { const team = await buildTeam(); @@ -350,42 +363,42 @@ describe("#membershipUserIds", () => { teamId, }); const createdById = users[0].id; - await group1.addUser(users[0], { + await group1.$add("user", users[0], { through: { createdById, }, }); - await group1.addUser(users[1], { + await group1.$add("user", users[1], { through: { createdById, }, }); - await group2.addUser(users[2], { + await group2.$add("user", users[2], { through: { createdById, }, }); - await group2.addUser(users[3], { + await group2.$add("user", users[3], { through: { createdById, }, }); - await collection.addUser(users[4], { + await collection.$add("user", users[4], { through: { createdById, }, }); - await collection.addUser(users[5], { + await collection.$add("user", users[5], { through: { createdById, }, }); - await collection.addGroup(group1, { + await collection.$add("group", group1, { through: { createdById, }, }); - await collection.addGroup(group2, { + await collection.$add("group", group2, { through: { createdById, }, @@ -394,18 +407,19 @@ describe("#membershipUserIds", () => { expect(membershipUserIds.length).toBe(6); }); }); + describe("#findByPk", () => { test("should return collection with collection Id", async () => { const collection = await buildCollection(); const response = await Collection.findByPk(collection.id); - expect(response.id).toBe(collection.id); + expect(response!.id).toBe(collection.id); }); test("should return collection when urlId is present", async () => { const collection = await buildCollection(); const id = `${slugify(collection.name)}-${collection.urlId}`; const response = await Collection.findByPk(id); - expect(response.id).toBe(collection.id); + expect(response!.id).toBe(collection.id); }); test("should return undefined when incorrect uuid type", async () => { diff --git a/server/models/Collection.ts b/server/models/Collection.ts index d84dff656..6b6038793 100644 --- a/server/models/Collection.ts +++ b/server/models/Collection.ts @@ -1,141 +1,84 @@ -import { find, findIndex, concat, remove, uniq } from "lodash"; +import { find, findIndex, remove, uniq } from "lodash"; import randomstring from "randomstring"; +import { + Identifier, + Transaction, + Op, + FindOptions, + SaveOptions, +} from "sequelize"; +import { + Sequelize, + Table, + Column, + Unique, + IsIn, + Default, + BeforeValidate, + BeforeSave, + AfterDestroy, + AfterCreate, + HasMany, + BelongsToMany, + BelongsTo, + ForeignKey, + Scopes, + DataType, +} from "sequelize-typescript"; import isUUID from "validator/lib/isUUID"; import { SLUG_URL_REGEX } from "@shared/utils/routeHelpers"; import slugify from "@server/utils/slugify"; -import { Op, DataTypes, sequelize } from "../sequelize"; +import { NavigationNode } from "~/types"; +import CollectionGroup from "./CollectionGroup"; import CollectionUser from "./CollectionUser"; import Document from "./Document"; +import Group from "./Group"; +import GroupUser from "./GroupUser"; +import Team from "./Team"; import User from "./User"; +import ParanoidModel from "./base/ParanoidModel"; +import Fix from "./decorators/Fix"; -const Collection = sequelize.define( - "collection", - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - urlId: { - type: DataTypes.STRING, - unique: true, - }, - name: DataTypes.STRING, - description: DataTypes.STRING, - icon: DataTypes.STRING, - color: DataTypes.STRING, - index: { - type: DataTypes.STRING, - defaultValue: null, - }, - permission: { - type: DataTypes.STRING, - defaultValue: null, - allowNull: true, - validate: { - isIn: [["read", "read_write"]], - }, - }, - maintainerApprovalRequired: DataTypes.BOOLEAN, - documentStructure: DataTypes.JSONB, - sharing: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: true, - }, - sort: { - type: DataTypes.JSONB, - validate: { - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'value' implicitly has an 'any' type. - isSort(value) { - if ( - typeof value !== "object" || - !value.direction || - !value.field || - Object.keys(value).length !== 2 - ) { - throw new Error("Sort must be an object with field,direction"); - } - - if (!["asc", "desc"].includes(value.direction)) { - throw new Error("Sort direction must be one of asc,desc"); - } - - if (!["title", "index"].includes(value.field)) { - throw new Error("Sort field must be one of title,index"); - } - }, - }, - }, - }, - { - tableName: "collections", - paranoid: true, - hooks: { - // @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message - beforeValidate: (collection: Collection) => { - collection.urlId = collection.urlId || randomstring.generate(10); - }, - }, - getterMethods: { - url() { - if (!this.name) return `/collection/untitled-${this.urlId}`; - return `/collection/${slugify(this.name)}-${this.urlId}`; - }, - }, - } -); -Collection.DEFAULT_SORT = { - field: "index", - direction: "asc", -}; -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'model' implicitly has an 'any' type. -Collection.addHook("beforeSave", async (model) => { - if (model.icon === "collection") { - model.icon = null; - } -}); - -// Class methods -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -Collection.associate = (models) => { - Collection.hasMany(models.Document, { - as: "documents", - foreignKey: "collectionId", - onDelete: "cascade", - }); - Collection.hasMany(models.CollectionUser, { - as: "memberships", - foreignKey: "collectionId", - onDelete: "cascade", - }); - Collection.hasMany(models.CollectionGroup, { - as: "collectionGroupMemberships", - foreignKey: "collectionId", - onDelete: "cascade", - }); - Collection.belongsToMany(models.User, { - as: "users", - through: models.CollectionUser, - foreignKey: "collectionId", - }); - Collection.belongsToMany(models.Group, { - as: "groups", - through: models.CollectionGroup, - foreignKey: "collectionId", - }); - Collection.belongsTo(models.User, { - as: "user", - foreignKey: "createdById", - }); - Collection.belongsTo(models.Team, { - as: "team", - }); - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'userId' implicitly has an 'any' type. - Collection.addScope("withMembership", (userId) => ({ +@Scopes(() => ({ + withAllMemberships: { include: [ { - model: models.CollectionUser, + model: CollectionUser, + as: "memberships", + required: false, + }, + { + model: CollectionGroup, + as: "collectionGroupMemberships", + required: false, + // use of "separate" property: sequelize breaks when there are + // nested "includes" with alternating values for "required" + // see https://github.com/sequelize/sequelize/issues/9869 + separate: true, + // include for groups that are members of this collection, + // of which userId is a member of, resulting in: + // CollectionGroup [inner join] Group [inner join] GroupUser [where] userId + include: [ + { + model: Group, + as: "group", + required: true, + include: [ + { + model: GroupUser, + as: "groupMemberships", + required: true, + }, + ], + }, + ], + }, + ], + }, + withMembership: (userId: string) => ({ + include: [ + { + model: CollectionUser, as: "memberships", where: { userId, @@ -143,7 +86,7 @@ Collection.associate = (models) => { required: false, }, { - model: models.CollectionGroup, + model: CollectionGroup, as: "collectionGroupMemberships", required: false, // use of "separate" property: sequelize breaks when there are @@ -153,444 +96,563 @@ Collection.associate = (models) => { // include for groups that are members of this collection, // of which userId is a member of, resulting in: // CollectionGroup [inner join] Group [inner join] GroupUser [where] userId - include: { - model: models.Group, - as: "group", - required: true, - include: { - model: models.GroupUser, - as: "groupMemberships", + include: [ + { + model: Group, + as: "group", required: true, - where: { - userId, - }, + include: [ + { + model: GroupUser, + as: "groupMemberships", + required: true, + where: { + userId, + }, + }, + ], }, - }, + ], }, ], - })); - Collection.addScope("withAllMemberships", { - include: [ - { - model: models.CollectionUser, - as: "memberships", - required: false, - }, - { - model: models.CollectionGroup, - as: "collectionGroupMemberships", - required: false, - // use of "separate" property: sequelize breaks when there are - // nested "includes" with alternating values for "required" - // see https://github.com/sequelize/sequelize/issues/9869 - separate: true, - // include for groups that are members of this collection, - // of which userId is a member of, resulting in: - // CollectionGroup [inner join] Group [inner join] GroupUser [where] userId - include: { - model: models.Group, - as: "group", - required: true, - include: { - model: models.GroupUser, - as: "groupMemberships", - required: true, - }, - }, - }, - ], - }); -}; + }), +})) +@Table({ tableName: "collections", modelName: "collection" }) +@Fix +class Collection extends ParanoidModel { + @Unique + @Column + urlId: string; -// @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message -Collection.addHook("afterDestroy", async (model: Collection) => { - await Document.destroy({ - where: { - collectionId: model.id, - archivedAt: { - [Op.eq]: null, + @Column + name: string; + + @Column + description: string; + + @Column + icon: string | null; + + @Column + color: string | null; + + @Column + index: string | null; + + @IsIn([["read", "read_write"]]) + @Column + permission: "read" | "read_write" | null; + + @Default(false) + @Column + maintainerApprovalRequired: boolean; + + @Column(DataType.JSONB) + documentStructure: NavigationNode[] | null; + + @Default(true) + @Column + sharing: boolean; + + @Column({ + type: DataType.JSONB, + validate: { + isSort(value: any) { + if ( + typeof value !== "object" || + !value.direction || + !value.field || + Object.keys(value).length !== 2 + ) { + throw new Error("Sort must be an object with field,direction"); + } + + if (!["asc", "desc"].includes(value.direction)) { + throw new Error("Sort direction must be one of asc,desc"); + } + + if (!["title", "index"].includes(value.field)) { + throw new Error("Sort field must be one of title,index"); + } }, }, - }); -}); -// @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message -Collection.addHook("afterCreate", (model: Collection, options) => { - if (model.permission !== "read_write") { - return CollectionUser.findOrCreate({ + }) + sort: { + field: string; + direction: "asc" | "desc"; + }; + + // getters + + get url(): string { + if (!this.name) return `/collection/untitled-${this.urlId}`; + return `/collection/${slugify(this.name)}-${this.urlId}`; + } + + // hooks + + @BeforeValidate + static async onBeforeValidate(model: Collection) { + model.urlId = model.urlId || randomstring.generate(10); + } + + @BeforeSave + static async onBeforeSave(model: Collection) { + if (model.icon === "collection") { + model.icon = null; + } + } + + @AfterDestroy + static async onAfterDestroy(model: Collection) { + await Document.destroy({ where: { collectionId: model.id, - userId: model.createdById, + archivedAt: { + [Op.is]: null, + }, }, - defaults: { - permission: "read_write", - createdById: model.createdById, - }, - transaction: options.transaction, }); } -}); -// Class methods -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'id' implicitly has an 'any' type. -Collection.findByPk = async function (id, options = {}) { - if (isUUID(id)) { + @AfterCreate + static async onAfterCreate( + model: Collection, + options: { transaction: Transaction } + ) { + if (model.permission !== "read_write") { + return CollectionUser.findOrCreate({ + where: { + collectionId: model.id, + userId: model.createdById, + }, + defaults: { + permission: "read_write", + createdById: model.createdById, + }, + transaction: options.transaction, + }); + } + + return undefined; + } + + // associations + + @HasMany(() => Document, "collectionId") + documents: Document[]; + + @HasMany(() => CollectionUser, "collectionId") + memberships: CollectionUser[]; + + @HasMany(() => CollectionGroup, "collectionId") + collectionGroupMemberships: CollectionGroup[]; + + @BelongsToMany(() => User, () => CollectionUser) + users: User[]; + + @BelongsToMany(() => Group, () => CollectionGroup) + groups: Group[]; + + @BelongsTo(() => User, "createdById") + user: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + createdById: string; + + @BelongsTo(() => Team, "teamId") + team: Team; + + @ForeignKey(() => Team) + @Column(DataType.UUID) + teamId: string; + + static DEFAULT_SORT = { + field: "index", + direction: "asc", + }; + + /** + * Returns an array of unique userIds that are members of a collection, + * either via group or direct membership + * + * @param collectionId + * @returns userIds + */ + static async membershipUserIds(collectionId: string) { + const collection = await this.scope("withAllMemberships").findByPk( + collectionId + ); + + if (!collection) { + return []; + } + + const groupMemberships = collection.collectionGroupMemberships + .map((cgm) => cgm.group.groupMemberships) + .flat(); + const membershipUserIds = [ + ...groupMemberships, + ...collection.memberships, + ].map((membership) => membership.userId); + return uniq(membershipUserIds); + } + + /** + * Overrides the standard findByPk behavior to allow also querying by urlId + * + * @param id uuid or urlId + * @returns collection instance + */ + static async findByPk(id: Identifier, options: FindOptions = {}) { + if (typeof id !== "string") { + return undefined; + } + + if (isUUID(id)) { + return this.findOne({ + where: { + id, + }, + ...options, + }); + } + + const match = id.match(SLUG_URL_REGEX); + if (match) { + return this.findOne({ + where: { + urlId: match[1], + }, + ...options, + }); + } + + return undefined; + } + + /** + * Find the first collection that the specified user has access to. + * + * @param user User object + * @returns collection First collection in the sidebar order + */ + static async findFirstCollectionForUser(user: User) { + const id = await user.collectionIds(); return this.findOne({ where: { id, }, - ...options, - }); - } else if (id.match(SLUG_URL_REGEX)) { - return this.findOne({ - where: { - urlId: id.match(SLUG_URL_REGEX)[1], - }, - ...options, + order: [ + // using LC_COLLATE:"C" because we need byte order to drive the sorting + Sequelize.literal('"collection"."index" collate "C"'), + ["updatedAt", "DESC"], + ], }); } -}; -/** - * Find the first collection that the specified user has access to. - * - * @param user User object - * @returns collection First collection in the sidebar order - */ -// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message -Collection.findFirstCollectionForUser = async (user: User) => { - const id = await user.collectionIds(); - return Collection.findOne({ - where: { - id, - }, - order: [ - // using LC_COLLATE:"C" because we need byte order to drive the sorting - sequelize.literal('"collection"."index" collate "C"'), - ["updatedAt", "DESC"], - ], - }); -}; + getDocumentTree = function (documentId: string): NavigationNode { + let result: NavigationNode; -// get all the membership relationshps a user could have with the collection -Collection.membershipUserIds = async (collectionId: string) => { - const collection = await Collection.scope("withAllMemberships").findByPk( - collectionId - ); + const loopChildren = (documents: NavigationNode[]) => { + if (result) { + return; + } - if (!collection) { - return []; - } + documents.forEach((document) => { + if (result) { + return; + } - const groupMemberships = collection.collectionGroupMemberships - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'cgm' implicitly has an 'any' type. - .map((cgm) => cgm.group.groupMemberships) - .flat(); - const membershipUserIds = concat( - groupMemberships, - collection.memberships - ).map((membership) => membership.userId); - return uniq(membershipUserIds); -}; + if (document.id === documentId) { + result = document; + } else { + loopChildren(document.children); + } + }); + }; -// Instance methods -Collection.prototype.addDocumentToStructure = async function ( - document: Document, - index: number, - options = {} -) { - if (!this.documentStructure) { - this.documentStructure = []; - } + loopChildren(this.documentStructure); - let transaction; + // @ts-expect-error used before undefined + return result; + }; - try { - // documentStructure can only be updated by one request at a time - // @ts-expect-error ts-migrate(2339) FIXME: Property 'save' does not exist on type '{}'. - if (options.save !== false) { - transaction = await sequelize.transaction(); + deleteDocument = async function (document: Document) { + await this.removeDocumentInStructure(document); + + // Helper to destroy all child documents for a document + const loopChildren = async ( + documentId: string, + opts?: FindOptions + ) => { + const childDocuments = await Document.findAll({ + where: { + parentDocumentId: documentId, + }, + }); + childDocuments.forEach(async (child) => { + await loopChildren(child.id, opts); + await child.destroy(opts); + }); + }; + + await loopChildren(document.id); + await document.destroy(); + }; + + removeDocumentInStructure = async function ( + document: Document, + options?: SaveOptions & { + save?: boolean; + } + ) { + if (!this.documentStructure) { + return; } - // If moving existing document with children, use existing structure - // @ts-expect-error ts-migrate(2339) FIXME: Property 'toJSON' does not exist on type 'Document... Remove this comment to see the full error message - const documentJson = { ...document.toJSON(), ...options.documentJson }; + let result: [NavigationNode, number] | undefined; + let transaction; - // @ts-expect-error ts-migrate(2339) FIXME: Property 'parentDocumentId' does not exist on type... Remove this comment to see the full error message - if (!document.parentDocumentId) { - // Note: Index is supported on DB level but it's being ignored - // by the API presentation until we build product support for it. - this.documentStructure.splice( - index !== undefined ? index : this.documentStructure.length, - 0, - documentJson - ); - } else { - // Recursively place document - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documentList' implicitly has an 'any' t... Remove this comment to see the full error message - const placeDocument = (documentList) => { - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'childDocument' implicitly has an 'any' ... Remove this comment to see the full error message - return documentList.map((childDocument) => { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'parentDocumentId' does not exist on type... Remove this comment to see the full error message - if (document.parentDocumentId === childDocument.id) { - childDocument.children.splice( - index !== undefined ? index : childDocument.children.length, - 0, - documentJson - ); - } else { - childDocument.children = placeDocument(childDocument.children); + try { + // documentStructure can only be updated by one request at the time + transaction = await this.sequelize.transaction(); + + const removeFromChildren = async ( + children: NavigationNode[], + id: string + ) => { + children = await Promise.all( + children.map(async (childDocument) => { + return { + ...childDocument, + children: await removeFromChildren(childDocument.children, id), + }; + }) + ); + const match = find(children, { + id, + }); + + if (match) { + if (!result) { + result = [ + match, + findIndex(children, { + id, + }), + ]; } - return childDocument; - }); + remove(children, { + id, + }); + } + + return children; }; - this.documentStructure = placeDocument(this.documentStructure); - } + this.documentStructure = await removeFromChildren( + this.documentStructure, + document.id + ); - // Sequelize doesn't seem to set the value with splice on JSONB field - // https://github.com/sequelize/sequelize/blob/e1446837196c07b8ff0c23359b958d68af40fd6d/src/model.js#L3937 - this.changed("documentStructure", true); - - // @ts-expect-error ts-migrate(2339) FIXME: Property 'save' does not exist on type '{}'. - if (options.save !== false) { + // Sequelize doesn't seem to set the value with splice on JSONB field + // https://github.com/sequelize/sequelize/blob/e1446837196c07b8ff0c23359b958d68af40fd6d/src/model.js#L3937 + this.changed("documentStructure", true); await this.save({ ...options, fields: ["documentStructure"], transaction, }); - + await transaction.commit(); + } catch (err) { if (transaction) { - await transaction.commit(); + await transaction.rollback(); } - } - } catch (err) { - if (transaction) { - await transaction.rollback(); + + throw err; } - throw err; - } - - return this; -}; - -/** - * Update document's title and url in the documentStructure - */ -Collection.prototype.updateDocument = async function ( - updatedDocument: Document -) { - if (!this.documentStructure) return; - let transaction; - - try { - // documentStructure can only be updated by one request at the time - transaction = await sequelize.transaction(); - // @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'. - const { id } = updatedDocument; - - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documents' implicitly has an 'any' type... Remove this comment to see the full error message - const updateChildren = (documents) => { - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type. - return documents.map((document) => { - if (document.id === id) { - document = { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'toJSON' does not exist on type 'Document... Remove this comment to see the full error message - ...updatedDocument.toJSON(), - children: document.children, - }; - } else { - document.children = updateChildren(document.children); - } - - return document; - }); - }; - - this.documentStructure = updateChildren(this.documentStructure); - // Sequelize doesn't seem to set the value with splice on JSONB field - // https://github.com/sequelize/sequelize/blob/e1446837196c07b8ff0c23359b958d68af40fd6d/src/model.js#L3937 - this.changed("documentStructure", true); - await this.save({ - fields: ["documentStructure"], - transaction, - }); - await transaction.commit(); - } catch (err) { - if (transaction) { - await transaction.rollback(); - } - - throw err; - } - - return this; -}; - -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type. -Collection.prototype.deleteDocument = async function (document) { - await this.removeDocumentInStructure(document); - await document.deleteWithChildren(); -}; - -Collection.prototype.isChildDocument = function ( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'parentDocumentId' implicitly has an 'an... Remove this comment to see the full error message - parentDocumentId, - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documentId' implicitly has an 'any' typ... Remove this comment to see the full error message - documentId -): boolean { - let result = false; - - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documents' implicitly has an 'any' type... Remove this comment to see the full error message - const loopChildren = (documents, input) => { - if (result) { - return; - } - - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type. - documents.forEach((document) => { - const parents = [...input]; - - if (document.id === documentId) { - result = parents.includes(parentDocumentId); - } else { - parents.push(document.id); - loopChildren(document.children, parents); - } - }); + return result; }; - loopChildren(this.documentStructure, []); - return result; -}; + getDocumentParents = function (documentId: string): string[] | void { + let result: string[]; -Collection.prototype.getDocumentTree = function (documentId: string) { - // @ts-expect-error ts-migrate(7034) FIXME: Variable 'result' implicitly has type 'any' in som... Remove this comment to see the full error message - let result; - - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documents' implicitly has an 'any' type... Remove this comment to see the full error message - const loopChildren = (documents) => { - // @ts-expect-error ts-migrate(7005) FIXME: Variable 'result' implicitly has an 'any' type. - if (result) { - return; - } - - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type. - documents.forEach((document) => { - // @ts-expect-error ts-migrate(7005) FIXME: Variable 'result' implicitly has an 'any' type. + const loopChildren = (documents: NavigationNode[], path: string[] = []) => { if (result) { return; } - if (document.id === documentId) { - result = document; - } else { - loopChildren(document.children); - } - }); - }; - - loopChildren(this.documentStructure); - return result; -}; - -Collection.prototype.getDocumentParents = function ( - documentId: string -): string[] | void { - // @ts-expect-error ts-migrate(7034) FIXME: Variable 'result' implicitly has type 'any' in som... Remove this comment to see the full error message - let result; - - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documents' implicitly has an 'any' type... Remove this comment to see the full error message - const loopChildren = (documents, path = []) => { - // @ts-expect-error ts-migrate(7005) FIXME: Variable 'result' implicitly has an 'any' type. - if (result) { - return; - } - - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type. - documents.forEach((document) => { - if (document.id === documentId) { - result = path; - } else { - // @ts-expect-error ts-migrate(2322) FIXME: Type 'any' is not assignable to type 'never'. - loopChildren(document.children, [...path, document.id]); - } - }); - }; - - if (this.documentStructure) { - loopChildren(this.documentStructure); - } - - return result; -}; - -Collection.prototype.removeDocumentInStructure = async function ( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type. - document, - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'options' implicitly has an 'any' type. - options -) { - if (!this.documentStructure) return; - // @ts-expect-error ts-migrate(7034) FIXME: Variable 'returnValue' implicitly has type 'any' i... Remove this comment to see the full error message - let returnValue; - let transaction; - - try { - // documentStructure can only be updated by one request at the time - transaction = await sequelize.transaction(); - - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'children' implicitly has an 'any' type. - const removeFromChildren = async (children, id) => { - children = await Promise.all( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'childDocument' implicitly has an 'any' ... Remove this comment to see the full error message - children.map(async (childDocument) => { - return { - ...childDocument, - children: await removeFromChildren(childDocument.children, id), - }; - }) - ); - const match = find(children, { - id, + documents.forEach((document) => { + if (document.id === documentId) { + result = path; + } else { + loopChildren(document.children, [...path, document.id]); + } }); - - if (match) { - // @ts-expect-error ts-migrate(7005) FIXME: Variable 'returnValue' implicitly has an 'any' typ... Remove this comment to see the full error message - if (!returnValue) - returnValue = [ - match, - findIndex(children, { - id, - }), - ]; - remove(children, { - id, - }); - } - - return children; }; - this.documentStructure = await removeFromChildren( - this.documentStructure, - document.id - ); - // Sequelize doesn't seem to set the value with splice on JSONB field - // https://github.com/sequelize/sequelize/blob/e1446837196c07b8ff0c23359b958d68af40fd6d/src/model.js#L3937 - this.changed("documentStructure", true); - await this.save({ ...options, fields: ["documentStructure"], transaction }); - await transaction.commit(); - } catch (err) { - if (transaction) { - await transaction.rollback(); + if (this.documentStructure) { + loopChildren(this.documentStructure); } - throw err; - } + // @ts-expect-error used before undefined + return result; + }; - return returnValue; -}; + isChildDocument = function ( + parentDocumentId?: string, + documentId?: string + ): boolean { + let result = false; + + const loopChildren = (documents: NavigationNode[], input: string[]) => { + if (result) { + return; + } + + documents.forEach((document) => { + const parents = [...input]; + + if (document.id === documentId && parentDocumentId) { + result = parents.includes(parentDocumentId); + } else { + parents.push(document.id); + loopChildren(document.children, parents); + } + }); + }; + + loopChildren(this.documentStructure, []); + return result; + }; + + /** + * Update document's title and url in the documentStructure + */ + updateDocument = async function (updatedDocument: Document) { + if (!this.documentStructure) return; + let transaction; + + try { + // documentStructure can only be updated by one request at the time + transaction = await this.sequelize.transaction(); + const { id } = updatedDocument; + + const updateChildren = (documents: NavigationNode[]) => { + return documents.map((document) => { + if (document.id === id) { + document = { + ...(updatedDocument.toJSON() as NavigationNode), + children: document.children, + }; + } else { + document.children = updateChildren(document.children); + } + + return document; + }); + }; + + this.documentStructure = updateChildren(this.documentStructure); + // Sequelize doesn't seem to set the value with splice on JSONB field + // https://github.com/sequelize/sequelize/blob/e1446837196c07b8ff0c23359b958d68af40fd6d/src/model.js#L3937 + this.changed("documentStructure", true); + await this.save({ + fields: ["documentStructure"], + transaction, + }); + await transaction.commit(); + } catch (err) { + if (transaction) { + await transaction.rollback(); + } + + throw err; + } + + return this; + }; + + addDocumentToStructure = async function ( + document: Document, + index?: number, + options: { + save?: boolean; + documentJson?: NavigationNode; + } = {} + ) { + if (!this.documentStructure) { + this.documentStructure = []; + } + let transaction; + + try { + // documentStructure can only be updated by one request at a time + if (options?.save !== false) { + transaction = await this.sequelize.transaction(); + } + + // If moving existing document with children, use existing structure + const documentJson = { ...document.toJSON(), ...options.documentJson }; + + if (!document.parentDocumentId) { + // Note: Index is supported on DB level but it's being ignored + // by the API presentation until we build product support for it. + this.documentStructure.splice( + index !== undefined ? index : this.documentStructure.length, + 0, + documentJson + ); + } else { + // Recursively place document + const placeDocument = (documentList: NavigationNode[]) => { + return documentList.map((childDocument) => { + if (document.parentDocumentId === childDocument.id) { + childDocument.children.splice( + index !== undefined ? index : childDocument.children.length, + 0, + documentJson + ); + } else { + childDocument.children = placeDocument(childDocument.children); + } + + return childDocument; + }); + }; + + this.documentStructure = placeDocument(this.documentStructure); + } + + // Sequelize doesn't seem to set the value with splice on JSONB field + // https://github.com/sequelize/sequelize/blob/e1446837196c07b8ff0c23359b958d68af40fd6d/src/model.js#L3937 + this.changed("documentStructure", true); + + if (options?.save !== false) { + await this.save({ + ...options, + fields: ["documentStructure"], + transaction, + }); + + if (transaction) { + await transaction.commit(); + } + } + } catch (err) { + if (transaction) { + await transaction.rollback(); + } + + throw err; + } + + return this; + }; +} export default Collection; diff --git a/server/models/CollectionGroup.ts b/server/models/CollectionGroup.ts index 522f8cfcb..d2c32d48a 100644 --- a/server/models/CollectionGroup.ts +++ b/server/models/CollectionGroup.ts @@ -1,38 +1,44 @@ -import { DataTypes, sequelize } from "../sequelize"; +import { + BelongsTo, + Column, + Default, + ForeignKey, + IsIn, + Model, + Table, + DataType, +} from "sequelize-typescript"; +import Collection from "./Collection"; +import Group from "./Group"; +import User from "./User"; +import Fix from "./decorators/Fix"; -const CollectionGroup = sequelize.define( - "collection_group", - { - permission: { - type: DataTypes.STRING, - defaultValue: "read_write", - validate: { - isIn: [["read", "read_write", "maintainer"]], - }, - }, - }, - { - timestamps: true, - paranoid: true, - } -); +@Table({ tableName: "collection_groups", modelName: "collection_group" }) +@Fix +class CollectionGroup extends Model { + @Default("read_write") + @IsIn([["read", "read_write", "maintainer"]]) + @Column + permission: string; -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -CollectionGroup.associate = (models) => { - CollectionGroup.belongsTo(models.Collection, { - as: "collection", - foreignKey: "collectionId", - primary: true, - }); - CollectionGroup.belongsTo(models.Group, { - as: "group", - foreignKey: "groupId", - primary: true, - }); - CollectionGroup.belongsTo(models.User, { - as: "createdBy", - foreignKey: "createdById", - }); -}; + // associations + + @BelongsTo(() => Collection, "collectionId") + collection: Collection; + + @ForeignKey(() => Collection) + @Column(DataType.UUID) + collectionId: string; + + @BelongsTo(() => Group, "groupId") + group: Group; + + @ForeignKey(() => Group) + @Column(DataType.UUID) + groupId: string; + + @BelongsTo(() => User, "createdById") + createdBy: User; +} export default CollectionGroup; diff --git a/server/models/CollectionUser.ts b/server/models/CollectionUser.ts index 0da92ddf5..711a5c514 100644 --- a/server/models/CollectionUser.ts +++ b/server/models/CollectionUser.ts @@ -1,35 +1,47 @@ -import { DataTypes, sequelize } from "../sequelize"; +import { + Column, + ForeignKey, + BelongsTo, + Default, + IsIn, + Table, + DataType, + Model, +} from "sequelize-typescript"; +import Collection from "./Collection"; +import User from "./User"; +import Fix from "./decorators/Fix"; -const CollectionUser = sequelize.define( - "collection_user", - { - permission: { - type: DataTypes.STRING, - defaultValue: "read_write", - validate: { - isIn: [["read", "read_write", "maintainer"]], - }, - }, - }, - { - timestamps: true, - } -); +@Table({ tableName: "collection_users", modelName: "collection_user" }) +@Fix +class CollectionUser extends Model { + @Default("read_write") + @IsIn([["read", "read_write", "maintainer"]]) + @Column + permission: string; -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -CollectionUser.associate = (models) => { - CollectionUser.belongsTo(models.Collection, { - as: "collection", - foreignKey: "collectionId", - }); - CollectionUser.belongsTo(models.User, { - as: "user", - foreignKey: "userId", - }); - CollectionUser.belongsTo(models.User, { - as: "createdBy", - foreignKey: "createdById", - }); -}; + // associations + + @BelongsTo(() => Collection, "collectionId") + collection: Collection; + + @ForeignKey(() => Collection) + @Column(DataType.UUID) + collectionId: string; + + @BelongsTo(() => User, "userId") + user: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + userId: string; + + @BelongsTo(() => User, "createdById") + createdBy: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + createdById: string; +} export default CollectionUser; diff --git a/server/models/Document.test.ts b/server/models/Document.test.ts index 9eeb94ad5..e0e5fc153 100644 --- a/server/models/Document.test.ts +++ b/server/models/Document.test.ts @@ -1,4 +1,4 @@ -import { Document } from "@server/models"; +import Document from "@server/models/Document"; import { buildDocument, buildCollection, @@ -10,6 +10,7 @@ import slugify from "@server/utils/slugify"; beforeEach(() => flushdb()); beforeEach(jest.resetAllMocks); + describe("#getSummary", () => { test("should strip markdown", async () => { const document = await buildDocument({ @@ -23,7 +24,7 @@ paragraph 2`, test("should strip title when no version", async () => { const document = await buildDocument({ - version: null, + version: 0, text: `# Heading *paragraph*`, @@ -31,6 +32,7 @@ paragraph 2`, expect(document.getSummary()).toBe("paragraph"); }); }); + describe("#migrateVersion", () => { test("should maintain empty paragraph under headings", async () => { const document = await buildDocument({ @@ -155,6 +157,7 @@ paragraph`); `); }); }); + describe("#searchForTeam", () => { test("should return search results from public collections", async () => { const team = await buildTeam(); @@ -168,7 +171,7 @@ describe("#searchForTeam", () => { }); const { results } = await Document.searchForTeam(team, "test"); expect(results.length).toBe(1); - expect(results[0].document.id).toBe(document.id); + expect(results[0].document?.id).toBe(document.id); }); test("should not return search results from private collections", async () => { @@ -252,6 +255,7 @@ describe("#searchForTeam", () => { expect(totalCount).toBe("0"); }); }); + describe("#searchForUser", () => { test("should return search results from collections", async () => { const team = await buildTeam(); @@ -270,7 +274,7 @@ describe("#searchForUser", () => { }); const { results } = await Document.searchForUser(user, "test"); expect(results.length).toBe(1); - expect(results[0].document.id).toBe(document.id); + expect(results[0].document?.id).toBe(document.id); }); test("should handle no collections", async () => { @@ -352,44 +356,47 @@ describe("#searchForUser", () => { expect(totalCount).toBe("0"); }); }); + describe("#delete", () => { test("should soft delete and set last modified", async () => { - let document = await buildDocument(); + const document = await buildDocument(); const user = await buildUser(); await document.delete(user.id); - document = await Document.findByPk(document.id, { + + const newDocument = await Document.findByPk(document.id, { paranoid: false, }); - expect(document.lastModifiedById).toBe(user.id); - expect(document.deletedAt).toBeTruthy(); + expect(newDocument?.lastModifiedById).toBe(user.id); + expect(newDocument?.deletedAt).toBeTruthy(); }); test("should soft delete templates", async () => { - let document = await buildDocument({ + const document = await buildDocument({ template: true, }); const user = await buildUser(); await document.delete(user.id); - document = await Document.findByPk(document.id, { + const newDocument = await Document.findByPk(document.id, { paranoid: false, }); - expect(document.lastModifiedById).toBe(user.id); - expect(document.deletedAt).toBeTruthy(); + expect(newDocument?.lastModifiedById).toBe(user.id); + expect(newDocument?.deletedAt).toBeTruthy(); }); test("should soft delete archived", async () => { - let document = await buildDocument({ + const document = await buildDocument({ archivedAt: new Date(), }); const user = await buildUser(); await document.delete(user.id); - document = await Document.findByPk(document.id, { + const newDocument = await Document.findByPk(document.id, { paranoid: false, }); - expect(document.lastModifiedById).toBe(user.id); - expect(document.deletedAt).toBeTruthy(); + expect(newDocument?.lastModifiedById).toBe(user.id); + expect(newDocument?.deletedAt).toBeTruthy(); }); }); + describe("#save", () => { test("should have empty previousTitles by default", async () => { const document = await buildDocument(); @@ -414,14 +421,16 @@ describe("#save", () => { expect(document.previousTitles.length).toBe(3); }); }); + describe("#findByPk", () => { test("should return document when urlId is correct", async () => { const { document } = await seed(); const id = `${slugify(document.title)}-${document.urlId}`; const response = await Document.findByPk(id); - expect(response.id).toBe(document.id); + expect(response?.id).toBe(document.id); }); }); + describe("tasks", () => { test("should consider all the possible checkTtems", async () => { const document = await buildDocument({ diff --git a/server/models/Document.ts b/server/models/Document.ts index a588b0883..683f278b1 100644 --- a/server/models/Document.ts +++ b/server/models/Document.ts @@ -1,9 +1,31 @@ -// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module '@tom... Remove this comment to see the full error message import removeMarkdown from "@tommoor/remove-markdown"; import { compact, find, map, uniq } from "lodash"; import randomstring from "randomstring"; -import Sequelize, { Transaction } from "sequelize"; -// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'slat... Remove this comment to see the full error message +import { + Transaction, + Op, + QueryTypes, + FindOptions, + ScopeOptions, +} from "sequelize"; +import { + ForeignKey, + BelongsTo, + Column, + Default, + Length, + PrimaryKey, + Table, + BeforeValidate, + BeforeCreate, + BeforeUpdate, + HasMany, + BeforeSave, + DefaultScope, + AfterCreate, + Scopes, + DataType, +} from "sequelize-typescript"; import MarkdownSerializer from "slate-md-serializer"; import isUUID from "validator/lib/isUUID"; import { MAX_TITLE_LENGTH } from "@shared/constants"; @@ -12,181 +34,66 @@ import getTasks from "@shared/utils/getTasks"; import parseTitle from "@shared/utils/parseTitle"; import { SLUG_URL_REGEX } from "@shared/utils/routeHelpers"; import unescape from "@shared/utils/unescape"; -import { Collection, User } from "@server/models"; import slugify from "@server/utils/slugify"; -import { DataTypes, sequelize } from "../sequelize"; +import Backlink from "./Backlink"; +import Collection from "./Collection"; import Revision from "./Revision"; +import Star from "./Star"; +import Team from "./Team"; +import User from "./User"; +import View from "./View"; +import ParanoidModel from "./base/ParanoidModel"; +import Fix from "./decorators/Fix"; + +type SearchResponse = { + results: { + ranking: number; + context: string; + document: Document; + }[]; + totalCount: number; +}; + +type SearchOptions = { + limit?: number; + offset?: number; + collectionId?: string; + dateFilter?: DateFilter; + collaboratorIds?: string[]; + includeArchived?: boolean; + includeDrafts?: boolean; +}; -const Op = Sequelize.Op; const serializer = new MarkdownSerializer(); export const DOCUMENT_VERSION = 2; -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'doc' implicitly has an 'any' type. -const createUrlId = (doc) => { - return (doc.urlId = doc.urlId || randomstring.generate(10)); -}; - -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'doc' implicitly has an 'any' type. -const beforeCreate = async (doc) => { - if (doc.version === undefined) { - doc.version = DOCUMENT_VERSION; - } - - return beforeSave(doc); -}; - -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'doc' implicitly has an 'any' type. -const beforeSave = async (doc) => { - const { emoji } = parseTitle(doc.text); - // emoji in the title is split out for easier display - doc.emoji = emoji; - // ensure documents have a title - doc.title = doc.title || ""; - - if (doc.previous("title") && doc.previous("title") !== doc.title) { - if (!doc.previousTitles) doc.previousTitles = []; - doc.previousTitles = uniq(doc.previousTitles.concat(doc.previous("title"))); - } - - // add the current user as a collaborator on this doc - if (!doc.collaboratorIds) doc.collaboratorIds = []; - doc.collaboratorIds = uniq(doc.collaboratorIds.concat(doc.lastModifiedById)); - // increment revision - doc.revisionCount += 1; - return doc; -}; - -const Document = sequelize.define( - "document", - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, +@DefaultScope(() => ({ + include: [ + { + model: User, + as: "createdBy", + paranoid: false, }, - urlId: { - type: DataTypes.STRING, - primaryKey: true, + { + model: User, + as: "updatedBy", + paranoid: false, }, - title: { - type: DataTypes.STRING, - validate: { - len: { - args: [0, MAX_TITLE_LENGTH], - msg: `Document title must be less than ${MAX_TITLE_LENGTH} characters`, - }, - }, + ], + where: { + publishedAt: { + [Op.ne]: null, }, - previousTitles: DataTypes.ARRAY(DataTypes.STRING), - version: DataTypes.SMALLINT, - template: DataTypes.BOOLEAN, - fullWidth: DataTypes.BOOLEAN, - editorVersion: DataTypes.STRING, - text: DataTypes.TEXT, - state: DataTypes.BLOB, - isWelcome: { - type: DataTypes.BOOLEAN, - defaultValue: false, - }, - revisionCount: { - type: DataTypes.INTEGER, - defaultValue: 0, - }, - archivedAt: DataTypes.DATE, - publishedAt: DataTypes.DATE, - parentDocumentId: DataTypes.UUID, - collaboratorIds: DataTypes.ARRAY(DataTypes.UUID), }, - { - paranoid: true, - hooks: { - beforeValidate: createUrlId, - beforeCreate: beforeCreate, - beforeUpdate: beforeSave, - }, - getterMethods: { - url: function () { - if (!this.title) return `/doc/untitled-${this.urlId}`; - const slugifiedTitle = slugify(this.title); - return `/doc/${slugifiedTitle}-${this.urlId}`; - }, - tasks: function () { - return getTasks(this.text || ""); - }, - }, - } -); - -// Class methods -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -Document.associate = (models) => { - Document.belongsTo(models.Collection, { - as: "collection", - foreignKey: "collectionId", - onDelete: "cascade", - }); - Document.belongsTo(models.Team, { - as: "team", - foreignKey: "teamId", - }); - Document.belongsTo(models.Document, { - as: "document", - foreignKey: "templateId", - }); - Document.belongsTo(models.User, { - as: "createdBy", - foreignKey: "createdById", - }); - Document.belongsTo(models.User, { - as: "updatedBy", - foreignKey: "lastModifiedById", - }); - /** Deprecated – use Pins relationship instead */ - Document.belongsTo(models.User, { - as: "pinnedBy", - foreignKey: "pinnedById", - }); - Document.hasMany(models.Revision, { - as: "revisions", - onDelete: "cascade", - }); - Document.hasMany(models.Backlink, { - as: "backlinks", - onDelete: "cascade", - }); - Document.hasMany(models.Star, { - as: "starred", - onDelete: "cascade", - }); - Document.hasMany(models.View, { - as: "views", - }); - Document.addScope("defaultScope", { - include: [ - { - model: models.User, - as: "createdBy", - paranoid: false, - }, - { - model: models.User, - as: "updatedBy", - paranoid: false, - }, - ], - where: { - publishedAt: { - [Op.ne]: null, - }, - }, - }); - Document.addScope("withCollection", (userId: string, paranoid = true) => { +})) +@Scopes(() => ({ + withCollection: (userId: string, paranoid = true) => { if (userId) { return { include: [ { - model: models.Collection.scope({ + model: Collection.scope({ method: ["withMembership", userId], }), as: "collection", @@ -199,32 +106,32 @@ Document.associate = (models) => { return { include: [ { - model: models.Collection, + model: Collection, as: "collection", }, ], }; - }); - Document.addScope("withUnpublished", { + }, + withUnpublished: { include: [ { - model: models.User, + model: User, as: "createdBy", paranoid: false, }, { - model: models.User, + model: User, as: "updatedBy", paranoid: false, }, ], - }); - Document.addScope("withViews", (userId: string) => { + }, + withViews: (userId: string) => { if (!userId) return {}; return { include: [ { - model: models.View, + model: View, as: "views", where: { userId, @@ -234,11 +141,11 @@ Document.associate = (models) => { }, ], }; - }); - Document.addScope("withStarred", (userId: string) => ({ + }, + withStarred: (userId: string) => ({ include: [ { - model: models.Star, + model: Star, as: "starred", where: { userId, @@ -247,118 +154,297 @@ Document.associate = (models) => { separate: true, }, ], - })); - Document.defaultScopeWithUser = (userId: string) => { - const starredScope = { + }), +})) +@Table({ tableName: "documents", modelName: "document" }) +@Fix +class Document extends ParanoidModel { + @PrimaryKey + @Column + urlId: string; + + @Length({ + min: 0, + max: MAX_TITLE_LENGTH, + msg: `Document title must be less than ${MAX_TITLE_LENGTH} characters`, + }) + @Column + title: string; + + @Column(DataType.ARRAY(DataType.STRING)) + previousTitles: string[] = []; + + @Column(DataType.SMALLINT) + version: number; + + @Column + template: boolean; + + @Column + fullWidth: boolean; + + @Column + editorVersion: string; + + @Column + emoji: string | null; + + @Column(DataType.TEXT) + text: string; + + @Column(DataType.BLOB) + state: Uint8Array; + + @Default(false) + @Column + isWelcome: boolean; + + @Default(0) + @Column(DataType.INTEGER) + revisionCount: number; + + @Column + archivedAt: Date | null; + + @Column + publishedAt: Date | null; + + @Column(DataType.ARRAY(DataType.UUID)) + collaboratorIds: string[] = []; + + // getters + + get url() { + if (!this.title) return `/doc/untitled-${this.urlId}`; + const slugifiedTitle = slugify(this.title); + return `/doc/${slugifiedTitle}-${this.urlId}`; + } + + get tasks() { + return getTasks(this.text || ""); + } + + // hooks + + @BeforeSave + static async updateInCollectionStructure(model: Document) { + if (!model.publishedAt || model.template) { + return; + } + + const collection = await Collection.findByPk(model.collectionId); + + if (!collection) { + return; + } + + await collection.updateDocument(model); + model.collection = collection; + } + + @AfterCreate + static async addDocumentToCollectionStructure(model: Document) { + if (!model.publishedAt || model.template) { + return; + } + + const collection = await Collection.findByPk(model.collectionId); + + if (!collection) { + return; + } + + await collection.addDocumentToStructure(model, 0); + model.collection = collection; + } + + @BeforeValidate + static createUrlId(model: Document) { + return (model.urlId = model.urlId || randomstring.generate(10)); + } + + @BeforeCreate + static setDocumentVersion(model: Document) { + if (model.version === undefined) { + model.version = DOCUMENT_VERSION; + } + + return this.processUpdate(model); + } + + @BeforeUpdate + static processUpdate(model: Document) { + const { emoji } = parseTitle(model.text); + // emoji in the title is split out for easier display + model.emoji = emoji || null; + + // ensure documents have a title + model.title = model.title || ""; + + if (model.previous("title") && model.previous("title") !== model.title) { + if (!model.previousTitles) { + model.previousTitles = []; + } + + model.previousTitles = uniq( + model.previousTitles.concat(model.previous("title")) + ); + } + + // add the current user as a collaborator on this doc + if (!model.collaboratorIds) { + model.collaboratorIds = []; + } + + model.collaboratorIds = uniq( + model.collaboratorIds.concat(model.lastModifiedById) + ); + + // increment revision + model.revisionCount += 1; + } + + // associations + + @BelongsTo(() => Document, "parentDocumentId") + parentDocument: Document | null; + + @ForeignKey(() => Document) + @Column(DataType.UUID) + parentDocumentId: string | null; + + @BelongsTo(() => User, "lastModifiedById") + updatedBy: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + lastModifiedById: string; + + @BelongsTo(() => User, "createdById") + createdBy: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + createdById: string; + + @BelongsTo(() => Document, "templateId") + document: Document; + + @ForeignKey(() => Document) + @Column(DataType.UUID) + templateId: string; + + @BelongsTo(() => Team, "teamId") + team: Team; + + @ForeignKey(() => Team) + @Column(DataType.UUID) + teamId: string; + + @BelongsTo(() => Collection, "collectionId") + collection: Collection; + + @ForeignKey(() => Collection) + @Column(DataType.UUID) + collectionId: string; + + @HasMany(() => Revision) + revisions: Revision[]; + + @HasMany(() => Backlink) + backlinks: Backlink[]; + + @HasMany(() => Star) + starred: Star[]; + + @HasMany(() => View) + views: View[]; + + static defaultScopeWithUser(userId: string) { + const starredScope: Readonly = { method: ["withStarred", userId], }; - const collectionScope = { + const collectionScope: Readonly = { method: ["withCollection", userId], }; - const viewScope = { + const viewScope: Readonly = { method: ["withViews", userId], }; - return Document.scope( + return this.scope([ "defaultScope", starredScope, collectionScope, - viewScope - ); - }; -}; + viewScope, + ]); + } -Document.findByPk = async function ( - id: string, - options: { - userId?: string; - paranoid?: boolean; - } = {} -) { - // allow default preloading of collection membership if `userId` is passed in find options - // almost every endpoint needs the collection membership to determine policy permissions. - const scope = this.scope( - "withUnpublished", - { - method: ["withCollection", options.userId, options.paranoid], - }, - { - method: ["withViews", options.userId], + static async findByPk( + id: string, + options: FindOptions & { + userId?: string; + } = {} + ) { + // allow default preloading of collection membership if `userId` is passed in find options + // almost every endpoint needs the collection membership to determine policy permissions. + const scope = this.scope([ + "withUnpublished", + { + method: ["withCollection", options.userId, options.paranoid], + }, + { + method: ["withViews", options.userId], + }, + ]); + + if (isUUID(id)) { + return scope.findOne({ + where: { + id, + }, + ...options, + }); } - ); - if (isUUID(id)) { - return scope.findOne({ - where: { - id, - }, - ...options, - }); + const match = id.match(SLUG_URL_REGEX); + if (match) { + return scope.findOne({ + where: { + urlId: match[1], + }, + ...options, + }); + } + + return undefined; } - const match = id.match(SLUG_URL_REGEX); - if (match) { - return scope.findOne({ - where: { - urlId: match[1], - }, - ...options, - }); - } -}; + static async searchForTeam( + team: Team, + query: string, + options: SearchOptions = {} + ): Promise { + const limit = options.limit || 15; + const offset = options.offset || 0; + const wildcardQuery = `${escape(query)}:*`; + const collectionIds = await team.collectionIds(); -type SearchResponse = { - results: { - ranking: number; - context: string; - document: Document; - }[]; - totalCount: number; -}; -type SearchOptions = { - limit?: number; - offset?: number; - collectionId?: string; - dateFilter?: DateFilter; - collaboratorIds?: string[]; - includeArchived?: boolean; - includeDrafts?: boolean; -}; + // If the team has access no public collections then shortcircuit the rest of this + if (!collectionIds.length) { + return { + results: [], + totalCount: 0, + }; + } -function escape(query: string): string { - // replace "\" with escaped "\\" because sequelize.escape doesn't do it - // https://github.com/sequelize/sequelize/issues/2950 - return sequelize.escape(query).replace(/\\/g, "\\\\"); -} - -Document.searchForTeam = async ( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'team' implicitly has an 'any' type. - team, - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'query' implicitly has an 'any' type. - query, - options: SearchOptions = {} -): Promise => { - const limit = options.limit || 15; - const offset = options.offset || 0; - const wildcardQuery = `${escape(query)}:*`; - const collectionIds = await team.collectionIds(); - - // If the team has access no public collections then shortcircuit the rest of this - if (!collectionIds.length) { - return { - results: [], - totalCount: 0, - }; - } - - // Build the SQL query to get documentIds, ranking, and search term context - const whereClause = ` + // Build the SQL query to get documentIds, ranking, and search term context + const whereClause = ` "searchVector" @@ to_tsquery('english', :query) AND "teamId" = :teamId AND "collectionId" IN(:collectionIds) AND "deletedAt" IS NULL AND "publishedAt" IS NOT NULL `; - const selectSql = ` + const selectSql = ` SELECT id, ts_rank(documents."searchVector", to_tsquery('english', :query)) as "searchRanking", @@ -371,98 +457,101 @@ Document.searchForTeam = async ( LIMIT :limit OFFSET :offset; `; - const countSql = ` + const countSql = ` SELECT COUNT(id) FROM documents WHERE ${whereClause} `; - const queryReplacements = { - teamId: team.id, - query: wildcardQuery, - collectionIds, - }; - const resultsQuery = sequelize.query(selectSql, { - type: sequelize.QueryTypes.SELECT, - replacements: { ...queryReplacements, limit, offset }, - }); - const countQuery = sequelize.query(countSql, { - type: sequelize.QueryTypes.SELECT, - replacements: queryReplacements, - }); - const [results, [{ count }]] = await Promise.all([resultsQuery, countQuery]); - // Final query to get associated document data - const documents = await Document.findAll({ - where: { - id: map(results, "id"), - }, - include: [ - { - model: Collection, - as: "collection", + const queryReplacements = { + teamId: team.id, + query: wildcardQuery, + collectionIds, + }; + const resultsQuery = this.sequelize!.query(selectSql, { + type: QueryTypes.SELECT, + replacements: { ...queryReplacements, limit, offset }, + }); + const countQuery = this.sequelize!.query(countSql, { + type: QueryTypes.SELECT, + replacements: queryReplacements, + }); + const [results, [{ count }]]: [any, any] = await Promise.all([ + resultsQuery, + countQuery, + ]); + // Final query to get associated document data + const documents = await this.findAll({ + where: { + id: map(results, "id"), }, - { - model: User, - as: "createdBy", - paranoid: false, - }, - { - model: User, - as: "updatedBy", - paranoid: false, - }, - ], - }); - return { - results: map(results, (result) => ({ - ranking: result.searchRanking, - context: removeMarkdown(unescape(result.searchContext), { - stripHTML: false, - }), - document: find(documents, { - id: result.id, - }), - })), - totalCount: count, - }; -}; + include: [ + { + model: Collection, + as: "collection", + }, + { + model: User, + as: "createdBy", + paranoid: false, + }, + { + model: User, + as: "updatedBy", + paranoid: false, + }, + ], + }); -Document.searchForUser = async ( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'user' implicitly has an 'any' type. - user, - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'query' implicitly has an 'any' type. - query, - options: SearchOptions = {} -): Promise => { - const limit = options.limit || 15; - const offset = options.offset || 0; - const wildcardQuery = `${escape(query)}:*`; - // Ensure we're filtering by the users accessible collections. If - // collectionId is passed as an option it is assumed that the authorization - // has already been done in the router - let collectionIds; - - if (options.collectionId) { - collectionIds = [options.collectionId]; - } else { - collectionIds = await user.collectionIds(); - } - - // If the user has access to no collections then shortcircuit the rest of this - if (!collectionIds.length) { return { - results: [], - totalCount: 0, + results: map(results, (result: any) => ({ + ranking: result.searchRanking, + context: removeMarkdown(unescape(result.searchContext), { + stripHTML: false, + }), + document: find(documents, { + id: result.id, + }) as Document, + })), + totalCount: count, }; } - let dateFilter; + static async searchForUser( + user: User, + query: string, + options: SearchOptions = {} + ): Promise { + const limit = options.limit || 15; + const offset = options.offset || 0; + const wildcardQuery = `${escape(query)}:*`; - if (options.dateFilter) { - dateFilter = `1 ${options.dateFilter}`; - } + // Ensure we're filtering by the users accessible collections. If + // collectionId is passed as an option it is assumed that the authorization + // has already been done in the router + let collectionIds; - // Build the SQL query to get documentIds, ranking, and search term context - const whereClause = ` + if (options.collectionId) { + collectionIds = [options.collectionId]; + } else { + collectionIds = await user.collectionIds(); + } + + // If the user has access to no collections then shortcircuit the rest of this + if (!collectionIds.length) { + return { + results: [], + totalCount: 0, + }; + } + + let dateFilter; + + if (options.dateFilter) { + dateFilter = `1 ${options.dateFilter}`; + } + + // Build the SQL query to get documentIds, ranking, and search term context + const whereClause = ` "searchVector" @@ to_tsquery('english', :query) AND "teamId" = :teamId AND "collectionId" IN(:collectionIds) AND @@ -482,7 +571,7 @@ Document.searchForUser = async ( : '"publishedAt" IS NOT NULL' } `; - const selectSql = ` + const selectSql = ` SELECT id, ts_rank(documents."searchVector", to_tsquery('english', :query)) as "searchRanking", @@ -495,330 +584,288 @@ Document.searchForUser = async ( LIMIT :limit OFFSET :offset; `; - const countSql = ` + const countSql = ` SELECT COUNT(id) FROM documents WHERE ${whereClause} `; - const queryReplacements = { - teamId: user.teamId, - userId: user.id, - collaboratorIds: options.collaboratorIds, - query: wildcardQuery, - collectionIds, - dateFilter, - }; - const resultsQuery = sequelize.query(selectSql, { - type: sequelize.QueryTypes.SELECT, - replacements: { ...queryReplacements, limit, offset }, - }); - const countQuery = sequelize.query(countSql, { - type: sequelize.QueryTypes.SELECT, - replacements: queryReplacements, - }); - const [results, [{ count }]] = await Promise.all([resultsQuery, countQuery]); - // Final query to get associated document data - const documents = await Document.scope( - { - method: ["withViews", user.id], - }, - { - method: ["withCollection", user.id], - } - ).findAll({ - where: { - id: map(results, "id"), - }, - include: [ + const queryReplacements = { + teamId: user.teamId, + userId: user.id, + collaboratorIds: options.collaboratorIds, + query: wildcardQuery, + collectionIds, + dateFilter, + }; + const resultsQuery = this.sequelize!.query(selectSql, { + type: QueryTypes.SELECT, + replacements: { ...queryReplacements, limit, offset }, + }); + const countQuery = this.sequelize!.query(countSql, { + type: QueryTypes.SELECT, + replacements: queryReplacements, + }); + const [results, [{ count }]]: [any, any] = await Promise.all([ + resultsQuery, + countQuery, + ]); + + // Final query to get associated document data + const documents = await this.scope([ { - model: User, - as: "createdBy", - paranoid: false, + method: ["withViews", user.id], }, { - model: User, - as: "updatedBy", - paranoid: false, + method: ["withCollection", user.id], }, - ], - }); - return { - results: map(results, (result) => ({ - ranking: result.searchRanking, - context: removeMarkdown(unescape(result.searchContext), { - stripHTML: false, - }), - document: find(documents, { - id: result.id, - }), - })), - totalCount: count, - }; -}; - -// Hooks -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'model' implicitly has an 'any' type. -Document.addHook("beforeSave", async (model) => { - if (!model.publishedAt || model.template) { - return; - } - - const collection = await Collection.findByPk(model.collectionId); - - if (!collection) { - return; - } - - await collection.updateDocument(model); - model.collection = collection; -}); -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'model' implicitly has an 'any' type. -Document.addHook("afterCreate", async (model) => { - if (!model.publishedAt || model.template) { - return; - } - - const collection = await Collection.findByPk(model.collectionId); - - if (!collection) { - return; - } - - await collection.addDocumentToStructure(model, 0); - model.collection = collection; - return model; -}); - -// Instance methods -Document.prototype.toMarkdown = function () { - const text = unescape(this.text); - - if (this.version) { - return `# ${this.title}\n\n${text}`; - } - - return text; -}; - -Document.prototype.migrateVersion = function () { - let migrated = false; - - // migrate from document version 0 -> 1 - if (!this.version) { - // removing the title from the document text attribute - this.text = this.text.replace(/^#\s(.*)\n/, ""); - this.version = 1; - migrated = true; - } - - // migrate from document version 1 -> 2 - if (this.version === 1) { - const nodes = serializer.deserialize(this.text); - this.text = serializer.serialize(nodes, { - version: 2, - }); - this.version = 2; - migrated = true; - } - - if (migrated) { - return this.save({ - silent: true, - hooks: false, - }); - } -}; - -// Note: This method marks the document and it's children as deleted -// in the database, it does not permanently delete them OR remove -// from the collection structure. -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'options' implicitly has an 'any' type. -Document.prototype.deleteWithChildren = async function (options) { - // Helper to destroy all child documents for a document - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documentId' implicitly has an 'any' typ... Remove this comment to see the full error message - const loopChildren = async (documentId, opts) => { - const childDocuments = await Document.findAll({ + ]).findAll({ where: { - parentDocumentId: documentId, + id: map(results, "id"), }, - }); - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'child' implicitly has an 'any' type. - childDocuments.forEach(async (child) => { - await loopChildren(child.id, opts); - await child.destroy(opts); - }); - }; - - await loopChildren(this.id, options); - await this.destroy(options); -}; - -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'userId' implicitly has an 'any' type. -Document.prototype.archiveWithChildren = async function (userId, options) { - const archivedAt = new Date(); - - // Helper to archive all child documents for a document - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'parentDocumentId' implicitly has an 'an... Remove this comment to see the full error message - const archiveChildren = async (parentDocumentId) => { - const childDocuments = await Document.findAll({ - where: { - parentDocumentId, - }, - }); - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'child' implicitly has an 'any' type. - childDocuments.forEach(async (child) => { - await archiveChildren(child.id); - child.archivedAt = archivedAt; - child.lastModifiedById = userId; - await child.save(options); - }); - }; - - await archiveChildren(this.id); - this.archivedAt = archivedAt; - this.lastModifiedById = userId; - return this.save(options); -}; - -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'options' implicitly has an 'any' type. -Document.prototype.publish = async function (userId: string, options) { - if (this.publishedAt) return this.save(options); - - if (!this.template) { - const collection = await Collection.findByPk(this.collectionId); - await collection.addDocumentToStructure(this, 0); - } - - this.lastModifiedById = userId; - this.publishedAt = new Date(); - await this.save(options); - return this; -}; - -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'options' implicitly has an 'any' type. -Document.prototype.unpublish = async function (userId: string, options) { - if (!this.publishedAt) return this; - const collection = await this.getCollection(); - await collection.removeDocumentInStructure(this); - // unpublishing a document converts the "ownership" to yourself, so that it - // can appear in your drafts rather than the original creators - this.userId = userId; - this.lastModifiedById = userId; - this.publishedAt = null; - await this.save(options); - return this; -}; - -// Moves a document from being visible to the team within a collection -// to the archived area, where it can be subsequently restored. -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'userId' implicitly has an 'any' type. -Document.prototype.archive = async function (userId) { - // archive any children and remove from the document structure - const collection = await this.getCollection(); - await collection.removeDocumentInStructure(this); - this.collection = collection; - await this.archiveWithChildren(userId); - return this; -}; - -// Restore an archived document back to being visible to the team -Document.prototype.unarchive = async function (userId: string) { - const collection = await this.getCollection(); - - // check to see if the documents parent hasn't been archived also - // If it has then restore the document to the collection root. - if (this.parentDocumentId) { - const parent = await Document.findOne({ - where: { - id: this.parentDocumentId, - archivedAt: { - [Op.eq]: null, + include: [ + { + model: User, + as: "createdBy", + paranoid: false, }, - }, + { + model: User, + as: "updatedBy", + paranoid: false, + }, + ], }); - if (!parent) this.parentDocumentId = null; + return { + results: map(results, (result: any) => ({ + ranking: result.searchRanking, + context: removeMarkdown(unescape(result.searchContext), { + stripHTML: false, + }), + document: find(documents, { + id: result.id, + }) as Document, + })), + totalCount: count, + }; } - if (!this.template) { - await collection.addDocumentToStructure(this); - this.collection = collection; - } + // instance methods - if (this.deletedAt) { - await this.restore(); - } + toMarkdown = () => { + const text = unescape(this.text); - this.archivedAt = null; - this.lastModifiedById = userId; - await this.save(); - return this; -}; + if (this.version) { + return `# ${this.title}\n\n${text}`; + } -// Delete a document, archived or otherwise. -Document.prototype.delete = function (userId: string) { - return sequelize.transaction( - async (transaction: Transaction): Promise => { - if (!this.archivedAt && !this.template) { - // delete any children and remove from the document structure - const collection = await this.getCollection({ - transaction, - }); - if (collection) - await collection.deleteDocument(this, { + return text; + }; + + migrateVersion = () => { + let migrated = false; + + // migrate from document version 0 -> 1 + if (!this.version) { + // removing the title from the document text attribute + this.text = this.text.replace(/^#\s(.*)\n/, ""); + this.version = 1; + migrated = true; + } + + // migrate from document version 1 -> 2 + if (this.version === 1) { + const nodes = serializer.deserialize(this.text); + this.text = serializer.serialize(nodes, { + version: 2, + }); + this.version = 2; + migrated = true; + } + + if (migrated) { + return this.save({ + silent: true, + hooks: false, + }); + } + + return undefined; + }; + + archiveWithChildren = async ( + userId: string, + options?: FindOptions + ) => { + const archivedAt = new Date(); + + // Helper to archive all child documents for a document + const archiveChildren = async (parentDocumentId: string) => { + const childDocuments = await (this + .constructor as typeof Document).findAll({ + where: { + parentDocumentId, + }, + }); + childDocuments.forEach(async (child) => { + await archiveChildren(child.id); + child.archivedAt = archivedAt; + child.lastModifiedById = userId; + await child.save(options); + }); + }; + + await archiveChildren(this.id); + this.archivedAt = archivedAt; + this.lastModifiedById = userId; + return this.save(options); + }; + + publish = async (userId: string, options?: FindOptions) => { + if (this.publishedAt) return this.save(options); + + if (!this.template) { + const collection = await Collection.findByPk(this.collectionId); + await collection?.addDocumentToStructure(this, 0); + } + + this.lastModifiedById = userId; + this.publishedAt = new Date(); + await this.save(options); + return this; + }; + + unpublish = async (userId: string, options?: FindOptions) => { + if (!this.publishedAt) return this; + const collection = await this.$get("collection"); + await collection?.removeDocumentInStructure(this); + + // unpublishing a document converts the "ownership" to yourself, so that it + // can appear in your drafts rather than the original creators + this.createdById = userId; + this.lastModifiedById = userId; + this.publishedAt = null; + await this.save(options); + return this; + }; + + // Moves a document from being visible to the team within a collection + // to the archived area, where it can be subsequently restored. + archive = async (userId: string) => { + // archive any children and remove from the document structure + const collection = await this.$get("collection"); + if (collection) { + await collection.removeDocumentInStructure(this); + this.collection = collection; + } + + await this.archiveWithChildren(userId); + return this; + }; + + // Restore an archived document back to being visible to the team + unarchive = async (userId: string) => { + const collection = await this.$get("collection"); + + // check to see if the documents parent hasn't been archived also + // If it has then restore the document to the collection root. + if (this.parentDocumentId) { + const parent = await (this.constructor as typeof Document).findOne({ + where: { + id: this.parentDocumentId, + archivedAt: { + [Op.is]: null, + }, + }, + }); + if (!parent) this.parentDocumentId = null; + } + + if (!this.template && collection) { + await collection.addDocumentToStructure(this); + this.collection = collection; + } + + if (this.deletedAt) { + await this.restore(); + } + + this.archivedAt = null; + this.lastModifiedById = userId; + await this.save(); + return this; + }; + + // Delete a document, archived or otherwise. + delete = (userId: string) => { + return this.sequelize.transaction( + async (transaction: Transaction): Promise => { + if (!this.archivedAt && !this.template) { + // delete any children and remove from the document structure + const collection = await this.$get("collection", { transaction, }); - } else { - await this.destroy({ + if (collection) { + await collection.deleteDocument(this); + } + } else { + await this.destroy({ + transaction, + }); + } + + await Revision.destroy({ + where: { + documentId: this.id, + }, transaction, }); + await this.update( + { + lastModifiedById: userId, + }, + { + transaction, + } + ); + return this; } - - await Revision.destroy({ - where: { - documentId: this.id, - }, - transaction, - }); - await this.update( - { - lastModifiedById: userId, - }, - { - transaction, - } - ); - return this; - } - ); -}; - -Document.prototype.getTimestamp = function () { - return Math.round(new Date(this.updatedAt).getTime() / 1000); -}; - -Document.prototype.getSummary = function () { - const plain = removeMarkdown(unescape(this.text), { - stripHTML: false, - }); - const lines = compact(plain.split("\n")); - const notEmpty = lines.length >= 1; - - if (this.version) { - return notEmpty ? lines[0] : ""; - } - - return notEmpty ? lines[1] : ""; -}; - -Document.prototype.toJSON = function () { - // Warning: only use for new documents as order of children is - // handled in the collection's documentStructure - return { - id: this.id, - title: this.title, - url: this.url, - children: [], + ); }; -}; + + getTimestamp = () => { + return Math.round(new Date(this.updatedAt).getTime() / 1000); + }; + + getSummary = () => { + const plain = removeMarkdown(unescape(this.text), { + stripHTML: false, + }); + const lines = compact(plain.split("\n")); + const notEmpty = lines.length >= 1; + + if (this.version) { + return notEmpty ? lines[0] : ""; + } + + return notEmpty ? lines[1] : ""; + }; + + toJSON = () => { + // Warning: only use for new documents as order of children is + // handled in the collection's documentStructure + return { + id: this.id, + title: this.title, + url: this.url, + children: [], + }; + }; +} + +function escape(query: string): string { + // replace "\" with escaped "\\" because sequelize.escape doesn't do it + // https://github.com/sequelize/sequelize/issues/2950 + return Document.sequelize!.escape(query).replace(/\\/g, "\\\\"); +} export default Document; diff --git a/server/models/Event.ts b/server/models/Event.ts index 7620ea854..00546d2fa 100644 --- a/server/models/Event.ts +++ b/server/models/Event.ts @@ -1,126 +1,164 @@ +import { + ForeignKey, + AfterCreate, + BeforeCreate, + BelongsTo, + Column, + IsIP, + IsUUID, + Table, + DataType, +} from "sequelize-typescript"; import { globalEventQueue } from "../queues"; -import { DataTypes, sequelize } from "../sequelize"; +import Collection from "./Collection"; +import Document from "./Document"; +import Team from "./Team"; +import User from "./User"; +import BaseModel from "./base/BaseModel"; +import Fix from "./decorators/Fix"; -const Event = sequelize.define("event", { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - modelId: DataTypes.UUID, - name: DataTypes.STRING, - ip: DataTypes.STRING, - data: DataTypes.JSONB, -}); +@Table({ tableName: "events", modelName: "event" }) +@Fix +class Event extends BaseModel { + @IsUUID(4) + @Column(DataType.UUID) + modelId: string; -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -Event.associate = (models) => { - Event.belongsTo(models.User, { - as: "user", - foreignKey: "userId", - }); - Event.belongsTo(models.User, { - as: "actor", - foreignKey: "actorId", - }); - Event.belongsTo(models.Collection, { - as: "collection", - foreignKey: "collectionId", - }); - Event.belongsTo(models.Collection, { - as: "document", - foreignKey: "documentId", - }); - Event.belongsTo(models.Team, { - as: "team", - foreignKey: "teamId", - }); -}; + @Column + name: string; -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'event' implicitly has an 'any' type. -Event.beforeCreate((event) => { - if (event.ip) { - // cleanup IPV6 representations of IPV4 addresses - event.ip = event.ip.replace(/^::ffff:/, ""); + @IsIP + @Column + ip: string | null; + + @Column(DataType.JSONB) + data: Record; + + // hooks + + @BeforeCreate + static cleanupIp(model: Event) { + if (model.ip) { + // cleanup IPV6 representations of IPV4 addresses + model.ip = model.ip.replace(/^::ffff:/, ""); + } } -}); -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'event' implicitly has an 'any' type. -Event.afterCreate((event) => { - globalEventQueue.add(event); -}); -// add can be used to send events into the event system without recording them -// in the database or audit trail -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'event' implicitly has an 'any' type. -Event.add = (event) => { - const now = new Date(); - globalEventQueue.add( - Event.build({ - createdAt: now, - updatedAt: now, - ...event, - }) - ); -}; + @AfterCreate + static async enqueue(model: Event) { + globalEventQueue.add(model); + } -Event.ACTIVITY_EVENTS = [ - "collections.create", - "collections.delete", - "collections.move", - "collections.permission_changed", - "documents.publish", - "documents.archive", - "documents.unarchive", - "documents.move", - "documents.delete", - "documents.permanent_delete", - "documents.restore", - "revisions.create", - "users.create", -]; -Event.AUDIT_EVENTS = [ - "api_keys.create", - "api_keys.delete", - "authenticationProviders.update", - "collections.create", - "collections.update", - "collections.permission_changed", - "collections.move", - "collections.add_user", - "collections.remove_user", - "collections.add_group", - "collections.remove_group", - "collections.delete", - "collections.export_all", - "documents.create", - "documents.publish", - "documents.update", - "documents.archive", - "documents.unarchive", - "documents.move", - "documents.delete", - "documents.permanent_delete", - "documents.restore", - "groups.create", - "groups.update", - "groups.delete", - "pins.create", - "pins.update", - "pins.delete", - "revisions.create", - "shares.create", - "shares.update", - "shares.revoke", - "teams.update", - "users.create", - "users.update", - "users.signin", - "users.promote", - "users.demote", - "users.invite", - "users.suspend", - "users.activate", - "users.delete", -]; + // associations + + @BelongsTo(() => User, "userId") + user: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + userId: string; + + @BelongsTo(() => Document, "documentId") + document: Document; + + @ForeignKey(() => Document) + @Column(DataType.UUID) + documentId: string; + + @BelongsTo(() => User, "actorId") + actor: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + actorId: string; + + @BelongsTo(() => Collection, "collectionId") + collection: Collection; + + @ForeignKey(() => Collection) + @Column(DataType.UUID) + collectionId: string; + + @BelongsTo(() => Team, "teamId") + team: Team; + + @ForeignKey(() => Team) + @Column(DataType.UUID) + teamId: string; + + // add can be used to send events into the event system without recording them + // in the database or audit trail + static add(event: Partial) { + const now = new Date(); + globalEventQueue.add( + this.build({ + createdAt: now, + updatedAt: now, + ...event, + }) + ); + } + + static ACTIVITY_EVENTS = [ + "collections.create", + "collections.delete", + "collections.move", + "collections.permission_changed", + "documents.publish", + "documents.archive", + "documents.unarchive", + "documents.move", + "documents.delete", + "documents.permanent_delete", + "documents.restore", + "revisions.create", + "users.create", + ]; + + static AUDIT_EVENTS = [ + "api_keys.create", + "api_keys.delete", + "authenticationProviders.update", + "collections.create", + "collections.update", + "collections.permission_changed", + "collections.move", + "collections.add_user", + "collections.remove_user", + "collections.add_group", + "collections.remove_group", + "collections.delete", + "collections.export_all", + "documents.create", + "documents.publish", + "documents.update", + "documents.archive", + "documents.unarchive", + "documents.move", + "documents.delete", + "documents.permanent_delete", + "documents.restore", + "groups.create", + "groups.update", + "groups.delete", + "pins.create", + "pins.update", + "pins.delete", + "revisions.create", + "shares.create", + "shares.update", + "shares.revoke", + "teams.update", + "users.create", + "users.update", + "users.signin", + "users.promote", + "users.demote", + "users.invite", + "users.suspend", + "users.activate", + "users.delete", + ]; +} export default Event; diff --git a/server/models/FileOperation.ts b/server/models/FileOperation.ts index eb7c66c7c..64ef45b75 100644 --- a/server/models/FileOperation.ts +++ b/server/models/FileOperation.ts @@ -1,76 +1,88 @@ +import { + ForeignKey, + DefaultScope, + Column, + BeforeDestroy, + BelongsTo, + Table, + DataType, +} from "sequelize-typescript"; import { deleteFromS3 } from "@server/utils/s3"; -import { DataTypes, sequelize } from "../sequelize"; +import Collection from "./Collection"; +import Team from "./Team"; +import User from "./User"; +import BaseModel from "./base/BaseModel"; +import Fix from "./decorators/Fix"; -const FileOperation = sequelize.define("file_operations", { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - type: { - type: DataTypes.ENUM("import", "export"), - allowNull: false, - }, - state: { - type: DataTypes.ENUM( - "creating", - "uploading", - "complete", - "error", - "expired" - ), - allowNull: false, - }, - key: { - type: DataTypes.STRING, - }, - url: { - type: DataTypes.STRING, - }, - size: { - type: DataTypes.BIGINT, - allowNull: false, - }, -}); -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'model' implicitly has an 'any' type. -FileOperation.beforeDestroy(async (model) => { - await deleteFromS3(model.key); -}); +@DefaultScope(() => ({ + include: [ + { + model: User, + as: "user", + paranoid: false, + }, + { + model: Collection, + as: "collection", + paranoid: false, + }, + ], +})) +@Table({ tableName: "file_operations", modelName: "file_operation" }) +@Fix +class FileOperation extends BaseModel { + @Column(DataType.ENUM("import", "export")) + type: "import" | "export"; -FileOperation.prototype.expire = async function () { - this.state = "expired"; - await deleteFromS3(this.key); - await this.save(); -}; + @Column( + DataType.ENUM("creating", "uploading", "complete", "error", "expired") + ) + state: "creating" | "uploading" | "complete" | "error" | "expired"; -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -FileOperation.associate = (models) => { - FileOperation.belongsTo(models.User, { - as: "user", - foreignKey: "userId", - }); - FileOperation.belongsTo(models.Collection, { - as: "collection", - foreignKey: "collectionId", - }); - FileOperation.belongsTo(models.Team, { - as: "team", - foreignKey: "teamId", - }); - FileOperation.addScope("defaultScope", { - include: [ - { - model: models.User, - as: "user", - paranoid: false, - }, - { - model: models.Collection, - as: "collection", - paranoid: false, - }, - ], - }); -}; + @Column + key: string; + + @Column + url: string; + + @Column(DataType.BIGINT) + size: number; + + expire = async function () { + this.state = "expired"; + await deleteFromS3(this.key); + await this.save(); + }; + + // hooks + + @BeforeDestroy + static async deleteFileFromS3(model: FileOperation) { + await deleteFromS3(model.key); + } + + // associations + + @BelongsTo(() => User, "userId") + user: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + userId: string; + + @BelongsTo(() => Team, "teamId") + team: Team; + + @ForeignKey(() => Team) + @Column(DataType.UUID) + teamId: string; + + @BelongsTo(() => Collection, "collectionId") + collection: Collection; + + @ForeignKey(() => Collection) + @Column(DataType.UUID) + collectionId: string; +} export default FileOperation; diff --git a/server/models/Group.test.ts b/server/models/Group.test.ts index 8e1ad919d..73f6fc894 100644 --- a/server/models/Group.test.ts +++ b/server/models/Group.test.ts @@ -1,9 +1,11 @@ -import { CollectionGroup, GroupUser } from "@server/models"; import { buildUser, buildGroup, buildCollection } from "@server/test/factories"; import { flushdb } from "@server/test/support"; +import CollectionGroup from "./CollectionGroup"; +import GroupUser from "./GroupUser"; beforeEach(() => flushdb()); beforeEach(jest.resetAllMocks); + describe("afterDestroy hook", () => { test("should destroy associated group and collection join relations", async () => { const group = await buildGroup(); @@ -23,22 +25,22 @@ describe("afterDestroy hook", () => { teamId, }); const createdById = user1.id; - await group.addUser(user1, { + await group.$add("user", user1, { through: { createdById, }, }); - await group.addUser(user2, { + await group.$add("user", user2, { through: { createdById, }, }); - await collection1.addGroup(group, { + await collection1.$add("group", group, { through: { createdById, }, }); - await collection2.addGroup(group, { + await collection2.$add("group", group, { through: { createdById, }, diff --git a/server/models/Group.ts b/server/models/Group.ts index 92977f777..569073089 100644 --- a/server/models/Group.ts +++ b/server/models/Group.ts @@ -1,96 +1,100 @@ -import { CollectionGroup, GroupUser } from "@server/models"; -import { Op, DataTypes, sequelize } from "../sequelize"; +import { Op } from "sequelize"; +import { + AfterDestroy, + BelongsTo, + Column, + ForeignKey, + Table, + HasMany, + BelongsToMany, + DefaultScope, + DataType, +} from "sequelize-typescript"; +import CollectionGroup from "./CollectionGroup"; +import GroupUser from "./GroupUser"; +import Team from "./Team"; +import User from "./User"; +import ParanoidModel from "./base/ParanoidModel"; +import Fix from "./decorators/Fix"; -const Group = sequelize.define( - "group", - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, +@DefaultScope(() => ({ + include: [ + { + association: "groupMemberships", + required: false, }, - teamId: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - }, - name: { - type: DataTypes.STRING, - allowNull: false, + ], + order: [["name", "ASC"]], +})) +@Table({ + tableName: "groups", + modelName: "group", + validate: { + isUniqueNameInTeam: async function () { + const foundItem = await Group.findOne({ + where: { + teamId: this.teamId, + name: { + [Op.iLike]: this.name, + }, + id: { + [Op.not]: this.id, + }, + }, + }); + + if (foundItem) { + throw new Error("The name of this group is already in use"); + } }, }, - { - timestamps: true, - paranoid: true, - validate: { - isUniqueNameInTeam: async function () { - const foundItem = await Group.findOne({ - where: { - teamId: this.teamId, - name: { - [Op.iLike]: this.name, - }, - id: { - [Op.not]: this.id, - }, - }, - }); +}) +@Fix +class Group extends ParanoidModel { + @Column + name: string; - if (foundItem) { - throw new Error("The name of this group is already in use"); - } + // hooks + + @AfterDestroy + static async deleteGroupUsers(model: Group) { + if (!model.deletedAt) return; + await GroupUser.destroy({ + where: { + groupId: model.id, }, - }, + }); + await CollectionGroup.destroy({ + where: { + groupId: model.id, + }, + }); } -); -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -Group.associate = (models) => { - Group.hasMany(models.GroupUser, { - as: "groupMemberships", - foreignKey: "groupId", - }); - Group.hasMany(models.CollectionGroup, { - as: "collectionGroupMemberships", - foreignKey: "groupId", - }); - Group.belongsTo(models.Team, { - as: "team", - foreignKey: "teamId", - }); - Group.belongsTo(models.User, { - as: "createdBy", - foreignKey: "createdById", - }); - Group.belongsToMany(models.User, { - as: "users", - through: models.GroupUser, - foreignKey: "groupId", - }); - Group.addScope("defaultScope", { - include: [ - { - association: "groupMemberships", - required: false, - }, - ], - order: [["name", "ASC"]], - }); -}; + // associations -// Cascade deletes to group and collection relations -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'group' implicitly has an 'any' type. -Group.addHook("afterDestroy", async (group) => { - if (!group.deletedAt) return; - await GroupUser.destroy({ - where: { - groupId: group.id, - }, - }); - await CollectionGroup.destroy({ - where: { - groupId: group.id, - }, - }); -}); + @HasMany(() => GroupUser, "groupId") + groupMemberships: GroupUser[]; + + @HasMany(() => CollectionGroup, "groupId") + collectionGroupMemberships: CollectionGroup[]; + + @BelongsTo(() => Team, "teamId") + team: Team; + + @ForeignKey(() => Team) + @Column(DataType.UUID) + teamId: string; + + @BelongsTo(() => User, "createdById") + createdBy: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + createdById: string; + + @BelongsToMany(() => User, () => GroupUser) + users: User[]; +} export default Group; diff --git a/server/models/GroupUser.ts b/server/models/GroupUser.ts index 83b7b6449..8723e8795 100644 --- a/server/models/GroupUser.ts +++ b/server/models/GroupUser.ts @@ -1,37 +1,46 @@ -import { sequelize } from "../sequelize"; +import { + DefaultScope, + BelongsTo, + ForeignKey, + Column, + Table, + DataType, + Model, +} from "sequelize-typescript"; +import Group from "./Group"; +import User from "./User"; +import Fix from "./decorators/Fix"; -const GroupUser = sequelize.define( - "group_user", - {}, - { - timestamps: true, - paranoid: true, - } -); +@DefaultScope(() => ({ + include: [ + { + association: "user", + }, + ], +})) +@Table({ tableName: "group_users", modelName: "group_user", paranoid: true }) +@Fix +class GroupUser extends Model { + @BelongsTo(() => User, "userId") + user: User; -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -GroupUser.associate = (models) => { - GroupUser.belongsTo(models.Group, { - as: "group", - foreignKey: "groupId", - primary: true, - }); - GroupUser.belongsTo(models.User, { - as: "user", - foreignKey: "userId", - primary: true, - }); - GroupUser.belongsTo(models.User, { - as: "createdBy", - foreignKey: "createdById", - }); - GroupUser.addScope("defaultScope", { - include: [ - { - association: "user", - }, - ], - }); -}; + @ForeignKey(() => User) + @Column(DataType.UUID) + userId: string; + + @BelongsTo(() => Group, "groupId") + group: Group; + + @ForeignKey(() => Group) + @Column(DataType.UUID) + groupId: string; + + @BelongsTo(() => User, "createdById") + createdBy: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + createdById: string; +} export default GroupUser; diff --git a/server/models/Integration.ts b/server/models/Integration.ts index 7e6529741..5aa74b910 100644 --- a/server/models/Integration.ts +++ b/server/models/Integration.ts @@ -1,35 +1,61 @@ -import { DataTypes, sequelize } from "../sequelize"; +import { + ForeignKey, + BelongsTo, + Column, + Table, + DataType, +} from "sequelize-typescript"; +import Collection from "./Collection"; +import IntegrationAuthentication from "./IntegrationAuthentication"; +import Team from "./Team"; +import User from "./User"; +import BaseModel from "./base/BaseModel"; +import Fix from "./decorators/Fix"; -const Integration = sequelize.define("integration", { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - type: DataTypes.STRING, - service: DataTypes.STRING, - settings: DataTypes.JSONB, - events: DataTypes.ARRAY(DataTypes.STRING), -}); +@Table({ tableName: "integrations", modelName: "integration" }) +@Fix +class Integration extends BaseModel { + @Column + type: string; -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -Integration.associate = (models) => { - Integration.belongsTo(models.User, { - as: "user", - foreignKey: "userId", - }); - Integration.belongsTo(models.Team, { - as: "team", - foreignKey: "teamId", - }); - Integration.belongsTo(models.Collection, { - as: "collection", - foreignKey: "collectionId", - }); - Integration.belongsTo(models.IntegrationAuthentication, { - as: "authentication", - foreignKey: "authenticationId", - }); -}; + @Column + service: string; + + @Column(DataType.JSONB) + settings: any; + + @Column(DataType.ARRAY(DataType.STRING)) + events: string[]; + + // associations + + @BelongsTo(() => User, "userId") + user: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + userId: string; + + @BelongsTo(() => Team, "teamId") + team: Team; + + @ForeignKey(() => Team) + @Column(DataType.UUID) + teamId: string; + + @BelongsTo(() => Collection, "collectionId") + collection: Collection; + + @ForeignKey(() => Collection) + @Column(DataType.UUID) + collectionId: string; + + @BelongsTo(() => IntegrationAuthentication, "authenticationId") + authentication: IntegrationAuthentication; + + @ForeignKey(() => IntegrationAuthentication) + @Column(DataType.UUID) + authenticationId: string; +} export default Integration; diff --git a/server/models/IntegrationAuthentication.ts b/server/models/IntegrationAuthentication.ts index 4b48e232a..005744ca2 100644 --- a/server/models/IntegrationAuthentication.ts +++ b/server/models/IntegrationAuthentication.ts @@ -1,26 +1,53 @@ -import { DataTypes, sequelize, encryptedFields } from "../sequelize"; +import { + DataType, + Table, + ForeignKey, + BelongsTo, + Column, +} from "sequelize-typescript"; +import Team from "./Team"; +import User from "./User"; +import BaseModel from "./base/BaseModel"; +import Encrypted, { + getEncryptedColumn, + setEncryptedColumn, +} from "./decorators/Encrypted"; +import Fix from "./decorators/Fix"; -const IntegrationAuthentication = sequelize.define("authentication", { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - service: DataTypes.STRING, - scopes: DataTypes.ARRAY(DataTypes.STRING), - token: encryptedFields().vault("token"), -}); +@Table({ tableName: "authentications", modelName: "authentication" }) +@Fix +class IntegrationAuthentication extends BaseModel { + @Column + service: string; -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -IntegrationAuthentication.associate = (models) => { - IntegrationAuthentication.belongsTo(models.User, { - as: "user", - foreignKey: "userId", - }); - IntegrationAuthentication.belongsTo(models.Team, { - as: "team", - foreignKey: "teamId", - }); -}; + @Column(DataType.ARRAY(DataType.STRING)) + scopes: string[]; + + @Column(DataType.BLOB) + @Encrypted + get token() { + return getEncryptedColumn(this, "token"); + } + + set token(value: string) { + setEncryptedColumn(this, "token", value); + } + + // associations + + @BelongsTo(() => User, "userId") + user: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + userId: string; + + @BelongsTo(() => Team, "teamId") + team: Team; + + @ForeignKey(() => Team) + @Column(DataType.UUID) + teamId: string; +} export default IntegrationAuthentication; diff --git a/server/models/Notification.ts b/server/models/Notification.ts index ff6d329a1..8166e082c 100644 --- a/server/models/Notification.ts +++ b/server/models/Notification.ts @@ -1,36 +1,55 @@ -import { DataTypes, sequelize } from "../sequelize"; +import { + Table, + ForeignKey, + Model, + Column, + PrimaryKey, + IsUUID, + CreatedAt, + BelongsTo, + DataType, + Default, +} from "sequelize-typescript"; +import User from "./User"; +import Fix from "./decorators/Fix"; -const Notification = sequelize.define( - "notification", - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - event: { - type: DataTypes.STRING, - }, - email: { - type: DataTypes.BOOLEAN, - }, - }, - { - timestamps: true, - updatedAt: false, - } -); +@Table({ + tableName: "notifications", + modelName: "notification", + updatedAt: false, +}) +@Fix +class Notification extends Model { + @IsUUID(4) + @PrimaryKey + @Default(DataType.UUIDV4) + @Column(DataType.UUID) + id: string; -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -Notification.associate = (models) => { - Notification.belongsTo(models.User, { - as: "actor", - foreignKey: "actorId", - }); - Notification.belongsTo(models.User, { - as: "user", - foreignKey: "userId", - }); -}; + @CreatedAt + createdAt: Date; + + @Column + event: string; + + @Column + email: boolean; + + // associations + + @BelongsTo(() => User, "userId") + user: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + userId: string; + + @BelongsTo(() => User, "actorId") + actor: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + actorId: string; +} export default Notification; diff --git a/server/models/NotificationSetting.ts b/server/models/NotificationSetting.ts index 6650d3a0b..d794ad98f 100644 --- a/server/models/NotificationSetting.ts +++ b/server/models/NotificationSetting.ts @@ -1,62 +1,81 @@ import crypto from "crypto"; -import { DataTypes, sequelize } from "../sequelize"; +import { + Table, + ForeignKey, + Model, + Column, + PrimaryKey, + IsUUID, + CreatedAt, + BelongsTo, + IsIn, + Default, + DataType, +} from "sequelize-typescript"; +import Team from "./Team"; +import User from "./User"; +import Fix from "./decorators/Fix"; -const NotificationSetting = sequelize.define( - "notification_setting", - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - event: { - type: DataTypes.STRING, - validate: { - isIn: [ - [ - "documents.publish", - "documents.update", - "collections.create", - "emails.onboarding", - "emails.features", - ], - ], - }, - }, - }, - { - timestamps: true, - updatedAt: false, - getterMethods: { - unsubscribeUrl: function () { - const token = NotificationSetting.getUnsubscribeToken(this.userId); - return `${process.env.URL}/api/notificationSettings.unsubscribe?token=${token}&id=${this.id}`; - }, - unsubscribeToken: function () { - return NotificationSetting.getUnsubscribeToken(this.userId); - }, - }, +@Table({ + tableName: "notification_settings", + modelName: "notification_setting", + updatedAt: false, +}) +@Fix +class NotificationSetting extends Model { + @IsUUID(4) + @PrimaryKey + @Default(DataType.UUIDV4) + @Column + id: string; + + @CreatedAt + createdAt: Date; + + @IsIn([ + [ + "documents.publish", + "documents.update", + "collections.create", + "emails.onboarding", + "emails.features", + ], + ]) + @Column(DataType.STRING) + event: string; + + // getters + + get unsubscribeUrl() { + const token = NotificationSetting.getUnsubscribeToken(this.userId); + return `${process.env.URL}/api/notificationSettings.unsubscribe?token=${token}&id=${this.id}`; } -); -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'userId' implicitly has an 'any' type. -NotificationSetting.getUnsubscribeToken = (userId) => { - const hash = crypto.createHash("sha256"); - hash.update(`${userId}-${process.env.SECRET_KEY}`); - return hash.digest("hex"); -}; + get unsubscribeToken() { + return NotificationSetting.getUnsubscribeToken(this.userId); + } -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -NotificationSetting.associate = (models) => { - NotificationSetting.belongsTo(models.User, { - as: "user", - foreignKey: "userId", - onDelete: "cascade", - }); - NotificationSetting.belongsTo(models.Team, { - as: "team", - foreignKey: "teamId", - }); -}; + // associations + + @BelongsTo(() => User, "userId") + user: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + userId: string; + + @BelongsTo(() => Team, "teamId") + team: Team; + + @ForeignKey(() => Team) + @Column(DataType.UUID) + teamId: string; + + static getUnsubscribeToken = (userId: string) => { + const hash = crypto.createHash("sha256"); + hash.update(`${userId}-${process.env.SECRET_KEY}`); + return hash.digest("hex"); + }; +} export default NotificationSetting; diff --git a/server/models/Pin.ts b/server/models/Pin.ts index 4a33ac08e..1730ab8be 100644 --- a/server/models/Pin.ts +++ b/server/models/Pin.ts @@ -1,50 +1,52 @@ -import { DataTypes, sequelize } from "../sequelize"; +import { + DataType, + Column, + ForeignKey, + BelongsTo, + Table, +} from "sequelize-typescript"; +import Collection from "./Collection"; +import Document from "./Document"; +import Team from "./Team"; +import User from "./User"; +import BaseModel from "./base/BaseModel"; +import Fix from "./decorators/Fix"; -const Pin = sequelize.define( - "pins", - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - teamId: { - type: DataTypes.UUID, - }, - documentId: { - type: DataTypes.UUID, - }, - collectionId: { - type: DataTypes.UUID, - defaultValue: null, - }, - index: { - type: DataTypes.STRING, - defaultValue: null, - }, - }, - { - timestamps: true, - } -); +@Table({ tableName: "pins", modelName: "pin" }) +@Fix +class Pin extends BaseModel { + @Column + index: string | null; -Pin.associate = (models: any) => { - Pin.belongsTo(models.Document, { - as: "document", - foreignKey: "documentId", - }); - Pin.belongsTo(models.Collection, { - as: "collection", - foreignKey: "collectionId", - }); - Pin.belongsTo(models.Team, { - as: "team", - foreignKey: "teamId", - }); - Pin.belongsTo(models.User, { - as: "createdBy", - foreignKey: "createdById", - }); -}; + // associations + + @BelongsTo(() => User, "createdById") + createdBy: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + createdById: string; + + @BelongsTo(() => Collection, "collectionId") + collection: Collection; + + @ForeignKey(() => Collection) + @Column(DataType.UUID) + collectionId: string; + + @BelongsTo(() => Document, "documentId") + document: Document; + + @ForeignKey(() => Document) + @Column(DataType.UUID) + documentId: string; + + @BelongsTo(() => Team, "teamId") + team: Team; + + @ForeignKey(() => Team) + @Column(DataType.UUID) + teamId: string; +} export default Pin; diff --git a/server/models/Revision.test.ts b/server/models/Revision.test.ts index 298b9db02..e893038d1 100644 --- a/server/models/Revision.test.ts +++ b/server/models/Revision.test.ts @@ -1,9 +1,10 @@ -import { Revision } from "@server/models"; import { buildDocument } from "@server/test/factories"; import { flushdb } from "@server/test/support"; +import Revision from "./Revision"; beforeEach(() => flushdb()); beforeEach(jest.resetAllMocks); + describe("#findLatest", () => { test("should return latest revision", async () => { const document = await buildDocument({ @@ -18,7 +19,7 @@ describe("#findLatest", () => { await document.save(); await Revision.createFromDocument(document); const revision = await Revision.findLatest(document.id); - expect(revision.title).toBe("Changed 2"); - expect(revision.text).toBe("Content"); + expect(revision?.title).toBe("Changed 2"); + expect(revision?.text).toBe("Content"); }); }); diff --git a/server/models/Revision.ts b/server/models/Revision.ts index d2b44fdae..09fb485e8 100644 --- a/server/models/Revision.ts +++ b/server/models/Revision.ts @@ -1,103 +1,117 @@ -// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'slat... Remove this comment to see the full error message +import { FindOptions } from "sequelize"; +import { + DataType, + BelongsTo, + Column, + DefaultScope, + ForeignKey, + Table, +} from "sequelize-typescript"; import MarkdownSerializer from "slate-md-serializer"; -import { DataTypes, sequelize } from "../sequelize"; +import Document from "./Document"; +import User from "./User"; +import BaseModel from "./base/BaseModel"; +import Fix from "./decorators/Fix"; const serializer = new MarkdownSerializer(); -const Revision = sequelize.define("revision", { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - version: DataTypes.SMALLINT, - editorVersion: DataTypes.STRING, - title: DataTypes.STRING, - text: DataTypes.TEXT, -}); -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -Revision.associate = (models) => { - Revision.belongsTo(models.Document, { - as: "document", - foreignKey: "documentId", - onDelete: "cascade", - }); - Revision.belongsTo(models.User, { - as: "user", - foreignKey: "userId", - }); - Revision.addScope( - "defaultScope", +@DefaultScope(() => ({ + include: [ { - include: [ - { - model: models.User, - as: "user", - paranoid: false, - }, - ], + model: User, + as: "user", + paranoid: false, }, - { - override: true, + ], +})) +@Table({ tableName: "revisions", modelName: "revision" }) +@Fix +class Revision extends BaseModel { + @Column(DataType.SMALLINT) + version: number; + + @Column + editorVersion: string; + + @Column + title: string; + + @Column(DataType.TEXT) + text: string; + + // associations + + @BelongsTo(() => Document, "documentId") + document: Document; + + @ForeignKey(() => Document) + @Column(DataType.UUID) + documentId: string; + + @BelongsTo(() => User, "userId") + user: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + userId: string; + + static findLatest(documentId: string) { + return this.findOne({ + where: { + documentId, + }, + order: [["createdAt", "DESC"]], + }); + } + + static createFromDocument( + document: Document, + options?: FindOptions + ) { + return this.create( + { + title: document.title, + text: document.text, + userId: document.lastModifiedById, + editorVersion: document.editorVersion, + version: document.version, + documentId: document.id, + // revision time is set to the last time document was touched as this + // handler can be debounced in the case of an update + createdAt: document.updatedAt, + }, + options + ); + } + + migrateVersion = function () { + let migrated = false; + + // migrate from document version 0 -> 1 + if (!this.version) { + // removing the title from the document text attribute + this.text = this.text.replace(/^#\s(.*)\n/, ""); + this.version = 1; + migrated = true; } - ); -}; -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documentId' implicitly has an 'any' typ... Remove this comment to see the full error message -Revision.findLatest = function (documentId) { - return Revision.findOne({ - where: { - documentId, - }, - order: [["createdAt", "DESC"]], - }); -}; + // migrate from document version 1 -> 2 + if (this.version === 1) { + const nodes = serializer.deserialize(this.text); + this.text = serializer.serialize(nodes, { + version: 2, + }); + this.version = 2; + migrated = true; + } -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type. -Revision.createFromDocument = function (document, options) { - return Revision.create( - { - title: document.title, - text: document.text, - userId: document.lastModifiedById, - editorVersion: document.editorVersion, - version: document.version, - documentId: document.id, - // revision time is set to the last time document was touched as this - // handler can be debounced in the case of an update - createdAt: document.updatedAt, - }, - options - ); -}; - -Revision.prototype.migrateVersion = function () { - let migrated = false; - - // migrate from document version 0 -> 1 - if (!this.version) { - // removing the title from the document text attribute - this.text = this.text.replace(/^#\s(.*)\n/, ""); - this.version = 1; - migrated = true; - } - - // migrate from document version 1 -> 2 - if (this.version === 1) { - const nodes = serializer.deserialize(this.text); - this.text = serializer.serialize(nodes, { - version: 2, - }); - this.version = 2; - migrated = true; - } - - if (migrated) { - return this.save({ - silent: true, - hooks: false, - }); - } -}; + if (migrated) { + return this.save({ + silent: true, + hooks: false, + }); + } + }; +} export default Revision; diff --git a/server/models/SearchQuery.ts b/server/models/SearchQuery.ts index 8dd11ab76..5ada3547b 100644 --- a/server/models/SearchQuery.ts +++ b/server/models/SearchQuery.ts @@ -1,48 +1,65 @@ -import { DataTypes, sequelize } from "../sequelize"; +import { + Table, + ForeignKey, + Model, + Column, + PrimaryKey, + IsUUID, + CreatedAt, + BelongsTo, + DataType, + Default, +} from "sequelize-typescript"; +import Team from "./Team"; +import User from "./User"; +import Fix from "./decorators/Fix"; -const SearchQuery = sequelize.define( - "search_queries", - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - source: { - type: DataTypes.ENUM("slack", "app", "api"), - allowNull: false, - }, - query: { - type: DataTypes.STRING, +@Table({ + tableName: "search_queries", + modelName: "search_query", + updatedAt: false, +}) +@Fix +class SearchQuery extends Model { + @IsUUID(4) + @PrimaryKey + @Default(DataType.UUIDV4) + @Column(DataType.UUID) + id: string; - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'val' implicitly has an 'any' type. - set(val) { - this.setDataValue("query", val.substring(0, 255)); - }, + @CreatedAt + createdAt: Date; - allowNull: false, - }, - results: { - type: DataTypes.NUMBER, - allowNull: false, - }, - }, - { - timestamps: true, - updatedAt: false, + @Column(DataType.ENUM("slack", "app", "api")) + source: string; + + @Column + results: number; + + @Column(DataType.STRING) + set query(value: string) { + this.setDataValue("query", value.substring(0, 255)); } -); -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -SearchQuery.associate = (models) => { - SearchQuery.belongsTo(models.User, { - as: "user", - foreignKey: "userId", - }); - SearchQuery.belongsTo(models.Team, { - as: "team", - foreignKey: "teamId", - }); -}; + get query() { + return this.getDataValue("query"); + } + + // associations + + @BelongsTo(() => User, "userId") + user: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + userId: string; + + @BelongsTo(() => Team, "teamId") + team: Team; + + @ForeignKey(() => Team) + @Column(DataType.UUID) + teamId: string; +} export default SearchQuery; diff --git a/server/models/Share.ts b/server/models/Share.ts index 76645f7dc..45c897c35 100644 --- a/server/models/Share.ts +++ b/server/models/Share.ts @@ -1,67 +1,45 @@ -import { DataTypes, sequelize } from "../sequelize"; +import { + ForeignKey, + BelongsTo, + Column, + DefaultScope, + Table, + Scopes, + DataType, +} from "sequelize-typescript"; +import Collection from "./Collection"; +import Document from "./Document"; +import Team from "./Team"; +import User from "./User"; +import BaseModel from "./base/BaseModel"; +import Fix from "./decorators/Fix"; -const Share = sequelize.define( - "share", - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, +@DefaultScope(() => ({ + include: [ + { + association: "user", + paranoid: false, }, - published: DataTypes.BOOLEAN, - includeChildDocuments: DataTypes.BOOLEAN, - revokedAt: DataTypes.DATE, - revokedById: DataTypes.UUID, - lastAccessedAt: DataTypes.DATE, - }, - { - getterMethods: { - isRevoked() { - return !!this.revokedAt; - }, + { + association: "document", + required: false, }, - } -); - -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -Share.associate = (models) => { - Share.belongsTo(models.User, { - as: "user", - foreignKey: "userId", - }); - Share.belongsTo(models.Team, { - as: "team", - foreignKey: "teamId", - }); - Share.belongsTo(models.Document.scope("withUnpublished"), { - as: "document", - foreignKey: "documentId", - }); - Share.addScope("defaultScope", { - include: [ - { - association: "user", - paranoid: false, - }, - { - association: "document", - }, - { - association: "team", - }, - ], - }); - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'userId' implicitly has an 'any' type. - Share.addScope("withCollection", (userId) => { + { + association: "team", + }, + ], +})) +@Scopes(() => ({ + withCollection: (userId: string) => { return { include: [ { - model: models.Document, + model: Document, paranoid: true, as: "document", include: [ { - model: models.Collection.scope({ + model: Collection.scope({ method: ["withMembership", userId], }), as: "collection", @@ -77,14 +55,64 @@ Share.associate = (models) => { }, ], }; - }); -}; + }, +})) +@Table({ tableName: "shares", modelName: "share" }) +@Fix +class Share extends BaseModel { + @Column + published: boolean; -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'userId' implicitly has an 'any' type. -Share.prototype.revoke = function (userId) { - this.revokedAt = new Date(); - this.revokedById = userId; - return this.save(); -}; + @Column + includeChildDocuments: boolean; + + @Column + revokedAt: Date | null; + + @Column + lastAccessedAt: Date | null; + + // getters + + get isRevoked() { + return !!this.revokedAt; + } + + // associations + + @BelongsTo(() => User, "revokedById") + revokedBy: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + revokedById: string; + + @BelongsTo(() => User, "userId") + user: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + userId: string; + + @BelongsTo(() => Team, "teamId") + team: Team; + + @ForeignKey(() => Team) + @Column(DataType.UUID) + teamId: string; + + @BelongsTo(() => Document, "documentId") + document: Document; + + @ForeignKey(() => Document) + @Column(DataType.UUID) + documentId: string; + + revoke(userId: string) { + this.revokedAt = new Date(); + this.revokedById = userId; + return this.save(); + } +} export default Share; diff --git a/server/models/Star.ts b/server/models/Star.ts index f938f41ae..c0e00ac01 100644 --- a/server/models/Star.ts +++ b/server/models/Star.ts @@ -1,17 +1,31 @@ -import { DataTypes, sequelize } from "../sequelize"; +import { + Column, + DataType, + BelongsTo, + ForeignKey, + Table, +} from "sequelize-typescript"; +import Document from "./Document"; +import User from "./User"; +import BaseModel from "./base/BaseModel"; +import Fix from "./decorators/Fix"; -const Star = sequelize.define("star", { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, -}); +@Table({ tableName: "stars", modelName: "star" }) +@Fix +class Star extends BaseModel { + @BelongsTo(() => User, "userId") + user: User; -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -Star.associate = (models) => { - Star.belongsTo(models.Document); - Star.belongsTo(models.User); -}; + @ForeignKey(() => User) + @Column(DataType.UUID) + userId: string; + + @BelongsTo(() => Document, "documentId") + document: Document; + + @ForeignKey(() => Document) + @Column(DataType.UUID) + documentId: string; +} export default Star; diff --git a/server/models/Team.test.ts b/server/models/Team.test.ts index 586e21e04..e64f3eb13 100644 --- a/server/models/Team.test.ts +++ b/server/models/Team.test.ts @@ -2,6 +2,7 @@ import { buildTeam, buildCollection } from "@server/test/factories"; import { flushdb } from "@server/test/support"; beforeEach(() => flushdb()); + describe("collectionIds", () => { it("should return non-private collection ids", async () => { const team = await buildTeam(); @@ -20,6 +21,7 @@ describe("collectionIds", () => { expect(response[0]).toEqual(collection.id); }); }); + describe("provisionSubdomain", () => { it("should set subdomain if available", async () => { const team = await buildTeam(); diff --git a/server/models/Team.ts b/server/models/Team.ts index 2c223d54f..c25f6a06d 100644 --- a/server/models/Team.ts +++ b/server/models/Team.ts @@ -2,258 +2,246 @@ import fs from "fs"; import path from "path"; import { URL } from "url"; import util from "util"; +import { Op } from "sequelize"; +import { + Column, + IsLowercase, + NotIn, + Default, + Table, + Unique, + IsIn, + BeforeSave, + HasMany, + Scopes, + Length, + Is, + DataType, +} from "sequelize-typescript"; import { v4 as uuidv4 } from "uuid"; import { stripSubdomain, RESERVED_SUBDOMAINS } from "@shared/utils/domains"; import Logger from "@server/logging/logger"; import { generateAvatarUrl } from "@server/utils/avatars"; import { publicS3Endpoint, uploadToS3FromUrl } from "@server/utils/s3"; -import { DataTypes, sequelize, Op } from "../sequelize"; +import AuthenticationProvider from "./AuthenticationProvider"; import Collection from "./Collection"; import Document from "./Document"; +import User from "./User"; +import ParanoidModel from "./base/ParanoidModel"; +import Fix from "./decorators/Fix"; const readFile = util.promisify(fs.readFile); -const Team = sequelize.define( - "team", - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - name: DataTypes.STRING, - subdomain: { - type: DataTypes.STRING, - allowNull: true, - validate: { - isLowercase: true, - is: { - args: [/^[a-z\d-]+$/, "i"], - msg: "Must be only alphanumeric and dashes", - }, - len: { - args: [4, 32], - msg: "Must be between 4 and 32 characters", - }, - notIn: { - args: [RESERVED_SUBDOMAINS], - msg: "You chose a restricted word, please try another.", - }, - }, - unique: true, - }, - domain: { - type: DataTypes.STRING, - allowNull: true, - unique: true, - }, - slackId: { - type: DataTypes.STRING, - allowNull: true, - }, - googleId: { - type: DataTypes.STRING, - allowNull: true, - }, - avatarUrl: { - type: DataTypes.STRING, - allowNull: true, - }, - sharing: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: true, - }, - signupQueryParams: { - type: DataTypes.JSONB, - allowNull: true, - }, - guestSignin: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: true, - }, - documentEmbeds: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: true, - }, - collaborativeEditing: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - }, - defaultUserRole: { - type: DataTypes.STRING, - defaultValue: "member", - allowNull: false, - validate: { - isIn: [["viewer", "member"]], - }, - }, - }, - { - paranoid: true, - getterMethods: { - url() { - if (this.domain) { - return `https://${this.domain}`; - } - - if (!this.subdomain || process.env.SUBDOMAINS_ENABLED !== "true") { - return process.env.URL; - } - - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string | undefined' is not assig... Remove this comment to see the full error message - const url = new URL(process.env.URL); - url.host = `${this.subdomain}.${stripSubdomain(url.host)}`; - return url.href.replace(/\/$/, ""); - }, - - logoUrl() { - return ( - this.avatarUrl || - generateAvatarUrl({ - id: this.id, - name: this.name, - }) - ); - }, - }, - } -); - -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -Team.associate = (models) => { - Team.hasMany(models.Collection, { - as: "collections", - }); - Team.hasMany(models.Document, { - as: "documents", - }); - Team.hasMany(models.User, { - as: "users", - }); - Team.hasMany(models.AuthenticationProvider, { - as: "authenticationProviders", - }); - Team.addScope("withAuthenticationProviders", { +@Scopes(() => ({ + withAuthenticationProviders: { include: [ { - model: models.AuthenticationProvider, + model: AuthenticationProvider, as: "authenticationProviders", }, ], - }); -}; + }, +})) +@Table({ tableName: "teams", modelName: "team" }) +@Fix +class Team extends ParanoidModel { + @Column + name: string; -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'model' implicitly has an 'any' type. -const uploadAvatar = async (model) => { - const endpoint = publicS3Endpoint(); - const { avatarUrl } = model; + @IsLowercase + @Unique + @Length({ min: 4, max: 32, msg: "Must be between 4 and 32 characters" }) + @Is({ + args: [/^[a-z\d-]+$/, "i"], + msg: "Must be only alphanumeric and dashes", + }) + @NotIn({ + args: [RESERVED_SUBDOMAINS], + msg: "You chose a restricted word, please try another.", + }) + @Column + subdomain: string | null; - if ( - avatarUrl && - !avatarUrl.startsWith("/api") && - !avatarUrl.startsWith(endpoint) - ) { - try { - const newUrl = await uploadToS3FromUrl( - avatarUrl, - `avatars/${model.id}/${uuidv4()}`, - "public-read" - ); - if (newUrl) model.avatarUrl = newUrl; - } catch (err) { - Logger.error("Error uploading avatar to S3", err, { - url: avatarUrl, - }); + @Unique + @Column + domain: string | null; + + @Column + avatarUrl: string | null; + + @Default(true) + @Column + sharing: boolean; + + @Default(true) + @Column(DataType.JSONB) + signupQueryParams: { [key: string]: string } | null; + + @Default(true) + @Column + guestSignin: boolean; + + @Default(true) + @Column + documentEmbeds: boolean; + + @Default(false) + @Column + collaborativeEditing: boolean; + + @Default("member") + @IsIn([["viewer", "member"]]) + @Column + defaultUserRole: string; + + // getters + + get url() { + if (this.domain) { + return `https://${this.domain}`; } - } -}; -Team.prototype.provisionSubdomain = async function ( - requestedSubdomain: string, - options = {} -) { - if (this.subdomain) return this.subdomain; - let subdomain = requestedSubdomain; - let append = 0; - - for (;;) { - try { - await this.update( - { - subdomain, - }, - options - ); - break; - } catch (err) { - // subdomain was invalid or already used, try again - subdomain = `${requestedSubdomain}${++append}`; + if (!this.subdomain || process.env.SUBDOMAINS_ENABLED !== "true") { + return process.env.URL; } + + const url = new URL(process.env.URL || ""); + url.host = `${this.subdomain}.${stripSubdomain(url.host)}`; + return url.href.replace(/\/$/, ""); } - return subdomain; -}; - -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'userId' implicitly has an 'any' type. -Team.prototype.provisionFirstCollection = async function (userId) { - const collection = await Collection.create({ - name: "Welcome", - description: - "This collection is a quick guide to what Outline is all about. Feel free to delete this collection once your team is up to speed with the basics!", - teamId: this.id, - createdById: userId, - sort: Collection.DEFAULT_SORT, - permission: "read_write", - }); - // For the first collection we go ahead and create some intitial documents to get - // the team started. You can edit these in /server/onboarding/x.md - const onboardingDocs = [ - "Integrations & API", - "Our Editor", - "Getting Started", - "What is Outline", - ]; - - for (const title of onboardingDocs) { - const text = await readFile( - path.join(process.cwd(), "server", "onboarding", `${title}.md`), - "utf8" + get logoUrl() { + return ( + this.avatarUrl || + generateAvatarUrl({ + id: this.id, + name: this.name, + }) ); - const document = await Document.create({ - version: 2, - isWelcome: true, - parentDocumentId: null, - collectionId: collection.id, - teamId: collection.teamId, - userId: collection.createdById, - lastModifiedById: collection.createdById, - createdById: collection.createdById, - title, - text, - }); - await document.publish(collection.createdById); } -}; -Team.prototype.collectionIds = async function (paranoid = true) { - const models = await Collection.findAll({ - attributes: ["id"], - where: { + // TODO: Move to command + provisionSubdomain = async function ( + requestedSubdomain: string, + options = {} + ) { + if (this.subdomain) return this.subdomain; + let subdomain = requestedSubdomain; + let append = 0; + + for (;;) { + try { + await this.update( + { + subdomain, + }, + options + ); + break; + } catch (err) { + // subdomain was invalid or already used, try again + subdomain = `${requestedSubdomain}${++append}`; + } + } + + return subdomain; + }; + + provisionFirstCollection = async function (userId: string) { + const collection = await Collection.create({ + name: "Welcome", + description: + "This collection is a quick guide to what Outline is all about. Feel free to delete this collection once your team is up to speed with the basics!", teamId: this.id, - permission: { - [Op.ne]: null, - }, - }, - paranoid, - }); - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'c' implicitly has an 'any' type. - return models.map((c) => c.id); -}; + createdById: userId, + sort: Collection.DEFAULT_SORT, + permission: "read_write", + }); -Team.beforeSave(uploadAvatar); + // For the first collection we go ahead and create some intitial documents to get + // the team started. You can edit these in /server/onboarding/x.md + const onboardingDocs = [ + "Integrations & API", + "Our Editor", + "Getting Started", + "What is Outline", + ]; + + for (const title of onboardingDocs) { + const text = await readFile( + path.join(process.cwd(), "server", "onboarding", `${title}.md`), + "utf8" + ); + const document = await Document.create({ + version: 2, + isWelcome: true, + parentDocumentId: null, + collectionId: collection.id, + teamId: collection.teamId, + userId: collection.createdById, + lastModifiedById: collection.createdById, + createdById: collection.createdById, + title, + text, + }); + await document.publish(collection.createdById); + } + }; + + collectionIds = async function (paranoid = true) { + const models = await Collection.findAll({ + attributes: ["id"], + where: { + teamId: this.id, + permission: { + [Op.ne]: null, + }, + }, + paranoid, + }); + return models.map((c) => c.id); + }; + + // associations + + @HasMany(() => Collection) + collections: Collection[]; + + @HasMany(() => Document) + documents: Document[]; + + @HasMany(() => User) + users: User[]; + + @HasMany(() => AuthenticationProvider) + authenticationProviders: AuthenticationProvider[]; + + // hooks + + @BeforeSave + static uploadAvatar = async (model: Team) => { + const endpoint = publicS3Endpoint(); + const { avatarUrl } = model; + + if ( + avatarUrl && + !avatarUrl.startsWith("/api") && + !avatarUrl.startsWith(endpoint) + ) { + try { + const newUrl = await uploadToS3FromUrl( + avatarUrl, + `avatars/${model.id}/${uuidv4()}`, + "public-read" + ); + if (newUrl) model.avatarUrl = newUrl; + } catch (err) { + Logger.error("Error uploading avatar to S3", err, { + url: avatarUrl, + }); + } + } + }; +} export default Team; diff --git a/server/models/User.test.ts b/server/models/User.test.ts index 13c977c46..3a4121a2b 100644 --- a/server/models/User.test.ts +++ b/server/models/User.test.ts @@ -1,8 +1,10 @@ -import { UserAuthentication, CollectionUser } from "@server/models"; import { buildUser, buildTeam, buildCollection } from "@server/test/factories"; import { flushdb } from "@server/test/support"; +import CollectionUser from "./CollectionUser"; +import UserAuthentication from "./UserAuthentication"; beforeEach(() => flushdb()); + describe("user model", () => { describe("destroy", () => { it("should delete user authentications", async () => { diff --git a/server/models/User.ts b/server/models/User.ts index 1d13633da..70377f116 100644 --- a/server/models/User.ts +++ b/server/models/User.ts @@ -1,6 +1,25 @@ import crypto from "crypto"; import { addMinutes, subMinutes } from "date-fns"; import JWT from "jsonwebtoken"; +import { Transaction, QueryTypes, FindOptions, Op } from "sequelize"; +import { + Table, + Column, + IsIP, + IsEmail, + HasOne, + Default, + IsIn, + BeforeDestroy, + BeforeSave, + BeforeCreate, + AfterCreate, + BelongsTo, + ForeignKey, + DataType, + HasMany, + Scopes, +} from "sequelize-typescript"; import { v4 as uuidv4 } from "uuid"; import { languages } from "@shared/i18n"; import Logger from "@server/logging/logger"; @@ -8,427 +27,441 @@ import { DEFAULT_AVATAR_HOST } from "@server/utils/avatars"; import { palette } from "@server/utils/color"; import { publicS3Endpoint, uploadToS3FromUrl } from "@server/utils/s3"; import { ValidationError } from "../errors"; -import { DataTypes, sequelize, encryptedFields, Op } from "../sequelize"; -import { - UserAuthentication, - Star, - Collection, - NotificationSetting, - ApiKey, -} from "."; +import ApiKey from "./ApiKey"; +import Collection from "./Collection"; +import NotificationSetting from "./NotificationSetting"; +import Star from "./Star"; +import Team from "./Team"; +import UserAuthentication from "./UserAuthentication"; +import ParanoidModel from "./base/ParanoidModel"; +import Encrypted, { + setEncryptedColumn, + getEncryptedColumn, +} from "./decorators/Encrypted"; +import Fix from "./decorators/Fix"; -const User = sequelize.define( - "user", - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - email: { - type: DataTypes.STRING, - }, - username: { - type: DataTypes.STRING, - }, - name: DataTypes.STRING, - avatarUrl: { - type: DataTypes.STRING, - allowNull: true, - }, - isAdmin: DataTypes.BOOLEAN, - isViewer: { - type: DataTypes.BOOLEAN, - defaultValue: false, - allowNull: false, - }, - service: { - type: DataTypes.STRING, - allowNull: true, - }, - serviceId: { - type: DataTypes.STRING, - allowNull: true, - unique: true, - }, - jwtSecret: encryptedFields().vault("jwtSecret"), - lastActiveAt: DataTypes.DATE, - lastActiveIp: { - type: DataTypes.STRING, - allowNull: true, - }, - lastSignedInAt: DataTypes.DATE, - lastSignedInIp: { - type: DataTypes.STRING, - allowNull: true, - }, - lastSigninEmailSentAt: DataTypes.DATE, - suspendedAt: DataTypes.DATE, - suspendedById: DataTypes.UUID, - language: { - type: DataTypes.STRING, - defaultValue: process.env.DEFAULT_LANGUAGE, - validate: { - isIn: [languages], - }, - }, - }, - { - paranoid: true, - getterMethods: { - isSuspended() { - return !!this.suspendedAt; - }, - - isInvited() { - return !this.lastActiveAt; - }, - - avatarUrl() { - const original = this.getDataValue("avatarUrl"); - - if (original) { - return original; - } - - const color = this.color.replace(/^#/, ""); - const initial = this.name ? this.name[0] : "?"; - const hash = crypto - .createHash("md5") - .update(this.email || "") - .digest("hex"); - return `${DEFAULT_AVATAR_HOST}/avatar/${hash}/${initial}.png?c=${color}`; - }, - - color() { - const idAsHex = crypto.createHash("md5").update(this.id).digest("hex"); - const idAsNumber = parseInt(idAsHex, 16); - return palette[idAsNumber % palette.length]; - }, - }, - } -); - -// Class methods -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -User.associate = (models) => { - User.hasMany(models.ApiKey, { - as: "apiKeys", - onDelete: "cascade", - }); - User.hasMany(models.NotificationSetting, { - as: "notificationSettings", - onDelete: "cascade", - }); - User.hasMany(models.Document, { - as: "documents", - }); - User.hasMany(models.View, { - as: "views", - }); - User.hasMany(models.UserAuthentication, { - as: "authentications", - }); - User.belongsTo(models.Team); - User.addScope("withAuthentications", { +@Scopes(() => ({ + withAuthentications: { include: [ { - model: models.UserAuthentication, + model: UserAuthentication, as: "authentications", }, ], - }); -}; + }, +})) +@Table({ tableName: "users", modelName: "user" }) +@Fix +class User extends ParanoidModel { + @IsEmail + @Column + email: string | null; -// Instance methods -User.prototype.collectionIds = async function (options = {}) { - const collectionStubs = await Collection.scope({ - method: ["withMembership", this.id], - }).findAll({ - attributes: ["id", "permission"], - where: { - teamId: this.teamId, - }, - paranoid: true, - ...options, - }); - return ( - collectionStubs + @Column + username: string | null; + + @Column + name: string; + + @Default(false) + @Column + isAdmin: boolean; + + @Default(false) + @Column + isViewer: boolean; + + @Column(DataType.BLOB) + @Encrypted + get jwtSecret() { + return getEncryptedColumn(this, "jwtSecret"); + } + + set jwtSecret(value: string) { + setEncryptedColumn(this, "jwtSecret", value); + } + + @Column + lastActiveAt: Date | null; + + @IsIP + @Column + lastActiveIp: string | null; + + @Column + lastSignedInAt: Date | null; + + @IsIP + @Column + lastSignedInIp: string | null; + + @Column + lastSigninEmailSentAt: Date | null; + + @Column + suspendedAt: Date | null; + + @Default(process.env.DEFAULT_LANGUAGE) + @IsIn([languages]) + @Column + language: string; + + @Column(DataType.STRING) + get avatarUrl() { + const original = this.getDataValue("avatarUrl"); + + if (original) { + return original; + } + + const color = this.color.replace(/^#/, ""); + const initial = this.name ? this.name[0] : "?"; + const hash = crypto + .createHash("md5") + .update(this.email || "") + .digest("hex"); + return `${DEFAULT_AVATAR_HOST}/avatar/${hash}/${initial}.png?c=${color}`; + } + + set avatarUrl(value: string | null) { + this.setDataValue("avatarUrl", value); + } + + // associations + + @HasOne(() => User, "suspendedById") + suspendedBy: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + suspendedById: string; + + @BelongsTo(() => Team) + team: Team; + + @ForeignKey(() => Team) + @Column(DataType.UUID) + teamId: string; + + @HasMany(() => UserAuthentication) + authentications: UserAuthentication[]; + + // getters + + get isSuspended(): boolean { + return !!this.suspendedAt; + } + + get isInvited() { + return !this.lastActiveAt; + } + + get color() { + const idAsHex = crypto.createHash("md5").update(this.id).digest("hex"); + const idAsNumber = parseInt(idAsHex, 16); + return palette[idAsNumber % palette.length]; + } + + // instance methods + + collectionIds = async (options = {}) => { + const collectionStubs = await Collection.scope({ + method: ["withMembership", this.id], + }).findAll({ + attributes: ["id", "permission"], + where: { + teamId: this.teamId, + }, + paranoid: true, + ...options, + }); + + return collectionStubs .filter( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'c' implicitly has an 'any' type. (c) => c.permission === "read" || c.permission === "read_write" || c.memberships.length > 0 || c.collectionGroupMemberships.length > 0 ) - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'c' implicitly has an 'any' type. - .map((c) => c.id) - ); -}; + .map((c) => c.id); + }; -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'ip' implicitly has an 'any' type. -User.prototype.updateActiveAt = function (ip, force = false) { - const fiveMinutesAgo = subMinutes(new Date(), 5); + updateActiveAt = (ip: string, force = false) => { + const fiveMinutesAgo = subMinutes(new Date(), 5); - // ensure this is updated only every few minutes otherwise - // we'll be constantly writing to the DB as API requests happen - if (this.lastActiveAt < fiveMinutesAgo || force) { - this.lastActiveAt = new Date(); - this.lastActiveIp = ip; + // ensure this is updated only every few minutes otherwise + // we'll be constantly writing to the DB as API requests happen + if (!this.lastActiveAt || this.lastActiveAt < fiveMinutesAgo || force) { + this.lastActiveAt = new Date(); + this.lastActiveIp = ip; + + return this.save({ + hooks: false, + }); + } + + return this; + }; + + updateSignedIn = (ip: string) => { + this.lastSignedInAt = new Date(); + this.lastSignedInIp = ip; return this.save({ hooks: false, }); - } -}; - -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'ip' implicitly has an 'any' type. -User.prototype.updateSignedIn = function (ip) { - this.lastSignedInAt = new Date(); - this.lastSignedInIp = ip; - return this.save({ - hooks: false, - }); -}; - -// Returns a session token that is used to make API requests and is stored -// in the client browser cookies to remain logged in. -User.prototype.getJwtToken = function (expiresAt?: Date) { - return JWT.sign( - { - id: this.id, - expiresAt: expiresAt ? expiresAt.toISOString() : undefined, - type: "session", - }, - this.jwtSecret - ); -}; - -// Returns a temporary token that is only used for transferring a session -// between subdomains or domains. It has a short expiry and can only be used once -User.prototype.getTransferToken = function () { - return JWT.sign( - { - id: this.id, - createdAt: new Date().toISOString(), - expiresAt: addMinutes(new Date(), 1).toISOString(), - type: "transfer", - }, - this.jwtSecret - ); -}; - -// Returns a temporary token that is only used for logging in from an email -// It can only be used to sign in once and has a medium length expiry -User.prototype.getEmailSigninToken = function () { - return JWT.sign( - { - id: this.id, - createdAt: new Date().toISOString(), - type: "email-signin", - }, - this.jwtSecret - ); -}; - -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'model' implicitly has an 'any' type. -const uploadAvatar = async (model) => { - const endpoint = publicS3Endpoint(); - const { avatarUrl } = model; - - if ( - avatarUrl && - !avatarUrl.startsWith("/api") && - !avatarUrl.startsWith(endpoint) && - !avatarUrl.startsWith(DEFAULT_AVATAR_HOST) - ) { - try { - const newUrl = await uploadToS3FromUrl( - avatarUrl, - `avatars/${model.id}/${uuidv4()}`, - "public-read" - ); - if (newUrl) model.avatarUrl = newUrl; - } catch (err) { - Logger.error("Couldn't upload user avatar image to S3", err, { - url: avatarUrl, - }); - } - } -}; - -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'model' implicitly has an 'any' type. -const setRandomJwtSecret = (model) => { - model.jwtSecret = crypto.randomBytes(64).toString("hex"); -}; - -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'model' implicitly has an 'any' type. -const removeIdentifyingInfo = async (model, options) => { - await NotificationSetting.destroy({ - where: { - userId: model.id, - }, - transaction: options.transaction, - }); - await ApiKey.destroy({ - where: { - userId: model.id, - }, - transaction: options.transaction, - }); - await Star.destroy({ - where: { - userId: model.id, - }, - transaction: options.transaction, - }); - await UserAuthentication.destroy({ - where: { - userId: model.id, - }, - transaction: options.transaction, - }); - model.email = null; - model.name = "Unknown"; - model.avatarUrl = ""; - model.serviceId = null; - model.username = null; - model.lastActiveIp = null; - model.lastSignedInIp = null; - // this shouldn't be needed once this issue is resolved: - // https://github.com/sequelize/sequelize/issues/9318 - await model.save({ - hooks: false, - transaction: options.transaction, - }); -}; - -User.beforeDestroy(removeIdentifyingInfo); -User.beforeSave(uploadAvatar); -User.beforeCreate(setRandomJwtSecret); -// By default when a user signs up we subscribe them to email notifications -// when documents they created are edited by other team members and onboarding -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'user' implicitly has an 'any' type. -User.afterCreate(async (user, options) => { - await Promise.all([ - NotificationSetting.findOrCreate({ - where: { - userId: user.id, - teamId: user.teamId, - event: "documents.update", - }, - transaction: options.transaction, - }), - NotificationSetting.findOrCreate({ - where: { - userId: user.id, - teamId: user.teamId, - event: "emails.onboarding", - }, - transaction: options.transaction, - }), - NotificationSetting.findOrCreate({ - where: { - userId: user.id, - teamId: user.teamId, - event: "emails.features", - }, - transaction: options.transaction, - }), - ]); -}); - -User.getCounts = async function (teamId: string) { - const countSql = ` - SELECT - COUNT(CASE WHEN "suspendedAt" IS NOT NULL THEN 1 END) as "suspendedCount", - COUNT(CASE WHEN "isAdmin" = true THEN 1 END) as "adminCount", - COUNT(CASE WHEN "isViewer" = true THEN 1 END) as "viewerCount", - COUNT(CASE WHEN "lastActiveAt" IS NULL THEN 1 END) as "invitedCount", - COUNT(CASE WHEN "suspendedAt" IS NULL AND "lastActiveAt" IS NOT NULL THEN 1 END) as "activeCount", - COUNT(*) as count - FROM users - WHERE "deletedAt" IS NULL - AND "teamId" = :teamId - `; - const results = await sequelize.query(countSql, { - type: sequelize.QueryTypes.SELECT, - replacements: { - teamId, - }, - }); - const counts = results[0]; - return { - active: parseInt(counts.activeCount), - admins: parseInt(counts.adminCount), - viewers: parseInt(counts.viewerCount), - all: parseInt(counts.count), - invited: parseInt(counts.invitedCount), - suspended: parseInt(counts.suspendedCount), }; -}; -User.findAllInBatches = async ( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'query' implicitly has an 'any' type. - query, - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message - callback: (users: Array, query: Record) => Promise -) => { - if (!query.offset) query.offset = 0; - if (!query.limit) query.limit = 10; - let results; - - do { - results = await User.findAll(query); - await callback(results, query); - query.offset += query.limit; - } while (results.length >= query.limit); -}; - -User.prototype.demote = async function ( - teamId: string, - to: "member" | "viewer" -) { - const res = await User.findAndCountAll({ - where: { - teamId, - isAdmin: true, - id: { - [Op.ne]: this.id, + // Returns a session token that is used to make API requests and is stored + // in the client browser cookies to remain logged in. + getJwtToken = (expiresAt?: Date) => { + return JWT.sign( + { + id: this.id, + expiresAt: expiresAt ? expiresAt.toISOString() : undefined, + type: "session", }, - }, - limit: 1, - }); + this.jwtSecret + ); + }; - if (res.count >= 1) { - if (to === "member") { - return this.update({ - isAdmin: false, - isViewer: false, - }); - } else if (to === "viewer") { - return this.update({ - isAdmin: false, - isViewer: true, - }); + // Returns a temporary token that is only used for transferring a session + // between subdomains or domains. It has a short expiry and can only be used once + getTransferToken = () => { + return JWT.sign( + { + id: this.id, + createdAt: new Date().toISOString(), + expiresAt: addMinutes(new Date(), 1).toISOString(), + type: "transfer", + }, + this.jwtSecret + ); + }; + + // Returns a temporary token that is only used for logging in from an email + // It can only be used to sign in once and has a medium length expiry + getEmailSigninToken = () => { + return JWT.sign( + { + id: this.id, + createdAt: new Date().toISOString(), + type: "email-signin", + }, + this.jwtSecret + ); + }; + + demote = async (teamId: string, to: "member" | "viewer") => { + const res = await (this.constructor as typeof User).findAndCountAll({ + where: { + teamId, + isAdmin: true, + id: { + [Op.ne]: this.id, + }, + }, + limit: 1, + }); + + if (res.count >= 1) { + if (to === "member") { + return this.update({ + isAdmin: false, + isViewer: false, + }); + } else if (to === "viewer") { + return this.update({ + isAdmin: false, + isViewer: true, + }); + } + + return undefined; + } else { + throw ValidationError("At least one admin is required"); } - } else { - throw ValidationError("At least one admin is required"); + }; + + promote = () => { + return this.update({ + isAdmin: true, + isViewer: false, + }); + }; + + activate = () => { + return this.update({ + suspendedById: null, + suspendedAt: null, + }); + }; + + // hooks + + @BeforeDestroy + static removeIdentifyingInfo = async ( + model: User, + options: { transaction: Transaction } + ) => { + await NotificationSetting.destroy({ + where: { + userId: model.id, + }, + transaction: options.transaction, + }); + await ApiKey.destroy({ + where: { + userId: model.id, + }, + transaction: options.transaction, + }); + await Star.destroy({ + where: { + userId: model.id, + }, + transaction: options.transaction, + }); + await UserAuthentication.destroy({ + where: { + userId: model.id, + }, + transaction: options.transaction, + }); + model.email = null; + model.name = "Unknown"; + model.avatarUrl = null; + model.username = null; + model.lastActiveIp = null; + model.lastSignedInIp = null; + + // this shouldn't be needed once this issue is resolved: + // https://github.com/sequelize/sequelize/issues/9318 + await model.save({ + hooks: false, + transaction: options.transaction, + }); + }; + + @BeforeSave + static uploadAvatar = async (model: User) => { + const endpoint = publicS3Endpoint(); + const { avatarUrl } = model; + + if ( + avatarUrl && + !avatarUrl.startsWith("/api") && + !avatarUrl.startsWith(endpoint) && + !avatarUrl.startsWith(DEFAULT_AVATAR_HOST) + ) { + try { + const newUrl = await uploadToS3FromUrl( + avatarUrl, + `avatars/${model.id}/${uuidv4()}`, + "public-read" + ); + if (newUrl) model.avatarUrl = newUrl; + } catch (err) { + Logger.error("Couldn't upload user avatar image to S3", err, { + url: avatarUrl, + }); + } + } + }; + + @BeforeCreate + static setRandomJwtSecret = (model: User) => { + model.jwtSecret = crypto.randomBytes(64).toString("hex"); + }; + + // By default when a user signs up we subscribe them to email notifications + // when documents they created are edited by other team members and onboarding + @AfterCreate + static subscribeToNotifications = async ( + model: User, + options: { transaction: Transaction } + ) => { + await Promise.all([ + NotificationSetting.findOrCreate({ + where: { + userId: model.id, + teamId: model.teamId, + event: "documents.update", + }, + transaction: options.transaction, + }), + NotificationSetting.findOrCreate({ + where: { + userId: model.id, + teamId: model.teamId, + event: "emails.onboarding", + }, + transaction: options.transaction, + }), + NotificationSetting.findOrCreate({ + where: { + userId: model.id, + teamId: model.teamId, + event: "emails.features", + }, + transaction: options.transaction, + }), + ]); + }; + + static getCounts = async function (teamId: string) { + const countSql = ` + SELECT + COUNT(CASE WHEN "suspendedAt" IS NOT NULL THEN 1 END) as "suspendedCount", + COUNT(CASE WHEN "isAdmin" = true THEN 1 END) as "adminCount", + COUNT(CASE WHEN "isViewer" = true THEN 1 END) as "viewerCount", + COUNT(CASE WHEN "lastActiveAt" IS NULL THEN 1 END) as "invitedCount", + COUNT(CASE WHEN "suspendedAt" IS NULL AND "lastActiveAt" IS NOT NULL THEN 1 END) as "activeCount", + COUNT(*) as count + FROM users + WHERE "deletedAt" IS NULL + AND "teamId" = :teamId + `; + const [results] = await this.sequelize.query(countSql, { + type: QueryTypes.SELECT, + replacements: { + teamId, + }, + }); + + const counts: { + activeCount: string; + adminCount: string; + invitedCount: string; + suspendedCount: string; + viewerCount: string; + count: string; + } = results as any; + + return { + active: parseInt(counts.activeCount), + admins: parseInt(counts.adminCount), + viewers: parseInt(counts.viewerCount), + all: parseInt(counts.count), + invited: parseInt(counts.invitedCount), + suspended: parseInt(counts.suspendedCount), + }; + }; + + static async findAllInBatches( + query: FindOptions, + callback: (users: Array, query: FindOptions) => Promise + ) { + if (!query.offset) query.offset = 0; + if (!query.limit) query.limit = 10; + let results; + + do { + results = await this.findAll(query); + await callback(results, query); + query.offset += query.limit; + } while (results.length >= query.limit); } -}; - -User.prototype.promote = async function () { - return this.update({ - isAdmin: true, - isViewer: false, - }); -}; - -User.prototype.activate = async function () { - return this.update({ - suspendedById: null, - suspendedAt: null, - }); -}; +} export default User; diff --git a/server/models/UserAuthentication.ts b/server/models/UserAuthentication.ts index 10adcd013..8e080e3a9 100644 --- a/server/models/UserAuthentication.ts +++ b/server/models/UserAuthentication.ts @@ -1,24 +1,65 @@ -import { DataTypes, sequelize, encryptedFields } from "../sequelize"; +import { + BelongsTo, + Column, + DataType, + ForeignKey, + Table, + Unique, +} from "sequelize-typescript"; +import AuthenticationProvider from "./AuthenticationProvider"; +import User from "./User"; +import BaseModel from "./base/BaseModel"; +import Encrypted, { + getEncryptedColumn, + setEncryptedColumn, +} from "./decorators/Encrypted"; +import Fix from "./decorators/Fix"; -const UserAuthentication = sequelize.define("user_authentications", { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - scopes: DataTypes.ARRAY(DataTypes.STRING), - accessToken: encryptedFields().vault("accessToken"), - refreshToken: encryptedFields().vault("refreshToken"), - providerId: { - type: DataTypes.STRING, - unique: true, - }, -}); +@Table({ tableName: "user_authentications", modelName: "user_authentication" }) +@Fix +class UserAuthentication extends BaseModel { + @Column(DataType.ARRAY(DataType.STRING)) + scopes: string[]; -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -UserAuthentication.associate = (models) => { - UserAuthentication.belongsTo(models.AuthenticationProvider); - UserAuthentication.belongsTo(models.User); -}; + @Column(DataType.BLOB) + @Encrypted + get accessToken() { + return getEncryptedColumn(this, "accessToken"); + } + + set accessToken(value: string) { + setEncryptedColumn(this, "accessToken", value); + } + + @Column(DataType.BLOB) + @Encrypted + get refreshToken() { + return getEncryptedColumn(this, "refreshToken"); + } + + set refreshToken(value: string) { + setEncryptedColumn(this, "refreshToken", value); + } + + @Column + providerId: string; + + // associations + + @BelongsTo(() => User, "userId") + user: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + userId: string; + + @BelongsTo(() => AuthenticationProvider, "providerId") + authenticationProvider: AuthenticationProvider; + + @ForeignKey(() => AuthenticationProvider) + @Unique + @Column(DataType.UUID) + authenticationProviderId: string; +} export default UserAuthentication; diff --git a/server/models/View.ts b/server/models/View.ts index e2c76f2ea..ef6161d84 100644 --- a/server/models/View.ts +++ b/server/models/View.ts @@ -1,85 +1,105 @@ import { subMilliseconds } from "date-fns"; +import { Op } from "sequelize"; +import { + BelongsTo, + Column, + Default, + ForeignKey, + Table, + DataType, +} from "sequelize-typescript"; import { USER_PRESENCE_INTERVAL } from "@shared/constants"; -import { User } from "@server/models"; -import { DataTypes, Op, sequelize } from "../sequelize"; +import Document from "./Document"; +import User from "./User"; +import BaseModel from "./base/BaseModel"; +import Fix from "./decorators/Fix"; -const View = sequelize.define("view", { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - lastEditingAt: { - type: DataTypes.DATE, - }, - count: { - type: DataTypes.INTEGER, - defaultValue: 1, - }, -}); +@Table({ tableName: "views", modelName: "view" }) +@Fix +class View extends BaseModel { + @Column + lastEditingAt: Date | null; -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type. -View.associate = (models) => { - View.belongsTo(models.Document); - View.belongsTo(models.User); -}; + @Default(1) + @Column(DataType.INTEGER) + count: number; -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'where' implicitly has an 'any' type. -View.increment = async (where) => { - const [model, created] = await View.findOrCreate({ - where, - }); + // associations - if (!created) { - model.count += 1; - model.save(); + @BelongsTo(() => User, "userId") + user: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + userId: string; + + @BelongsTo(() => Document, "documentId") + document: Document; + + @ForeignKey(() => Document) + @Column(DataType.UUID) + documentId: string; + + static async incrementOrCreate(where: { + userId?: string; + documentId?: string; + collectionId?: string; + }) { + const [model, created] = await this.findOrCreate({ + where, + }); + + if (!created) { + model.count += 1; + model.save(); + } + + return model; } - return model; -}; - -View.findByDocument = async (documentId: string) => { - return View.findAll({ - where: { - documentId, - }, - order: [["updatedAt", "DESC"]], - include: [ - { - model: User, - paranoid: false, + static async findByDocument(documentId: string) { + return this.findAll({ + where: { + documentId, }, - ], - }); -}; - -View.findRecentlyEditingByDocument = async (documentId: string) => { - return View.findAll({ - where: { - documentId, - lastEditingAt: { - [Op.gt]: subMilliseconds(new Date(), USER_PRESENCE_INTERVAL * 2), - }, - }, - order: [["lastEditingAt", "DESC"]], - }); -}; - -View.touch = async (documentId: string, userId: string, isEditing: boolean) => { - const [view] = await View.findOrCreate({ - where: { - userId, - documentId, - }, - }); - - if (isEditing) { - const lastEditingAt = new Date(); - view.lastEditingAt = lastEditingAt; - await view.save(); + order: [["updatedAt", "DESC"]], + include: [ + { + model: User, + paranoid: false, + }, + ], + }); } - return view; -}; + static async findRecentlyEditingByDocument(documentId: string) { + return this.findAll({ + where: { + documentId, + lastEditingAt: { + [Op.gt]: subMilliseconds(new Date(), USER_PRESENCE_INTERVAL * 2), + }, + }, + order: [["lastEditingAt", "DESC"]], + }); + } + + static async touch(documentId: string, userId: string, isEditing: boolean) { + const [view] = await this.findOrCreate({ + where: { + userId, + documentId, + }, + }); + + if (isEditing) { + const lastEditingAt = new Date(); + view.lastEditingAt = lastEditingAt; + await view.save(); + } + + return view; + } +} export default View; diff --git a/server/models/base/BaseModel.ts b/server/models/base/BaseModel.ts new file mode 100644 index 000000000..444a79d1f --- /dev/null +++ b/server/models/base/BaseModel.ts @@ -0,0 +1,26 @@ +import { + CreatedAt, + UpdatedAt, + Column, + PrimaryKey, + IsUUID, + DataType, + Model, + Default, +} from "sequelize-typescript"; + +class BaseModel extends Model { + @IsUUID(4) + @PrimaryKey + @Default(DataType.UUIDV4) + @Column(DataType.UUID) + id: string; + + @CreatedAt + createdAt: Date; + + @UpdatedAt + updatedAt: Date; +} + +export default BaseModel; diff --git a/server/models/base/ParanoidModel.ts b/server/models/base/ParanoidModel.ts new file mode 100644 index 000000000..94b46eb97 --- /dev/null +++ b/server/models/base/ParanoidModel.ts @@ -0,0 +1,9 @@ +import { DeletedAt } from "sequelize-typescript"; +import BaseModel from "./BaseModel"; + +class ParanoidModel extends BaseModel { + @DeletedAt + deletedAt: Date | null; +} + +export default ParanoidModel; diff --git a/server/models/decorators/Encrypted.ts b/server/models/decorators/Encrypted.ts new file mode 100644 index 000000000..1b917bed9 --- /dev/null +++ b/server/models/decorators/Encrypted.ts @@ -0,0 +1,30 @@ +import vaults from "@server/database/vaults"; + +const key = "sequelize:vault"; + +/** + * A decorator that stores the encrypted vault for a particular database column + * so that it can be used by getters and setters. Must be accompanied by a + * @Column(DataType.BLOB) annotation. + */ +export default function Encrypted(target: any, propertyKey: string) { + Reflect.defineMetadata(key, vaults().vault(propertyKey), target, propertyKey); +} + +/** + * Get the value of an encrypted column given the target and the property key. + */ +export function getEncryptedColumn(target: any, propertyKey: string): string { + return Reflect.getMetadata(key, target, propertyKey).get.call(target); +} + +/** + * Set the value of an encrypted column given the target and the property key. + */ +export function setEncryptedColumn( + target: any, + propertyKey: string, + value: string +) { + Reflect.getMetadata(key, target, propertyKey).set.call(target, value); +} diff --git a/server/models/decorators/Fix.ts b/server/models/decorators/Fix.ts new file mode 100644 index 000000000..0376b3e6d --- /dev/null +++ b/server/models/decorators/Fix.ts @@ -0,0 +1,50 @@ +/** + * A decorator that must be applied to every model definition to workaround + * babel <> typescript incompatibility. See the following issue: + * https://github.com/RobinBuschmann/sequelize-typescript/issues/612#issuecomment-491890977 + * + * @param target model class + */ + +export default function Fix(target: any): void { + return class extends target { + constructor(...args: any[]) { + super(...args); + + const rawAttributes = Object.keys(new.target.rawAttributes); + const associations = Object.keys(new.target.associations); + + rawAttributes.forEach((propertyKey) => { + // check if we already defined getter/setter – if so, do not override + const desc = Object.getOwnPropertyDescriptor( + target.prototype, + propertyKey + ); + if (desc) { + return; + } + + Object.defineProperty(this, propertyKey, { + get() { + return this.getDataValue(propertyKey); + }, + set(value) { + this.setDataValue(propertyKey, value); + }, + }); + }); + + associations.forEach((propertyKey) => { + Object.defineProperty(this, propertyKey, { + get() { + return this.dataValues[propertyKey]; + }, + set(value) { + // sets without changing the "changed" flag for associations + this.dataValues[propertyKey] = value; + }, + }); + }); + } + } as any; +} diff --git a/server/models/index.ts b/server/models/index.ts index 5b40e7737..1d8c8e4e6 100644 --- a/server/models/index.ts +++ b/server/models/index.ts @@ -1,88 +1,49 @@ -import ApiKey from "./ApiKey"; -import Attachment from "./Attachment"; -import AuthenticationProvider from "./AuthenticationProvider"; -import Backlink from "./Backlink"; -import Collection from "./Collection"; -import CollectionGroup from "./CollectionGroup"; -import CollectionUser from "./CollectionUser"; -import Document from "./Document"; -import Event from "./Event"; -import FileOperation from "./FileOperation"; -import Group from "./Group"; -import GroupUser from "./GroupUser"; -import Integration from "./Integration"; -import IntegrationAuthentication from "./IntegrationAuthentication"; -import Notification from "./Notification"; -import NotificationSetting from "./NotificationSetting"; -import Pin from "./Pin"; -import Revision from "./Revision"; -import SearchQuery from "./SearchQuery"; -import Share from "./Share"; -import Star from "./Star"; -import Team from "./Team"; -import User from "./User"; -import UserAuthentication from "./UserAuthentication"; -import View from "./View"; +export { default as ApiKey } from "./ApiKey"; -const models = { - ApiKey, - Attachment, - AuthenticationProvider, - Backlink, - Collection, - CollectionGroup, - CollectionUser, - Document, - Event, - Group, - GroupUser, - Integration, - IntegrationAuthentication, - Notification, - NotificationSetting, - Pin, - Revision, - SearchQuery, - Share, - Star, - Team, - User, - UserAuthentication, - View, - FileOperation, -}; +export { default as Attachment } from "./Attachment"; -// based on https://github.com/sequelize/express-example/blob/master/models/index.js -Object.keys(models).forEach((modelName) => { - if ("associate" in models[modelName]) { - models[modelName].associate(models); - } -}); +export { default as AuthenticationProvider } from "./AuthenticationProvider"; -export { - ApiKey, - Attachment, - AuthenticationProvider, - Backlink, - Collection, - CollectionGroup, - CollectionUser, - Document, - Event, - Group, - GroupUser, - Integration, - IntegrationAuthentication, - Notification, - NotificationSetting, - Pin, - Revision, - SearchQuery, - Share, - Star, - Team, - User, - UserAuthentication, - View, - FileOperation, -}; +export { default as Backlink } from "./Backlink"; + +export { default as Collection } from "./Collection"; + +export { default as CollectionGroup } from "./CollectionGroup"; + +export { default as CollectionUser } from "./CollectionUser"; + +export { default as Document } from "./Document"; + +export { default as Event } from "./Event"; + +export { default as FileOperation } from "./FileOperation"; + +export { default as Group } from "./Group"; + +export { default as GroupUser } from "./GroupUser"; + +export { default as Integration } from "./Integration"; + +export { default as IntegrationAuthentication } from "./IntegrationAuthentication"; + +export { default as Notification } from "./Notification"; + +export { default as NotificationSetting } from "./NotificationSetting"; + +export { default as Pin } from "./Pin"; + +export { default as Revision } from "./Revision"; + +export { default as SearchQuery } from "./SearchQuery"; + +export { default as Share } from "./Share"; + +export { default as Star } from "./Star"; + +export { default as Team } from "./Team"; + +export { default as User } from "./User"; + +export { default as UserAuthentication } from "./UserAuthentication"; + +export { default as View } from "./View"; diff --git a/server/policies/apiKey.ts b/server/policies/apiKey.ts index 9caee5561..b5009b074 100644 --- a/server/policies/apiKey.ts +++ b/server/policies/apiKey.ts @@ -1,7 +1,5 @@ import { ApiKey, User, Team } from "@server/models"; -import policy from "./policy"; - -const { allow } = policy; +import { allow } from "./cancan"; allow(User, "createApiKey", Team, (user, team) => { if (!team || user.isViewer || user.teamId !== team.id) return false; @@ -9,6 +7,7 @@ allow(User, "createApiKey", Team, (user, team) => { }); allow(User, ["read", "update", "delete"], ApiKey, (user, apiKey) => { + if (!apiKey) return false; if (user.isViewer) return false; return user && user.id === apiKey.userId; }); diff --git a/server/policies/attachment.ts b/server/policies/attachment.ts index 9bf5fea36..c79eb3b8e 100644 --- a/server/policies/attachment.ts +++ b/server/policies/attachment.ts @@ -1,7 +1,5 @@ import { Attachment, User, Team } from "@server/models"; -import policy from "./policy"; - -const { allow } = policy; +import { allow } from "./cancan"; allow(User, "createAttachment", Team, (user, team) => { if (!team || user.isViewer || user.teamId !== team.id) return false; diff --git a/server/policies/authenticationProvider.ts b/server/policies/authenticationProvider.ts index ab2dc947f..25b773be4 100644 --- a/server/policies/authenticationProvider.ts +++ b/server/policies/authenticationProvider.ts @@ -1,8 +1,6 @@ import { AuthenticationProvider, User, Team } from "@server/models"; import { AdminRequiredError } from "../errors"; -import policy from "./policy"; - -const { allow } = policy; +import { allow } from "./cancan"; allow(User, "createAuthenticationProvider", Team, (actor, team) => { if (!team || actor.teamId !== team.id) return false; @@ -17,7 +15,7 @@ allow( AuthenticationProvider, (actor, authenticationProvider) => - actor && actor.teamId === authenticationProvider.teamId + actor && actor.teamId === authenticationProvider?.teamId ); allow( @@ -26,7 +24,7 @@ allow( AuthenticationProvider, (actor, authenticationProvider) => { - if (actor.teamId !== authenticationProvider.teamId) return false; + if (actor.teamId !== authenticationProvider?.teamId) return false; if (actor.isAdmin) return true; throw AdminRequiredError(); diff --git a/server/policies/cancan.ts b/server/policies/cancan.ts new file mode 100644 index 000000000..8ae1ccf84 --- /dev/null +++ b/server/policies/cancan.ts @@ -0,0 +1,13 @@ +import CanCan from "cancan"; + +const cancan = new CanCan(); + +export const _can = cancan.can; + +export const _authorize = cancan.authorize; + +export const _cannot = cancan.cannot; + +export const _abilities = cancan.abilities; + +export const allow = cancan.allow; diff --git a/server/policies/collection.test.ts b/server/policies/collection.test.ts index 0723a7a94..714d26ec6 100644 --- a/server/policies/collection.test.ts +++ b/server/policies/collection.test.ts @@ -4,6 +4,7 @@ import { flushdb } from "@server/test/support"; import { serialize } from "./index"; beforeEach(() => flushdb()); + describe("read_write permission", () => { it("should allow read write permissions for team member", async () => { const team = await buildTeam(); @@ -25,7 +26,7 @@ describe("read_write permission", () => { const user = await buildUser({ teamId: team.id, }); - let collection = await buildCollection({ + const collection = await buildCollection({ teamId: team.id, permission: "read_write", }); @@ -36,15 +37,16 @@ describe("read_write permission", () => { permission: "read", }); // reload to get membership - collection = await Collection.scope({ + const reloaded = await Collection.scope({ method: ["withMembership", user.id], }).findByPk(collection.id); - const abilities = serialize(user, collection); + const abilities = serialize(user, reloaded); expect(abilities.read).toEqual(true); expect(abilities.update).toEqual(true); expect(abilities.share).toEqual(true); }); }); + describe("read permission", () => { it("should allow read permissions for team member", async () => { const team = await buildTeam(); @@ -66,7 +68,7 @@ describe("read permission", () => { const user = await buildUser({ teamId: team.id, }); - let collection = await buildCollection({ + const collection = await buildCollection({ teamId: team.id, permission: "read", }); @@ -77,15 +79,16 @@ describe("read permission", () => { permission: "read_write", }); // reload to get membership - collection = await Collection.scope({ + const reloaded = await Collection.scope({ method: ["withMembership", user.id], }).findByPk(collection.id); - const abilities = serialize(user, collection); + const abilities = serialize(user, reloaded); expect(abilities.read).toEqual(true); expect(abilities.update).toEqual(true); expect(abilities.share).toEqual(true); }); }); + describe("no permission", () => { it("should allow no permissions for team member", async () => { const team = await buildTeam(); @@ -107,7 +110,7 @@ describe("no permission", () => { const user = await buildUser({ teamId: team.id, }); - let collection = await buildCollection({ + const collection = await buildCollection({ teamId: team.id, permission: null, }); @@ -118,10 +121,10 @@ describe("no permission", () => { permission: "read_write", }); // reload to get membership - collection = await Collection.scope({ + const reloaded = await Collection.scope({ method: ["withMembership", user.id], }).findByPk(collection.id); - const abilities = serialize(user, collection); + const abilities = serialize(user, reloaded); expect(abilities.read).toEqual(true); expect(abilities.update).toEqual(true); expect(abilities.share).toEqual(true); diff --git a/server/policies/collection.ts b/server/policies/collection.ts index a83f9d2d3..5a916e763 100644 --- a/server/policies/collection.ts +++ b/server/policies/collection.ts @@ -1,10 +1,8 @@ import invariant from "invariant"; -import { concat, some } from "lodash"; +import { some } from "lodash"; import { Collection, User, Team } from "@server/models"; import { AdminRequiredError } from "../errors"; -import policy from "./policy"; - -const { allow } = policy; +import { allow } from "./cancan"; allow(User, "createCollection", Team, (user, team) => { if (!team || user.isViewer || user.teamId !== team.id) return false; @@ -34,10 +32,10 @@ allow(User, "read", Collection, (user, collection) => { collection.memberships, "membership should be preloaded, did you forget withMembership scope?" ); - const allMemberships = concat( - collection.memberships, - collection.collectionGroupMemberships - ); + const allMemberships = [ + ...collection.memberships, + ...collection.collectionGroupMemberships, + ]; return some(allMemberships, (m) => ["read", "read_write", "maintainer"].includes(m.permission) ); @@ -56,10 +54,10 @@ allow(User, "share", Collection, (user, collection) => { collection.memberships, "membership should be preloaded, did you forget withMembership scope?" ); - const allMemberships = concat( - collection.memberships, - collection.collectionGroupMemberships - ); + const allMemberships = [ + ...collection.memberships, + ...collection.collectionGroupMemberships, + ]; return some(allMemberships, (m) => ["read_write", "maintainer"].includes(m.permission) ); @@ -77,10 +75,10 @@ allow(User, ["publish", "update"], Collection, (user, collection) => { collection.memberships, "membership should be preloaded, did you forget withMembership scope?" ); - const allMemberships = concat( - collection.memberships, - collection.collectionGroupMemberships - ); + const allMemberships = [ + ...collection.memberships, + ...collection.collectionGroupMemberships, + ]; return some(allMemberships, (m) => ["read_write", "maintainer"].includes(m.permission) ); @@ -98,10 +96,10 @@ allow(User, "delete", Collection, (user, collection) => { collection.memberships, "membership should be preloaded, did you forget withMembership scope?" ); - const allMemberships = concat( - collection.memberships, - collection.collectionGroupMemberships - ); + const allMemberships = [ + ...collection.memberships, + ...collection.collectionGroupMemberships, + ]; return some(allMemberships, (m) => ["read_write", "maintainer"].includes(m.permission) ); diff --git a/server/policies/document.test.ts b/server/policies/document.test.ts index 4ca23559f..4c468eeee 100644 --- a/server/policies/document.test.ts +++ b/server/policies/document.test.ts @@ -8,6 +8,7 @@ import { flushdb } from "@server/test/support"; import { serialize } from "./index"; beforeEach(() => flushdb()); + describe("read_write collection", () => { it("should allow read write permissions for team member", async () => { const team = await buildTeam(); @@ -33,6 +34,7 @@ describe("read_write collection", () => { expect(abilities.move).toEqual(true); }); }); + describe("read collection", () => { it("should allow read only permissions permissions for team member", async () => { const team = await buildTeam(); @@ -58,6 +60,7 @@ describe("read collection", () => { expect(abilities.move).toEqual(false); }); }); + describe("private collection", () => { it("should allow no permissions for team member", async () => { const team = await buildTeam(); diff --git a/server/policies/document.ts b/server/policies/document.ts index 9d4499416..21f870694 100644 --- a/server/policies/document.ts +++ b/server/policies/document.ts @@ -1,8 +1,7 @@ import invariant from "invariant"; import { Document, Revision, User, Team } from "@server/models"; -import policy from "./policy"; - -const { allow, cannot } = policy; +import { NavigationNode } from "~/types"; +import { allow, _cannot as cannot } from "./cancan"; allow(User, "createDocument", Team, (user, team) => { if (!team || user.isViewer || user.teamId !== team.id) return false; @@ -10,6 +9,8 @@ allow(User, "createDocument", Team, (user, team) => { }); allow(User, ["read", "download"], Document, (user, document) => { + if (!document) return false; + // existence of collection option is not required here to account for share tokens if (document.collection && cannot(user, "read", document.collection)) { return false; @@ -19,6 +20,7 @@ allow(User, ["read", "download"], Document, (user, document) => { }); allow(User, ["star", "unstar"], Document, (user, document) => { + if (!document) return false; if (document.archivedAt) return false; if (document.deletedAt) return false; if (document.template) return false; @@ -31,8 +33,13 @@ allow(User, ["star", "unstar"], Document, (user, document) => { }); allow(User, "share", Document, (user, document) => { + if (!document) return false; if (document.archivedAt) return false; if (document.deletedAt) return false; + invariant( + document.collection, + "collection is missing, did you forget to include in the query scope?" + ); if (cannot(user, "share", document.collection)) { return false; @@ -42,6 +49,7 @@ allow(User, "share", Document, (user, document) => { }); allow(User, "update", Document, (user, document) => { + if (!document) return false; if (document.archivedAt) return false; if (document.deletedAt) return false; @@ -53,6 +61,7 @@ allow(User, "update", Document, (user, document) => { }); allow(User, "createChildDocument", Document, (user, document) => { + if (!document) return false; if (document.archivedAt) return false; if (document.deletedAt) return false; if (document.template) return false; @@ -66,6 +75,7 @@ allow(User, "createChildDocument", Document, (user, document) => { }); allow(User, "move", Document, (user, document) => { + if (!document) return false; if (document.archivedAt) return false; if (document.deletedAt) return false; if (!document.publishedAt) return false; @@ -78,6 +88,7 @@ allow(User, "move", Document, (user, document) => { }); allow(User, ["pin", "unpin"], Document, (user, document) => { + if (!document) return false; if (document.archivedAt) return false; if (document.deletedAt) return false; if (document.template) return false; @@ -91,6 +102,7 @@ allow(User, ["pin", "unpin"], Document, (user, document) => { }); allow(User, ["pinToHome"], Document, (user, document) => { + if (!document) return false; if (document.archivedAt) return false; if (document.deletedAt) return false; if (document.template) return false; @@ -100,8 +112,9 @@ allow(User, ["pinToHome"], Document, (user, document) => { }); allow(User, "delete", Document, (user, document) => { - if (user.isViewer) return false; + if (!document) return false; if (document.deletedAt) return false; + if (user.isViewer) return false; // allow deleting document without a collection if (document.collection && cannot(user, "update", document.collection)) { @@ -121,8 +134,9 @@ allow(User, "delete", Document, (user, document) => { }); allow(User, "permanentDelete", Document, (user, document) => { - if (user.isViewer) return false; + if (!document) return false; if (!document.deletedAt) return false; + if (user.isViewer) return false; // allow deleting document without a collection if (document.collection && cannot(user, "update", document.collection)) { @@ -133,8 +147,9 @@ allow(User, "permanentDelete", Document, (user, document) => { }); allow(User, "restore", Document, (user, document) => { - if (user.isViewer) return false; + if (!document) return false; if (!document.deletedAt) return false; + if (user.isViewer) return false; if (document.collection && cannot(user, "update", document.collection)) { return false; @@ -144,6 +159,7 @@ allow(User, "restore", Document, (user, document) => { }); allow(User, "archive", Document, (user, document) => { + if (!document) return false; if (!document.publishedAt) return false; if (document.archivedAt) return false; if (document.deletedAt) return false; @@ -156,6 +172,7 @@ allow(User, "archive", Document, (user, document) => { }); allow(User, "unarchive", Document, (user, document) => { + if (!document) return false; invariant( document.collection, "collection is missing, did you forget to include in the query scope?" @@ -170,10 +187,11 @@ allow( Document, "restore", Revision, - (document, revision) => document.id === revision.documentId + (document, revision) => document.id === revision?.documentId ); allow(User, "unpublish", Document, (user, document) => { + if (!document) return false; invariant( document.collection, "collection is missing, did you forget to include in the query scope?" @@ -183,16 +201,14 @@ allow(User, "unpublish", Document, (user, document) => { if (cannot(user, "update", document.collection)) return false; const documentID = document.id; - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documents' implicitly has an 'any' type... Remove this comment to see the full error message - const hasChild = (documents) => - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'doc' implicitly has an 'any' type. + const hasChild = (documents: NavigationNode[]): boolean => documents.some((doc) => { if (doc.id === documentID) return doc.children.length > 0; return hasChild(doc.children); }); return ( - !hasChild(document.collection.documentStructure) && + !hasChild(document.collection.documentStructure || []) && user.teamId === document.teamId ); }); diff --git a/server/policies/group.ts b/server/policies/group.ts index a85b9f3f4..0afa2ac95 100644 --- a/server/policies/group.ts +++ b/server/policies/group.ts @@ -1,8 +1,6 @@ import { Group, User, Team } from "@server/models"; import { AdminRequiredError } from "../errors"; -import policy from "./policy"; - -const { allow } = policy; +import { allow } from "./cancan"; allow(User, "createGroup", Team, (actor, team) => { if (!team || actor.isViewer || actor.teamId !== team.id) return false; diff --git a/server/policies/index.test.ts b/server/policies/index.test.ts index 6985b07dd..963665abd 100644 --- a/server/policies/index.test.ts +++ b/server/policies/index.test.ts @@ -3,12 +3,14 @@ import { flushdb } from "@server/test/support"; import { serialize } from "./index"; beforeEach(() => flushdb()); + it("should serialize policy", async () => { const user = await buildUser(); const response = serialize(user, user); expect(response.update).toEqual(true); expect(response.delete).toEqual(true); }); + it("should serialize domain policies on Team", async () => { const team = await buildTeam(); const user = await buildUser({ diff --git a/server/policies/index.ts b/server/policies/index.ts index 653ec29c1..6398ab81a 100644 --- a/server/policies/index.ts +++ b/server/policies/index.ts @@ -6,7 +6,7 @@ import { Document, Group, } from "@server/models"; -import policy from "./policy"; +import { _abilities, _can, _cannot, _authorize } from "./cancan"; import "./apiKey"; import "./attachment"; import "./authenticationProvider"; @@ -21,19 +21,26 @@ import "./user"; import "./team"; import "./group"; -const { can, abilities } = policy; type Policy = Record; +// this should not be needed but is a workaround for this TypeScript issue: +// https://github.com/microsoft/TypeScript/issues/36931 +export const authorize: typeof _authorize = _authorize; + +export const can = _can; + +export const cannot = _cannot; + +export const abilities = _abilities; + /* * Given a user and a model – output an object which describes the actions the * user may take against the model. This serialized policy is used for testing * and sent in API responses to allow clients to adjust which UI is displayed. */ export function serialize( - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message model: User, - // @ts-expect-error ts-migrate(2749) FIXME: 'Attachment' refers to a value, but is being used ... Remove this comment to see the full error message - target: Attachment | Team | Collection | Document | Group + target: Attachment | Team | Collection | Document | User | Group | null ): Policy { const output = {}; abilities.forEach((ability) => { @@ -51,5 +58,3 @@ export function serialize( }); return output; } - -export default policy; diff --git a/server/policies/integration.ts b/server/policies/integration.ts index 773e8aa7a..bc9a5f91f 100644 --- a/server/policies/integration.ts +++ b/server/policies/integration.ts @@ -1,8 +1,6 @@ import { Integration, User, Team } from "@server/models"; import { AdminRequiredError } from "../errors"; -import policy from "./policy"; - -const { allow } = policy; +import { allow } from "./cancan"; allow(User, "createIntegration", Team, (actor, team) => { if (!team || actor.isViewer || actor.teamId !== team.id) return false; @@ -15,7 +13,7 @@ allow( User, "read", Integration, - (user, integration) => user.teamId === integration.teamId + (user, integration) => user.teamId === integration?.teamId ); allow(User, ["update", "delete"], Integration, (user, integration) => { diff --git a/server/policies/notificationSetting.ts b/server/policies/notificationSetting.ts index 7f90d58fc..e0461d739 100644 --- a/server/policies/notificationSetting.ts +++ b/server/policies/notificationSetting.ts @@ -1,7 +1,5 @@ import { NotificationSetting, Team, User } from "@server/models"; -import policy from "./policy"; - -const { allow } = policy; +import { allow } from "./cancan"; allow(User, "createNotificationSetting", Team, (user, team) => { if (!team || user.teamId !== team.id) return false; @@ -12,5 +10,5 @@ allow( User, ["read", "update", "delete"], NotificationSetting, - (user, setting) => user && user.id === setting.userId + (user, setting) => user && user.id === setting?.userId ); diff --git a/server/policies/pins.ts b/server/policies/pins.ts index 14a10916e..5235ce6f1 100644 --- a/server/policies/pins.ts +++ b/server/policies/pins.ts @@ -1,9 +1,9 @@ import { User, Pin } from "@server/models"; -import policy from "./policy"; +import { allow } from "./cancan"; -const { allow } = policy; - -allow(User, ["update", "delete"], Pin, (user, pin) => { - if (user.teamId === pin.teamId && user.isAdmin) return true; - return false; -}); +allow( + User, + ["update", "delete"], + Pin, + (user, pin) => user.teamId === pin?.teamId && user.isAdmin +); diff --git a/server/policies/policy.ts b/server/policies/policy.ts deleted file mode 100644 index 71476affb..000000000 --- a/server/policies/policy.ts +++ /dev/null @@ -1,3 +0,0 @@ -import CanCan from "cancan"; - -export default new CanCan(); diff --git a/server/policies/searchQuery.ts b/server/policies/searchQuery.ts index fa3901c97..8827cecea 100644 --- a/server/policies/searchQuery.ts +++ b/server/policies/searchQuery.ts @@ -1,8 +1,9 @@ import { SearchQuery, User } from "@server/models"; -import policy from "./policy"; +import { allow } from "./cancan"; -const { allow } = policy; - -allow(User, ["read", "delete"], SearchQuery, (user, searchQuery) => { - return user && user.id === searchQuery.userId; -}); +allow( + User, + ["read", "delete"], + SearchQuery, + (user, searchQuery) => user && user.id === searchQuery?.userId +); diff --git a/server/policies/share.ts b/server/policies/share.ts index 9db40016b..449f29ee9 100644 --- a/server/policies/share.ts +++ b/server/policies/share.ts @@ -1,14 +1,11 @@ import { Share, User } from "@server/models"; import { AdminRequiredError } from "../errors"; -import policy from "./policy"; +import { allow, _cannot as cannot } from "./cancan"; -const { allow, cannot } = policy; - -allow(User, "read", Share, (user, share) => { - return user.teamId === share.teamId; -}); +allow(User, "read", Share, (user, share) => user.teamId === share?.teamId); allow(User, "update", Share, (user, share) => { + if (!share) return false; if (user.isViewer) return false; // only the user who can share the document publicly can update the share. @@ -17,8 +14,9 @@ allow(User, "update", Share, (user, share) => { }); allow(User, "revoke", Share, (user, share) => { + if (!share) return false; if (user.isViewer) return false; - if (!share || user.teamId !== share.teamId) return false; + if (user.teamId !== share.teamId) return false; if (user.id === share.userId) return true; if (user.isAdmin) return true; diff --git a/server/policies/team.ts b/server/policies/team.ts index 97517564c..a6ee519d7 100644 --- a/server/policies/team.ts +++ b/server/policies/team.ts @@ -1,9 +1,7 @@ import { Team, User } from "@server/models"; -import policy from "./policy"; +import { allow } from "./cancan"; -const { allow } = policy; - -allow(User, "read", Team, (user, team) => team && user.teamId === team.id); +allow(User, "read", Team, (user, team) => user.teamId === team?.id); allow(User, "share", Team, (user, team) => { if (!team || user.isViewer || user.teamId !== team.id) return false; diff --git a/server/policies/user.ts b/server/policies/user.ts index ea5221b90..4af423506 100644 --- a/server/policies/user.ts +++ b/server/policies/user.ts @@ -1,13 +1,11 @@ import { User, Team } from "@server/models"; import { AdminRequiredError } from "../errors"; -import policy from "./policy"; +import { allow } from "./cancan"; -const { allow } = policy; allow( User, "read", User, - (actor, user) => user && user.teamId === actor.teamId ); diff --git a/server/presenters/__snapshots__/user.test.ts.snap b/server/presenters/__snapshots__/user.test.ts.snap index f080b50cf..060ab777f 100644 --- a/server/presenters/__snapshots__/user.test.ts.snap +++ b/server/presenters/__snapshots__/user.test.ts.snap @@ -2,13 +2,13 @@ exports[`presents a user 1`] = ` Object { - "avatarUrl": undefined, - "color": undefined, + "avatarUrl": "https://tiley.herokuapp.com/avatar/d41d8cd98f00b204e9800998ecf8427e/T.png?c=FF5C80", + "color": "#FF5C80", "createdAt": undefined, "id": "123", - "isAdmin": undefined, - "isSuspended": undefined, - "isViewer": undefined, + "isAdmin": false, + "isSuspended": false, + "isViewer": false, "lastActiveAt": undefined, "name": "Test User", } @@ -16,13 +16,13 @@ Object { exports[`presents a user without slack data 1`] = ` Object { - "avatarUrl": undefined, - "color": undefined, + "avatarUrl": "https://tiley.herokuapp.com/avatar/d41d8cd98f00b204e9800998ecf8427e/T.png?c=FF5C80", + "color": "#FF5C80", "createdAt": undefined, "id": "123", - "isAdmin": undefined, - "isSuspended": undefined, - "isViewer": undefined, + "isAdmin": false, + "isSuspended": false, + "isViewer": false, "lastActiveAt": undefined, "name": "Test User", } diff --git a/server/presenters/apiKey.ts b/server/presenters/apiKey.ts index 972629c80..26fe6595a 100644 --- a/server/presenters/apiKey.ts +++ b/server/presenters/apiKey.ts @@ -1,6 +1,5 @@ -import { ApiKey } from "@server/models"; +import ApiKey from "@server/models/ApiKey"; -// @ts-expect-error ts-migrate(2749) FIXME: 'ApiKey' refers to a value, but is being used as a... Remove this comment to see the full error message export default function present(key: ApiKey) { return { id: key.id, diff --git a/server/presenters/authenticationProvider.ts b/server/presenters/authenticationProvider.ts index 838659276..4bf136247 100644 --- a/server/presenters/authenticationProvider.ts +++ b/server/presenters/authenticationProvider.ts @@ -1,7 +1,6 @@ import { AuthenticationProvider } from "@server/models"; export default function present( - // @ts-expect-error ts-migrate(2749) FIXME: 'AuthenticationProvider' refers to a value, but is... Remove this comment to see the full error message authenticationProvider: AuthenticationProvider ) { return { diff --git a/server/presenters/collection.ts b/server/presenters/collection.ts index 2e634fc7f..1401fe0e3 100644 --- a/server/presenters/collection.ts +++ b/server/presenters/collection.ts @@ -1,7 +1,6 @@ import { sortNavigationNodes } from "@shared/utils/collections"; -import { Collection } from "@server/models"; +import Collection from "@server/models/Collection"; -// @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message export default function present(collection: Collection) { const data = { id: collection.id, diff --git a/server/presenters/collectionGroupMembership.ts b/server/presenters/collectionGroupMembership.ts index 51e3b041e..aa927c58f 100644 --- a/server/presenters/collectionGroupMembership.ts +++ b/server/presenters/collectionGroupMembership.ts @@ -7,7 +7,6 @@ type Membership = { permission: string; }; -// @ts-expect-error ts-migrate(2749) FIXME: 'CollectionGroup' refers to a value, but is being ... Remove this comment to see the full error message export default (membership: CollectionGroup): Membership => { return { id: `${membership.groupId}-${membership.collectionId}`, diff --git a/server/presenters/document.ts b/server/presenters/document.ts index 1efd1a8d1..40f26f3e7 100644 --- a/server/presenters/document.ts +++ b/server/presenters/document.ts @@ -1,4 +1,5 @@ -import { Attachment } from "@server/models"; +import { Document } from "@server/models"; +import Attachment from "@server/models/Attachment"; import parseAttachmentIds from "@server/utils/parseAttachmentIds"; import { getSignedUrl } from "@server/utils/s3"; import presentUser from "./user"; @@ -25,7 +26,7 @@ async function replaceImageAttachments(text: string) { } export default async function present( - document: any, + document: Document, options: Options | null | undefined = {} ) { options = { @@ -36,7 +37,8 @@ export default async function present( const text = options.isPublic ? await replaceImageAttachments(document.text) : document.text; - const data = { + + const data: Record = { id: document.id, url: document.url, urlId: document.urlId, @@ -70,9 +72,7 @@ export default async function present( if (!options.isPublic) { data.collectionId = document.collectionId; data.parentDocumentId = document.parentDocumentId; - // @ts-expect-error ts-migrate(2322) FIXME: Type 'UserPresentation | null | undefined' is not ... Remove this comment to see the full error message data.createdBy = presentUser(document.createdBy); - // @ts-expect-error ts-migrate(2322) FIXME: Type 'UserPresentation | null | undefined' is not ... Remove this comment to see the full error message data.updatedBy = presentUser(document.updatedBy); data.collaboratorIds = document.collaboratorIds; } diff --git a/server/presenters/event.ts b/server/presenters/event.ts index 925e6997c..912172fe5 100644 --- a/server/presenters/event.ts +++ b/server/presenters/event.ts @@ -3,25 +3,15 @@ import presentUser from "./user"; export default function present(event: Event, isAdmin = false) { const data = { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Event'. id: event.id, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'name' does not exist on type 'Event'. name: event.name, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'modelId' does not exist on type 'Event'. modelId: event.modelId, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'actorId' does not exist on type 'Event'. actorId: event.actorId, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'ip' does not exist on type 'Event'. - actorIpAddress: event.ip, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'collectionId' does not exist on type 'Ev... Remove this comment to see the full error message + actorIpAddress: event.ip || undefined, collectionId: event.collectionId, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'documentId' does not exist on type 'Even... Remove this comment to see the full error message documentId: event.documentId, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'createdAt' does not exist on type 'Event... Remove this comment to see the full error message createdAt: event.createdAt, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'data' does not exist on type 'Event'. data: event.data, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'actor' does not exist on type 'Event'. actor: presentUser(event.actor), }; diff --git a/server/presenters/fileOperation.ts b/server/presenters/fileOperation.ts index 3e49bdb4b..5cb6a5a79 100644 --- a/server/presenters/fileOperation.ts +++ b/server/presenters/fileOperation.ts @@ -1,7 +1,6 @@ import { FileOperation } from "@server/models"; import { presentCollection, presentUser } from "."; -// @ts-expect-error ts-migrate(2749) FIXME: 'FileOperation' refers to a value, but is being us... Remove this comment to see the full error message export default function present(data: FileOperation) { return { id: data.id, diff --git a/server/presenters/group.ts b/server/presenters/group.ts index 9d330d16f..3ce5d4463 100644 --- a/server/presenters/group.ts +++ b/server/presenters/group.ts @@ -1,6 +1,5 @@ -import { Group } from "@server/models"; +import Group from "@server/models/Group"; -// @ts-expect-error ts-migrate(2749) FIXME: 'Group' refers to a value, but is being used as a ... Remove this comment to see the full error message export default function present(group: Group) { return { id: group.id, diff --git a/server/presenters/groupMembership.ts b/server/presenters/groupMembership.ts index ada619797..4c59607d2 100644 --- a/server/presenters/groupMembership.ts +++ b/server/presenters/groupMembership.ts @@ -1,19 +1,18 @@ -import { GroupUser } from "@server/models"; +import GroupUser from "@server/models/GroupUser"; import { presentUser } from "."; type GroupMembership = { id: string; userId: string; groupId: string; + user: ReturnType; }; -// @ts-expect-error ts-migrate(2749) FIXME: 'GroupUser' refers to a value, but is being used a... Remove this comment to see the full error message export default (membership: GroupUser): GroupMembership => { return { id: `${membership.userId}-${membership.groupId}`, userId: membership.userId, groupId: membership.groupId, - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ id: string; userId: any; groupId: any; use... Remove this comment to see the full error message user: presentUser(membership.user), }; }; diff --git a/server/presenters/integration.ts b/server/presenters/integration.ts index 315067547..d3f65c9d4 100644 --- a/server/presenters/integration.ts +++ b/server/presenters/integration.ts @@ -1,6 +1,5 @@ import { Integration } from "@server/models"; -// @ts-expect-error ts-migrate(2749) FIXME: 'Integration' refers to a value, but is being used... Remove this comment to see the full error message export default function present(integration: Integration) { return { id: integration.id, diff --git a/server/presenters/membership.ts b/server/presenters/membership.ts index 629007b21..99614194c 100644 --- a/server/presenters/membership.ts +++ b/server/presenters/membership.ts @@ -7,7 +7,6 @@ type Membership = { permission: string; }; -// @ts-expect-error ts-migrate(2749) FIXME: 'CollectionUser' refers to a value, but is being u... Remove this comment to see the full error message export default (membership: CollectionUser): Membership => { return { id: `${membership.userId}-${membership.collectionId}`, diff --git a/server/presenters/notificationSetting.ts b/server/presenters/notificationSetting.ts index 13e444e63..c3ab4d72e 100644 --- a/server/presenters/notificationSetting.ts +++ b/server/presenters/notificationSetting.ts @@ -1,6 +1,5 @@ import { NotificationSetting } from "@server/models"; -// @ts-expect-error ts-migrate(2749) FIXME: 'NotificationSetting' refers to a value, but is be... Remove this comment to see the full error message export default function present(setting: NotificationSetting) { return { id: setting.id, diff --git a/server/presenters/pin.ts b/server/presenters/pin.ts index 8363f1d7d..faa4500c6 100644 --- a/server/presenters/pin.ts +++ b/server/presenters/pin.ts @@ -1,4 +1,6 @@ -export default function present(pin: any) { +import { Pin } from "@server/models"; + +export default function present(pin: Pin) { return { id: pin.id, documentId: pin.documentId, diff --git a/server/presenters/policy.ts b/server/presenters/policy.ts index 2aaf13d98..8dfe2a878 100644 --- a/server/presenters/policy.ts +++ b/server/presenters/policy.ts @@ -6,7 +6,6 @@ type Policy = { }; export default function present( - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message user: User, objects: Record[] ): Policy[] { diff --git a/server/presenters/revision.ts b/server/presenters/revision.ts index f2be949a7..474e90c6d 100644 --- a/server/presenters/revision.ts +++ b/server/presenters/revision.ts @@ -1,7 +1,6 @@ import { Revision } from "@server/models"; import presentUser from "./user"; -// @ts-expect-error ts-migrate(2749) FIXME: 'Revision' refers to a value, but is being used as... Remove this comment to see the full error message export default async function present(revision: Revision) { await revision.migrateVersion(); return { diff --git a/server/presenters/searchQuery.ts b/server/presenters/searchQuery.ts index cf1425494..3df6db8c0 100644 --- a/server/presenters/searchQuery.ts +++ b/server/presenters/searchQuery.ts @@ -1,4 +1,6 @@ -export default function present(searchQuery: any) { +import { SearchQuery } from "@server/models"; + +export default function present(searchQuery: SearchQuery) { return { id: searchQuery.id, query: searchQuery.query, diff --git a/server/presenters/share.ts b/server/presenters/share.ts index 523b0a098..d54e6da74 100644 --- a/server/presenters/share.ts +++ b/server/presenters/share.ts @@ -1,7 +1,6 @@ import { Share } from "@server/models"; import { presentUser } from "."; -// @ts-expect-error ts-migrate(2749) FIXME: 'Share' refers to a value, but is being used as a ... Remove this comment to see the full error message export default function present(share: Share, isAdmin = false) { const data = { id: share.id, @@ -12,7 +11,7 @@ export default function present(share: Share, isAdmin = false) { url: `${share.team.url}/share/${share.id}`, createdBy: presentUser(share.user), includeChildDocuments: share.includeChildDocuments, - lastAccessedAt: share.lastAccessedAt, + lastAccessedAt: share.lastAccessedAt || undefined, createdAt: share.createdAt, updatedAt: share.updatedAt, }; diff --git a/server/presenters/slackAttachment.ts b/server/presenters/slackAttachment.ts index abcb8391a..55f2ebd9a 100644 --- a/server/presenters/slackAttachment.ts +++ b/server/presenters/slackAttachment.ts @@ -9,9 +9,7 @@ type Action = { export default function present( document: Document, - // @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message collection: Collection, - // @ts-expect-error ts-migrate(2749) FIXME: 'Team' refers to a value, but is being used as a t... Remove this comment to see the full error message team: Team, context?: string, actions?: Action[] @@ -20,19 +18,15 @@ export default function present( // to the markdown format that slack expects to receive. const text = context ? context.replace(/<\/?b>/g, "*").replace(/\n/g, "") - : // @ts-expect-error ts-migrate(2339) FIXME: Property 'getSummary' does not exist on type 'Docu... Remove this comment to see the full error message - document.getSummary(); + : document.getSummary(); return { color: collection.color, title: document.title, - // @ts-expect-error ts-migrate(2551) FIXME: Property 'url' does not exist on type 'Document'. ... Remove this comment to see the full error message title_link: `${team.url}${document.url}`, footer: collection.name, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'. callback_id: document.id, text, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'getTimestamp' does not exist on type 'Do... Remove this comment to see the full error message ts: document.getTimestamp(), actions, }; diff --git a/server/presenters/team.ts b/server/presenters/team.ts index 026c0ee06..d5b7df64b 100644 --- a/server/presenters/team.ts +++ b/server/presenters/team.ts @@ -1,6 +1,5 @@ import { Team } from "@server/models"; -// @ts-expect-error ts-migrate(2749) FIXME: 'Team' refers to a value, but is being used as a t... Remove this comment to see the full error message export default function present(team: Team) { return { id: team.id, diff --git a/server/presenters/user.test.ts b/server/presenters/user.test.ts index c575cdac1..6ab827070 100644 --- a/server/presenters/user.test.ts +++ b/server/presenters/user.test.ts @@ -1,18 +1,24 @@ +import { User } from "@server/models"; import presentUser from "./user"; it("presents a user", async () => { - const user = presentUser({ - id: "123", - name: "Test User", - username: "testuser", - }); + const user = presentUser( + User.build({ + id: "123", + name: "Test User", + username: "testuser", + }) + ); expect(user).toMatchSnapshot(); }); + it("presents a user without slack data", async () => { - const user = presentUser({ - id: "123", - name: "Test User", - username: "testuser", - }); + const user = presentUser( + User.build({ + id: "123", + name: "Test User", + username: "testuser", + }) + ); expect(user).toMatchSnapshot(); }); diff --git a/server/presenters/user.ts b/server/presenters/user.ts index 0d467fcef..1cd07c95f 100644 --- a/server/presenters/user.ts +++ b/server/presenters/user.ts @@ -16,7 +16,6 @@ type UserPresentation = { }; export default ( - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message user: User, options: Options = {} ): UserPresentation | null | undefined => { diff --git a/server/presenters/view.ts b/server/presenters/view.ts index 6b126acfa..61f114cb2 100644 --- a/server/presenters/view.ts +++ b/server/presenters/view.ts @@ -1,7 +1,6 @@ import { View } from "@server/models"; import { presentUser } from "../presenters"; -// @ts-expect-error ts-migrate(2749) FIXME: 'View' refers to a value, but is being used as a t... Remove this comment to see the full error message export default function present(view: View) { return { id: view.id, diff --git a/server/queues/processors/backlinks.test.ts b/server/queues/processors/backlinks.test.ts index a6c842e91..270299bbc 100644 --- a/server/queues/processors/backlinks.test.ts +++ b/server/queues/processors/backlinks.test.ts @@ -33,7 +33,7 @@ describe("documents.publish", () => { const otherDocument = await buildDocument(); await otherDocument.destroy(); const document = await buildDocument({ - version: null, + version: 0, text: `[ ] checklist item`, }); document.text = `[this is a link](${otherDocument.url})`; @@ -54,6 +54,7 @@ describe("documents.publish", () => { expect(backlinks.length).toBe(0); }); }); + describe("documents.update", () => { test("should not fail on a document with no previous revisions", async () => { const otherDocument = await buildDocument(); @@ -79,7 +80,7 @@ describe("documents.update", () => { test("should not fail when previous revision is different document version", async () => { const otherDocument = await buildDocument(); const document = await buildDocument({ - version: null, + version: undefined, text: `[ ] checklist item`, }); document.text = `[this is a link](${otherDocument.url})`; @@ -158,6 +159,7 @@ describe("documents.update", () => { expect(backlinks[0].documentId).toBe(yetAnotherDocument.id); }); }); + describe("documents.delete", () => { test("should destroy related backlinks", async () => { const otherDocument = await buildDocument(); @@ -188,6 +190,7 @@ describe("documents.delete", () => { expect(backlinks.length).toBe(0); }); }); + describe("documents.title_change", () => { test("should update titles in backlinked documents", async () => { const newTitle = "test"; diff --git a/server/queues/processors/backlinks.ts b/server/queues/processors/backlinks.ts index 4007d88ec..3a0e6754e 100644 --- a/server/queues/processors/backlinks.ts +++ b/server/queues/processors/backlinks.ts @@ -1,5 +1,5 @@ +import { Op } from "sequelize"; import { Document, Backlink, Team } from "@server/models"; -import { Op } from "@server/sequelize"; import parseDocumentIds from "@server/utils/parseDocumentIds"; import slugify from "@server/utils/slugify"; import { DocumentEvent, RevisionEvent } from "../../types"; @@ -105,7 +105,6 @@ export default class BacklinksProcessor { ], }); await Promise.all( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'backlink' implicitly has an 'any' type. backlinks.map(async (backlink) => { const previousUrl = `/doc/${slugify(previousTitle)}-${ document.urlId diff --git a/server/queues/processors/debouncer.ts b/server/queues/processors/debouncer.ts index 9b03cf8ef..ed4d85ec2 100644 --- a/server/queues/processors/debouncer.ts +++ b/server/queues/processors/debouncer.ts @@ -1,4 +1,4 @@ -import { Document } from "@server/models"; +import Document from "@server/models/Document"; import { globalEventQueue } from "../../queues"; import { Event } from "../../types"; @@ -17,7 +17,7 @@ export default class DebounceProcessor { case "documents.update.delayed": { const document = await Document.findByPk(event.documentId, { - fields: ["updatedAt"], + attributes: ["updatedAt"], }); // If the document has been deleted then prevent further processing diff --git a/server/queues/processors/exports.ts b/server/queues/processors/exports.ts index 7bbfc65aa..9b3dd7579 100644 --- a/server/queues/processors/exports.ts +++ b/server/queues/processors/exports.ts @@ -1,10 +1,11 @@ import fs from "fs"; +import invariant from "invariant"; +import Logger from "@server/logging/logger"; +import mailer from "@server/mailer"; import { FileOperation, Collection, Event, Team, User } from "@server/models"; +import { Event as TEvent } from "@server/types"; import { uploadToS3FromBuffer } from "@server/utils/s3"; import { archiveCollections } from "@server/utils/zip"; -import Logger from "../../logging/logger"; -import mailer from "../../mailer"; -import { Event as TEvent } from "../../types"; export default class ExportsProcessor { async on(event: TEvent) { @@ -13,8 +14,14 @@ export default class ExportsProcessor { case "collections.export_all": { const { actorId, teamId } = event; const team = await Team.findByPk(teamId); + invariant(team, "team operation not found"); + const user = await User.findByPk(actorId); + invariant(user, "user operation not found"); + const exportData = await FileOperation.findByPk(event.modelId); + invariant(exportData, "exportData not found"); + const collectionIds = // @ts-expect-error ts-migrate(2339) FIXME: Property 'collectionId' does not exist on type 'Co... Remove this comment to see the full error message event.collectionId || (await user.collectionIds()); @@ -91,7 +98,6 @@ export default class ExportsProcessor { } async updateFileOperation( - // @ts-expect-error ts-migrate(2749) FIXME: 'FileOperation' refers to a value, but is being us... Remove this comment to see the full error message fileOperation: FileOperation, actorId: string, teamId: string, @@ -102,6 +108,7 @@ export default class ExportsProcessor { name: "fileOperations.update", teamId, actorId, + // @ts-expect-error dataValues exists data: fileOperation.dataValues, }); } diff --git a/server/queues/processors/imports.ts b/server/queues/processors/imports.ts index 96d69138c..871494e10 100644 --- a/server/queues/processors/imports.ts +++ b/server/queues/processors/imports.ts @@ -1,6 +1,7 @@ import fs from "fs"; import os from "os"; import File from "formidable/lib/file"; +import invariant from "invariant"; import collectionImporter from "@server/commands/collectionImporter"; import { Attachment, User } from "@server/models"; import { Event } from "../../types"; @@ -11,14 +12,18 @@ export default class ImportsProcessor { case "collections.import": { const { type } = event.data; const attachment = await Attachment.findByPk(event.modelId); + invariant(attachment, "attachment not found"); + const user = await User.findByPk(event.actorId); - const buffer = await attachment.buffer; + invariant(user, "user not found"); + + const buffer: any = await attachment.buffer; const tmpDir = os.tmpdir(); const tmpFilePath = `${tmpDir}/upload-${event.modelId}`; await fs.promises.writeFile(tmpFilePath, buffer); const file = new File({ name: attachment.name, - type: attachment.type, + type: attachment.contentType, path: tmpFilePath, }); await collectionImporter({ diff --git a/server/queues/processors/notifications.test.ts b/server/queues/processors/notifications.test.ts index f8f49ddd4..f6fe0a373 100644 --- a/server/queues/processors/notifications.test.ts +++ b/server/queues/processors/notifications.test.ts @@ -1,3 +1,4 @@ +import mailer from "@server/mailer"; import { View, NotificationSetting } from "@server/models"; import { buildDocument, @@ -5,13 +6,14 @@ import { buildUser, } from "@server/test/factories"; import { flushdb } from "@server/test/support"; -import mailer from "../../mailer"; import NotificationsService from "./notifications"; -jest.mock("../../mailer"); +jest.mock("@server/mailer"); + const Notifications = new NotificationsService(); beforeEach(() => flushdb()); beforeEach(jest.resetAllMocks); + describe("documents.publish", () => { test("should not send a notification to author", async () => { const user = await buildUser(); @@ -24,13 +26,16 @@ describe("documents.publish", () => { teamId: user.teamId, event: "documents.publish", }); - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{ name: "documents.publish"; doc... Remove this comment to see the full error message await Notifications.on({ name: "documents.publish", documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, actorId: document.createdById, + ip: "127.0.0.1", + data: { + title: document.title, + }, }); expect(mailer.documentNotification).not.toHaveBeenCalled(); }); @@ -45,13 +50,17 @@ describe("documents.publish", () => { teamId: user.teamId, event: "documents.publish", }); - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{ name: "documents.publish"; doc... Remove this comment to see the full error message + await Notifications.on({ name: "documents.publish", documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, actorId: document.createdById, + ip: "127.0.0.1", + data: { + title: document.title, + }, }); expect(mailer.documentNotification).toHaveBeenCalled(); }); @@ -71,17 +80,21 @@ describe("documents.publish", () => { teamId: user.teamId, event: "documents.publish", }); - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{ name: "documents.publish"; doc... Remove this comment to see the full error message await Notifications.on({ name: "documents.publish", documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, actorId: document.createdById, + ip: "127.0.0.1", + data: { + title: document.title, + }, }); expect(mailer.documentNotification).not.toHaveBeenCalled(); }); }); + describe("revisions.create", () => { test("should send a notification to other collaborators", async () => { const document = await buildDocument(); @@ -100,8 +113,6 @@ describe("revisions.create", () => { documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{ name: "revisions.create"; docu... Remove this comment to see the full error message - actorId: document.createdById, }); expect(mailer.documentNotification).toHaveBeenCalled(); }); @@ -124,8 +135,6 @@ describe("revisions.create", () => { documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{ name: "revisions.create"; docu... Remove this comment to see the full error message - actorId: document.createdById, }); expect(mailer.documentNotification).not.toHaveBeenCalled(); }); @@ -146,8 +155,6 @@ describe("revisions.create", () => { documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{ name: "revisions.create"; docu... Remove this comment to see the full error message - actorId: document.createdById, }); expect(mailer.documentNotification).not.toHaveBeenCalled(); }); diff --git a/server/queues/processors/notifications.ts b/server/queues/processors/notifications.ts index 41efe15ba..1813794a7 100644 --- a/server/queues/processors/notifications.ts +++ b/server/queues/processors/notifications.ts @@ -1,3 +1,6 @@ +import { Op } from "sequelize"; +import Logger from "@server/logging/logger"; +import mailer from "@server/mailer"; import { View, Document, @@ -6,15 +9,12 @@ import { User, NotificationSetting, } from "@server/models"; -import { Op } from "@server/sequelize"; -import Logger from "../../logging/logger"; -import mailer from "../../mailer"; import { DocumentEvent, CollectionEvent, RevisionEvent, Event, -} from "../../types"; +} from "@server/types"; export default class NotificationsProcessor { async on(event: Event) { @@ -106,6 +106,10 @@ export default class NotificationsProcessor { continue; } + if (!setting.user.email) { + continue; + } + mailer.documentNotification({ to: setting.user.email, eventName, @@ -149,7 +153,7 @@ export default class NotificationsProcessor { for (const setting of notificationSettings) { // Suppress notifications for suspended users - if (setting.user.isSuspended) { + if (setting.user.isSuspended || !setting.user.email) { continue; } diff --git a/server/queues/processors/slack.ts b/server/queues/processors/slack.ts index e21fc7ad0..84716a791 100644 --- a/server/queues/processors/slack.ts +++ b/server/queues/processors/slack.ts @@ -70,7 +70,7 @@ export default class SlackProcessor { Document.findByPk(event.documentId), Team.findByPk(event.teamId), ]); - if (!document) return; + if (!document || !team) return; // never send notifications for draft documents if (!document.publishedAt) return; diff --git a/server/queues/processors/websockets.ts b/server/queues/processors/websockets.ts index 5bf7c2893..304ecf759 100644 --- a/server/queues/processors/websockets.ts +++ b/server/queues/processors/websockets.ts @@ -1,4 +1,5 @@ import { subHours } from "date-fns"; +import { Op } from "sequelize"; import { Document, Collection, @@ -8,7 +9,6 @@ import { Pin, } from "@server/models"; import { presentPin } from "@server/presenters"; -import { Op } from "@server/sequelize"; import { Event } from "../../types"; export default class WebsocketsProcessor { @@ -21,6 +21,10 @@ export default class WebsocketsProcessor { const document = await Document.findByPk(event.documentId, { paranoid: false, }); + if (!document) { + return; + } + const channel = document.publishedAt ? `collection-${document.collectionId}` : `user-${event.actorId}`; @@ -44,6 +48,9 @@ export default class WebsocketsProcessor { const document = await Document.findByPk(event.documentId, { paranoid: false, }); + if (!document) { + return; + } if (!document.publishedAt) { return socketio.to(`user-${document.createdById}`).emit("entities", { @@ -87,6 +94,9 @@ export default class WebsocketsProcessor { const document = await Document.findByPk(event.documentId, { paranoid: false, }); + if (!document) { + return; + } const channel = document.publishedAt ? `collection-${document.collectionId}` : `user-${event.actorId}`; @@ -103,6 +113,9 @@ export default class WebsocketsProcessor { case "documents.create": { const document = await Document.findByPk(event.documentId); + if (!document) { + return; + } return socketio.to(`user-${event.actorId}`).emit("entities", { event: event.name, documentIds: [ @@ -133,7 +146,6 @@ export default class WebsocketsProcessor { }, paranoid: false, }); - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type. documents.forEach((document) => { socketio.to(`collection-${document.collectionId}`).emit("entities", { event: event.name, @@ -162,6 +174,9 @@ export default class WebsocketsProcessor { const collection = await Collection.findByPk(event.collectionId, { paranoid: false, }); + if (!collection) { + return; + } socketio .to( collection.permission @@ -194,6 +209,9 @@ export default class WebsocketsProcessor { const collection = await Collection.findByPk(event.collectionId, { paranoid: false, }); + if (!collection) { + return; + } return socketio.to(`team-${collection.teamId}`).emit("entities", { event: event.name, collectionIds: [ @@ -270,6 +288,9 @@ export default class WebsocketsProcessor { case "collections.add_group": { const group = await Group.findByPk(event.data.groupId); + if (!group) { + return; + } // the users being added are not yet in the websocket channel for the collection // so they need to be notified separately @@ -293,6 +314,10 @@ export default class WebsocketsProcessor { case "collections.remove_group": { const group = await Group.findByPk(event.data.groupId); + if (!group) { + return; + } + const membershipUserIds = await Collection.membershipUserIds( event.collectionId ); @@ -337,6 +362,9 @@ export default class WebsocketsProcessor { case "pins.create": case "pins.update": { const pin = await Pin.findByPk(event.modelId); + if (!pin) { + return; + } return socketio .to( pin.collectionId @@ -363,6 +391,9 @@ export default class WebsocketsProcessor { const group = await Group.findByPk(event.modelId, { paranoid: false, }); + if (!group) { + return; + } return socketio.to(`team-${group.teamId}`).emit("entities", { event: event.name, groupIds: [ @@ -462,6 +493,10 @@ export default class WebsocketsProcessor { const group = await Group.findByPk(event.modelId, { paranoid: false, }); + if (!group) { + return; + } + socketio.to(`team-${group.teamId}`).emit("entities", { event: event.name, groupIds: [ diff --git a/server/routes/api/apiKeys.ts b/server/routes/api/apiKeys.ts index 0e38f739e..33b16b02d 100644 --- a/server/routes/api/apiKeys.ts +++ b/server/routes/api/apiKeys.ts @@ -1,23 +1,24 @@ import Router from "koa-router"; import auth from "@server/middlewares/authentication"; import { ApiKey, Event } from "@server/models"; -import policy from "@server/policies"; +import { authorize } from "@server/policies"; import { presentApiKey } from "@server/presenters"; import { assertUuid, assertPresent } from "@server/validation"; import pagination from "./middlewares/pagination"; -const { authorize } = policy; const router = new Router(); router.post("apiKeys.create", auth(), async (ctx) => { const { name } = ctx.body; assertPresent(name, "name is required"); - const user = ctx.state.user; + const { user } = ctx.state; + authorize(user, "createApiKey", user.team); const key = await ApiKey.create({ name, userId: user.id, }); + await Event.create({ name: "api_keys.create", modelId: key.id, @@ -28,13 +29,14 @@ router.post("apiKeys.create", auth(), async (ctx) => { }, ip: ctx.request.ip, }); + ctx.body = { data: presentApiKey(key), }; }); router.post("apiKeys.list", auth(), pagination(), async (ctx) => { - const user = ctx.state.user; + const { user } = ctx.state; const keys = await ApiKey.findAll({ where: { userId: user.id, @@ -43,6 +45,7 @@ router.post("apiKeys.list", auth(), pagination(), async (ctx) => { offset: ctx.state.pagination.offset, limit: ctx.state.pagination.limit, }); + ctx.body = { pagination: ctx.state.pagination, data: keys.map(presentApiKey), @@ -52,9 +55,10 @@ router.post("apiKeys.list", auth(), pagination(), async (ctx) => { router.post("apiKeys.delete", auth(), async (ctx) => { const { id } = ctx.body; assertUuid(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; const key = await ApiKey.findByPk(id); authorize(user, "delete", key); + await key.destroy(); await Event.create({ name: "api_keys.delete", @@ -66,6 +70,7 @@ router.post("apiKeys.delete", auth(), async (ctx) => { }, ip: ctx.request.ip, }); + ctx.body = { success: true, }; diff --git a/server/routes/api/attachments.test.ts b/server/routes/api/attachments.test.ts index e022da392..a442cf24c 100644 --- a/server/routes/api/attachments.test.ts +++ b/server/routes/api/attachments.test.ts @@ -1,6 +1,6 @@ // @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'fetc... Remove this comment to see the full error message import TestServer from "fetch-test-server"; -import { Attachment } from "@server/models"; +import Attachment from "@server/models/Attachment"; import webService from "@server/services/web"; import { buildUser, @@ -13,19 +13,10 @@ import { flushdb } from "@server/test/support"; const app = webService(); const server = new TestServer(app.callback()); -jest.mock("aws-sdk", () => { - const mS3 = { - createPresignedPost: jest.fn(), - deleteObject: jest.fn().mockReturnThis(), - promise: jest.fn(), - }; - return { - S3: jest.fn(() => mS3), - Endpoint: jest.fn(), - }; -}); + beforeEach(() => flushdb()); afterAll(() => server.close()); + describe("#attachments.delete", () => { it("should require authentication", async () => { const res = await server.post("/api/attachments.delete"); @@ -120,12 +111,12 @@ describe("#attachments.delete", () => { }); const document = await buildDocument({ teamId: collection.teamId, - userId: collection.userId, + userId: collection.createdById, collectionId: collection.id, }); const attachment = await buildAttachment({ teamId: document.teamId, - userId: document.userId, + userId: document.createdById, documentId: document.id, acl: "private", }); @@ -138,6 +129,7 @@ describe("#attachments.delete", () => { expect(res.status).toEqual(403); }); }); + describe("#attachments.redirect", () => { it("should require authentication", async () => { const res = await server.post("/api/attachments.redirect"); @@ -221,12 +213,12 @@ describe("#attachments.redirect", () => { }); const document = await buildDocument({ teamId: collection.teamId, - userId: collection.userId, + userId: collection.createdById, collectionId: collection.id, }); const attachment = await buildAttachment({ teamId: document.teamId, - userId: document.userId, + userId: document.createdById, documentId: document.id, acl: "private", }); diff --git a/server/routes/api/attachments.ts b/server/routes/api/attachments.ts index 2530b830c..96e5febd8 100644 --- a/server/routes/api/attachments.ts +++ b/server/routes/api/attachments.ts @@ -3,7 +3,7 @@ import { v4 as uuidv4 } from "uuid"; import { NotFoundError } from "@server/errors"; import auth from "@server/middlewares/authentication"; import { Attachment, Document, Event } from "@server/models"; -import policy from "@server/policies"; +import { authorize } from "@server/policies"; import { getPresignedPost, publicS3Endpoint, @@ -11,7 +11,6 @@ import { } from "@server/utils/s3"; import { assertPresent } from "@server/validation"; -const { authorize } = policy; const router = new Router(); const AWS_S3_ACL = process.env.AWS_S3_ACL || "private"; @@ -61,6 +60,7 @@ router.post("attachments.create", auth(), async (ctx) => { userId: user.id, ip: ctx.request.ip, }); + ctx.body = { data: { maxUploadSize: process.env.AWS_S3_UPLOAD_MAX_SIZE, @@ -85,7 +85,7 @@ router.post("attachments.create", auth(), async (ctx) => { router.post("attachments.delete", auth(), async (ctx) => { const { id } = ctx.body; assertPresent(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; const attachment = await Attachment.findByPk(id); if (!attachment) { @@ -107,6 +107,7 @@ router.post("attachments.delete", auth(), async (ctx) => { userId: user.id, ip: ctx.request.ip, }); + ctx.body = { success: true, }; @@ -115,7 +116,7 @@ router.post("attachments.delete", auth(), async (ctx) => { router.post("attachments.redirect", auth(), async (ctx) => { const { id } = ctx.body; assertPresent(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; const attachment = await Attachment.findByPk(id); if (!attachment) { diff --git a/server/routes/api/auth.test.ts b/server/routes/api/auth.test.ts index 85023162a..da2d97b8b 100644 --- a/server/routes/api/auth.test.ts +++ b/server/routes/api/auth.test.ts @@ -8,6 +8,7 @@ const app = webService(); const server = new TestServer(app.callback()); beforeEach(() => flushdb()); afterAll(() => server.close()); + describe("#auth.info", () => { it("should return current authentication", async () => { const team = await buildTeam(); @@ -44,6 +45,7 @@ describe("#auth.info", () => { expect(res.status).toEqual(401); }); }); + describe("#auth.config", () => { it("should return available SSO providers", async () => { const res = await server.post("/api/auth.config"); diff --git a/server/routes/api/auth.ts b/server/routes/api/auth.ts index c125e3dc7..d5d59a39c 100644 --- a/server/routes/api/auth.ts +++ b/server/routes/api/auth.ts @@ -1,3 +1,4 @@ +import invariant from "invariant"; import Router from "koa-router"; import { find } from "lodash"; import { parseDomain, isCustomSubdomain } from "@shared/utils/domains"; @@ -5,14 +6,11 @@ import auth from "@server/middlewares/authentication"; import { Team } from "@server/models"; import { presentUser, presentTeam, presentPolicies } from "@server/presenters"; import { isCustomDomain } from "@server/utils/domains"; -// @ts-expect-error ts-migrate(7034) FIXME: Variable 'providers' implicitly has type 'any[]' i... Remove this comment to see the full error message import providers from "../auth/providers"; const router = new Router(); -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'team' implicitly has an 'any' type. -function filterProviders(team) { - // @ts-expect-error ts-migrate(7005) FIXME: Variable 'providers' implicitly has an 'any[]' typ... Remove this comment to see the full error message +function filterProviders(team: Team) { return providers .sort((provider) => (provider.id === "email" ? 1 : -1)) .filter((provider) => { @@ -112,8 +110,10 @@ router.post("auth.config", async (ctx) => { }); router.post("auth.info", auth(), async (ctx) => { - const user = ctx.state.user; + const { user } = ctx.state; const team = await Team.findByPk(user.teamId); + invariant(team, "Team not found"); + ctx.body = { data: { user: presentUser(user, { diff --git a/server/routes/api/authenticationProviders.test.ts b/server/routes/api/authenticationProviders.test.ts index 5ff791379..14d6ff8b3 100644 --- a/server/routes/api/authenticationProviders.test.ts +++ b/server/routes/api/authenticationProviders.test.ts @@ -17,7 +17,7 @@ describe("#authenticationProviders.info", () => { const user = await buildUser({ teamId: team.id, }); - const authenticationProviders = await team.getAuthenticationProviders(); + const authenticationProviders = await team.$get("authenticationProviders"); const res = await server.post("/api/authenticationProviders.info", { body: { id: authenticationProviders[0].id, @@ -36,7 +36,7 @@ describe("#authenticationProviders.info", () => { it("should require authorization", async () => { const team = await buildTeam(); const user = await buildUser(); - const authenticationProviders = await team.getAuthenticationProviders(); + const authenticationProviders = await team.$get("authenticationProviders"); const res = await server.post("/api/authenticationProviders.info", { body: { id: authenticationProviders[0].id, @@ -48,7 +48,7 @@ describe("#authenticationProviders.info", () => { it("should require authentication", async () => { const team = await buildTeam(); - const authenticationProviders = await team.getAuthenticationProviders(); + const authenticationProviders = await team.$get("authenticationProviders"); const res = await server.post("/api/authenticationProviders.info", { body: { id: authenticationProviders[0].id, @@ -57,13 +57,14 @@ describe("#authenticationProviders.info", () => { expect(res.status).toEqual(401); }); }); + describe("#authenticationProviders.update", () => { it("should not allow admins to disable when last authentication provider", async () => { const team = await buildTeam(); const user = await buildAdmin({ teamId: team.id, }); - const authenticationProviders = await team.getAuthenticationProviders(); + const authenticationProviders = await team.$get("authenticationProviders"); const res = await server.post("/api/authenticationProviders.update", { body: { id: authenticationProviders[0].id, @@ -79,11 +80,11 @@ describe("#authenticationProviders.update", () => { const user = await buildAdmin({ teamId: team.id, }); - await team.createAuthenticationProvider({ + await team.$create("authenticationProvider", { name: "google", providerId: uuidv4(), }); - const authenticationProviders = await team.getAuthenticationProviders(); + const authenticationProviders = await team.$get("authenticationProviders"); const res = await server.post("/api/authenticationProviders.update", { body: { id: authenticationProviders[0].id, @@ -103,7 +104,7 @@ describe("#authenticationProviders.update", () => { const user = await buildUser({ teamId: team.id, }); - const authenticationProviders = await team.getAuthenticationProviders(); + const authenticationProviders = await team.$get("authenticationProviders"); const res = await server.post("/api/authenticationProviders.update", { body: { id: authenticationProviders[0].id, @@ -116,7 +117,7 @@ describe("#authenticationProviders.update", () => { it("should require authentication", async () => { const team = await buildTeam(); - const authenticationProviders = await team.getAuthenticationProviders(); + const authenticationProviders = await team.$get("authenticationProviders"); const res = await server.post("/api/authenticationProviders.update", { body: { id: authenticationProviders[0].id, @@ -126,6 +127,7 @@ describe("#authenticationProviders.update", () => { expect(res.status).toEqual(401); }); }); + describe("#authenticationProviders.list", () => { it("should return enabled and available auth providers", async () => { const team = await buildTeam(); diff --git a/server/routes/api/authenticationProviders.ts b/server/routes/api/authenticationProviders.ts index ce2aed89d..82e40503b 100644 --- a/server/routes/api/authenticationProviders.ts +++ b/server/routes/api/authenticationProviders.ts @@ -1,24 +1,23 @@ import Router from "koa-router"; import auth from "@server/middlewares/authentication"; import { AuthenticationProvider, Event } from "@server/models"; -import policy from "@server/policies"; +import { authorize } from "@server/policies"; import { presentAuthenticationProvider, presentPolicies, } from "@server/presenters"; import { assertUuid, assertPresent } from "@server/validation"; -// @ts-expect-error ts-migrate(7034) FIXME: Variable 'allAuthenticationProviders' implicitly h... Remove this comment to see the full error message import allAuthenticationProviders from "../auth/providers"; const router = new Router(); -const { authorize } = policy; router.post("authenticationProviders.info", auth(), async (ctx) => { const { id } = ctx.body; assertUuid(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; const authenticationProvider = await AuthenticationProvider.findByPk(id); authorize(user, "read", authenticationProvider); + ctx.body = { data: presentAuthenticationProvider(authenticationProvider), policies: presentPolicies(user, [authenticationProvider]), @@ -29,7 +28,7 @@ router.post("authenticationProviders.update", auth(), async (ctx) => { const { id, isEnabled } = ctx.body; assertUuid(id, "id is required"); assertPresent(isEnabled, "isEnabled is required"); - const user = ctx.state.user; + const { user } = ctx.state; const authenticationProvider = await AuthenticationProvider.findByPk(id); authorize(user, "update", authenticationProvider); const enabled = !!isEnabled; @@ -50,6 +49,7 @@ router.post("authenticationProviders.update", auth(), async (ctx) => { actorId: user.id, ip: ctx.request.ip, }); + ctx.body = { data: presentAuthenticationProvider(authenticationProvider), policies: presentPolicies(user, [authenticationProvider]), @@ -57,10 +57,13 @@ router.post("authenticationProviders.update", auth(), async (ctx) => { }); router.post("authenticationProviders.list", auth(), async (ctx) => { - const user = ctx.state.user; + const { user } = ctx.state; authorize(user, "read", user.team); - const teamAuthenticationProviders = await user.team.getAuthenticationProviders(); - // @ts-expect-error ts-migrate(7005) FIXME: Variable 'allAuthenticationProviders' implicitly h... Remove this comment to see the full error message + + const teamAuthenticationProviders = await user.team.$get( + "authenticationProviders" + ); + const otherAuthenticationProviders = allAuthenticationProviders.filter( (p) => // @ts-expect-error ts-migrate(7006) FIXME: Parameter 't' implicitly has an 'any' type. @@ -69,6 +72,7 @@ router.post("authenticationProviders.list", auth(), async (ctx) => { // wants to be here in the future – we'll need to migrate more data though p.id !== "email" ); + ctx.body = { data: { authenticationProviders: [ diff --git a/server/routes/api/collections.test.ts b/server/routes/api/collections.test.ts index ad06e1719..dda2e35d6 100644 --- a/server/routes/api/collections.test.ts +++ b/server/routes/api/collections.test.ts @@ -15,6 +15,7 @@ const app = webService(); const server = new TestServer(app.callback()); beforeEach(() => flushdb()); afterAll(() => server.close()); + describe("#collections.list", () => { it("should require authentication", async () => { const res = await server.post("/api/collections.list"); @@ -93,12 +94,12 @@ describe("#collections.list", () => { const group = await buildGroup({ teamId: user.teamId, }); - await group.addUser(user, { + await group.$add("user", user, { through: { createdById: user.id, }, }); - await collection.addGroup(group, { + await collection.$add("group", group, { through: { permission: "read", createdById: user.id, @@ -116,6 +117,7 @@ describe("#collections.list", () => { expect(body.policies[0].abilities.read).toEqual(true); }); }); + describe("#collections.import", () => { it("should error if no attachmentId is passed", async () => { const user = await buildUser(); @@ -134,6 +136,7 @@ describe("#collections.import", () => { expect(body).toMatchSnapshot(); }); }); + describe("#collections.move", () => { it("should require authentication", async () => { const res = await server.post("/api/collections.move"); @@ -265,6 +268,7 @@ describe("#collections.move", () => { expect(movedCollectionC.data.index < "b").toBeTruthy(); }); }); + describe("#collections.export", () => { it("should not allow export of private collection not a member", async () => { const { admin } = await seed(); @@ -309,12 +313,12 @@ describe("#collections.export", () => { const group = await buildGroup({ teamId: admin.teamId, }); - await group.addUser(admin, { + await group.$add("user", admin, { through: { createdById: admin.id, }, }); - await collection.addGroup(group, { + await collection.$add("group", group, { through: { permission: "read_write", createdById: admin.id, @@ -364,6 +368,7 @@ describe("#collections.export", () => { expect(body.data.fileOperation.state).toBe("creating"); }); }); + describe("#collections.export_all", () => { it("should require authentication", async () => { const res = await server.post("/api/collections.export_all"); @@ -392,6 +397,7 @@ describe("#collections.export_all", () => { expect(res.status).toEqual(200); }); }); + describe("#collections.add_user", () => { it("should add user to collection", async () => { const user = await buildUser(); @@ -410,7 +416,7 @@ describe("#collections.add_user", () => { userId: anotherUser.id, }, }); - const users = await collection.getUsers(); + const users = await collection.$get("users"); expect(res.status).toEqual(200); expect(users.length).toEqual(2); }); @@ -455,6 +461,7 @@ describe("#collections.add_user", () => { expect(res.status).toEqual(403); }); }); + describe("#collections.add_group", () => { it("should add group to collection", async () => { const user = await buildAdmin(); @@ -473,7 +480,7 @@ describe("#collections.add_group", () => { groupId: group.id, }, }); - const groups = await collection.getGroups(); + const groups = await collection.$get("groups"); expect(groups.length).toEqual(1); expect(res.status).toEqual(200); }); @@ -519,6 +526,7 @@ describe("#collections.add_group", () => { expect(res.status).toEqual(403); }); }); + describe("#collections.remove_group", () => { it("should remove group from collection", async () => { const user = await buildAdmin(); @@ -537,7 +545,7 @@ describe("#collections.remove_group", () => { groupId: group.id, }, }); - let users = await collection.getGroups(); + let users = await collection.$get("groups"); expect(users.length).toEqual(1); const res = await server.post("/api/collections.remove_group", { body: { @@ -546,7 +554,7 @@ describe("#collections.remove_group", () => { groupId: group.id, }, }); - users = await collection.getGroups(); + users = await collection.$get("groups"); expect(res.status).toEqual(200); expect(users.length).toEqual(0); }); @@ -591,6 +599,7 @@ describe("#collections.remove_group", () => { expect(res.status).toEqual(403); }); }); + describe("#collections.remove_user", () => { it("should remove user from collection", async () => { const user = await buildUser(); @@ -616,7 +625,7 @@ describe("#collections.remove_user", () => { userId: anotherUser.id, }, }); - const users = await collection.getUsers(); + const users = await collection.$get("users"); expect(res.status).toEqual(200); expect(users.length).toEqual(1); }); @@ -661,6 +670,7 @@ describe("#collections.remove_user", () => { expect(res.status).toEqual(403); }); }); + describe("#collections.users", () => { it("should return users in private collection", async () => { const { collection, user } = await seed(); @@ -702,6 +712,7 @@ describe("#collections.users", () => { expect(res.status).toEqual(403); }); }); + describe("#collections.group_memberships", () => { it("should return groups in private collection", async () => { const user = await buildUser(); @@ -850,6 +861,7 @@ describe("#collections.group_memberships", () => { expect(res.status).toEqual(403); }); }); + describe("#collections.memberships", () => { it("should return members in private collection", async () => { const { collection, user } = await seed(); @@ -952,6 +964,7 @@ describe("#collections.memberships", () => { expect(res.status).toEqual(403); }); }); + describe("#collections.info", () => { it("should return collection", async () => { const { user, collection } = await seed(); @@ -1019,6 +1032,7 @@ describe("#collections.info", () => { expect(res.status).toEqual(403); }); }); + describe("#collections.create", () => { it("should require authentication", async () => { const res = await server.post("/api/collections.create"); @@ -1163,6 +1177,7 @@ describe("#collections.create", () => { expect(createdCollection.data.index < "b").toBeTruthy(); }); }); + describe("#collections.update", () => { it("should require authentication", async () => { const collection = await buildCollection(); @@ -1317,12 +1332,12 @@ describe("#collections.update", () => { const group = await buildGroup({ teamId: user.teamId, }); - await group.addUser(user, { + await group.$add("user", user, { through: { createdById: user.id, }, }); - await collection.addGroup(group, { + await collection.$add("group", group, { through: { permission: "read_write", createdById: user.id, @@ -1393,6 +1408,7 @@ describe("#collections.update", () => { expect(res.status).toEqual(400); }); }); + describe("#collections.delete", () => { it("should require authentication", async () => { const res = await server.post("/api/collections.delete"); @@ -1484,12 +1500,12 @@ describe("#collections.delete", () => { const group = await buildGroup({ teamId: user.teamId, }); - await group.addUser(user, { + await group.$add("user", user, { through: { createdById: user.id, }, }); - await collection.addGroup(group, { + await collection.$add("group", group, { through: { permission: "read_write", createdById: user.id, diff --git a/server/routes/api/collections.ts b/server/routes/api/collections.ts index 816ed262a..7579b563e 100644 --- a/server/routes/api/collections.ts +++ b/server/routes/api/collections.ts @@ -1,5 +1,7 @@ import fractionalIndex from "fractional-index"; +import invariant from "invariant"; import Router from "koa-router"; +import { Sequelize, Op, WhereOptions } from "sequelize"; import collectionExporter from "@server/commands/collectionExporter"; import { ValidationError } from "@server/errors"; import auth from "@server/middlewares/authentication"; @@ -13,7 +15,7 @@ import { Group, Attachment, } from "@server/models"; -import policy from "@server/policies"; +import { authorize } from "@server/policies"; import { presentCollection, presentUser, @@ -23,7 +25,6 @@ import { presentCollectionGroupMembership, presentFileOperation, } from "@server/presenters"; -import { Op, sequelize } from "@server/sequelize"; import collectionIndexing from "@server/utils/collectionIndexing"; import removeIndexCollision from "@server/utils/removeIndexCollision"; import { @@ -35,7 +36,6 @@ import { } from "@server/validation"; import pagination from "./middlewares/pagination"; -const { authorize } = policy; const router = new Router(); router.post("collections.create", auth(), async (ctx) => { @@ -55,7 +55,7 @@ router.post("collections.create", auth(), async (ctx) => { assertHexColor(color, "Invalid hex value (please use format #FFFFFF)"); } - const user = ctx.state.user; + const { user } = ctx.state; authorize(user, "createCollection", user.team); if (index) { @@ -70,7 +70,7 @@ router.post("collections.create", auth(), async (ctx) => { limit: 1, order: [ // using LC_COLLATE:"C" because we need byte order to drive the sorting - sequelize.literal('"collection"."index" collate "C"'), + Sequelize.literal('"collection"."index" collate "C"'), ["updatedAt", "DESC"], ], }); @@ -82,7 +82,7 @@ router.post("collections.create", auth(), async (ctx) => { } index = await removeIndexCollision(user.teamId, index); - let collection = await Collection.create({ + const collection = await Collection.create({ name, description, icon, @@ -105,23 +105,27 @@ router.post("collections.create", auth(), async (ctx) => { ip: ctx.request.ip, }); // we must reload the collection to get memberships for policy presenter - collection = await Collection.scope({ + const reloaded = await Collection.scope({ method: ["withMembership", user.id], }).findByPk(collection.id); + invariant(reloaded, "collection not found"); + ctx.body = { - data: presentCollection(collection), - policies: presentPolicies(user, [collection]), + data: presentCollection(reloaded), + policies: presentPolicies(user, [reloaded]), }; }); router.post("collections.info", auth(), async (ctx) => { const { id } = ctx.body; assertPresent(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; const collection = await Collection.scope({ method: ["withMembership", user.id], }).findByPk(id); + authorize(user, "read", collection); + ctx.body = { data: presentCollection(collection), policies: presentPolicies(user, [collection]), @@ -132,7 +136,7 @@ router.post("collections.import", auth(), async (ctx) => { const { type, attachmentId } = ctx.body; assertIn(type, ["outline"], "type must be one of 'outline'"); assertUuid(attachmentId, "attachmentId is required"); - const user = ctx.state.user; + const { user } = ctx.state; authorize(user, "importCollection", user.team); const attachment = await Attachment.findByPk(attachmentId); authorize(user, "read", attachment); @@ -146,6 +150,7 @@ router.post("collections.import", auth(), async (ctx) => { }, ip: ctx.request.ip, }); + ctx.body = { success: true, }; @@ -155,12 +160,15 @@ router.post("collections.add_group", auth(), async (ctx) => { const { id, groupId, permission = "read_write" } = ctx.body; assertUuid(id, "id is required"); assertUuid(groupId, "groupId is required"); + const collection = await Collection.scope({ method: ["withMembership", ctx.state.user.id], }).findByPk(id); authorize(ctx.state.user, "update", collection); + const group = await Group.findByPk(groupId); authorize(ctx.state.user, "read", group); + let membership = await CollectionGroup.findOne({ where: { collectionId: id, @@ -191,6 +199,7 @@ router.post("collections.add_group", auth(), async (ctx) => { }, ip: ctx.request.ip, }); + ctx.body = { data: { collectionGroupMemberships: [ @@ -204,13 +213,16 @@ router.post("collections.remove_group", auth(), async (ctx) => { const { id, groupId } = ctx.body; assertUuid(id, "id is required"); assertUuid(groupId, "groupId is required"); + const collection = await Collection.scope({ method: ["withMembership", ctx.state.user.id], }).findByPk(id); authorize(ctx.state.user, "update", collection); + const group = await Group.findByPk(groupId); authorize(ctx.state.user, "read", group); - await collection.removeGroup(group); + + await collection.$remove("group", group); await Event.create({ name: "collections.remove_group", collectionId: collection.id, @@ -222,6 +234,7 @@ router.post("collections.remove_group", auth(), async (ctx) => { }, ip: ctx.request.ip, }); + ctx.body = { success: true, }; @@ -234,12 +247,14 @@ router.post( async (ctx) => { const { id, query, permission } = ctx.body; assertUuid(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; + const collection = await Collection.scope({ method: ["withMembership", user.id], }).findByPk(id); authorize(user, "read", collection); - let where = { + + let where: WhereOptions = { collectionId: id, }; let groupWhere; @@ -253,7 +268,6 @@ router.post( } if (permission) { - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ permission: any; collectionId: any; }' is ... Remove this comment to see the full error message where = { ...where, permission }; } @@ -277,7 +291,6 @@ router.post( collectionGroupMemberships: memberships.map( presentCollectionGroupMembership ), - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'membership' implicitly has an 'any' typ... Remove this comment to see the full error message groups: memberships.map((membership) => presentGroup(membership.group)), }, }; @@ -288,12 +301,15 @@ router.post("collections.add_user", auth(), async (ctx) => { const { id, userId, permission = "read_write" } = ctx.body; assertUuid(id, "id is required"); assertUuid(userId, "userId is required"); + const collection = await Collection.scope({ method: ["withMembership", ctx.state.user.id], }).findByPk(id); authorize(ctx.state.user, "update", collection); + const user = await User.findByPk(userId); authorize(ctx.state.user, "read", user); + let membership = await CollectionUser.findOne({ where: { collectionId: id, @@ -324,6 +340,7 @@ router.post("collections.add_user", auth(), async (ctx) => { }, ip: ctx.request.ip, }); + ctx.body = { data: { users: [presentUser(user)], @@ -336,13 +353,16 @@ router.post("collections.remove_user", auth(), async (ctx) => { const { id, userId } = ctx.body; assertUuid(id, "id is required"); assertUuid(userId, "userId is required"); + const collection = await Collection.scope({ method: ["withMembership", ctx.state.user.id], }).findByPk(id); authorize(ctx.state.user, "update", collection); + const user = await User.findByPk(userId); authorize(ctx.state.user, "read", user); - await collection.removeUser(user); + + await collection.$remove("user", user); await Event.create({ name: "collections.remove_user", userId, @@ -354,34 +374,41 @@ router.post("collections.remove_user", auth(), async (ctx) => { }, ip: ctx.request.ip, }); + ctx.body = { success: true, }; }); + // DEPRECATED: Use collection.memberships which has pagination, filtering and permissions router.post("collections.users", auth(), async (ctx) => { const { id } = ctx.body; assertUuid(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; + const collection = await Collection.scope({ method: ["withMembership", user.id], }).findByPk(id); authorize(user, "read", collection); - const users = await collection.getUsers(); + + const users = await collection.$get("users"); + ctx.body = { - data: users.map(presentUser), + data: users.map((user) => presentUser(user)), }; }); router.post("collections.memberships", auth(), pagination(), async (ctx) => { const { id, query, permission } = ctx.body; assertUuid(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; + const collection = await Collection.scope({ method: ["withMembership", user.id], }).findByPk(id); authorize(user, "read", collection); - let where = { + + let where: WhereOptions = { collectionId: id, }; let userWhere; @@ -395,7 +422,6 @@ router.post("collections.memberships", auth(), pagination(), async (ctx) => { } if (permission) { - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ permission: any; collectionId: any; }' is ... Remove this comment to see the full error message where = { ...where, permission }; } @@ -413,11 +439,11 @@ router.post("collections.memberships", auth(), pagination(), async (ctx) => { }, ], }); + ctx.body = { pagination: ctx.state.pagination, data: { memberships: memberships.map(presentMembership), - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'membership' implicitly has an 'any' typ... Remove this comment to see the full error message users: memberships.map((membership) => presentUser(membership.user)), }, }; @@ -426,20 +452,22 @@ router.post("collections.memberships", auth(), pagination(), async (ctx) => { router.post("collections.export", auth(), async (ctx) => { const { id } = ctx.body; assertUuid(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; const team = await Team.findByPk(user.teamId); authorize(user, "export", team); + const collection = await Collection.scope({ method: ["withMembership", user.id], }).findByPk(id); - assertPresent(collection, "Collection should be present"); authorize(user, "read", collection); + const fileOperation = await collectionExporter({ collection, user, team, ip: ctx.request.ip, }); + ctx.body = { success: true, data: { @@ -449,14 +477,16 @@ router.post("collections.export", auth(), async (ctx) => { }); router.post("collections.export_all", auth(), async (ctx) => { - const user = ctx.state.user; + const { user } = ctx.state; const team = await Team.findByPk(user.teamId); authorize(user, "export", team); + const fileOperation = await collectionExporter({ user, team, ip: ctx.request.ip, }); + ctx.body = { success: true, data: { @@ -481,7 +511,7 @@ router.post("collections.update", auth(), async (ctx) => { assertHexColor(color, "Invalid hex value (please use format #FFFFFF)"); } - const user = ctx.state.user; + const { user } = ctx.state; const collection = await Collection.scope({ method: ["withMembership", user.id], }).findByPk(id); @@ -580,7 +610,7 @@ router.post("collections.update", auth(), async (ctx) => { }); router.post("collections.list", auth(), pagination(), async (ctx) => { - const user = ctx.state.user; + const { user } = ctx.state; const collectionIds = await user.collectionIds(); const collections = await Collection.scope({ method: ["withMembership", user.id], @@ -594,13 +624,11 @@ router.post("collections.list", auth(), pagination(), async (ctx) => { limit: ctx.state.pagination.limit, }); const nullIndexCollection = collections.findIndex( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'collection' implicitly has an 'any' typ... Remove this comment to see the full error message (collection) => collection.index === null ); if (nullIndexCollection !== -1) { const indexedCollections = await collectionIndexing(ctx.state.user.teamId); - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'collection' implicitly has an 'any' typ... Remove this comment to see the full error message collections.forEach((collection) => { collection.index = indexedCollections[collection.id]; }); @@ -615,7 +643,7 @@ router.post("collections.list", auth(), pagination(), async (ctx) => { router.post("collections.delete", auth(), async (ctx) => { const { id } = ctx.body; - const user = ctx.state.user; + const { user } = ctx.state; assertUuid(id, "id is required"); const collection = await Collection.scope({ @@ -637,6 +665,7 @@ router.post("collections.delete", auth(), async (ctx) => { }, ip: ctx.request.ip, }); + ctx.body = { success: true, }; @@ -648,9 +677,11 @@ router.post("collections.move", auth(), async (ctx) => { assertPresent(index, "index is required"); assertIndexCharacters(index); assertUuid(id, "id must be a uuid"); - const user = ctx.state.user; + const { user } = ctx.state; + const collection = await Collection.findByPk(id); authorize(user, "move", collection); + index = await removeIndexCollision(user.teamId, index); await collection.update({ index, @@ -665,6 +696,7 @@ router.post("collections.move", auth(), async (ctx) => { }, ip: ctx.request.ip, }); + ctx.body = { success: true, data: { diff --git a/server/routes/api/documents.test.ts b/server/routes/api/documents.test.ts index 5923bca78..d37333bd0 100644 --- a/server/routes/api/documents.test.ts +++ b/server/routes/api/documents.test.ts @@ -23,6 +23,7 @@ const app = webService(); const server = new TestServer(app.callback()); beforeEach(() => flushdb()); afterAll(() => server.close()); + describe("#documents.info", () => { it("should return published document", async () => { const { user, document } = await seed(); @@ -190,7 +191,7 @@ describe("#documents.info", () => { expect(body.data.document.id).toEqual(childDocument.id); expect(body.data.document.createdBy).toEqual(undefined); expect(body.data.document.updatedBy).toEqual(undefined); - expect(body.data.sharedTree).toEqual(collection.documentStructure[0]); + expect(body.data.sharedTree).toEqual(collection.documentStructure?.[0]); await share.reload(); expect(share.lastAccessedAt).toBeTruthy(); }); @@ -457,6 +458,7 @@ describe("#documents.info", () => { expect(res.status).toEqual(400); }); }); + describe("#documents.export", () => { it("should return published document", async () => { const { user, document } = await seed(); @@ -656,6 +658,7 @@ describe("#documents.export", () => { expect(res.status).toEqual(400); }); }); + describe("#documents.list", () => { it("should return documents", async () => { const { user, document } = await seed(); @@ -875,6 +878,7 @@ describe("#documents.drafts", () => { expect(body.data.length).toEqual(0); }); }); + describe("#documents.search_titles", () => { it("should return case insensitive results for partial query", async () => { const user = await buildUser(); @@ -925,6 +929,7 @@ describe("#documents.search_titles", () => { expect(res.status).toEqual(401); }); }); + describe("#documents.search", () => { it("should return results", async () => { const { user } = await seed(); @@ -1278,8 +1283,7 @@ describe("#documents.search", () => { expect(body).toMatchSnapshot(); }); - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type '(done: DoneCallback) => Promise<... Remove this comment to see the full error message - it("should save search term, hits and source", async (done) => { + it("should save search term, hits and source", async () => { const { user } = await seed(); await server.post("/api/documents.search", { body: { @@ -1287,21 +1291,25 @@ describe("#documents.search", () => { query: "my term", }, }); - // setTimeout is needed here because SearchQuery is saved asynchronously - // in order to not slow down the response time. - setTimeout(async () => { - const searchQuery = await SearchQuery.findAll({ - where: { - query: "my term", - }, - }); - expect(searchQuery.length).toBe(1); - expect(searchQuery[0].results).toBe(0); - expect(searchQuery[0].source).toBe("app"); - done(); - }, 100); + + return new Promise((resolve) => { + // setTimeout is needed here because SearchQuery is saved asynchronously + // in order to not slow down the response time. + setTimeout(async () => { + const searchQuery = await SearchQuery.findAll({ + where: { + query: "my term", + }, + }); + expect(searchQuery.length).toBe(1); + expect(searchQuery[0].results).toBe(0); + expect(searchQuery[0].source).toBe("app"); + resolve(undefined); + }, 100); + }); }); }); + describe("#documents.archived", () => { it("should return archived documents", async () => { const { user } = await seed(); @@ -1326,7 +1334,7 @@ describe("#documents.archived", () => { userId: user.id, teamId: user.teamId, }); - await document.delete(); + await document.delete(user.id); const res = await server.post("/api/documents.archived", { body: { token: user.getJwtToken(), @@ -1362,6 +1370,7 @@ describe("#documents.archived", () => { expect(res.status).toEqual(401); }); }); + describe("#documents.viewed", () => { it("should return empty result if no views", async () => { const { user } = await seed(); @@ -1377,7 +1386,7 @@ describe("#documents.viewed", () => { it("should return recently viewed documents", async () => { const { user, document } = await seed(); - await View.increment({ + await View.incrementOrCreate({ documentId: document.id, userId: user.id, }); @@ -1395,7 +1404,7 @@ describe("#documents.viewed", () => { it("should not return recently viewed but deleted documents", async () => { const { user, document } = await seed(); - await View.increment({ + await View.incrementOrCreate({ documentId: document.id, userId: user.id, }); @@ -1412,7 +1421,7 @@ describe("#documents.viewed", () => { it("should not return recently viewed documents in collection not a member of", async () => { const { user, document, collection } = await seed(); - await View.increment({ + await View.incrementOrCreate({ documentId: document.id, userId: user.id, }); @@ -1435,6 +1444,7 @@ describe("#documents.viewed", () => { expect(body).toMatchSnapshot(); }); }); + describe("#documents.starred", () => { it("should return empty result if no stars", async () => { const { user } = await seed(); @@ -1524,10 +1534,11 @@ describe("#documents.move", () => { expect(res.status).toEqual(403); }); }); + describe("#documents.restore", () => { it("should allow restore of trashed documents", async () => { const { user, document } = await seed(); - await document.destroy(user.id); + await document.destroy(); const res = await server.post("/api/documents.restore", { body: { token: user.getJwtToken(), @@ -1545,7 +1556,7 @@ describe("#documents.restore", () => { userId: user.id, teamId: user.teamId, }); - await document.destroy(user.id); + await document.destroy(); const res = await server.post("/api/documents.restore", { body: { token: user.getJwtToken(), @@ -1561,7 +1572,7 @@ describe("#documents.restore", () => { it("should not allow restore of documents in deleted collection", async () => { const { user, document, collection } = await seed(); - await document.destroy(user.id); + await document.destroy(); await collection.destroy(); const res = await server.post("/api/documents.restore", { body: { @@ -1575,7 +1586,7 @@ describe("#documents.restore", () => { it("should not allow restore of trashed documents to collection user cannot access", async () => { const { user, document } = await seed(); const collection = await buildCollection(); - await document.destroy(user.id); + await document.destroy(); const res = await server.post("/api/documents.restore", { body: { token: user.getJwtToken(), @@ -1749,6 +1760,7 @@ describe("#documents.star", () => { expect(res.status).toEqual(403); }); }); + describe("#documents.unstar", () => { it("should unstar the document", async () => { const { user, document } = await seed(); @@ -1786,6 +1798,7 @@ describe("#documents.unstar", () => { expect(res.status).toEqual(403); }); }); + describe("#documents.import", () => { it("should error if no file is passed", async () => { const user = await buildUser(); @@ -1807,6 +1820,7 @@ describe("#documents.import", () => { expect(res.status).toEqual(401); }); }); + describe("#documents.create", () => { it("should create as a new document", async () => { const { user, collection } = await seed(); @@ -1822,8 +1836,8 @@ describe("#documents.create", () => { const body = await res.json(); const newDocument = await Document.findByPk(body.data.id); expect(res.status).toEqual(200); - expect(newDocument.parentDocumentId).toBe(null); - expect(newDocument.collectionId).toBe(collection.id); + expect(newDocument!.parentDocumentId).toBe(null); + expect(newDocument!.collectionId).toBe(collection.id); expect(body.policies[0].abilities.update).toEqual(true); }); @@ -1892,6 +1906,7 @@ describe("#documents.create", () => { expect(body.policies[0].abilities.update).toEqual(true); }); }); + describe("#documents.update", () => { it("should update document details in the root", async () => { const { user, document } = await seed(); @@ -1901,7 +1916,7 @@ describe("#documents.update", () => { id: document.id, title: "Updated title", text: "Updated text", - lastRevision: document.revision, + lastRevision: document.revisionCount, }, }); const body = await res.json(); @@ -1929,7 +1944,7 @@ describe("#documents.update", () => { id: template.id, title: "Updated title", text: "Updated text", - lastRevision: template.revision, + lastRevision: template.revisionCount, publish: true, }, }); @@ -1960,7 +1975,7 @@ describe("#documents.update", () => { id: document.id, title: "Updated title", text: "Updated text", - lastRevision: document.revision, + lastRevision: document.revisionCount, publish: true, }, }); @@ -1974,14 +1989,14 @@ describe("#documents.update", () => { it("should not edit archived document", async () => { const { user, document } = await seed(); - await document.archive(); + await document.archive(user.id); const res = await server.post("/api/documents.update", { body: { token: user.getJwtToken(), id: document.id, title: "Updated title", text: "Updated text", - lastRevision: document.revision, + lastRevision: document.revisionCount, }, }); expect(res.status).toEqual(403); @@ -2048,7 +2063,7 @@ describe("#documents.update", () => { token: admin.getJwtToken(), id: document.id, text: "Changed text", - lastRevision: document.revision, + lastRevision: document.revisionCount, }, }); const body = await res.json(); @@ -2072,7 +2087,7 @@ describe("#documents.update", () => { token: user.getJwtToken(), id: document.id, text: "Changed text", - lastRevision: document.revision, + lastRevision: document.revisionCount, }, }); expect(res.status).toEqual(403); @@ -2087,7 +2102,7 @@ describe("#documents.update", () => { token: user.getJwtToken(), id: document.id, text: "Changed text", - lastRevision: document.revision, + lastRevision: document.revisionCount, }, }); expect(res.status).toEqual(403); @@ -2100,7 +2115,7 @@ describe("#documents.update", () => { token: user.getJwtToken(), id: document.id, text: "Additional text", - lastRevision: document.revision, + lastRevision: document.revisionCount, append: true, }, }); @@ -2116,7 +2131,7 @@ describe("#documents.update", () => { body: { token: user.getJwtToken(), id: document.id, - lastRevision: document.revision, + lastRevision: document.revisionCount, title: "Updated Title", append: true, }, @@ -2132,7 +2147,7 @@ describe("#documents.update", () => { body: { token: user.getJwtToken(), id: document.id, - lastRevision: document.revision, + lastRevision: document.revisionCount, title: "Updated Title", text: "", }, @@ -2148,7 +2163,7 @@ describe("#documents.update", () => { body: { token: user.getJwtToken(), id: document.id, - lastRevision: document.revision, + lastRevision: document.revisionCount, title: document.title, text: document.text, }, @@ -2184,6 +2199,7 @@ describe("#documents.update", () => { expect(res.status).toEqual(403); }); }); + describe("#documents.archive", () => { it("should allow archiving document", async () => { const { user, document } = await seed(); @@ -2209,6 +2225,7 @@ describe("#documents.archive", () => { expect(res.status).toEqual(401); }); }); + describe("#documents.delete", () => { it("should allow deleting document", async () => { const { user, document } = await seed(); @@ -2251,6 +2268,7 @@ describe("#documents.delete", () => { const { user, document, collection } = await seed(); // delete collection without hooks to trigger document deletion await collection.destroy({ + // @ts-expect-error type is incorrect here hooks: false, }); const res = await server.post("/api/documents.delete", { @@ -2276,6 +2294,7 @@ describe("#documents.delete", () => { expect(body).toMatchSnapshot(); }); }); + describe("#documents.unpublish", () => { it("should unpublish a document", async () => { const { user, document } = await seed(); @@ -2291,12 +2310,12 @@ describe("#documents.unpublish", () => { expect(body.data.publishedAt).toBeNull(); const reloaded = await Document.unscoped().findByPk(document.id); - expect(reloaded.userId).toEqual(user.id); + expect(reloaded!.createdById).toEqual(user.id); }); it("should unpublish another users document", async () => { const { user, collection } = await seed(); - let document = await buildDocument({ + const document = await buildDocument({ teamId: user.teamId, collectionId: collection.id, }); @@ -2310,8 +2329,9 @@ describe("#documents.unpublish", () => { expect(res.status).toEqual(200); expect(body.data.id).toEqual(document.id); expect(body.data.publishedAt).toBeNull(); - document = await Document.unscoped().findByPk(document.id); - expect(document.userId).toEqual(user.id); + + const reloaded = await Document.unscoped().findByPk(document.id); + expect(reloaded!.createdById).toEqual(user.id); }); it("should fail to unpublish a draft document", async () => { @@ -2329,7 +2349,7 @@ describe("#documents.unpublish", () => { it("should fail to unpublish a deleted document", async () => { const { user, document } = await seed(); - await document.delete(); + await document.delete(user.id); const res = await server.post("/api/documents.unpublish", { body: { token: user.getJwtToken(), @@ -2341,7 +2361,7 @@ describe("#documents.unpublish", () => { it("should fail to unpublish an archived document", async () => { const { user, document } = await seed(); - await document.archive(); + await document.archive(user.id); const res = await server.post("/api/documents.unpublish", { body: { token: user.getJwtToken(), diff --git a/server/routes/api/documents.ts b/server/routes/api/documents.ts index b1a192bbd..f6a59f4b0 100644 --- a/server/routes/api/documents.ts +++ b/server/routes/api/documents.ts @@ -1,5 +1,6 @@ +import invariant from "invariant"; import Router from "koa-router"; -import Sequelize from "sequelize"; +import { Op, ScopeOptions, WhereOptions } from "sequelize"; import { subtractDate } from "@shared/utils/date"; import documentCreator from "@server/commands/documentCreator"; import documentImporter from "@server/commands/documentImporter"; @@ -24,13 +25,12 @@ import { View, Team, } from "@server/models"; -import policy from "@server/policies"; +import { authorize, cannot, can } from "@server/policies"; import { presentCollection, presentDocument, presentPolicies, } from "@server/presenters"; -import { sequelize } from "@server/sequelize"; import { assertUuid, assertSort, @@ -41,8 +41,6 @@ import { import env from "../../env"; import pagination from "./middlewares/pagination"; -const Op = Sequelize.Op; -const { authorize, cannot, can } = policy; const router = new Router(); router.post("documents.list", auth(), pagination(), async (ctx) => { @@ -54,16 +52,15 @@ router.post("documents.list", auth(), pagination(), async (ctx) => { let direction = ctx.body.direction; if (direction !== "ASC") direction = "DESC"; // always filter by the current team - const user = ctx.state.user; - let where = { + const { user } = ctx.state; + let where: WhereOptions = { teamId: user.teamId, archivedAt: { - [Op.eq]: null, + [Op.is]: null, }, }; if (template) { - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ template: boolean; teamId: any; archivedAt... Remove this comment to see the full error message where = { ...where, template: true }; } @@ -71,17 +68,14 @@ router.post("documents.list", auth(), pagination(), async (ctx) => { // exist in the team then nothing will be returned, so no need to check auth if (createdById) { assertUuid(createdById, "user must be a UUID"); - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ createdById: any; teamId: any; archivedAt:... Remove this comment to see the full error message where = { ...where, createdById }; } - // @ts-expect-error ts-migrate(7034) FIXME: Variable 'documentIds' implicitly has type 'any[]'... Remove this comment to see the full error message - let documentIds = []; + let documentIds: string[] = []; // if a specific collection is passed then we need to check auth to view it if (collectionId) { assertUuid(collectionId, "collection must be a UUID"); - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ collectionId: any; teamId: any; archivedAt... Remove this comment to see the full error message where = { ...where, collectionId }; const collection = await Collection.scope({ method: ["withMembership", user.id], @@ -91,22 +85,18 @@ router.post("documents.list", auth(), pagination(), async (ctx) => { // index sort is special because it uses the order of the documents in the // collection.documentStructure rather than a database column if (sort === "index") { - documentIds = (collection.documentStructure || []) - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'node' implicitly has an 'any' type. + documentIds = (collection?.documentStructure || []) .map((node) => node.id) .slice(ctx.state.pagination.offset, ctx.state.pagination.limit); - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ id: any; teamId: any; archivedAt: { [Seque... Remove this comment to see the full error message where = { ...where, id: documentIds }; } // otherwise, filter by all collections the user has access to } else { const collectionIds = await user.collectionIds(); - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ collectionId: any; teamId: any; archivedAt... Remove this comment to see the full error message where = { ...where, collectionId: collectionIds }; } if (parentDocumentId) { assertUuid(parentDocumentId, "parentDocumentId must be a UUID"); - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ parentDocumentId: any; teamId: any; archiv... Remove this comment to see the full error message where = { ...where, parentDocumentId }; } @@ -115,9 +105,8 @@ router.post("documents.list", auth(), pagination(), async (ctx) => { if (parentDocumentId === null) { where = { ...where, - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ parentDocumentId: { [Sequelize.Op.eq]: nul... Remove this comment to see the full error message parentDocumentId: { - [Op.eq]: null, + [Op.is]: null, }, }; } @@ -132,7 +121,6 @@ router.post("documents.list", auth(), pagination(), async (ctx) => { }); where = { ...where, - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ id: any; teamId: any; archivedAt: { [Seque... Remove this comment to see the full error message id: backlinks.map((backlink) => backlink.reverseDocumentId), }; } @@ -154,13 +142,11 @@ router.post("documents.list", auth(), pagination(), async (ctx) => { // collection.documentStructure rather than a database column if (documentIds.length) { documents.sort( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'a' implicitly has an 'any' type. (a, b) => documentIds.indexOf(a.id) - documentIds.indexOf(b.id) ); } const data = await Promise.all( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type. documents.map((document) => presentDocument(document)) ); const policies = presentPolicies(user, documents); @@ -177,19 +163,19 @@ router.post("documents.archived", auth(), pagination(), async (ctx) => { assertSort(sort, Document); let direction = ctx.body.direction; if (direction !== "ASC") direction = "DESC"; - const user = ctx.state.user; + const { user } = ctx.state; const collectionIds = await user.collectionIds(); - const collectionScope = { + const collectionScope: Readonly = { method: ["withCollection", user.id], }; - const viewScope = { + const viewScope: Readonly = { method: ["withViews", user.id], }; - const documents = await Document.scope( + const documents = await Document.scope([ "defaultScope", collectionScope, - viewScope - ).findAll({ + viewScope, + ]).findAll({ where: { teamId: user.teamId, collectionId: collectionIds, @@ -202,10 +188,10 @@ router.post("documents.archived", auth(), pagination(), async (ctx) => { limit: ctx.state.pagination.limit, }); const data = await Promise.all( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type. documents.map((document) => presentDocument(document)) ); const policies = presentPolicies(user, documents); + ctx.body = { pagination: ctx.state.pagination, data, @@ -219,17 +205,17 @@ router.post("documents.deleted", auth(), pagination(), async (ctx) => { assertSort(sort, Document); let direction = ctx.body.direction; if (direction !== "ASC") direction = "DESC"; - const user = ctx.state.user; + const { user } = ctx.state; const collectionIds = await user.collectionIds({ paranoid: false, }); - const collectionScope = { + const collectionScope: Readonly = { method: ["withCollection", user.id], }; - const viewScope = { + const viewScope: Readonly = { method: ["withViews", user.id], }; - const documents = await Document.scope(collectionScope, viewScope).findAll({ + const documents = await Document.scope([collectionScope, viewScope]).findAll({ where: { teamId: user.teamId, collectionId: collectionIds, @@ -255,10 +241,10 @@ router.post("documents.deleted", auth(), pagination(), async (ctx) => { limit: ctx.state.pagination.limit, }); const data = await Promise.all( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type. documents.map((document) => presentDocument(document)) ); const policies = presentPolicies(user, documents); + ctx.body = { pagination: ctx.state.pagination, data, @@ -272,7 +258,7 @@ router.post("documents.viewed", auth(), pagination(), async (ctx) => { assertSort(sort, Document); if (direction !== "ASC") direction = "DESC"; - const user = ctx.state.user; + const { user } = ctx.state; const collectionIds = await user.collectionIds(); const userId = user.id; const views = await View.findAll({ @@ -309,17 +295,16 @@ router.post("documents.viewed", auth(), pagination(), async (ctx) => { offset: ctx.state.pagination.offset, limit: ctx.state.pagination.limit, }); - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'view' implicitly has an 'any' type. const documents = views.map((view) => { const document = view.document; document.views = [view]; return document; }); const data = await Promise.all( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type. documents.map((document) => presentDocument(document)) ); const policies = presentPolicies(user, documents); + ctx.body = { pagination: ctx.state.pagination, data, @@ -333,7 +318,7 @@ router.post("documents.starred", auth(), pagination(), async (ctx) => { assertSort(sort, Document); if (direction !== "ASC") direction = "DESC"; - const user = ctx.state.user; + const { user } = ctx.state; const collectionIds = await user.collectionIds(); const stars = await Star.findAll({ where: { @@ -366,13 +351,13 @@ router.post("documents.starred", auth(), pagination(), async (ctx) => { offset: ctx.state.pagination.offset, limit: ctx.state.pagination.limit, }); - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'star' implicitly has an 'any' type. + const documents = stars.map((star) => star.document); const data = await Promise.all( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type. documents.map((document) => presentDocument(document)) ); const policies = presentPolicies(user, documents); + ctx.body = { pagination: ctx.state.pagination, data, @@ -386,7 +371,7 @@ router.post("documents.drafts", auth(), pagination(), async (ctx) => { assertSort(sort, Document); if (direction !== "ASC") direction = "DESC"; - const user = ctx.state.user; + const { user } = ctx.state; if (collectionId) { assertUuid(collectionId, "collectionId must be a UUID"); @@ -399,13 +384,12 @@ router.post("documents.drafts", auth(), pagination(), async (ctx) => { const collectionIds = collectionId ? [collectionId] : await user.collectionIds(); - const whereConditions = { - userId: user.id, + const where: WhereOptions = { + createdById: user.id, collectionId: collectionIds, publishedAt: { - [Op.eq]: null, + [Op.is]: null, }, - updatedAt: undefined, }; if (dateFilter) { @@ -414,31 +398,30 @@ router.post("documents.drafts", auth(), pagination(), async (ctx) => { ["day", "week", "month", "year"], "dateFilter must be one of day,week,month,year" ); - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ [Sequelize.Op.gte]: Date; }' is not assign... Remove this comment to see the full error message - whereConditions.updatedAt = { + where.updatedAt = { [Op.gte]: subtractDate(new Date(), dateFilter), }; } else { - delete whereConditions.updatedAt; + delete where.updatedAt; } - const collectionScope = { + const collectionScope: Readonly = { method: ["withCollection", user.id], }; - const documents = await Document.scope( + const documents = await Document.scope([ "defaultScope", - collectionScope - ).findAll({ - where: whereConditions, + collectionScope, + ]).findAll({ + where, order: [[sort, direction]], offset: ctx.state.pagination.offset, limit: ctx.state.pagination.limit, }); const data = await Promise.all( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type. documents.map((document) => presentDocument(document)) ); const policies = presentPolicies(user, documents); + ctx.body = { pagination: ctx.state.pagination, data, @@ -447,17 +430,16 @@ router.post("documents.drafts", auth(), pagination(), async (ctx) => { }); async function loadDocument({ - // @ts-expect-error ts-migrate(7031) FIXME: Binding element 'id' implicitly has an 'any' type. id, - // @ts-expect-error ts-migrate(7031) FIXME: Binding element 'shareId' implicitly has an 'any' ... Remove this comment to see the full error message shareId, - // @ts-expect-error ts-migrate(7031) FIXME: Binding element 'user' implicitly has an 'any' typ... Remove this comment to see the full error message user, +}: { + id?: string; + shareId?: string; + user: User; }): Promise<{ document: Document; - // @ts-expect-error ts-migrate(2749) FIXME: 'Share' refers to a value, but is being used as a ... Remove this comment to see the full error message share?: Share; - // @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message collection: Collection; }> { let document; @@ -468,7 +450,7 @@ async function loadDocument({ share = await Share.findOne({ where: { revokedAt: { - [Op.eq]: null, + [Op.is]: null, }, id: shareId, }, @@ -518,6 +500,8 @@ async function loadDocument({ document = share.document; } + invariant(document, "document not found"); + // If the user has access to read the document, we can just update // the last access date and return the document without additional checks. const canReadDocument = can(user, "read", document); @@ -542,6 +526,7 @@ async function loadDocument({ // It is possible to disable sharing at the collection so we must check collection = await Collection.findByPk(document.collectionId); + invariant(collection, "collection not found"); if (!collection.sharing) { throw AuthorizationError(); @@ -561,6 +546,7 @@ async function loadDocument({ // It is possible to disable sharing at the team level so we must check const team = await Team.findByPk(document.teamId); + invariant(team, "team not found"); if (!team.sharing) { throw AuthorizationError(); @@ -570,7 +556,7 @@ async function loadDocument({ lastAccessedAt: new Date(), }); } else { - document = await Document.findByPk(id, { + document = await Document.findByPk(id as string, { userId: user ? user.id : undefined, paranoid: false, }); @@ -641,14 +627,13 @@ router.post( async (ctx) => { const { id, shareId } = ctx.body; assertPresent(id || shareId, "id or shareId is required"); - const user = ctx.state.user; + const { user } = ctx.state; const { document } = await loadDocument({ id, shareId, user, }); ctx.body = { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'toMarkdown' does not exist on type 'Docu... Remove this comment to see the full error message data: document.toMarkdown(), }; } @@ -657,7 +642,7 @@ router.post( router.post("documents.restore", auth(), async (ctx) => { const { id, collectionId, revisionId } = ctx.body; assertPresent(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; const document = await Document.findByPk(id, { userId: user.id, paranoid: false, @@ -722,7 +707,9 @@ router.post("documents.restore", auth(), async (ctx) => { // restore a document to a specific revision authorize(user, "update", document); const revision = await Revision.findByPk(revisionId); + authorize(document, "restore", revision); + document.text = revision.text; document.title = revision.title; await document.save(); @@ -750,25 +737,25 @@ router.post("documents.restore", auth(), async (ctx) => { router.post("documents.search_titles", auth(), pagination(), async (ctx) => { const { query } = ctx.body; const { offset, limit } = ctx.state.pagination; - const user = ctx.state.user; + const { user } = ctx.state; assertPresent(query, "query is required"); const collectionIds = await user.collectionIds(); - const documents = await Document.scope( + const documents = await Document.scope([ { method: ["withViews", user.id], }, { method: ["withCollection", user.id], - } - ).findAll({ + }, + ]).findAll({ where: { title: { [Op.iLike]: `%${query}%`, }, collectionId: collectionIds, archivedAt: { - [Op.eq]: null, + [Op.is]: null, }, }, order: [["updatedAt", "DESC"]], @@ -789,9 +776,9 @@ router.post("documents.search_titles", auth(), pagination(), async (ctx) => { }); const policies = presentPolicies(user, documents); const data = await Promise.all( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type. documents.map((document) => presentDocument(document)) ); + ctx.body = { pagination: ctx.state.pagination, data, @@ -809,7 +796,7 @@ router.post("documents.search", auth(), pagination(), async (ctx) => { dateFilter, } = ctx.body; const { offset, limit } = ctx.state.pagination; - const user = ctx.state.user; + const { user } = ctx.state; assertPresent(query, "query is required"); @@ -845,10 +832,10 @@ router.post("documents.search", auth(), pagination(), async (ctx) => { offset, limit, }); - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'result' implicitly has an 'any' type. + const documents = results.map((result) => result.document); + const data = await Promise.all( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'result' implicitly has an 'any' type. results.map(async (result) => { const document = await presentDocument(result.document); return { ...result, document }; @@ -868,6 +855,7 @@ router.post("documents.search", auth(), pagination(), async (ctx) => { } const policies = presentPolicies(user, documents); + ctx.body = { pagination: ctx.state.pagination, data, @@ -878,17 +866,20 @@ router.post("documents.search", auth(), pagination(), async (ctx) => { router.post("documents.star", auth(), async (ctx) => { const { id } = ctx.body; assertPresent(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; + const document = await Document.findByPk(id, { userId: user.id, }); authorize(user, "read", document); + await Star.findOrCreate({ where: { documentId: document.id, userId: user.id, }, }); + await Event.create({ name: "documents.star", documentId: document.id, @@ -900,6 +891,7 @@ router.post("documents.star", auth(), async (ctx) => { }, ip: ctx.request.ip, }); + ctx.body = { success: true, }; @@ -908,11 +900,13 @@ router.post("documents.star", auth(), async (ctx) => { router.post("documents.unstar", auth(), async (ctx) => { const { id } = ctx.body; assertPresent(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; + const document = await Document.findByPk(id, { userId: user.id, }); authorize(user, "read", document); + await Star.destroy({ where: { documentId: document.id, @@ -930,6 +924,7 @@ router.post("documents.unstar", auth(), async (ctx) => { }, ip: ctx.request.ip, }); + ctx.body = { success: true, }; @@ -938,12 +933,14 @@ router.post("documents.unstar", auth(), async (ctx) => { router.post("documents.templatize", auth(), async (ctx) => { const { id } = ctx.body; assertPresent(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; + const original = await Document.findByPk(id, { userId: user.id, }); authorize(user, "update", original); - let document = await Document.create({ + + const document = await Document.create({ editorVersion: original.editorVersion, collectionId: original.collectionId, teamId: original.teamId, @@ -967,13 +964,16 @@ router.post("documents.templatize", auth(), async (ctx) => { }, ip: ctx.request.ip, }); + // reload to get all of the data needed to present (user, collection etc) - document = await Document.findByPk(document.id, { + const reloaded = await Document.findByPk(document.id, { userId: user.id, }); + invariant(reloaded, "document not found"); + ctx.body = { - data: await presentDocument(document), - policies: presentPolicies(user, [document]), + data: await presentDocument(reloaded), + policies: presentPolicies(user, [reloaded]), }; }); @@ -990,11 +990,12 @@ router.post("documents.update", auth(), async (ctx) => { templateId, append, } = ctx.body; - const editorVersion = ctx.headers["x-editor-version"]; + const editorVersion = ctx.headers["x-editor-version"] as string | undefined; assertPresent(id, "id is required"); assertPresent(title || text, "title or text is required"); if (append) assertPresent(text, "Text is required while appending"); - const user = ctx.state.user; + const { user } = ctx.state; + const document = await Document.findByPk(id, { userId: user.id, }); @@ -1026,7 +1027,7 @@ router.post("documents.update", auth(), async (ctx) => { let transaction; try { - transaction = await sequelize.transaction(); + transaction = await document.sequelize.transaction(); if (publish) { await document.publish(user.id, { @@ -1034,7 +1035,6 @@ router.post("documents.update", auth(), async (ctx) => { }); } else { await document.save({ - autosave, transaction, }); } @@ -1093,6 +1093,7 @@ router.post("documents.update", auth(), async (ctx) => { document.updatedBy = user; document.collection = collection; + ctx.body = { data: await presentDocument(document), policies: presentPolicies(user, [document]), @@ -1118,11 +1119,12 @@ router.post("documents.move", auth(), async (ctx) => { ); } - const user = ctx.state.user; + const { user } = ctx.state; const document = await Document.findByPk(id, { userId: user.id, }); authorize(user, "move", document); + const collection = await Collection.scope({ method: ["withMembership", user.id], }).findByPk(collectionId); @@ -1143,6 +1145,7 @@ router.post("documents.move", auth(), async (ctx) => { index, ip: ctx.request.ip, }); + ctx.body = { data: { documents: await Promise.all( @@ -1159,11 +1162,13 @@ router.post("documents.move", auth(), async (ctx) => { router.post("documents.archive", auth(), async (ctx) => { const { id } = ctx.body; assertPresent(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; + const document = await Document.findByPk(id, { userId: user.id, }); authorize(user, "archive", document); + await document.archive(user.id); await Event.create({ name: "documents.archive", @@ -1176,6 +1181,7 @@ router.post("documents.archive", auth(), async (ctx) => { }, ip: ctx.request.ip, }); + ctx.body = { data: await presentDocument(document), policies: presentPolicies(user, [document]), @@ -1185,7 +1191,7 @@ router.post("documents.archive", auth(), async (ctx) => { router.post("documents.delete", auth(), async (ctx) => { const { id, permanent } = ctx.body; assertPresent(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; if (permanent) { const document = await Document.findByPk(id, { @@ -1193,6 +1199,7 @@ router.post("documents.delete", auth(), async (ctx) => { paranoid: false, }); authorize(user, "permanentDelete", document); + await Document.update( { parentDocumentId: null, @@ -1220,7 +1227,9 @@ router.post("documents.delete", auth(), async (ctx) => { const document = await Document.findByPk(id, { userId: user.id, }); + authorize(user, "delete", document); + await document.delete(user.id); await Event.create({ name: "documents.delete", @@ -1243,11 +1252,13 @@ router.post("documents.delete", auth(), async (ctx) => { router.post("documents.unpublish", auth(), async (ctx) => { const { id } = ctx.body; assertPresent(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; + const document = await Document.findByPk(id, { userId: user.id, }); authorize(user, "unpublish", document); + await document.unpublish(user.id); await Event.create({ name: "documents.unpublish", @@ -1260,6 +1271,7 @@ router.post("documents.unpublish", auth(), async (ctx) => { }, ip: ctx.request.ip, }); + ctx.body = { data: await presentDocument(document), policies: presentPolicies(user, [document]), @@ -1277,8 +1289,7 @@ router.post("documents.import", auth(), async (ctx) => { const file: any = Object.values(ctx.request.files)[0]; assertPresent(file, "file is required"); - // @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'. - if (file.size > env.MAXIMUM_IMPORT_SIZE) { + if (env.MAXIMUM_IMPORT_SIZE && file.size > env.MAXIMUM_IMPORT_SIZE) { throw InvalidRequestError("The selected file was too large to import"); } @@ -1289,8 +1300,9 @@ router.post("documents.import", auth(), async (ctx) => { } if (index) assertPositiveInteger(index, "index must be an integer (>=0)"); - const user = ctx.state.user; + const { user } = ctx.state; authorize(user, "createDocument", user.team); + const collection = await Collection.scope({ method: ["withMembership", user.id], }).findOne({ @@ -1330,8 +1342,8 @@ router.post("documents.import", auth(), async (ctx) => { user, ip: ctx.request.ip, }); - // @ts-expect-error ts-migrate(2339) FIXME: Property 'collection' does not exist on type 'Docu... Remove this comment to see the full error message document.collection = collection; + return (ctx.body = { data: await presentDocument(document), policies: presentPolicies(user, [document]), @@ -1349,7 +1361,7 @@ router.post("documents.create", auth(), async (ctx) => { template, index, } = ctx.body; - const editorVersion = ctx.headers["x-editor-version"]; + const editorVersion = ctx.headers["x-editor-version"] as string | undefined; assertUuid(collectionId, "collectionId must be an uuid"); if (parentDocumentId) { @@ -1357,8 +1369,9 @@ router.post("documents.create", auth(), async (ctx) => { } if (index) assertPositiveInteger(index, "index must be an integer (>=0)"); - const user = ctx.state.user; + const { user } = ctx.state; authorize(user, "createDocument", user.team); + const collection = await Collection.scope({ method: ["withMembership", user.id], }).findOne({ @@ -1368,6 +1381,7 @@ router.post("documents.create", auth(), async (ctx) => { }, }); authorize(user, "publish", collection); + let parentDocument; if (parentDocumentId) { @@ -1401,12 +1415,11 @@ router.post("documents.create", auth(), async (ctx) => { template, index, user, - // @ts-expect-error ts-migrate(2322) FIXME: Type 'string | string[] | undefined' is not assign... Remove this comment to see the full error message editorVersion, ip: ctx.request.ip, }); - // @ts-expect-error ts-migrate(2339) FIXME: Property 'collection' does not exist on type 'Docu... Remove this comment to see the full error message document.collection = collection; + return (ctx.body = { data: await presentDocument(document), policies: presentPolicies(user, [document]), diff --git a/server/routes/api/events.test.ts b/server/routes/api/events.test.ts index 71133017d..98d4d3d22 100644 --- a/server/routes/api/events.test.ts +++ b/server/routes/api/events.test.ts @@ -8,6 +8,7 @@ const app = webService(); const server = new TestServer(app.callback()); beforeEach(() => flushdb()); afterAll(() => server.close()); + describe("#events.list", () => { it("should only return activity events", async () => { const { user, admin, document, collection } = await seed(); diff --git a/server/routes/api/events.ts b/server/routes/api/events.ts index 12017a7ce..af82f0bc9 100644 --- a/server/routes/api/events.ts +++ b/server/routes/api/events.ts @@ -1,18 +1,16 @@ import Router from "koa-router"; -import Sequelize from "sequelize"; +import { Op, WhereOptions } from "sequelize"; import auth from "@server/middlewares/authentication"; import { Event, User, Collection } from "@server/models"; -import policy from "@server/policies"; +import { authorize } from "@server/policies"; import { presentEvent } from "@server/presenters"; import { assertSort, assertUuid } from "@server/validation"; import pagination from "./middlewares/pagination"; -const Op = Sequelize.Op; -const { authorize } = policy; const router = new Router(); router.post("events.list", auth(), pagination(), async (ctx) => { - const user = ctx.state.user; + const { user } = ctx.state; let { direction } = ctx.body; const { sort = "createdAt", @@ -24,27 +22,35 @@ router.post("events.list", auth(), pagination(), async (ctx) => { } = ctx.body; if (direction !== "ASC") direction = "DESC"; assertSort(sort, Event); - let where = { + + let where: WhereOptions = { name: Event.ACTIVITY_EVENTS, teamId: user.teamId, }; if (actorId) { assertUuid(actorId, "actorId must be a UUID"); - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ actorId: any; name: any; teamId: any; }' i... Remove this comment to see the full error message where = { ...where, actorId }; } if (documentId) { assertUuid(documentId, "documentId must be a UUID"); - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ documentId: any; name: any; teamId: any; }... Remove this comment to see the full error message where = { ...where, documentId }; } + if (auditLog) { + authorize(user, "manage", user.team); + where.name = Event.AUDIT_EVENTS; + } + + if (name && (where.name as string[]).includes(name)) { + where.name = name; + } + if (collectionId) { assertUuid(collectionId, "collection must be a UUID"); - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ collectionId: any; name: any; teamId: any;... Remove this comment to see the full error message where = { ...where, collectionId }; + const collection = await Collection.scope({ method: ["withMembership", user.id], }).findByPk(collectionId); @@ -55,29 +61,19 @@ router.post("events.list", auth(), pagination(), async (ctx) => { }); where = { ...where, - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ [Sequelize.Op.or]: { collectionId: any; }[... Remove this comment to see the full error message [Op.or]: [ { collectionId: collectionIds, }, { collectionId: { - [Op.eq]: null, + [Op.is]: null, }, }, ], }; } - if (auditLog) { - authorize(user, "manage", user.team); - where.name = Event.AUDIT_EVENTS; - } - - if (name && where.name.includes(name)) { - where.name = name; - } - const events = await Event.findAll({ where, order: [[sort, direction]], @@ -91,10 +87,12 @@ router.post("events.list", auth(), pagination(), async (ctx) => { offset: ctx.state.pagination.offset, limit: ctx.state.pagination.limit, }); + ctx.body = { pagination: ctx.state.pagination, - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'event' implicitly has an 'any' type. - data: events.map((event) => presentEvent(event, auditLog)), + data: await Promise.all( + events.map((event) => presentEvent(event, auditLog)) + ), }; }); diff --git a/server/routes/api/fileOperations.test.ts b/server/routes/api/fileOperations.test.ts index 441750a82..348e459c9 100644 --- a/server/routes/api/fileOperations.test.ts +++ b/server/routes/api/fileOperations.test.ts @@ -13,19 +13,10 @@ import { flushdb } from "@server/test/support"; const app = webService(); const server = new TestServer(app.callback()); -jest.mock("aws-sdk", () => { - const mS3 = { - createPresignedPost: jest.fn(), - deleteObject: jest.fn().mockReturnThis(), - promise: jest.fn(), - }; - return { - S3: jest.fn(() => mS3), - Endpoint: jest.fn(), - }; -}); + beforeEach(() => flushdb()); afterAll(() => server.close()); + describe("#fileOperations.info", () => { it("should return fileOperation", async () => { const team = await buildTeam(); @@ -73,6 +64,7 @@ describe("#fileOperations.info", () => { expect(res.status).toEqual(403); }); }); + describe("#fileOperations.list", () => { it("should return fileOperations list", async () => { const team = await buildTeam(); @@ -212,6 +204,7 @@ describe("#fileOperations.list", () => { expect(res.status).toEqual(403); }); }); + describe("#fileOperations.redirect", () => { it("should not redirect when file operation is not complete", async () => { const team = await buildTeam(); @@ -234,6 +227,7 @@ describe("#fileOperations.redirect", () => { expect(body.message).toEqual("export is not complete yet"); }); }); + describe("#fileOperations.info", () => { it("should return file operation", async () => { const team = await buildTeam(); @@ -279,6 +273,7 @@ describe("#fileOperations.info", () => { expect(res.status).toBe(403); }); }); + describe("#fileOperations.delete", () => { it("should delete file operation", async () => { const team = await buildTeam(); diff --git a/server/routes/api/fileOperations.ts b/server/routes/api/fileOperations.ts index aaf9931d9..52398e8ad 100644 --- a/server/routes/api/fileOperations.ts +++ b/server/routes/api/fileOperations.ts @@ -1,23 +1,26 @@ +import invariant from "invariant"; import Router from "koa-router"; +import { WhereOptions } from "sequelize/types"; import fileOperationDeleter from "@server/commands/fileOperationDeleter"; import { NotFoundError, ValidationError } from "@server/errors"; import auth from "@server/middlewares/authentication"; import { FileOperation, Team } from "@server/models"; -import policy from "@server/policies"; +import { authorize } from "@server/policies"; import { presentFileOperation } from "@server/presenters"; import { getSignedUrl } from "@server/utils/s3"; import { assertPresent, assertIn, assertUuid } from "@server/validation"; import pagination from "./middlewares/pagination"; -const { authorize } = policy; const router = new Router(); router.post("fileOperations.info", auth(), async (ctx) => { const { id } = ctx.body; assertUuid(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; const team = await Team.findByPk(user.teamId); const fileOperation = await FileOperation.findByPk(id); + invariant(fileOperation, "File operation not found"); + authorize(user, fileOperation.type, team); if (!fileOperation) { @@ -40,13 +43,14 @@ router.post("fileOperations.list", auth(), pagination(), async (ctx) => { ); if (direction !== "ASC") direction = "DESC"; - const user = ctx.state.user; - const where = { + const { user } = ctx.state; + const where: WhereOptions = { teamId: user.teamId, type, }; const team = await Team.findByPk(user.teamId); authorize(user, type, team); + const [exports, total] = await Promise.all([ await FileOperation.findAll({ where, @@ -58,6 +62,7 @@ router.post("fileOperations.list", auth(), pagination(), async (ctx) => { where, }), ]); + ctx.body = { pagination: { ...ctx.state.pagination, total }, data: exports.map(presentFileOperation), @@ -68,7 +73,7 @@ router.post("fileOperations.redirect", auth(), async (ctx) => { const { id } = ctx.body; assertUuid(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; const team = await Team.findByPk(user.teamId); const fileOp = await FileOperation.unscoped().findByPk(id); @@ -90,7 +95,7 @@ router.post("fileOperations.delete", auth(), async (ctx) => { const { id } = ctx.body; assertUuid(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; const team = await Team.findByPk(user.teamId); const fileOp = await FileOperation.findByPk(id); @@ -100,6 +105,7 @@ router.post("fileOperations.delete", auth(), async (ctx) => { authorize(user, fileOp.type, team); await fileOperationDeleter(fileOp, user, ctx.request.ip); + ctx.body = { success: true, }; diff --git a/server/routes/api/groups.test.ts b/server/routes/api/groups.test.ts index 783503f67..c1c1d4d1a 100644 --- a/server/routes/api/groups.test.ts +++ b/server/routes/api/groups.test.ts @@ -9,6 +9,7 @@ const app = webService(); const server = new TestServer(app.callback()); beforeEach(() => flushdb()); afterAll(() => server.close()); + describe("#groups.create", () => { it("should create a group", async () => { const name = "hello I am a group"; @@ -24,6 +25,7 @@ describe("#groups.create", () => { expect(body.data.name).toEqual(name); }); }); + describe("#groups.update", () => { it("should require authentication", async () => { const group = await buildGroup(); @@ -127,6 +129,7 @@ describe("#groups.update", () => { }); }); }); + describe("#groups.list", () => { it("should require authentication", async () => { const res = await server.post("/api/groups.list"); @@ -140,7 +143,7 @@ describe("#groups.list", () => { const group = await buildGroup({ teamId: user.teamId, }); - await group.addUser(user, { + await group.$add("user", user, { through: { createdById: user.id, }, @@ -169,12 +172,12 @@ describe("#groups.list", () => { const group = await buildGroup({ teamId: user.teamId, }); - await group.addUser(user, { + await group.$add("user", user, { through: { createdById: me.id, }, }); - await group.addUser(me, { + await group.$add("user", me, { through: { createdById: me.id, }, @@ -196,6 +199,7 @@ describe("#groups.list", () => { expect(body.policies[0].abilities.read).toEqual(true); }); }); + describe("#groups.info", () => { it("should return group if admin", async () => { const user = await buildAdmin(); @@ -218,7 +222,7 @@ describe("#groups.info", () => { const group = await buildGroup({ teamId: user.teamId, }); - await group.addUser(user, { + await group.$add("user", user, { through: { createdById: user.id, }, @@ -271,6 +275,7 @@ describe("#groups.info", () => { expect(res.status).toEqual(403); }); }); + describe("#groups.delete", () => { it("should require authentication", async () => { const group = await buildGroup(); @@ -324,13 +329,14 @@ describe("#groups.delete", () => { expect(body.success).toEqual(true); }); }); + describe("#groups.memberships", () => { it("should return members in a group", async () => { const user = await buildUser(); const group = await buildGroup({ teamId: user.teamId, }); - await group.addUser(user, { + await group.$add("user", user, { through: { createdById: user.id, }, @@ -361,17 +367,17 @@ describe("#groups.memberships", () => { const group = await buildGroup({ teamId: user.teamId, }); - await group.addUser(user, { + await group.$add("user", user, { through: { createdById: user.id, }, }); - await group.addUser(user2, { + await group.$add("user", user2, { through: { createdById: user.id, }, }); - await group.addUser(user3, { + await group.$add("user", user3, { through: { createdById: user.id, }, @@ -409,6 +415,7 @@ describe("#groups.memberships", () => { expect(res.status).toEqual(403); }); }); + describe("#groups.add_user", () => { it("should add user to group", async () => { const user = await buildAdmin(); @@ -422,7 +429,7 @@ describe("#groups.add_user", () => { userId: user.id, }, }); - const users = await group.getUsers(); + const users = await group.$get("users"); expect(res.status).toEqual(200); expect(users.length).toEqual(1); }); @@ -470,6 +477,7 @@ describe("#groups.add_user", () => { expect(body).toMatchSnapshot(); }); }); + describe("#groups.remove_user", () => { it("should remove user from group", async () => { const user = await buildAdmin(); @@ -483,7 +491,7 @@ describe("#groups.remove_user", () => { userId: user.id, }, }); - const users = await group.getUsers(); + const users = await group.$get("users"); expect(users.length).toEqual(1); const res = await server.post("/api/groups.remove_user", { body: { @@ -492,7 +500,7 @@ describe("#groups.remove_user", () => { userId: user.id, }, }); - const users1 = await group.getUsers(); + const users1 = await group.$get("users"); expect(res.status).toEqual(200); expect(users1.length).toEqual(0); }); diff --git a/server/routes/api/groups.ts b/server/routes/api/groups.ts index df1f2016c..a2fc1fe61 100644 --- a/server/routes/api/groups.ts +++ b/server/routes/api/groups.ts @@ -1,19 +1,19 @@ +import invariant from "invariant"; import Router from "koa-router"; +import { Op } from "sequelize"; import { MAX_AVATAR_DISPLAY } from "@shared/constants"; import auth from "@server/middlewares/authentication"; import { User, Event, Group, GroupUser } from "@server/models"; -import policy from "@server/policies"; +import { authorize } from "@server/policies"; import { presentGroup, presentPolicies, presentUser, presentGroupMembership, } from "@server/presenters"; -import { Op } from "@server/sequelize"; import { assertPresent, assertUuid, assertSort } from "@server/validation"; import pagination from "./middlewares/pagination"; -const { authorize } = policy; const router = new Router(); router.post("groups.list", auth(), pagination(), async (ctx) => { @@ -22,7 +22,7 @@ router.post("groups.list", auth(), pagination(), async (ctx) => { if (direction !== "ASC") direction = "DESC"; assertSort(sort, Group); - const user = ctx.state.user; + const { user } = ctx.state; const groups = await Group.findAll({ where: { teamId: user.teamId, @@ -37,10 +37,8 @@ router.post("groups.list", auth(), pagination(), async (ctx) => { data: { groups: groups.map(presentGroup), groupMemberships: groups - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'g' implicitly has an 'any' type. .map((g) => g.groupMemberships - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'membership' implicitly has an 'any' typ... Remove this comment to see the full error message .filter((membership) => !!membership.user) .slice(0, MAX_AVATAR_DISPLAY) ) @@ -55,9 +53,10 @@ router.post("groups.info", auth(), async (ctx) => { const { id } = ctx.body; assertUuid(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; const group = await Group.findByPk(id); authorize(user, "read", group); + ctx.body = { data: presentGroup(group), policies: presentPolicies(user, [group]), @@ -68,15 +67,18 @@ router.post("groups.create", auth(), async (ctx) => { const { name } = ctx.body; assertPresent(name, "name is required"); - const user = ctx.state.user; + const { user } = ctx.state; authorize(user, "createGroup", user.team); - let group = await Group.create({ + const g = await Group.create({ name, teamId: user.teamId, createdById: user.id, }); + // reload to get default scope - group = await Group.findByPk(group.id); + const group = await Group.findByPk(g.id); + invariant(group, "group not found"); + await Event.create({ name: "groups.create", actorId: user.id, @@ -87,6 +89,7 @@ router.post("groups.create", auth(), async (ctx) => { }, ip: ctx.request.ip, }); + ctx.body = { data: presentGroup(group), policies: presentPolicies(user, [group]), @@ -98,9 +101,10 @@ router.post("groups.update", auth(), async (ctx) => { assertPresent(name, "name is required"); assertUuid(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; const group = await Group.findByPk(id); authorize(user, "update", group); + group.name = name; if (group.changed()) { @@ -130,6 +134,7 @@ router.post("groups.delete", auth(), async (ctx) => { const { user } = ctx.state; const group = await Group.findByPk(id); authorize(user, "delete", group); + await group.destroy(); await Event.create({ name: "groups.delete", @@ -141,6 +146,7 @@ router.post("groups.delete", auth(), async (ctx) => { }, ip: ctx.request.ip, }); + ctx.body = { success: true, }; @@ -150,7 +156,7 @@ router.post("groups.memberships", auth(), pagination(), async (ctx) => { const { id, query } = ctx.body; assertUuid(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; const group = await Group.findByPk(id); authorize(user, "read", group); let userWhere; @@ -179,11 +185,11 @@ router.post("groups.memberships", auth(), pagination(), async (ctx) => { }, ], }); + ctx.body = { pagination: ctx.state.pagination, data: { groupMemberships: memberships.map(presentGroupMembership), - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'membership' implicitly has an 'any' typ... Remove this comment to see the full error message users: memberships.map((membership) => presentUser(membership.user)), }, }; @@ -196,8 +202,10 @@ router.post("groups.add_user", auth(), async (ctx) => { const user = await User.findByPk(userId); authorize(ctx.state.user, "read", user); + let group = await Group.findByPk(id); authorize(ctx.state.user, "update", group); + let membership = await GroupUser.findOne({ where: { groupId: id, @@ -206,7 +214,7 @@ router.post("groups.add_user", auth(), async (ctx) => { }); if (!membership) { - await group.addUser(user, { + await group.$add("user", user, { through: { createdById: ctx.state.user.id, }, @@ -218,8 +226,12 @@ router.post("groups.add_user", auth(), async (ctx) => { userId, }, }); + invariant(membership, "membership not found"); + // reload to get default scope group = await Group.findByPk(id); + invariant(group, "group not found"); + await Event.create({ name: "groups.add_user", userId, @@ -249,9 +261,11 @@ router.post("groups.remove_user", auth(), async (ctx) => { let group = await Group.findByPk(id); authorize(ctx.state.user, "update", group); + const user = await User.findByPk(userId); authorize(ctx.state.user, "read", user); - await group.removeUser(user); + + await group.$remove("user", user); await Event.create({ name: "groups.remove_user", userId, @@ -263,8 +277,11 @@ router.post("groups.remove_user", auth(), async (ctx) => { }, ip: ctx.request.ip, }); + // reload to get default scope group = await Group.findByPk(id); + invariant(group, "group not found"); + ctx.body = { data: { groups: [presentGroup(group)], diff --git a/server/routes/api/hooks.test.ts b/server/routes/api/hooks.test.ts index a8572a861..f5fbf50b7 100644 --- a/server/routes/api/hooks.test.ts +++ b/server/routes/api/hooks.test.ts @@ -13,6 +13,7 @@ afterAll(() => server.close()); jest.mock("../../utils/slack", () => ({ post: jest.fn(), })); + describe("#hooks.unfurl", () => { it("should return documents", async () => { const { user, document } = await seed(); @@ -45,6 +46,7 @@ describe("#hooks.unfurl", () => { expect(Slack.post).toHaveBeenCalled(); }); }); + describe("#hooks.slack", () => { it("should return no matches", async () => { const { user, team } = await seed(); @@ -126,8 +128,8 @@ describe("#hooks.slack", () => { "This title *contains* a search term" ); }); - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type '(done: DoneCallback) => Promise<... Remove this comment to see the full error message - it("should save search term, hits and source", async (done) => { + + it("should save search term, hits and source", async () => { const { user, team } = await seed(); await server.post("/api/hooks.slack", { body: { @@ -137,19 +139,22 @@ describe("#hooks.slack", () => { text: "contains", }, }); - // setTimeout is needed here because SearchQuery is saved asynchronously - // in order to not slow down the response time. - setTimeout(async () => { - const searchQuery = await SearchQuery.findAll({ - where: { - query: "contains", - }, - }); - expect(searchQuery.length).toBe(1); - expect(searchQuery[0].results).toBe(0); - expect(searchQuery[0].source).toBe("slack"); - done(); - }, 100); + + return new Promise((resolve) => { + // setTimeout is needed here because SearchQuery is saved asynchronously + // in order to not slow down the response time. + setTimeout(async () => { + const searchQuery = await SearchQuery.findAll({ + where: { + query: "contains", + }, + }); + expect(searchQuery.length).toBe(1); + expect(searchQuery[0].results).toBe(0); + expect(searchQuery[0].source).toBe("slack"); + resolve(undefined); + }, 100); + }); }); it("should respond with help content for help keyword", async () => { @@ -259,6 +264,7 @@ describe("#hooks.slack", () => { expect(res.status).toEqual(401); }); }); + describe("#hooks.interactive", () => { it("should respond with replacement message", async () => { const { user, team } = await seed(); diff --git a/server/routes/api/hooks.ts b/server/routes/api/hooks.ts index ac5b4489d..2f03f60ad 100644 --- a/server/routes/api/hooks.ts +++ b/server/routes/api/hooks.ts @@ -1,3 +1,4 @@ +import invariant from "invariant"; import Router from "koa-router"; import { escapeRegExp } from "lodash"; import { AuthenticationError, InvalidRequestError } from "@server/errors"; @@ -93,6 +94,8 @@ router.post("hooks.interactive", async (ctx) => { } const team = await Team.findByPk(document.teamId); + invariant(team, "team not found"); + // respond with a public message that will be posted in the original channel ctx.body = { response_type: "in_channel", diff --git a/server/routes/api/index.test.ts b/server/routes/api/index.test.ts index fae7c6697..59dfce538 100644 --- a/server/routes/api/index.test.ts +++ b/server/routes/api/index.test.ts @@ -7,12 +7,14 @@ const app = webService(); const server = new TestServer(app.callback()); beforeEach(() => flushdb()); afterAll(() => server.close()); + describe("POST unknown endpoint", () => { it("should be not found", async () => { const res = await server.post("/api/blah"); expect(res.status).toEqual(404); }); }); + describe("GET unknown endpoint", () => { it("should be not found", async () => { const res = await server.get("/api/blah"); diff --git a/server/routes/api/integrations.ts b/server/routes/api/integrations.ts index 44216341d..c1b55494c 100644 --- a/server/routes/api/integrations.ts +++ b/server/routes/api/integrations.ts @@ -2,12 +2,11 @@ import Router from "koa-router"; import auth from "@server/middlewares/authentication"; import { Event } from "@server/models"; import Integration from "@server/models/Integration"; -import policy from "@server/policies"; +import { authorize } from "@server/policies"; import { presentIntegration } from "@server/presenters"; import { assertSort, assertUuid, assertArray } from "@server/validation"; import pagination from "./middlewares/pagination"; -const { authorize } = policy; const router = new Router(); router.post("integrations.list", auth(), pagination(), async (ctx) => { @@ -16,7 +15,7 @@ router.post("integrations.list", auth(), pagination(), async (ctx) => { if (direction !== "ASC") direction = "DESC"; assertSort(sort, Integration); - const user = ctx.state.user; + const { user } = ctx.state; const integrations = await Integration.findAll({ where: { teamId: user.teamId, @@ -25,6 +24,7 @@ router.post("integrations.list", auth(), pagination(), async (ctx) => { offset: ctx.state.pagination.offset, limit: ctx.state.pagination.limit, }); + ctx.body = { pagination: ctx.state.pagination, data: integrations.map(presentIntegration), diff --git a/server/routes/api/middlewares/pagination.test.ts b/server/routes/api/middlewares/pagination.test.ts index 0ebbad683..30f8b56ba 100644 --- a/server/routes/api/middlewares/pagination.test.ts +++ b/server/routes/api/middlewares/pagination.test.ts @@ -7,6 +7,7 @@ const app = webService(); const server = new TestServer(app.callback()); beforeEach(() => flushdb()); afterAll(() => server.close()); + describe("#pagination", () => { it("should allow offset and limit", async () => { const { user } = await seed(); diff --git a/server/routes/api/notificationSettings.ts b/server/routes/api/notificationSettings.ts index 9716dd7ec..9729dd5d7 100644 --- a/server/routes/api/notificationSettings.ts +++ b/server/routes/api/notificationSettings.ts @@ -1,18 +1,17 @@ import Router from "koa-router"; import auth from "@server/middlewares/authentication"; import { Team, NotificationSetting } from "@server/models"; -import policy from "@server/policies"; +import { authorize } from "@server/policies"; import { presentNotificationSetting } from "@server/presenters"; import { assertPresent, assertUuid } from "@server/validation"; -const { authorize } = policy; const router = new Router(); router.post("notificationSettings.create", auth(), async (ctx) => { const { event } = ctx.body; assertPresent(event, "event is required"); - const user = ctx.state.user; + const { user } = ctx.state; authorize(user, "createNotificationSetting", user.team); const [setting] = await NotificationSetting.findOrCreate({ where: { @@ -21,18 +20,20 @@ router.post("notificationSettings.create", auth(), async (ctx) => { event, }, }); + ctx.body = { data: presentNotificationSetting(setting), }; }); router.post("notificationSettings.list", auth(), async (ctx) => { - const user = ctx.state.user; + const { user } = ctx.state; const settings = await NotificationSetting.findAll({ where: { userId: user.id, }, }); + ctx.body = { data: settings.map(presentNotificationSetting), }; @@ -42,10 +43,12 @@ router.post("notificationSettings.delete", auth(), async (ctx) => { const { id } = ctx.body; assertUuid(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; const setting = await NotificationSetting.findByPk(id); authorize(user, "delete", setting); + await setting.destroy(); + ctx.body = { success: true, }; diff --git a/server/routes/api/pins.ts b/server/routes/api/pins.ts index 155585e0f..88422af22 100644 --- a/server/routes/api/pins.ts +++ b/server/routes/api/pins.ts @@ -1,20 +1,20 @@ +import invariant from "invariant"; import Router from "koa-router"; +import { Sequelize, Op } from "sequelize"; import pinCreator from "@server/commands/pinCreator"; import pinDestroyer from "@server/commands/pinDestroyer"; import pinUpdater from "@server/commands/pinUpdater"; import auth from "@server/middlewares/authentication"; import { Collection, Document, Pin } from "@server/models"; -import policy from "@server/policies"; +import { authorize } from "@server/policies"; import { presentPin, presentDocument, presentPolicies, } from "@server/presenters"; -import { sequelize, Op } from "@server/sequelize"; import { assertUuid, assertIndexCharacters } from "@server/validation"; import pagination from "./middlewares/pagination"; -const { authorize } = policy; const router = new Router(); router.post("pins.create", auth(), async (ctx) => { @@ -65,11 +65,11 @@ router.post("pins.list", auth(), pagination(), async (ctx) => { where: { ...(collectionId ? { collectionId } - : { collectionId: { [Op.eq]: null } }), + : { collectionId: { [Op.is]: null } }), teamId: user.teamId, }, order: [ - sequelize.literal('"pins"."index" collate "C"'), + Sequelize.literal('"pin"."index" collate "C"'), ["updatedAt", "DESC"], ], offset: ctx.state.pagination.offset, @@ -80,7 +80,7 @@ router.post("pins.list", auth(), pagination(), async (ctx) => { const documents = await Document.defaultScopeWithUser(user.id).findAll({ where: { - id: pins.map((pin: any) => pin.documentId), + id: pins.map((pin) => pin.documentId), collectionId: collectionIds, }, }); @@ -92,7 +92,7 @@ router.post("pins.list", auth(), pagination(), async (ctx) => { data: { pins: pins.map(presentPin), documents: await Promise.all( - documents.map((document: any) => presentDocument(document)) + documents.map((document: Document) => presentDocument(document)) ), }, policies, @@ -107,6 +107,8 @@ router.post("pins.update", auth(), async (ctx) => { const { user } = ctx.state; let pin = await Pin.findByPk(id); + invariant(pin, "pin not found"); + const document = await Document.findByPk(pin.documentId, { userId: user.id, }); @@ -136,6 +138,8 @@ router.post("pins.delete", auth(), async (ctx) => { const { user } = ctx.state; const pin = await Pin.findByPk(id); + invariant(pin, "pin not found"); + const document = await Document.findByPk(pin.documentId, { userId: user.id, }); diff --git a/server/routes/api/revisions.test.ts b/server/routes/api/revisions.test.ts index 28de329dc..43d019dbb 100644 --- a/server/routes/api/revisions.test.ts +++ b/server/routes/api/revisions.test.ts @@ -9,6 +9,7 @@ const app = webService(); const server = new TestServer(app.callback()); beforeEach(() => flushdb()); afterAll(() => server.close()); + describe("#revisions.info", () => { it("should return a document revision", async () => { const { user, document } = await seed(); @@ -38,6 +39,7 @@ describe("#revisions.info", () => { expect(res.status).toEqual(403); }); }); + describe("#revisions.list", () => { it("should return a document's revisions", async () => { const { user, document } = await seed(); diff --git a/server/routes/api/revisions.ts b/server/routes/api/revisions.ts index a590580bf..0b72110ae 100644 --- a/server/routes/api/revisions.ts +++ b/server/routes/api/revisions.ts @@ -2,18 +2,17 @@ import Router from "koa-router"; import { NotFoundError } from "@server/errors"; import auth from "@server/middlewares/authentication"; import { Document, Revision } from "@server/models"; -import policy from "@server/policies"; +import { authorize } from "@server/policies"; import { presentRevision } from "@server/presenters"; import { assertPresent, assertSort } from "@server/validation"; import pagination from "./middlewares/pagination"; -const { authorize } = policy; const router = new Router(); router.post("revisions.info", auth(), async (ctx) => { const { id } = ctx.body; assertPresent(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; const revision = await Revision.findByPk(id); if (!revision) { @@ -38,11 +37,12 @@ router.post("revisions.list", auth(), pagination(), async (ctx) => { assertSort(sort, Revision); assertPresent(documentId, "documentId is required"); - const user = ctx.state.user; + const { user } = ctx.state; const document = await Document.findByPk(documentId, { userId: user.id, }); authorize(user, "read", document); + const revisions = await Revision.findAll({ where: { documentId: document.id, @@ -52,9 +52,9 @@ router.post("revisions.list", auth(), pagination(), async (ctx) => { limit: ctx.state.pagination.limit, }); const data = await Promise.all( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'revision' implicitly has an 'any' type. revisions.map((revision) => presentRevision(revision)) ); + ctx.body = { pagination: ctx.state.pagination, data, diff --git a/server/routes/api/searches.ts b/server/routes/api/searches.ts index 2ed905baa..73b60bd4c 100644 --- a/server/routes/api/searches.ts +++ b/server/routes/api/searches.ts @@ -8,7 +8,7 @@ import pagination from "./middlewares/pagination"; const router = new Router(); router.post("searches.list", auth(), pagination(), async (ctx) => { - const user = ctx.state.user; + const { user } = ctx.state; const searches = await SearchQuery.findAll({ where: { diff --git a/server/routes/api/shares.test.ts b/server/routes/api/shares.test.ts index cd0a6f222..08fcccd4b 100644 --- a/server/routes/api/shares.test.ts +++ b/server/routes/api/shares.test.ts @@ -9,6 +9,7 @@ const app = webService(); const server = new TestServer(app.callback()); beforeEach(() => flushdb()); afterAll(() => server.close()); + describe("#shares.list", () => { it("should only return shares created by user", async () => { const { user, admin, document } = await seed(); @@ -133,6 +134,7 @@ describe("#shares.list", () => { expect(body).toMatchSnapshot(); }); }); + describe("#shares.create", () => { it("should allow creating a share record for document", async () => { const { user, document } = await seed(); @@ -183,7 +185,7 @@ describe("#shares.create", () => { teamId: user.teamId, userId: user.id, }); - await share.revoke(); + await share.revoke(user.id); const res = await server.post("/api/shares.create", { body: { token: user.getJwtToken(), @@ -284,6 +286,7 @@ describe("#shares.create", () => { expect(res.status).toEqual(403); }); }); + describe("#shares.info", () => { it("should allow reading share by id", async () => { const { user, document } = await seed(); @@ -375,7 +378,7 @@ describe("#shares.info", () => { teamId: user.teamId, userId: user.id, }); - await share.revoke(); + await share.revoke(user.id); const res = await server.post("/api/shares.info", { body: { token: user.getJwtToken(), @@ -551,6 +554,7 @@ describe("#shares.info", () => { expect(res.status).toEqual(403); }); }); + describe("#shares.update", () => { it("should allow user to update a share", async () => { const { user, document } = await seed(); @@ -647,6 +651,7 @@ describe("#shares.update", () => { expect(res.status).toEqual(403); }); }); + describe("#shares.revoke", () => { it("should allow author to revoke a share", async () => { const { user, document } = await seed(); diff --git a/server/routes/api/shares.ts b/server/routes/api/shares.ts index 7858912cd..5eaac7a2b 100644 --- a/server/routes/api/shares.ts +++ b/server/routes/api/shares.ts @@ -1,22 +1,20 @@ import Router from "koa-router"; -import Sequelize from "sequelize"; +import { Op, WhereOptions } from "sequelize"; import { NotFoundError } from "@server/errors"; import auth from "@server/middlewares/authentication"; import { Document, User, Event, Share, Team, Collection } from "@server/models"; -import policy from "@server/policies"; +import { authorize } from "@server/policies"; import { presentShare, presentPolicies } from "@server/presenters"; import { assertUuid, assertSort, assertPresent } from "@server/validation"; import pagination from "./middlewares/pagination"; -const Op = Sequelize.Op; -const { authorize } = policy; const router = new Router(); router.post("shares.info", auth(), async (ctx) => { const { id, documentId, apiVersion } = ctx.body; assertUuid(id || documentId, "id or documentId is required"); - const user = ctx.state.user; + const { user } = ctx.state; const shares = []; const share = await Share.scope({ method: ["withCollection", user.id], @@ -25,14 +23,14 @@ router.post("shares.info", auth(), async (ctx) => { ? { id, revokedAt: { - [Op.eq]: null, + [Op.is]: null, }, } : { documentId, teamId: user.teamId, revokedAt: { - [Op.eq]: null, + [Op.is]: null, }, }, }); @@ -72,7 +70,7 @@ router.post("shares.info", auth(), async (ctx) => { documentId: parentIds, teamId: user.teamId, revokedAt: { - [Op.eq]: null, + [Op.is]: null, }, includeChildDocuments: true, published: true, @@ -105,13 +103,13 @@ router.post("shares.list", auth(), pagination(), async (ctx) => { if (direction !== "ASC") direction = "DESC"; assertSort(sort, Share); - const user = ctx.state.user; - const where = { + const { user } = ctx.state; + const where: WhereOptions = { teamId: user.teamId, userId: user.id, published: true, revokedAt: { - [Op.eq]: null, + [Op.is]: null, }, }; @@ -155,9 +153,9 @@ router.post("shares.list", auth(), pagination(), async (ctx) => { offset: ctx.state.pagination.offset, limit: ctx.state.pagination.limit, }); + ctx.body = { pagination: ctx.state.pagination, - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'share' implicitly has an 'any' type. data: shares.map((share) => presentShare(share, user.isAdmin)), policies: presentPolicies(user, shares), }; @@ -174,6 +172,7 @@ router.post("shares.update", auth(), async (ctx) => { const share = await Share.scope({ method: ["withCollection", user.id], }).findByPk(id); + authorize(user, "update", share); if (published !== undefined) { @@ -203,6 +202,7 @@ router.post("shares.update", auth(), async (ctx) => { }, ip: ctx.request.ip, }); + ctx.body = { data: presentShare(share, user.isAdmin), policies: presentPolicies(user, [share]), @@ -213,14 +213,16 @@ router.post("shares.create", auth(), async (ctx) => { const { documentId } = ctx.body; assertPresent(documentId, "documentId is required"); - const user = ctx.state.user; + const { user } = ctx.state; const document = await Document.findByPk(documentId, { userId: user.id, }); - const team = await Team.findByPk(user.teamId); + // user could be creating the share link to share with team members authorize(user, "read", document); + const team = await Team.findByPk(user.teamId); + const [share, isCreated] = await Share.findOrCreate({ where: { documentId, @@ -247,9 +249,12 @@ router.post("shares.create", auth(), async (ctx) => { }); } - share.team = team; + if (team) { + share.team = team; + } share.user = user; share.document = document; + ctx.body = { data: presentShare(share), policies: presentPolicies(user, [share]), @@ -260,15 +265,16 @@ router.post("shares.revoke", auth(), async (ctx) => { const { id } = ctx.body; assertUuid(id, "id is required"); - const user = ctx.state.user; + const { user } = ctx.state; const share = await Share.findByPk(id); - authorize(user, "revoke", share); - const document = await Document.findByPk(share.documentId); - if (!document) { + if (!share?.document) { throw NotFoundError(); } + authorize(user, "revoke", share); + const { document } = share; + await share.revoke(user.id); await Event.create({ name: "shares.revoke", @@ -282,6 +288,7 @@ router.post("shares.revoke", auth(), async (ctx) => { }, ip: ctx.request.ip, }); + ctx.body = { success: true, }; diff --git a/server/routes/api/team.test.ts b/server/routes/api/team.test.ts index 94ea46371..d32620ffa 100644 --- a/server/routes/api/team.test.ts +++ b/server/routes/api/team.test.ts @@ -7,6 +7,7 @@ const app = webService(); const server = new TestServer(app.callback()); beforeEach(() => flushdb()); afterAll(() => server.close()); + describe("#team.update", () => { it("should update team details", async () => { const { admin } = await seed(); diff --git a/server/routes/api/team.ts b/server/routes/api/team.ts index 03924f069..11f197ef9 100644 --- a/server/routes/api/team.ts +++ b/server/routes/api/team.ts @@ -1,10 +1,9 @@ import Router from "koa-router"; import auth from "@server/middlewares/authentication"; import { Event, Team } from "@server/models"; -import policy from "@server/policies"; +import { authorize } from "@server/policies"; import { presentTeam, presentPolicies } from "@server/presenters"; -const { authorize } = policy; const router = new Router(); router.post("team.update", auth(), async (ctx) => { @@ -18,7 +17,7 @@ router.post("team.update", auth(), async (ctx) => { collaborativeEditing, defaultUserRole, } = ctx.body; - const user = ctx.state.user; + const { user } = ctx.state; const team = await Team.findByPk(user.teamId); authorize(user, "update", team); diff --git a/server/routes/api/users.test.ts b/server/routes/api/users.test.ts index 3c58ade08..f7eddd1af 100644 --- a/server/routes/api/users.test.ts +++ b/server/routes/api/users.test.ts @@ -8,6 +8,7 @@ const app = webService(); const server = new TestServer(app.callback()); beforeEach(() => flushdb()); afterAll(() => server.close()); + describe("#users.list", () => { it("should allow filtering by user name", async () => { const user = await buildUser({ @@ -103,6 +104,7 @@ describe("#users.list", () => { expect(body.data[1].id).toEqual(admin.id); }); }); + describe("#users.info", () => { it("should return current user with no id", async () => { const user = await buildUser(); @@ -154,6 +156,7 @@ describe("#users.info", () => { expect(res.status).toEqual(401); }); }); + describe("#users.invite", () => { it("should return sent invites", async () => { const user = await buildAdmin(); @@ -274,6 +277,7 @@ describe("#users.invite", () => { expect(res.status).toEqual(401); }); }); + describe("#users.delete", () => { it("should not allow deleting without confirmation", async () => { const user = await buildUser(); @@ -352,6 +356,7 @@ describe("#users.delete", () => { expect(body).toMatchSnapshot(); }); }); + describe("#users.update", () => { it("should update user profile information", async () => { const { user } = await seed(); @@ -373,6 +378,7 @@ describe("#users.update", () => { expect(body).toMatchSnapshot(); }); }); + describe("#users.promote", () => { it("should promote a new admin", async () => { const { admin, user } = await seed(); @@ -400,6 +406,7 @@ describe("#users.promote", () => { expect(body).toMatchSnapshot(); }); }); + describe("#users.demote", () => { it("should demote an admin", async () => { const { admin, user } = await seed(); @@ -480,6 +487,7 @@ describe("#users.demote", () => { expect(body).toMatchSnapshot(); }); }); + describe("#users.suspend", () => { it("should suspend an user", async () => { const { admin, user } = await seed(); @@ -520,6 +528,7 @@ describe("#users.suspend", () => { expect(body).toMatchSnapshot(); }); }); + describe("#users.activate", () => { it("should activate a suspended user", async () => { const { admin, user } = await seed(); @@ -552,6 +561,7 @@ describe("#users.activate", () => { expect(body).toMatchSnapshot(); }); }); + describe("#users.count", () => { it("should count active users", async () => { const team = await buildTeam(); diff --git a/server/routes/api/users.ts b/server/routes/api/users.ts index c42fa58d0..a290b64ab 100644 --- a/server/routes/api/users.ts +++ b/server/routes/api/users.ts @@ -1,12 +1,12 @@ import Router from "koa-router"; +import { Op, WhereOptions } from "sequelize"; import userDestroyer from "@server/commands/userDestroyer"; import userInviter from "@server/commands/userInviter"; import userSuspender from "@server/commands/userSuspender"; import auth from "@server/middlewares/authentication"; import { Event, User, Team } from "@server/models"; -import policy from "@server/policies"; +import { can, authorize } from "@server/policies"; import { presentUser, presentPolicies } from "@server/presenters"; -import { Op } from "@server/sequelize"; import { assertIn, assertSort, @@ -15,7 +15,6 @@ import { } from "@server/validation"; import pagination from "./middlewares/pagination"; -const { can, authorize } = policy; const router = new Router(); router.post("users.list", auth(), pagination(), async (ctx) => { @@ -33,25 +32,22 @@ router.post("users.list", auth(), pagination(), async (ctx) => { } const actor = ctx.state.user; - let where = { + let where: WhereOptions = { teamId: actor.teamId, }; switch (filter) { case "invited": { - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ lastActiveAt: null; teamId: any; }' is not... Remove this comment to see the full error message where = { ...where, lastActiveAt: null }; break; } case "viewers": { - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ isViewer: boolean; teamId: any; }' is not ... Remove this comment to see the full error message where = { ...where, isViewer: true }; break; } case "admins": { - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ isAdmin: boolean; teamId: any; }' is not a... Remove this comment to see the full error message where = { ...where, isAdmin: true }; break; } @@ -59,7 +55,6 @@ router.post("users.list", auth(), pagination(), async (ctx) => { case "suspended": { where = { ...where, - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ suspendedAt: { [Op.ne]: null; }; teamId: a... Remove this comment to see the full error message suspendedAt: { [Op.ne]: null, }, @@ -74,9 +69,8 @@ router.post("users.list", auth(), pagination(), async (ctx) => { default: { where = { ...where, - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ suspendedAt: { [Op.eq]: null; }; teamId: a... Remove this comment to see the full error message suspendedAt: { - [Op.eq]: null, + [Op.is]: null, }, }; break; @@ -86,7 +80,6 @@ router.post("users.list", auth(), pagination(), async (ctx) => { if (query) { where = { ...where, - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ name: { [Op.iLike]: string; }; teamId: any... Remove this comment to see the full error message name: { [Op.iLike]: `%${query}%`, }, @@ -104,9 +97,9 @@ router.post("users.list", auth(), pagination(), async (ctx) => { where, }), ]); + ctx.body = { pagination: { ...ctx.state.pagination, total }, - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'user' implicitly has an 'any' type. data: users.map((user) => presentUser(user, { includeDetails: can(actor, "readDetails", user), @@ -119,6 +112,7 @@ router.post("users.list", auth(), pagination(), async (ctx) => { router.post("users.count", auth(), async (ctx) => { const { user } = ctx.state; const counts = await User.getCounts(user.teamId); + ctx.body = { data: { counts, @@ -132,6 +126,7 @@ router.post("users.info", auth(), async (ctx) => { const user = id ? await User.findByPk(id) : actor; authorize(actor, "read", user); const includeDetails = can(actor, "readDetails", user); + ctx.body = { data: presentUser(user, { includeDetails, @@ -154,12 +149,14 @@ router.post("users.update", auth(), async (ctx) => { teamId: user.teamId, ip: ctx.request.ip, }); + ctx.body = { data: presentUser(user, { includeDetails: true, }), }; }); + // Admin specific router.post("users.promote", auth(), async (ctx) => { const userId = ctx.body.id; @@ -168,6 +165,7 @@ router.post("users.promote", auth(), async (ctx) => { assertPresent(userId, "id is required"); const user = await User.findByPk(userId); authorize(actor, "promote", user); + await user.promote(); await Event.create({ name: "users.promote", @@ -180,6 +178,7 @@ router.post("users.promote", auth(), async (ctx) => { ip: ctx.request.ip, }); const includeDetails = can(actor, "readDetails", user); + ctx.body = { data: presentUser(user, { includeDetails, @@ -197,6 +196,7 @@ router.post("users.demote", auth(), async (ctx) => { to = to === "viewer" ? "viewer" : "member"; const user = await User.findByPk(userId); authorize(actor, "demote", user); + await user.demote(teamId, to); await Event.create({ name: "users.demote", @@ -209,6 +209,7 @@ router.post("users.demote", auth(), async (ctx) => { ip: ctx.request.ip, }); const includeDetails = can(actor, "readDetails", user); + ctx.body = { data: presentUser(user, { includeDetails, @@ -223,12 +224,14 @@ router.post("users.suspend", auth(), async (ctx) => { assertPresent(userId, "id is required"); const user = await User.findByPk(userId); authorize(actor, "suspend", user); + await userSuspender({ user, actorId: actor.id, ip: ctx.request.ip, }); const includeDetails = can(actor, "readDetails", user); + ctx.body = { data: presentUser(user, { includeDetails, @@ -244,6 +247,7 @@ router.post("users.activate", auth(), async (ctx) => { assertPresent(userId, "id is required"); const user = await User.findByPk(userId); authorize(actor, "activate", user); + await user.activate(); await Event.create({ name: "users.activate", @@ -256,6 +260,7 @@ router.post("users.activate", auth(), async (ctx) => { ip: ctx.request.ip, }); const includeDetails = can(actor, "readDetails", user); + ctx.body = { data: presentUser(user, { includeDetails, @@ -270,11 +275,13 @@ router.post("users.invite", auth(), async (ctx) => { const { user } = ctx.state; const team = await Team.findByPk(user.teamId); authorize(user, "inviteUser", team); + const response = await userInviter({ user, invites, ip: ctx.request.ip, }); + ctx.body = { data: { sent: response.sent, @@ -299,6 +306,7 @@ router.post("users.delete", auth(), async (ctx) => { actor, ip: ctx.request.ip, }); + ctx.body = { success: true, }; diff --git a/server/routes/api/utils.test.ts b/server/routes/api/utils.test.ts index b2736a37f..8a247e4b4 100644 --- a/server/routes/api/utils.test.ts +++ b/server/routes/api/utils.test.ts @@ -2,26 +2,16 @@ import { subDays } from "date-fns"; // @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'fetc... Remove this comment to see the full error message import TestServer from "fetch-test-server"; import { Document, FileOperation } from "@server/models"; -import { Op } from "@server/sequelize"; import webService from "@server/services/web"; import { buildDocument, buildFileOperation } from "@server/test/factories"; import { flushdb } from "@server/test/support"; const app = webService(); const server = new TestServer(app.callback()); -jest.mock("aws-sdk", () => { - const mS3 = { - createPresignedPost: jest.fn(), - deleteObject: jest.fn().mockReturnThis(), - promise: jest.fn(), - }; - return { - S3: jest.fn(() => mS3), - Endpoint: jest.fn(), - }; -}); + beforeEach(() => flushdb()); afterAll(() => server.close()); + describe("#utils.gc", () => { it("should not destroy documents not deleted", async () => { await buildDocument({ @@ -112,9 +102,7 @@ describe("#utils.gc", () => { const data = await FileOperation.count({ where: { type: "export", - state: { - [Op.eq]: "expired", - }, + state: "expired", }, }); expect(res.status).toEqual(200); @@ -139,9 +127,7 @@ describe("#utils.gc", () => { const data = await FileOperation.count({ where: { type: "export", - state: { - [Op.eq]: "expired", - }, + state: "expired", }, }); expect(res.status).toEqual(200); diff --git a/server/routes/api/utils.ts b/server/routes/api/utils.ts index d3d402e7a..6867e7210 100644 --- a/server/routes/api/utils.ts +++ b/server/routes/api/utils.ts @@ -1,10 +1,10 @@ import { subDays } from "date-fns"; import Router from "koa-router"; +import { Op } from "sequelize"; import documentPermanentDeleter from "@server/commands/documentPermanentDeleter"; import teamPermanentDeleter from "@server/commands/teamPermanentDeleter"; import { AuthenticationError } from "@server/errors"; import { Document, Team, FileOperation } from "@server/models"; -import { Op } from "@server/sequelize"; import Logger from "../../logging/logger"; const router = new Router(); @@ -48,7 +48,6 @@ router.post("utils.gc", async (ctx) => { }, }); await Promise.all( - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'e' implicitly has an 'any' type. exports.map(async (e) => { await e.expire(); }) diff --git a/server/routes/api/views.test.ts b/server/routes/api/views.test.ts index 95674ef2c..51c636d4d 100644 --- a/server/routes/api/views.test.ts +++ b/server/routes/api/views.test.ts @@ -9,10 +9,11 @@ const app = webService(); const server = new TestServer(app.callback()); beforeEach(() => flushdb()); afterAll(() => server.close()); + describe("#views.list", () => { it("should return views for a document", async () => { const { user, document } = await seed(); - await View.increment({ + await View.incrementOrCreate({ documentId: document.id, userId: user.id, }); @@ -38,7 +39,7 @@ describe("#views.list", () => { userId: user.id, permission: "read", }); - await View.increment({ + await View.incrementOrCreate({ documentId: document.id, userId: user.id, }); @@ -78,6 +79,7 @@ describe("#views.list", () => { expect(res.status).toEqual(403); }); }); + describe("#views.create", () => { it("should allow creating a view record for document", async () => { const { user, document } = await seed(); diff --git a/server/routes/api/views.ts b/server/routes/api/views.ts index 2185800bd..ee69585e4 100644 --- a/server/routes/api/views.ts +++ b/server/routes/api/views.ts @@ -1,23 +1,23 @@ import Router from "koa-router"; import auth from "@server/middlewares/authentication"; import { View, Document, Event } from "@server/models"; -import policy from "@server/policies"; +import { authorize } from "@server/policies"; import { presentView } from "@server/presenters"; import { assertUuid } from "@server/validation"; -const { authorize } = policy; const router = new Router(); router.post("views.list", auth(), async (ctx) => { const { documentId } = ctx.body; assertUuid(documentId, "documentId is required"); - const user = ctx.state.user; + const { user } = ctx.state; const document = await Document.findByPk(documentId, { userId: user.id, }); authorize(user, "read", document); const views = await View.findByDocument(documentId); + ctx.body = { data: views.map(presentView), }; @@ -27,15 +27,17 @@ router.post("views.create", auth(), async (ctx) => { const { documentId } = ctx.body; assertUuid(documentId, "documentId is required"); - const user = ctx.state.user; + const { user } = ctx.state; const document = await Document.findByPk(documentId, { userId: user.id, }); authorize(user, "read", document); - const view = await View.increment({ + + const view = await View.incrementOrCreate({ documentId, userId: user.id, }); + await Event.create({ name: "views.create", actorId: user.id, @@ -48,6 +50,7 @@ router.post("views.create", auth(), async (ctx) => { ip: ctx.request.ip, }); view.user = user; + ctx.body = { data: presentView(view), }; diff --git a/server/routes/auth/index.ts b/server/routes/auth/index.ts index e0aad9501..e13434250 100644 --- a/server/routes/auth/index.ts +++ b/server/routes/auth/index.ts @@ -6,7 +6,6 @@ import Router from "koa-router"; import { AuthenticationError } from "@server/errors"; import auth from "@server/middlewares/authentication"; import { Collection, Team, View } from "@server/models"; -// @ts-expect-error ts-migrate(7034) FIXME: Variable 'providers' implicitly has type 'any[]' i... Remove this comment to see the full error message import providers from "./providers"; const app = new Koa(); @@ -14,7 +13,6 @@ const router = new Router(); router.use(passport.initialize()); // dynamically load available authentication provider routes -// @ts-expect-error ts-migrate(7005) FIXME: Variable 'providers' implicitly has an 'any[]' typ... Remove this comment to see the full error message providers.forEach((provider) => { if (provider.enabled) { router.use("/", provider.router.routes()); @@ -22,7 +20,7 @@ providers.forEach((provider) => { }); router.get("/redirect", auth(), async (ctx) => { - const user = ctx.state.user; + const { user } = ctx.state; const jwtToken = user.getJwtToken(); if (jwtToken === ctx.params.token) { @@ -45,10 +43,11 @@ router.get("/redirect", auth(), async (ctx) => { }), ]); const hasViewedDocuments = !!view; + ctx.redirect( !hasViewedDocuments && collection - ? `${team.url}${collection.url}` - : `${team.url}/home` + ? `${team!.url}${collection.url}` + : `${team!.url}/home` ); }); diff --git a/server/routes/auth/providers/email.test.ts b/server/routes/auth/providers/email.test.ts index d52c0e57c..c314a5075 100644 --- a/server/routes/auth/providers/email.test.ts +++ b/server/routes/auth/providers/email.test.ts @@ -7,13 +7,12 @@ import { flushdb } from "@server/test/support"; const app = webService(); const server = new TestServer(app.callback()); -jest.mock("../../../mailer"); + beforeEach(async () => { await flushdb(); - // @ts-expect-error ts-migrate(2339) FIXME: Property 'mockReset' does not exist on type '(type... Remove this comment to see the full error message - mailer.sendTemplate.mockReset(); }); afterAll(() => server.close()); + describe("email", () => { it("should require email param", async () => { const res = await server.post("/auth/email", { @@ -26,6 +25,7 @@ describe("email", () => { }); it("should respond with redirect location when user is SSO enabled", async () => { + const spy = jest.spyOn(mailer, "sendTemplate"); const user = await buildUser(); const res = await server.post("/auth/email", { body: { @@ -35,13 +35,15 @@ describe("email", () => { const body = await res.json(); expect(res.status).toEqual(200); expect(body.redirect).toMatch("slack"); - expect(mailer.sendTemplate).not.toHaveBeenCalled(); + expect(spy).not.toHaveBeenCalled(); + spy.mockRestore(); }); it("should respond with redirect location when user is SSO enabled on another subdomain", async () => { process.env.URL = "http://localoutline.com"; process.env.SUBDOMAINS_ENABLED = "true"; const user = await buildUser(); + const spy = jest.spyOn(mailer, "sendTemplate"); await buildTeam({ subdomain: "example", }); @@ -56,10 +58,12 @@ describe("email", () => { const body = await res.json(); expect(res.status).toEqual(200); expect(body.redirect).toMatch("slack"); - expect(mailer.sendTemplate).not.toHaveBeenCalled(); + expect(spy).not.toHaveBeenCalled(); + spy.mockRestore(); }); it("should respond with success when user is not SSO enabled", async () => { + const spy = jest.spyOn(mailer, "sendTemplate"); const user = await buildGuestUser(); const res = await server.post("/auth/email", { body: { @@ -69,10 +73,12 @@ describe("email", () => { const body = await res.json(); expect(res.status).toEqual(200); expect(body.success).toEqual(true); - expect(mailer.sendTemplate).toHaveBeenCalled(); + expect(spy).toHaveBeenCalled(); + spy.mockRestore(); }); it("should respond with success regardless of whether successful to prevent crawling email logins", async () => { + const spy = jest.spyOn(mailer, "sendTemplate"); const res = await server.post("/auth/email", { body: { email: "user@example.com", @@ -81,10 +87,12 @@ describe("email", () => { const body = await res.json(); expect(res.status).toEqual(200); expect(body.success).toEqual(true); - expect(mailer.sendTemplate).not.toHaveBeenCalled(); + expect(spy).not.toHaveBeenCalled(); + spy.mockRestore(); }); describe("with multiple users matching email", () => { it("should default to current subdomain with SSO", async () => { + const spy = jest.spyOn(mailer, "sendTemplate"); process.env.URL = "http://localoutline.com"; process.env.SUBDOMAINS_ENABLED = "true"; const email = "sso-user@example.org"; @@ -109,9 +117,12 @@ describe("email", () => { const body = await res.json(); expect(res.status).toEqual(200); expect(body.redirect).toMatch("slack"); - expect(mailer.sendTemplate).not.toHaveBeenCalled(); + expect(spy).not.toHaveBeenCalled(); + spy.mockRestore(); }); + it("should default to current subdomain with guest email", async () => { + const spy = jest.spyOn(mailer, "sendTemplate"); process.env.URL = "http://localoutline.com"; process.env.SUBDOMAINS_ENABLED = "true"; const email = "guest-user@example.org"; @@ -136,9 +147,12 @@ describe("email", () => { const body = await res.json(); expect(res.status).toEqual(200); expect(body.success).toEqual(true); - expect(mailer.sendTemplate).toHaveBeenCalled(); + expect(spy).toHaveBeenCalled(); + spy.mockRestore(); }); + it("should default to custom domain with SSO", async () => { + const spy = jest.spyOn(mailer, "sendTemplate"); const email = "sso-user-2@example.org"; const team = await buildTeam({ domain: "docs.mycompany.com", @@ -161,9 +175,12 @@ describe("email", () => { const body = await res.json(); expect(res.status).toEqual(200); expect(body.redirect).toMatch("slack"); - expect(mailer.sendTemplate).not.toHaveBeenCalled(); + expect(spy).not.toHaveBeenCalled(); + spy.mockRestore(); }); + it("should default to custom domain with guest email", async () => { + const spy = jest.spyOn(mailer, "sendTemplate"); const email = "guest-user-2@example.org"; const team = await buildTeam({ domain: "docs.mycompany.com", @@ -186,7 +203,8 @@ describe("email", () => { const body = await res.json(); expect(res.status).toEqual(200); expect(body.success).toEqual(true); - expect(mailer.sendTemplate).toHaveBeenCalled(); + expect(spy).toHaveBeenCalled(); + spy.mockRestore(); }); }); }); diff --git a/server/routes/auth/providers/email.ts b/server/routes/auth/providers/email.ts index bbf5279b7..ea2eed91a 100644 --- a/server/routes/auth/providers/email.ts +++ b/server/routes/auth/providers/email.ts @@ -89,7 +89,7 @@ router.post("email", errorHandling(), async (ctx) => { id: user.authentications[0].authenticationProviderId, }); ctx.body = { - redirect: `${team.url}/auth/${authProvider.name}`, + redirect: `${team.url}/auth/${authProvider?.name}`, }; return; } diff --git a/server/routes/auth/providers/index.ts b/server/routes/auth/providers/index.ts index 2f7ceeb3b..feb58220b 100644 --- a/server/routes/auth/providers/index.ts +++ b/server/routes/auth/providers/index.ts @@ -1,8 +1,15 @@ import { signin } from "@shared/utils/routeHelpers"; import { requireDirectory } from "@server/utils/fs"; -// @ts-expect-error ts-migrate(7034) FIXME: Variable 'providers' implicitly has type 'any[]' i... Remove this comment to see the full error message -const providers = []; +interface AuthenicationProvider { + id: string; + name: string; + enabled: boolean; + authUrl: string; + router: any; +} + +const providers: AuthenicationProvider[] = []; requireDirectory(__dirname).forEach(([module, id]) => { // @ts-expect-error ts-migrate(2339) FIXME: Property 'config' does not exist on type 'unknown'... Remove this comment to see the full error message @@ -35,5 +42,4 @@ requireDirectory(__dirname).forEach(([module, id]) => { } }); -// @ts-expect-error ts-migrate(7005) FIXME: Variable 'providers' implicitly has an 'any[]' typ... Remove this comment to see the full error message export default providers; diff --git a/server/routes/auth/providers/slack.ts b/server/routes/auth/providers/slack.ts index 5c9f3277f..a3c492852 100644 --- a/server/routes/auth/providers/slack.ts +++ b/server/routes/auth/providers/slack.ts @@ -90,7 +90,7 @@ if (SLACK_CLIENT_ID) { }), async (ctx) => { const { code, state, error } = ctx.request.query; - const user = ctx.state.user; + const { user } = ctx.state; assertPresent(code || error, "code is required"); if (error) { @@ -104,9 +104,9 @@ if (SLACK_CLIENT_ID) { if (!user) { if (state) { try { - const team = await Team.findByPk(state); + const team = await Team.findByPk(state as string); return ctx.redirect( - `${team.url}/auth${ctx.request.path}?${ctx.request.querystring}` + `${team!.url}/auth${ctx.request.path}?${ctx.request.querystring}` ); } catch (err) { return ctx.redirect( @@ -151,7 +151,7 @@ if (SLACK_CLIENT_ID) { }), async (ctx) => { const { code, error, state } = ctx.request.query; - const user = ctx.state.user; + const { user } = ctx.state; assertPresent(code || error, "code is required"); const collectionId = state; @@ -167,10 +167,10 @@ if (SLACK_CLIENT_ID) { // appropriate subdomain to complete the oauth flow if (!user) { try { - const collection = await Collection.findByPk(state); - const team = await Team.findByPk(collection.teamId); + const collection = await Collection.findByPk(state as string); + const team = await Team.findByPk(collection!.teamId); return ctx.redirect( - `${team.url}/auth${ctx.request.path}?${ctx.request.querystring}` + `${team!.url}/auth${ctx.request.path}?${ctx.request.querystring}` ); } catch (err) { return ctx.redirect( diff --git a/server/routes/index.test.ts b/server/routes/index.test.ts index b26d5b8a1..a178f216b 100644 --- a/server/routes/index.test.ts +++ b/server/routes/index.test.ts @@ -8,6 +8,7 @@ const app = webService(); const server = new TestServer(app.callback()); beforeEach(() => flushdb()); afterAll(() => server.close()); + describe("/share/:id", () => { it("should return standard title in html when loading share", async () => { const share = await buildShare({ diff --git a/server/routes/index.ts b/server/routes/index.ts index 7d0ce824a..f37805553 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -151,6 +151,7 @@ router.get("/robots.txt", (ctx) => { router.get("/opensearch.xml", (ctx) => { ctx.type = "text/xml"; + ctx.body = opensearchResponse(); }); diff --git a/server/scripts/20210226232041-migrate-authentication.test.ts b/server/scripts/20210226232041-migrate-authentication.test.ts deleted file mode 100644 index 91b1669c1..000000000 --- a/server/scripts/20210226232041-migrate-authentication.test.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { - User, - Team, - UserAuthentication, - AuthenticationProvider, -} from "@server/models"; -import { flushdb } from "@server/test/support"; -import script from "./20210226232041-migrate-authentication"; - -beforeEach(() => flushdb()); -describe("#work", () => { - it("should create authentication record for users", async () => { - const team = await Team.create({ - name: `Test`, - slackId: "T123", - }); - const user = await User.create({ - email: `test@example.com`, - name: `Test`, - serviceId: "U123", - teamId: team.id, - }); - await script(); - const authProvider = await AuthenticationProvider.findOne({ - where: { - providerId: "T123", - }, - }); - const auth = await UserAuthentication.findOne({ - where: { - providerId: "U123", - }, - }); - expect(authProvider.name).toEqual("slack"); - expect(auth.userId).toEqual(user.id); - }); - - it("should create authentication record for deleted users", async () => { - const team = await Team.create({ - name: `Test`, - googleId: "domain.com", - }); - const user = await User.create({ - email: `test1@example.com`, - name: `Test`, - service: "google", - serviceId: "123456789", - teamId: team.id, - deletedAt: new Date(), - }); - await script(); - const authProvider = await AuthenticationProvider.findOne({ - where: { - providerId: "domain.com", - }, - }); - const auth = await UserAuthentication.findOne({ - where: { - providerId: "123456789", - }, - }); - expect(authProvider.name).toEqual("google"); - expect(auth.userId).toEqual(user.id); - }); - - it("should create authentication record for suspended users", async () => { - const team = await Team.create({ - name: `Test`, - googleId: "example.com", - }); - const user = await User.create({ - email: `test1@example.com`, - name: `Test`, - service: "google", - serviceId: "123456789", - teamId: team.id, - suspendedAt: new Date(), - }); - await script(); - const authProvider = await AuthenticationProvider.findOne({ - where: { - providerId: "example.com", - }, - }); - const auth = await UserAuthentication.findOne({ - where: { - providerId: "123456789", - }, - }); - expect(authProvider.name).toEqual("google"); - expect(auth.userId).toEqual(user.id); - }); - - it("should create correct authentication record when team has both slackId and googleId", async () => { - const team = await Team.create({ - name: `Test`, - slackId: "T456", - googleId: "example.com", - }); - const user = await User.create({ - email: `test1@example.com`, - name: `Test`, - service: "slack", - serviceId: "U456", - teamId: team.id, - }); - await script(); - const authProvider = await AuthenticationProvider.findOne({ - where: { - providerId: "T456", - }, - }); - const auth = await UserAuthentication.findOne({ - where: { - providerId: "U456", - }, - }); - expect(authProvider.name).toEqual("slack"); - expect(auth.userId).toEqual(user.id); - }); - - it("should skip invited users", async () => { - const team = await Team.create({ - name: `Test`, - slackId: "T789", - }); - await User.create({ - email: `test2@example.com`, - name: `Test`, - teamId: team.id, - }); - await script(); - const count = await UserAuthentication.count(); - expect(count).toEqual(0); - }); -}); diff --git a/server/scripts/20210226232041-migrate-authentication.ts b/server/scripts/20210226232041-migrate-authentication.ts deleted file mode 100644 index ecf5dbcad..000000000 --- a/server/scripts/20210226232041-migrate-authentication.ts +++ /dev/null @@ -1,104 +0,0 @@ -import "./bootstrap"; -import Logger from "@server/logging/logger"; -import { - Team, - User, - AuthenticationProvider, - UserAuthentication, -} from "@server/models"; -import { Op } from "../sequelize"; - -const cache = {}; -const page = 0; -const limit = 100; - -export default async function main(exit = false) { - // @ts-expect-error ts-migrate(7024) FIXME: Function implicitly has return type 'any' because ... Remove this comment to see the full error message - const work = async (page: number) => { - Logger.info("database", "Starting authentication migration"); - const users = await User.findAll({ - limit, - offset: page * limit, - paranoid: false, - order: [["createdAt", "ASC"]], - where: { - serviceId: { - [Op.ne]: "email", - }, - }, - include: [ - { - model: Team, - as: "team", - required: true, - paranoid: false, - }, - ], - }); - - for (const user of users) { - const provider = user.service; - const providerId = user.team[`${provider}Id`]; - - if (!providerId) { - Logger.info( - "database", - `User ${user.id} has serviceId ${user.serviceId}, but team ${provider}Id missing` - ); - continue; - } - - if (providerId.startsWith("transferred")) { - Logger.info( - "database", - `skipping previously transferred ${user.team.name} (${user.team.id})` - ); - continue; - } - - let authenticationProviderId = cache[providerId]; - - if (!authenticationProviderId) { - const [ - authenticationProvider, - ] = await AuthenticationProvider.findOrCreate({ - where: { - name: provider, - providerId, - teamId: user.teamId, - }, - }); - cache[providerId] = authenticationProviderId = - authenticationProvider.id; - } - - try { - await UserAuthentication.create({ - authenticationProviderId, - providerId: user.serviceId, - teamId: user.teamId, - userId: user.id, - }); - } catch (err) { - Logger.info( - "database", - `serviceId ${user.serviceId} exists, for user ${user.id}` - ); - continue; - } - } - - return users.length === limit ? work(page + 1) : undefined; - }; - - await work(page); - - if (exit) { - Logger.info("database", "Migration complete"); - process.exit(0); - } -} // In the test suite we import the script rather than run via node CLI - -if (process.env.NODE_ENV !== "test") { - main(true); -} diff --git a/server/scripts/20210716000000-backfill-revisions.test.ts b/server/scripts/20210716000000-backfill-revisions.test.ts index c2e0e6ae1..b6d1f3d8c 100644 --- a/server/scripts/20210716000000-backfill-revisions.test.ts +++ b/server/scripts/20210716000000-backfill-revisions.test.ts @@ -4,17 +4,18 @@ import { flushdb } from "@server/test/support"; import script from "./20210716000000-backfill-revisions"; beforeEach(() => flushdb()); + describe("#work", () => { it("should create events for revisions", async () => { const document = await buildDocument(); const revision = await Revision.createFromDocument(document); await script(); const event = await Event.findOne(); - expect(event.name).toEqual("revisions.create"); - expect(event.modelId).toEqual(revision.id); - expect(event.documentId).toEqual(document.id); - expect(event.teamId).toEqual(document.teamId); - expect(event.createdAt).toEqual(revision.createdAt); + expect(event!.name).toEqual("revisions.create"); + expect(event!.modelId).toEqual(revision.id); + expect(event!.documentId).toEqual(document.id); + expect(event!.teamId).toEqual(document.teamId); + expect(event!.createdAt).toEqual(revision.createdAt); }); it("should create events for revisions of deleted documents", async () => { @@ -23,11 +24,11 @@ describe("#work", () => { await document.destroy(); await script(); const event = await Event.findOne(); - expect(event.name).toEqual("revisions.create"); - expect(event.modelId).toEqual(revision.id); - expect(event.documentId).toEqual(document.id); - expect(event.teamId).toEqual(document.teamId); - expect(event.createdAt).toEqual(revision.createdAt); + expect(event!.name).toEqual("revisions.create"); + expect(event!.modelId).toEqual(revision.id); + expect(event!.documentId).toEqual(document.id); + expect(event!.teamId).toEqual(document.teamId); + expect(event!.createdAt).toEqual(revision.createdAt); }); it("should be idempotent", async () => { diff --git a/server/services/collaboration.ts b/server/services/collaboration.ts index 2764500ed..ed1468078 100644 --- a/server/services/collaboration.ts +++ b/server/services/collaboration.ts @@ -29,7 +29,6 @@ export default function init(app: Koa, server: http.Server) { const documentName = url.parse(req.url).pathname?.split("/").pop(); invariant(documentName, "Document name must be provided"); - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'Duplex' is not assignable to par... Remove this comment to see the full error message wss.handleUpgrade(req, socket, head, (client) => { hocuspocus.handleConnection(client, req, documentName); }); diff --git a/server/services/websockets.ts b/server/services/websockets.ts index 6533fdc4c..5d45a937b 100644 --- a/server/services/websockets.ts +++ b/server/services/websockets.ts @@ -7,14 +7,12 @@ import SocketAuth from "socketio-auth"; import Logger from "@server/logging/logger"; import Metrics from "@server/logging/metrics"; import { Document, Collection, View } from "@server/models"; +import { can } from "@server/policies"; import { getUserForJWT } from "@server/utils/jwt"; -import policy from "../policies"; import { websocketsQueue } from "../queues"; import WebsocketsProcessor from "../queues/processors/websockets"; import { client, subscriber } from "../redis"; -const { can } = policy; - export default function init(app: Koa, server: http.Server) { const path = "/realtime"; @@ -178,7 +176,6 @@ export default function init(app: Koa, server: http.Server) { socket.emit("document.presence", { documentId: event.documentId, userIds: Array.from(userIds.keys()), - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'view' implicitly has an 'any' type. editingIds: editing.map((view) => view.userId), }); }); diff --git a/server/test/factories.ts b/server/test/factories.ts index 0d7088099..27579f1ab 100644 --- a/server/test/factories.ts +++ b/server/test/factories.ts @@ -17,7 +17,7 @@ import { let count = 1; -export async function buildShare(overrides: Record = {}) { +export async function buildShare(overrides: Partial = {}) { if (!overrides.teamId) { const team = await buildTeam(); overrides.teamId = team.id; @@ -64,7 +64,7 @@ export function buildTeam(overrides: Record = {}) { ); } -export function buildEvent(overrides: Record = {}) { +export function buildEvent(overrides: Partial = {}) { return Event.create({ name: "documents.publish", ip: "127.0.0.1", @@ -72,7 +72,7 @@ export function buildEvent(overrides: Record = {}) { }); } -export async function buildGuestUser(overrides: Record = {}) { +export async function buildGuestUser(overrides: Partial = {}) { if (!overrides.teamId) { const team = await buildTeam(); overrides.teamId = team.id; @@ -88,10 +88,14 @@ export async function buildGuestUser(overrides: Record = {}) { }); } -export async function buildUser(overrides: Record = {}) { +export async function buildUser(overrides: Partial = {}) { + let team; + if (!overrides.teamId) { - const team = await buildTeam(); + team = await buildTeam(); overrides.teamId = team.id; + } else { + team = await Team.findByPk(overrides.teamId); } const authenticationProvider = await AuthenticationProvider.findOne({ @@ -109,7 +113,7 @@ export async function buildUser(overrides: Record = {}) { lastActiveAt: new Date("2018-01-01T00:00:00.000Z"), authentications: [ { - authenticationProviderId: authenticationProvider.id, + authenticationProviderId: authenticationProvider!.id, providerId: uuidv4(), }, ], @@ -121,11 +125,11 @@ export async function buildUser(overrides: Record = {}) { ); } -export async function buildAdmin(overrides: Record = {}) { +export async function buildAdmin(overrides: Partial = {}) { return buildUser({ ...overrides, isAdmin: true }); } -export async function buildInvite(overrides: Record = {}) { +export async function buildInvite(overrides: Partial = {}) { if (!overrides.teamId) { const team = await buildTeam(); overrides.teamId = team.id; @@ -140,7 +144,7 @@ export async function buildInvite(overrides: Record = {}) { }); } -export async function buildIntegration(overrides: Record = {}) { +export async function buildIntegration(overrides: Partial = {}) { if (!overrides.teamId) { const team = await buildTeam(); overrides.teamId = team.id; @@ -167,7 +171,9 @@ export async function buildIntegration(overrides: Record = {}) { }); } -export async function buildCollection(overrides: Record = {}) { +export async function buildCollection( + overrides: Partial & { userId?: string } = {} +) { if (!overrides.teamId) { const team = await buildTeam(); overrides.teamId = team.id; @@ -190,7 +196,9 @@ export async function buildCollection(overrides: Record = {}) { }); } -export async function buildGroup(overrides: Record = {}) { +export async function buildGroup( + overrides: Partial & { userId?: string } = {} +) { if (!overrides.teamId) { const team = await buildTeam(); overrides.teamId = team.id; @@ -211,7 +219,9 @@ export async function buildGroup(overrides: Record = {}) { }); } -export async function buildGroupUser(overrides: Record = {}) { +export async function buildGroupUser( + overrides: Partial & { userId?: string; teamId?: string } = {} +) { if (!overrides.teamId) { const team = await buildTeam(); overrides.teamId = team.id; @@ -231,7 +241,9 @@ export async function buildGroupUser(overrides: Record = {}) { }); } -export async function buildDocument(overrides: Record = {}) { +export async function buildDocument( + overrides: Partial & { userId?: string } = {} +) { if (!overrides.teamId) { const team = await buildTeam(); overrides.teamId = team.id; @@ -243,7 +255,10 @@ export async function buildDocument(overrides: Record = {}) { } if (!overrides.collectionId) { - const collection = await buildCollection(overrides); + const collection = await buildCollection({ + teamId: overrides.teamId, + userId: overrides.userId, + }); overrides.collectionId = collection.id; } @@ -258,7 +273,9 @@ export async function buildDocument(overrides: Record = {}) { }); } -export async function buildFileOperation(overrides: Record = {}) { +export async function buildFileOperation( + overrides: Partial = {} +) { if (!overrides.teamId) { const team = await buildTeam(); overrides.teamId = team.id; @@ -282,7 +299,7 @@ export async function buildFileOperation(overrides: Record = {}) { }); } -export async function buildAttachment(overrides: Record = {}) { +export async function buildAttachment(overrides: Partial = {}) { if (!overrides.teamId) { const team = await buildTeam(); overrides.teamId = team.id; @@ -296,7 +313,10 @@ export async function buildAttachment(overrides: Record = {}) { } if (!overrides.documentId) { - const document = await buildDocument(overrides); + const document = await buildDocument({ + teamId: overrides.teamId, + userId: overrides.userId, + }); overrides.documentId = document.id; } diff --git a/server/test/setup.ts b/server/test/setup.ts index 6dde86487..3fef84f80 100644 --- a/server/test/setup.ts +++ b/server/test/setup.ts @@ -1,4 +1,5 @@ import "../env"; +import "@server/database/sequelize"; // test environment variables process.env.DATABASE_URL = process.env.DATABASE_URL_TEST; @@ -7,5 +8,19 @@ process.env.GOOGLE_CLIENT_ID = "123"; process.env.SLACK_KEY = "123"; process.env.DEPLOYMENT = ""; process.env.ALLOWED_DOMAINS = "allowed-domain.com"; + // This is needed for the relative manual mock to be picked up jest.mock("../queues"); + +// We never want to make real S3 requests in test environment +jest.mock("aws-sdk", () => { + const mS3 = { + createPresignedPost: jest.fn(), + deleteObject: jest.fn().mockReturnThis(), + promise: jest.fn(), + }; + return { + S3: jest.fn(() => mS3), + Endpoint: jest.fn(), + }; +}); diff --git a/server/test/support.ts b/server/test/support.ts index 1a4c7d993..5c8d966fc 100644 --- a/server/test/support.ts +++ b/server/test/support.ts @@ -1,13 +1,15 @@ import { v4 as uuidv4 } from "uuid"; +import { sequelize } from "@server/database/sequelize"; import { User, Document, Collection, Team } from "@server/models"; -import { sequelize } from "../sequelize"; const sql = sequelize.getQueryInterface(); const tables = Object.keys(sequelize.models).map((model) => { const n = sequelize.models[model].getTableName(); - return sql.queryGenerator.quoteTable(typeof n === "string" ? n : n.tableName); + return (sql.queryGenerator as any).quoteTable( + typeof n === "string" ? n : n.tableName + ); }); -const flushQuery = `TRUNCATE ${tables.join(", ")}`; +const flushQuery = `TRUNCATE ${tables.join(", ")} CASCADE`; export function flushdb() { return sequelize.query(flushQuery); diff --git a/server/types.ts b/server/types.ts index c478a7119..44e7ae57e 100644 --- a/server/types.ts +++ b/server/types.ts @@ -3,7 +3,6 @@ import { User } from "./models"; export type ContextWithState = Context & { state: { - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message user: User; token: string; authType: "app" | "api"; diff --git a/server/typings/cancan.d.ts b/server/typings/cancan.d.ts index a8bc2e33a..104cea9d1 100644 --- a/server/typings/cancan.d.ts +++ b/server/typings/cancan.d.ts @@ -1,4 +1,6 @@ declare module "cancan" { + import { Model } from "sequelize-typescript"; + namespace CanCan { interface Option { instanceOf?: ((instance: any, model: any) => boolean) | undefined; @@ -9,20 +11,42 @@ declare module "cancan" { class CanCan { constructor(options?: CanCan.Option); - allow( - model: any, + allow< + T extends new (...args: any) => any, + U extends new (...args: any) => any + >( + model: U, actions: string | ReadonlyArray, targets: T | ReadonlyArray | string | ReadonlyArray, condition?: | object - | ((performer: any, target: any, options?: any) => boolean) + | (( + performer: InstanceType, + target: InstanceType | null, + options?: any + ) => boolean) ): void; - can(performer: any, action: string, target: any, options?: any): boolean; + can( + performer: Model, + action: string, + target: Model | null | undefined, + options?: any + ): boolean; - cannot(performer: any, action: string, target: any, options?: any): boolean; + cannot( + performer: Model, + action: string, + target: Model | null | undefined, + options?: any + ): boolean; - authorize(performer: any, action: string, target: any, options?: any): void; + authorize( + performer: Model, + action: string, + target: Model | null | undefined, + options?: any + ): asserts target; abilities: { model: any; diff --git a/server/typings/index.d.ts b/server/typings/index.d.ts new file mode 100644 index 000000000..64050f1be --- /dev/null +++ b/server/typings/index.d.ts @@ -0,0 +1,33 @@ +declare module "slate-md-serializer"; + +declare module "sequelize-encrypted"; + +declare module "styled-components-breakpoint"; + +declare module "formidable/lib/file"; + +declare module "socket.io-client"; + +declare module "@tommoor/remove-markdown" { + export default function removeMarkdown( + text: string, + options?: { + stripHTML: boolean; + } + ): string; +} + +declare module "socket.io-redis" { + import { Redis } from "ioredis"; + + type Config = { + pubClient: Redis; + subClient: Redis; + }; + + const socketRedisAdapter: (config: Config) => void; + + export = socketRedisAdapter; +} + +declare module "oy-vey"; diff --git a/server/typings/socketio-auth.d.ts b/server/typings/socketio-auth.d.ts new file mode 100644 index 000000000..defed7698 --- /dev/null +++ b/server/typings/socketio-auth.d.ts @@ -0,0 +1,28 @@ +declare module "socketio-auth" { + import IO from "socket.io"; + + type AuthenticatedSocket = IO.Socket & { + client: IO.Client & { + user: any; + }; + }; + + type AuthenticateCallback = ( + socket: AuthenticatedSocket, + data: { token: string }, + callback: (err: Error | null, allow: boolean) => void + ) => Promise; + + type PostAuthenticateCallback = ( + socket: AuthenticatedSocket + ) => Promise; + + type AuthenticationConfig = { + authenticate: AuthenticateCallback; + postAuthenticate: PostAuthenticateCallback; + }; + + const SocketAuth: (io: IO.Server, config: AuthenticationConfig) => void; + + export = SocketAuth; +} diff --git a/server/utils/authentication.ts b/server/utils/authentication.ts index 2d062c96f..0a900b71f 100644 --- a/server/utils/authentication.ts +++ b/server/utils/authentication.ts @@ -14,9 +14,7 @@ export function getAllowedDomains(): string[] { export async function signIn( ctx: Context, - // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message user: User, - // @ts-expect-error ts-migrate(2749) FIXME: 'Team' refers to a value, but is being used as a t... Remove this comment to see the full error message team: Team, service: string, _isNewUser = false, diff --git a/server/utils/collectionIndexing.ts b/server/utils/collectionIndexing.ts index 526ff5240..d43a5f6c7 100644 --- a/server/utils/collectionIndexing.ts +++ b/server/utils/collectionIndexing.ts @@ -11,15 +11,19 @@ export default async function collectionIndexing(teamId: string) { //no point in maintaining index of deleted collections. attributes: ["id", "index", "name"], }); - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'collection' implicitly has an 'any' typ... Remove this comment to see the full error message - let sortableCollections = collections.map((collection) => { - return [collection, collection.index]; - }); + + let sortableCollections: [Collection, string | null][] = collections.map( + (collection) => { + return [collection, collection.index]; + } + ); + sortableCollections = naturalSort( sortableCollections, // @ts-expect-error ts-migrate(2345) FIXME: Argument of type '(collection: any) => any' is not... Remove this comment to see the full error message (collection) => collection[0].name ); + //for each collection with null index, use previous collection index to create new index let previousCollectionIndex = null; @@ -34,7 +38,6 @@ export default async function collectionIndexing(teamId: string) { } const indexedCollections = {}; - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'collection' implicitly has an 'any' typ... Remove this comment to see the full error message sortableCollections.forEach((collection) => { indexedCollections[collection[0].id] = collection[0].index; }); diff --git a/server/utils/fs.test.ts b/server/utils/fs.test.ts index 612c1e90f..c36cd1e84 100644 --- a/server/utils/fs.test.ts +++ b/server/utils/fs.test.ts @@ -15,6 +15,7 @@ describe("serializeFilename", () => { ); }); }); + describe("deserializeFilename", () => { it("should deserialize forward slashes", () => { expect(deserializeFilename("%2F")).toBe("/"); diff --git a/server/utils/jwt.ts b/server/utils/jwt.ts index 919146994..497ceab3d 100644 --- a/server/utils/jwt.ts +++ b/server/utils/jwt.ts @@ -1,44 +1,39 @@ import { subMinutes } from "date-fns"; +import invariant from "invariant"; import JWT from "jsonwebtoken"; import { Team, User } from "@server/models"; import { AuthenticationError } from "../errors"; -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'token' implicitly has an 'any' type. -function getJWTPayload(token) { +function getJWTPayload(token: string) { let payload; try { payload = JWT.decode(token); + + if (!payload) { + throw AuthenticationError("Invalid token"); + } + + return payload as JWT.JwtPayload; } catch (err) { throw AuthenticationError("Unable to decode JWT token"); } - - if (!payload) { - throw AuthenticationError("Invalid token"); - } - - return payload; } -// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message export async function getUserForJWT(token: string): Promise { const payload = getJWTPayload(token); - // @ts-expect-error ts-migrate(2339) FIXME: Property 'type' does not exist on type 'string | J... Remove this comment to see the full error message if (payload.type === "email-signin") { throw AuthenticationError("Invalid token"); } // check the token is within it's expiration time - // @ts-expect-error ts-migrate(2339) FIXME: Property 'expiresAt' does not exist on type 'strin... Remove this comment to see the full error message if (payload.expiresAt) { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'expiresAt' does not exist on type 'strin... Remove this comment to see the full error message if (new Date(payload.expiresAt) < new Date()) { throw AuthenticationError("Expired token"); } } - // @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'string | Jwt... Remove this comment to see the full error message const user = await User.findByPk(payload.id, { include: [ { @@ -48,13 +43,18 @@ export async function getUserForJWT(token: string): Promise { }, ], }); + if (!user) { + throw AuthenticationError("Invalid token"); + } - // @ts-expect-error ts-migrate(2339) FIXME: Property 'type' does not exist on type 'string | J... Remove this comment to see the full error message if (payload.type === "transfer") { // If the user has made a single API request since the transfer token was // created then it's no longer valid, they'll need to sign in again. - // @ts-expect-error ts-migrate(2339) FIXME: Property 'createdAt' does not exist on type 'strin... Remove this comment to see the full error message - if (user.lastActiveAt > new Date(payload.createdAt)) { + if ( + user.lastActiveAt && + payload.createdAt && + user.lastActiveAt > new Date(payload.createdAt) + ) { throw AuthenticationError("Token has already been used"); } } @@ -68,39 +68,37 @@ export async function getUserForJWT(token: string): Promise { return user; } -// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message export async function getUserForEmailSigninToken(token: string): Promise { const payload = getJWTPayload(token); - // @ts-expect-error ts-migrate(2339) FIXME: Property 'type' does not exist on type 'string | J... Remove this comment to see the full error message if (payload.type !== "email-signin") { throw AuthenticationError("Invalid token"); } // check the token is within it's expiration time - // @ts-expect-error ts-migrate(2339) FIXME: Property 'createdAt' does not exist on type 'strin... Remove this comment to see the full error message if (payload.createdAt) { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'createdAt' does not exist on type 'strin... Remove this comment to see the full error message if (new Date(payload.createdAt) < subMinutes(new Date(), 10)) { throw AuthenticationError("Expired token"); } } - // @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'string | Jwt... Remove this comment to see the full error message const user = await User.findByPk(payload.id, { include: [ { model: Team, - as: "team", required: true, }, ], }); + invariant(user, "User not found"); // if user has signed in at all since the token was created then // it's no longer valid, they'll need a new one. - // @ts-expect-error ts-migrate(2339) FIXME: Property 'createdAt' does not exist on type 'strin... Remove this comment to see the full error message - if (user.lastSignedInAt > payload.createdAt) { + if ( + user.lastSignedInAt && + payload.createdAt && + user.lastSignedInAt > new Date(payload.createdAt) + ) { throw AuthenticationError("Token has already been used"); } diff --git a/server/utils/removeIndexCollision.ts b/server/utils/removeIndexCollision.ts index a0025ac5f..54ed1e4b3 100644 --- a/server/utils/removeIndexCollision.ts +++ b/server/utils/removeIndexCollision.ts @@ -1,6 +1,6 @@ import fractionalIndex from "fractional-index"; -import { Collection } from "@server/models"; -import { sequelize, Op } from "../sequelize"; +import { Op, Sequelize } from "sequelize"; +import Collection from "@server/models/Collection"; /** * @@ -35,7 +35,7 @@ export default async function removeIndexCollision( attributes: ["id", "index"], limit: 1, order: [ - sequelize.literal('"collection"."index" collate "C"'), + Sequelize.literal('"collection"."index" collate "C"'), ["updatedAt", "DESC"], ], }); diff --git a/server/utils/s3.ts b/server/utils/s3.ts index 93d1c5031..193f111a5 100644 --- a/server/utils/s3.ts +++ b/server/utils/s3.ts @@ -7,6 +7,7 @@ import { v4 as uuidv4 } from "uuid"; import Logger from "@server/logging/logger"; const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY; +const AWS_S3_UPLOAD_BUCKET_URL = process.env.AWS_S3_UPLOAD_BUCKET_URL || ""; const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID; const AWS_REGION = process.env.AWS_REGION || ""; const AWS_S3_UPLOAD_BUCKET_NAME = process.env.AWS_S3_UPLOAD_BUCKET_NAME || ""; @@ -16,13 +17,9 @@ const s3 = new AWS.S3({ accessKeyId: AWS_ACCESS_KEY_ID, secretAccessKey: AWS_SECRET_ACCESS_KEY, region: AWS_REGION, - // @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'. - endpoint: process.env.AWS_S3_UPLOAD_BUCKET_URL.includes( - AWS_S3_UPLOAD_BUCKET_NAME - ) + endpoint: AWS_S3_UPLOAD_BUCKET_URL.includes(AWS_S3_UPLOAD_BUCKET_NAME) ? undefined - : // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string | undefined' is not assig... Remove this comment to see the full error message - new AWS.Endpoint(process.env.AWS_S3_UPLOAD_BUCKET_URL), + : new AWS.Endpoint(AWS_S3_UPLOAD_BUCKET_URL), signatureVersion: "v4", }); const createPresignedPost = util.promisify(s3.createPresignedPost).bind(s3); @@ -121,14 +118,12 @@ export const getPresignedPost = ( export const publicS3Endpoint = (isServerUpload?: boolean) => { // lose trailing slash if there is one and convert fake-s3 url to localhost // for access outside of docker containers in local development - // @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'. - const isDocker = process.env.AWS_S3_UPLOAD_BUCKET_URL.match(/http:\/\/s3:/); + const isDocker = AWS_S3_UPLOAD_BUCKET_URL.match(/http:\/\/s3:/); - // @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'. - const host = process.env.AWS_S3_UPLOAD_BUCKET_URL.replace( - "s3:", - "localhost:" - ).replace(/\/$/, ""); + const host = AWS_S3_UPLOAD_BUCKET_URL.replace("s3:", "localhost:").replace( + /\/$/, + "" + ); // support old path-style S3 uploads and new virtual host uploads by checking // for the bucket name in the endpoint url before appending. @@ -204,8 +199,7 @@ export const deleteFromS3 = (key: string) => { }; export const getSignedUrl = async (key: string) => { - // @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'. - const isDocker = process.env.AWS_S3_UPLOAD_BUCKET_URL.match(/http:\/\/s3:/); + const isDocker = AWS_S3_UPLOAD_BUCKET_URL.match(/http:\/\/s3:/); const params = { Bucket: AWS_S3_UPLOAD_BUCKET_NAME, Key: key, diff --git a/server/utils/startup.ts b/server/utils/startup.ts index 377a79664..9166d969d 100644 --- a/server/utils/startup.ts +++ b/server/utils/startup.ts @@ -1,6 +1,7 @@ import chalk from "chalk"; import Logger from "@server/logging/logger"; -import { Team, AuthenticationProvider } from "@server/models"; +import AuthenticationProvider from "@server/models/AuthenticationProvider"; +import Team from "@server/models/Team"; export async function checkMigrations() { if (process.env.DEPLOYMENT === "hosted") { diff --git a/server/utils/updates.ts b/server/utils/updates.ts index 71f311f58..837a8a605 100644 --- a/server/utils/updates.ts +++ b/server/utils/updates.ts @@ -1,7 +1,10 @@ import crypto from "crypto"; import fetch from "fetch-with-proxy"; import invariant from "invariant"; -import { User, Team, Collection, Document } from "@server/models"; +import Collection from "@server/models/Collection"; +import Document from "@server/models/Document"; +import Team from "@server/models/Team"; +import User from "@server/models/User"; import packageInfo from "../../package.json"; import { client } from "../redis"; diff --git a/server/utils/zip.ts b/server/utils/zip.ts index ad93cd029..0250d4a68 100644 --- a/server/utils/zip.ts +++ b/server/utils/zip.ts @@ -2,7 +2,9 @@ import fs from "fs"; import JSZip from "jszip"; import tmp from "tmp"; import Logger from "@server/logging/logger"; -import { Attachment, Collection, Document } from "@server/models"; +import Attachment from "@server/models/Attachment"; +import Collection from "@server/models/Collection"; +import Document from "@server/models/Document"; import { NavigationNode } from "~/types"; import { serializeFilename } from "./fs"; import { getFileByKey } from "./s3"; @@ -31,7 +33,6 @@ async function addToArchive(zip: JSZip, documents: NavigationNode[]) { zip.file(`${title}.md`, text, { date: document.updatedAt, comment: JSON.stringify({ - pinned: document.pinned, createdAt: document.createdAt, updatedAt: document.updatedAt, }), @@ -84,7 +85,6 @@ async function archiveToPath(zip: JSZip) { }); } -// @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message export async function archiveCollections(collections: Collection[]) { const zip = new JSZip(); diff --git a/shared/theme.ts b/shared/theme.ts index 9054423a8..b80ef5df6 100644 --- a/shared/theme.ts +++ b/shared/theme.ts @@ -1,5 +1,4 @@ import { darken, lighten } from "polished"; -import { DefaultTheme } from "styled-components"; const colors = { transparent: "transparent", @@ -123,7 +122,7 @@ export const base = { }, }; -export const light: DefaultTheme = { +export const light = { ...base, background: colors.white, secondaryBackground: colors.warmGrey, @@ -172,7 +171,7 @@ export const light: DefaultTheme = { scrollbarThumb: darken(0.15, colors.smokeDark), }; -export const dark: DefaultTheme = { +export const dark = { ...base, background: colors.almostBlack, secondaryBackground: colors.black50, diff --git a/shared/typings/index.d.ts b/shared/typings/index.d.ts new file mode 100644 index 000000000..80af8845f --- /dev/null +++ b/shared/typings/index.d.ts @@ -0,0 +1,4 @@ +declare module "emoji-regex" { + const RegExpFactory: () => RegExp; + export = RegExpFactory; +} diff --git a/yarn.lock b/yarn.lock index 2a2f32fba..9f6c86f53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -33,7 +33,7 @@ "@nicolo-ribaudo/chokidar-2" "^2.1.8" chokidar "^3.4.0" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.16.0": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.0.tgz#0dfc80309beec8411e65e706461c408b0bb9b431" integrity sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA== @@ -45,19 +45,19 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.16.0.tgz#ea269d7f78deb3a7826c39a4048eecda541ebdaa" integrity sha512-DGjt2QZse5SGd9nfOSqO4WLJ8NN/oHkijbXbPrxuoJO3oIPJL3TciZs9FX+cOHNiY9E9l0opL8g7BmLe3T+9ew== -"@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.16.0", "@babel/core@^7.7.5": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.16.0.tgz#c4ff44046f5fe310525cc9eb4ef5147f0c5374d4" - integrity sha512-mYZEvshBRHGsIAiyH5PzCFTCfbWfoYbO/jcSdXQSUQu1/pW0xDZAUP7KEc32heqWTAfAHhV9j1vH8Sav7l+JNQ== +"@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.16.0", "@babel/core@^7.7.2", "@babel/core@^7.7.5": + version "7.16.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.16.5.tgz#924aa9e1ae56e1e55f7184c8bf073a50d8677f5c" + integrity sha512-wUcenlLzuWMZ9Zt8S0KmFwGlH6QKRh3vsm/dhDA3CHkiTA45YuG1XkHRcNRl73EFPXDp/d5kVOU0/y7x2w6OaQ== dependencies: "@babel/code-frame" "^7.16.0" - "@babel/generator" "^7.16.0" - "@babel/helper-compilation-targets" "^7.16.0" - "@babel/helper-module-transforms" "^7.16.0" - "@babel/helpers" "^7.16.0" - "@babel/parser" "^7.16.0" + "@babel/generator" "^7.16.5" + "@babel/helper-compilation-targets" "^7.16.3" + "@babel/helper-module-transforms" "^7.16.5" + "@babel/helpers" "^7.16.5" + "@babel/parser" "^7.16.5" "@babel/template" "^7.16.0" - "@babel/traverse" "^7.16.0" + "@babel/traverse" "^7.16.5" "@babel/types" "^7.16.0" convert-source-map "^1.7.0" debug "^4.1.0" @@ -66,10 +66,10 @@ semver "^6.3.0" source-map "^0.5.0" -"@babel/generator@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.0.tgz#d40f3d1d5075e62d3500bccb67f3daa8a95265b2" - integrity sha512-RR8hUCfRQn9j9RPKEVXo9LiwoxLPYn6hNZlvUOR8tSnaxlD0p0+la00ZP9/SnRt6HchKr+X0fO2r8vrETiJGew== +"@babel/generator@^7.16.5", "@babel/generator@^7.7.2": + version "7.16.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.5.tgz#26e1192eb8f78e0a3acaf3eede3c6fc96d22bedf" + integrity sha512-kIvCdjZqcdKqoDbVVdt5R99icaRtrtYhYK/xux5qiWCBmfdvEYMFZ68QCrpE5cbFM1JsuArUNs1ZkuKtTtUcZA== dependencies: "@babel/types" "^7.16.0" jsesc "^2.5.1" @@ -90,7 +90,7 @@ "@babel/helper-explode-assignable-expression" "^7.16.0" "@babel/types" "^7.16.0" -"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.0": +"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.0", "@babel/helper-compilation-targets@^7.16.3": version "7.16.3" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.3.tgz#5b480cd13f68363df6ec4dc8ac8e2da11363cbf0" integrity sha512-vKsoSQAyBmxS35JUOOt+07cLc6Nk/2ljLIHwmq2/NM6hdioUaqEXq/S+nXvbvXbZkNDlWOymPanJGOc4CBjSJA== @@ -134,6 +134,13 @@ resolve "^1.14.2" semver "^6.1.2" +"@babel/helper-environment-visitor@^7.16.5": + version "7.16.5" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.5.tgz#f6a7f38b3c6d8b07c88faea083c46c09ef5451b8" + integrity sha512-ODQyc5AnxmZWm/R2W7fzhamOk1ey8gSguo5SGvF0zcB3uUzRpTRmM/jmLSm9bDMyPlvbyJ+PwPEK0BWIoZ9wjg== + dependencies: + "@babel/types" "^7.16.0" + "@babel/helper-explode-assignable-expression@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.0.tgz#753017337a15f46f9c09f674cff10cee9b9d7778" @@ -178,18 +185,18 @@ dependencies: "@babel/types" "^7.16.0" -"@babel/helper-module-transforms@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.0.tgz#1c82a8dd4cb34577502ebd2909699b194c3e9bb5" - integrity sha512-My4cr9ATcaBbmaEa8M0dZNA74cfI6gitvUAskgDtAFmAqyFKDSHQo5YstxPbN+lzHl2D9l/YOEFqb2mtUh4gfA== +"@babel/helper-module-transforms@^7.16.0", "@babel/helper-module-transforms@^7.16.5": + version "7.16.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.5.tgz#530ebf6ea87b500f60840578515adda2af470a29" + integrity sha512-CkvMxgV4ZyyioElFwcuWnDCcNIeyqTkCm9BxXZi73RR1ozqlpboqsbGUNvRTflgZtFbbJ1v5Emvm+lkjMYY/LQ== dependencies: + "@babel/helper-environment-visitor" "^7.16.5" "@babel/helper-module-imports" "^7.16.0" - "@babel/helper-replace-supers" "^7.16.0" "@babel/helper-simple-access" "^7.16.0" "@babel/helper-split-export-declaration" "^7.16.0" "@babel/helper-validator-identifier" "^7.15.7" "@babel/template" "^7.16.0" - "@babel/traverse" "^7.16.0" + "@babel/traverse" "^7.16.5" "@babel/types" "^7.16.0" "@babel/helper-optimise-call-expression@^7.16.0": @@ -199,10 +206,10 @@ dependencies: "@babel/types" "^7.16.0" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9" - integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ== +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.16.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.5.tgz#afe37a45f39fce44a3d50a7958129ea5b1a5c074" + integrity sha512-59KHWHXxVA9K4HNF4sbHCf+eJeFe0Te/ZFGqBT4OjXhrwvA04sGfaEGsVTdsjoszq0YTP49RC9UKe5g8uN2RwQ== "@babel/helper-remap-async-to-generator@^7.16.0": version "7.16.0" @@ -264,13 +271,13 @@ "@babel/traverse" "^7.16.0" "@babel/types" "^7.16.0" -"@babel/helpers@^7.16.0": - version "7.16.3" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.16.3.tgz#27fc64f40b996e7074dc73128c3e5c3e7f55c43c" - integrity sha512-Xn8IhDlBPhvYTvgewPKawhADichOsbkZuzN7qz2BusOM0brChsyXMDJvldWaYMMUNiCQdQzNEioXTp3sC8Nt8w== +"@babel/helpers@^7.16.5": + version "7.16.5" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.16.5.tgz#29a052d4b827846dd76ece16f565b9634c554ebd" + integrity sha512-TLgi6Lh71vvMZGEkFuIxzaPsyeYCHQ5jJOOX1f0xXn0uciFuE8cEk0wyBquMcCxBXZ5BJhE2aUB7pnWTD150Tw== dependencies: "@babel/template" "^7.16.0" - "@babel/traverse" "^7.16.3" + "@babel/traverse" "^7.16.5" "@babel/types" "^7.16.0" "@babel/highlight@^7.16.0": @@ -282,10 +289,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.16.0", "@babel/parser@^7.16.3", "@babel/parser@^7.7.0": - version "7.16.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.3.tgz#271bafcb811080905a119222edbc17909c82261d" - integrity sha512-dcNwU1O4sx57ClvLBVFbEgx0UZWfd0JQX5X6fxFRCLHelFBGXFfSz6Y0FAq2PEwUqlqLkdVjVr4VASEOuUnLJw== +"@babel/parser@^7.1.0", "@babel/parser@^7.16.0", "@babel/parser@^7.16.5", "@babel/parser@^7.7.0", "@babel/parser@^7.7.2": + version "7.16.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.6.tgz#8f194828193e8fa79166f34a4b4e52f3e769a314" + integrity sha512-Gr86ujcNuPDnNOY8mi383Hvi8IYrJVJYuf3XcuBM/Dgd+bINn/7tHqsj+tKkoreMbmGsFLsltI/JJd8fOFWGDQ== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.0": version "7.16.2" @@ -566,12 +573,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-typescript@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.0.tgz#2feeb13d9334cc582ea9111d3506f773174179bb" - integrity sha512-Xv6mEXqVdaqCBfJFyeab0fH2DnUoMsDmhamxsSi4j8nLd4Vtw213WMJr55xxqipC/YVWyPY3K0blJncPYji+dQ== +"@babel/plugin-syntax-typescript@^7.16.0", "@babel/plugin-syntax-typescript@^7.7.2": + version "7.16.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.5.tgz#f47a33e4eee38554f00fb6b2f894fa1f5649b0b3" + integrity sha512-/d4//lZ1Vqb4mZ5xTep3dDK888j7BGM/iKqBmndBaoYAFPlPKrGU608VVBz5JeyAb6YQDjRu1UKqj86UhwWVgw== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.16.5" "@babel/plugin-transform-arrow-functions@^7.16.0": version "7.16.0" @@ -1011,17 +1018,18 @@ "@babel/parser" "^7.16.0" "@babel/types" "^7.16.0" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.16.0", "@babel/traverse@^7.16.3", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.0": - version "7.16.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.3.tgz#f63e8a938cc1b780f66d9ed3c54f532ca2d14787" - integrity sha512-eolumr1vVMjqevCpwVO99yN/LoGL0EyHiLO5I043aYQvwOJ9eR5UsZSClHVCzfhBduMAsSzgA/6AyqPjNayJag== +"@babel/traverse@^7.1.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.16.0", "@babel/traverse@^7.16.5", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.0", "@babel/traverse@^7.7.2": + version "7.16.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.5.tgz#d7d400a8229c714a59b87624fc67b0f1fbd4b2b3" + integrity sha512-FOCODAzqUMROikDYLYxl4nmwiLlu85rNqBML/A5hKRVXG2LV8d0iMqgPzdYTcIpjZEBB7D6UDU9vxRZiriASdQ== dependencies: "@babel/code-frame" "^7.16.0" - "@babel/generator" "^7.16.0" + "@babel/generator" "^7.16.5" + "@babel/helper-environment-visitor" "^7.16.5" "@babel/helper-function-name" "^7.16.0" "@babel/helper-hoist-variables" "^7.16.0" "@babel/helper-split-export-declaration" "^7.16.0" - "@babel/parser" "^7.16.3" + "@babel/parser" "^7.16.5" "@babel/types" "^7.16.0" debug "^4.1.0" globals "^11.1.0" @@ -1116,14 +1124,6 @@ framesync "5.3.0" lodash.mergewith "4.6.2" -"@cnakazawa/watch@^1.0.3": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" - integrity sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ== - dependencies: - exec-sh "^0.3.2" - minimist "^1.2.0" - "@dabh/diagnostics@^2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.2.tgz#290d08f7b381b8f94607dc8f471a12c675f9db31" @@ -1309,93 +1309,94 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== -"@jest/console@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.6.2.tgz#4e04bc464014358b03ab4937805ee36a0aeb98f2" - integrity sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g== +"@jest/console@^27.4.2": + version "27.4.2" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.4.2.tgz#7a95612d38c007ddb528ee446fe5e5e785e685ce" + integrity sha512-xknHThRsPB/To1FUbi6pCe43y58qFC03zfb6R7fDb/FfC7k2R3i1l+izRBJf8DI46KhYGRaF14Eo9A3qbBoixg== dependencies: - "@jest/types" "^26.6.2" + "@jest/types" "^27.4.2" "@types/node" "*" chalk "^4.0.0" - jest-message-util "^26.6.2" - jest-util "^26.6.2" + jest-message-util "^27.4.2" + jest-util "^27.4.2" slash "^3.0.0" -"@jest/core@^26.6.3": - version "26.6.3" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-26.6.3.tgz#7639fcb3833d748a4656ada54bde193051e45fad" - integrity sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw== +"@jest/core@^27.4.5": + version "27.4.5" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.4.5.tgz#cae2dc34259782f4866c6606c3b480cce920ed4c" + integrity sha512-3tm/Pevmi8bDsgvo73nX8p/WPng6KWlCyScW10FPEoN1HU4pwI83tJ3TsFvi1FfzsjwUlMNEPowgb/rPau/LTQ== dependencies: - "@jest/console" "^26.6.2" - "@jest/reporters" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/transform" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/console" "^27.4.2" + "@jest/reporters" "^27.4.5" + "@jest/test-result" "^27.4.2" + "@jest/transform" "^27.4.5" + "@jest/types" "^27.4.2" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" + emittery "^0.8.1" exit "^0.1.2" graceful-fs "^4.2.4" - jest-changed-files "^26.6.2" - jest-config "^26.6.3" - jest-haste-map "^26.6.2" - jest-message-util "^26.6.2" - jest-regex-util "^26.0.0" - jest-resolve "^26.6.2" - jest-resolve-dependencies "^26.6.3" - jest-runner "^26.6.3" - jest-runtime "^26.6.3" - jest-snapshot "^26.6.2" - jest-util "^26.6.2" - jest-validate "^26.6.2" - jest-watcher "^26.6.2" - micromatch "^4.0.2" - p-each-series "^2.1.0" + jest-changed-files "^27.4.2" + jest-config "^27.4.5" + jest-haste-map "^27.4.5" + jest-message-util "^27.4.2" + jest-regex-util "^27.4.0" + jest-resolve "^27.4.5" + jest-resolve-dependencies "^27.4.5" + jest-runner "^27.4.5" + jest-runtime "^27.4.5" + jest-snapshot "^27.4.5" + jest-util "^27.4.2" + jest-validate "^27.4.2" + jest-watcher "^27.4.2" + micromatch "^4.0.4" rimraf "^3.0.0" slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-26.6.2.tgz#ba364cc72e221e79cc8f0a99555bf5d7577cf92c" - integrity sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA== +"@jest/environment@^27.4.4": + version "27.4.4" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.4.4.tgz#66ebebc79673d84aad29d2bb70a8c51e6c29bb4d" + integrity sha512-q+niMx7cJgt/t/b6dzLOh4W8Ef/8VyKG7hxASK39jakijJzbFBGpptx3RXz13FFV7OishQ9lTbv+dQ5K3EhfDQ== dependencies: - "@jest/fake-timers" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/fake-timers" "^27.4.2" + "@jest/types" "^27.4.2" "@types/node" "*" - jest-mock "^26.6.2" + jest-mock "^27.4.2" -"@jest/fake-timers@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.6.2.tgz#459c329bcf70cee4af4d7e3f3e67848123535aad" - integrity sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA== +"@jest/fake-timers@^27.4.2": + version "27.4.2" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.4.2.tgz#d217f86c3ba2027bf29e0b731fd0cb761a72d093" + integrity sha512-f/Xpzn5YQk5adtqBgvw1V6bF8Nx3hY0OIRRpCvWcfPl0EAjdqWPdhH3t/3XpiWZqtjIEHDyMKP9ajpva1l4Zmg== dependencies: - "@jest/types" "^26.6.2" - "@sinonjs/fake-timers" "^6.0.1" + "@jest/types" "^27.4.2" + "@sinonjs/fake-timers" "^8.0.1" "@types/node" "*" - jest-message-util "^26.6.2" - jest-mock "^26.6.2" - jest-util "^26.6.2" + jest-message-util "^27.4.2" + jest-mock "^27.4.2" + jest-util "^27.4.2" -"@jest/globals@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-26.6.2.tgz#5b613b78a1aa2655ae908eba638cc96a20df720a" - integrity sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA== +"@jest/globals@^27.4.4": + version "27.4.4" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.4.4.tgz#fe501a80c23ea2dab585c42be2a519bb5e38530d" + integrity sha512-bqpqQhW30BOreXM8bA8t8JbOQzsq/WnPTnBl+It3UxAD9J8yxEAaBEylHx1dtBapAr/UBk8GidXbzmqnee8tYQ== dependencies: - "@jest/environment" "^26.6.2" - "@jest/types" "^26.6.2" - expect "^26.6.2" + "@jest/environment" "^27.4.4" + "@jest/types" "^27.4.2" + expect "^27.4.2" -"@jest/reporters@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.6.2.tgz#1f518b99637a5f18307bd3ecf9275f6882a667f6" - integrity sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw== +"@jest/reporters@^27.4.5": + version "27.4.5" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.4.5.tgz#e229acca48d18ea39e805540c1c322b075ae63ad" + integrity sha512-3orsG4vi8zXuBqEoy2LbnC1kuvkg1KQUgqNxmxpQgIOQEPeV0onvZu+qDQnEoX8qTQErtqn/xzcnbpeTuOLSiA== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/transform" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/console" "^27.4.2" + "@jest/test-result" "^27.4.2" + "@jest/transform" "^27.4.5" + "@jest/types" "^27.4.2" + "@types/node" "*" chalk "^4.0.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" @@ -1406,84 +1407,70 @@ istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" istanbul-reports "^3.0.2" - jest-haste-map "^26.6.2" - jest-resolve "^26.6.2" - jest-util "^26.6.2" - jest-worker "^26.6.2" + jest-haste-map "^27.4.5" + jest-resolve "^27.4.5" + jest-util "^27.4.2" + jest-worker "^27.4.5" slash "^3.0.0" source-map "^0.6.0" string-length "^4.0.1" terminal-link "^2.0.0" - v8-to-istanbul "^7.0.0" - optionalDependencies: - node-notifier "^8.0.0" + v8-to-istanbul "^8.1.0" -"@jest/source-map@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-26.6.2.tgz#29af5e1e2e324cafccc936f218309f54ab69d535" - integrity sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA== +"@jest/source-map@^27.4.0": + version "27.4.0" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.4.0.tgz#2f0385d0d884fb3e2554e8f71f8fa957af9a74b6" + integrity sha512-Ntjx9jzP26Bvhbm93z/AKcPRj/9wrkI88/gK60glXDx1q+IeI0rf7Lw2c89Ch6ofonB0On/iRDreQuQ6te9pgQ== dependencies: callsites "^3.0.0" graceful-fs "^4.2.4" source-map "^0.6.0" -"@jest/test-result@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-26.6.2.tgz#55da58b62df134576cc95476efa5f7949e3f5f18" - integrity sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ== +"@jest/test-result@^27.4.2": + version "27.4.2" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.4.2.tgz#05fd4a5466ec502f3eae0b39dff2b93ea4d5d9ec" + integrity sha512-kr+bCrra9jfTgxHXHa2UwoQjxvQk3Am6QbpAiJ5x/50LW8llOYrxILkqY0lZRW/hu8FXesnudbql263+EW9iNA== dependencies: - "@jest/console" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/console" "^27.4.2" + "@jest/types" "^27.4.2" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^26.6.3": - version "26.6.3" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz#98e8a45100863886d074205e8ffdc5a7eb582b17" - integrity sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw== +"@jest/test-sequencer@^27.4.5": + version "27.4.5" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.4.5.tgz#1d7e026844d343b60d2ca7fd82c579a17b445d7d" + integrity sha512-n5woIn/1v+FT+9hniymHPARA9upYUmfi5Pw9ewVwXCDlK4F5/Gkees9v8vdjGdAIJ2MPHLHodiajLpZZanWzEQ== dependencies: - "@jest/test-result" "^26.6.2" + "@jest/test-result" "^27.4.2" graceful-fs "^4.2.4" - jest-haste-map "^26.6.2" - jest-runner "^26.6.3" - jest-runtime "^26.6.3" + jest-haste-map "^27.4.5" + jest-runtime "^27.4.5" -"@jest/transform@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.6.2.tgz#5ac57c5fa1ad17b2aae83e73e45813894dcf2e4b" - integrity sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA== +"@jest/transform@^27.4.5": + version "27.4.5" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.4.5.tgz#3dfe2e3680cd4aa27356172bf25617ab5b94f195" + integrity sha512-PuMet2UlZtlGzwc6L+aZmR3I7CEBpqadO03pU40l2RNY2fFJ191b9/ITB44LNOhVtsyykx0OZvj0PCyuLm7Eew== dependencies: "@babel/core" "^7.1.0" - "@jest/types" "^26.6.2" + "@jest/types" "^27.4.2" babel-plugin-istanbul "^6.0.0" chalk "^4.0.0" convert-source-map "^1.4.0" fast-json-stable-stringify "^2.0.0" graceful-fs "^4.2.4" - jest-haste-map "^26.6.2" - jest-regex-util "^26.0.0" - jest-util "^26.6.2" - micromatch "^4.0.2" + jest-haste-map "^27.4.5" + jest-regex-util "^27.4.0" + jest-util "^27.4.2" + micromatch "^4.0.4" pirates "^4.0.1" slash "^3.0.0" source-map "^0.6.1" write-file-atomic "^3.0.0" -"@jest/types@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" - integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== - dependencies: - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^15.0.0" - chalk "^4.0.0" - -"@jest/types@^27.2.5": - version "27.2.5" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.2.5.tgz#420765c052605e75686982d24b061b4cbba22132" - integrity sha512-nmuM4VuDtCZcY+eTpw+0nvstwReMsjPoj7ZR80/BbixulhLaiX+fbv8oeLW8WZlJMcsGQsTmMKT/iTZu1Uy/lQ== +"@jest/types@^27.4.2": + version "27.4.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.4.2.tgz#96536ebd34da6392c2b7c7737d693885b5dd44a5" + integrity sha512-j35yw0PMTPpZsUoOBiuHzr1zTYoad1cVIE0ajEjcrJONxxrko/IRGKkXx3os0Nsi4Hu3+5VmDbVfq5WhG/pWAg== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^3.0.0" @@ -2499,10 +2486,10 @@ dependencies: type-detect "4.0.8" -"@sinonjs/fake-timers@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" - integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== +"@sinonjs/fake-timers@^8.0.1": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz#3fdc2b6cb58935b21bfb8d1625eb1300484316e7" + integrity sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg== dependencies: "@sinonjs/commons" "^1.7.0" @@ -2527,6 +2514,11 @@ resolved "https://registry.yarnpkg.com/@tommoor/remove-markdown/-/remove-markdown-0.3.2.tgz#5288ddd0e26b6b173e76ebb31c94653b0dcff45d" integrity sha512-awcc9hfLZqyyZHOGzAHbnjgZJpQGS1W1oZZ5GXOTTnbKVdKQ4OWYbrRWPUvXI2YAKJazrcS8rxPh67PX3rpGkQ== +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + "@types/accepts@*": version "1.3.5" resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575" @@ -2539,10 +2531,10 @@ resolved "https://registry.yarnpkg.com/@types/async-lock/-/async-lock-1.1.3.tgz#0d86017cf87abbcb941c55360e533d37a3f23b3d" integrity sha512-UpeDcjGKsYEQMeqEbfESm8OWJI305I7b9KE4ji3aBjoKWyN5CTdn8izcA1FM1DVDne30R5fNEnIy89vZw5LXJQ== -"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": - version "7.1.12" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.12.tgz#4d8e9e51eb265552a7e4f1ff2219ab6133bdfb2d" - integrity sha512-wMTHiiTiBAAPebqaPiPDLFA4LYPKr6Ph0Xq/6rq1Ur3v66HXyG+clfR9CNETkD7MQS8ZHvpQOtA53DLws5WAEQ== +"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": + version "7.1.17" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.17.tgz#f50ac9d20d64153b510578d84f9643f9a3afbe64" + integrity sha512-6zzkezS9QEIL8yCBvXWxPTJPNuMeECJVxSOhxNY/jfq9LxOTHivaYTqr37n9LknWWRTIkzqH2UilS5QFvfa90A== dependencies: "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" @@ -2957,9 +2949,14 @@ integrity sha512-WYMWhAQLuBym+6qQ2Ojptm6qIACnkkYYs08sj+PVgRCrB6b7k1QpTRk0yMmxhlpPn5MbXcSfd6sHOYlzaokU3w== "@types/node@*": - version "16.11.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.9.tgz#879be3ad7af29f4c1a5c433421bf99fab7047185" - integrity sha512-MKmdASMf3LtPzwLyRrFjtFFZ48cMf8jmX5VRYrDQiJa8Ybu5VAmkqBWqKU8fdCwD8ysw4mQ9nrEHvzg6gunR7A== + version "17.0.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.5.tgz#57ca67ec4e57ad9e4ef5a6bab48a15387a1c83e0" + integrity sha512-w3mrvNXLeDYV1GKTZorGJQivK6XLCoGwpnyJFbJVK/aTBQUxOCaa/GlFAAN3OTDFcb7h5tiFG+YXCO2By+riZw== + +"@types/node@15.12.2": + version "15.12.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.2.tgz#1f2b42c4be7156ff4a6f914b2fb03d05fa84e38d" + integrity sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww== "@types/node@^10.12.18": version "10.17.51" @@ -2971,11 +2968,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.41.tgz#045a4981318d31a581650ce70f340a32c3461198" integrity sha512-qLT9IvHiXJfdrje9VmsLzun7cQ65obsBTmtU3EOnCSLFOoSHx1hpiRHoBnpdbyFqnzqdUUIv81JcEJQCB8un9g== -"@types/node@^15.12.0": - version "15.14.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-15.14.9.tgz#bc43c990c3c9be7281868bbc7b8fdd6e2b57adfa" - integrity sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A== - "@types/nodemailer@^6.4.4": version "6.4.4" resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.4.tgz#c265f7e7a51df587597b3a49a023acaf0c741f4b" @@ -3021,10 +3013,10 @@ dependencies: "@types/express" "*" -"@types/prettier@^2.0.0": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.5.tgz#b6ab3bba29e16b821d84e09ecfaded462b816b00" - integrity sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ== +"@types/prettier@^2.1.5": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.2.tgz#4c62fae93eb479660c3bd93f9d24d561597a8281" + integrity sha512-ekoj4qOQYp7CvjX8ZDBgN86w3MqQhLE1hczEJbEIjgFEumDy+na/4AJAbLXfgEWFNB2pKadM5rPFtuSGMWK7xA== "@types/prop-types@*": version "15.7.4" @@ -3356,10 +3348,10 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f" integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg== -"@types/validator@*": - version "13.6.6" - resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.6.6.tgz#6e6e2d086148db5ae14851614971b715670cbd52" - integrity sha512-+qogUELb4gMhrMjSh/seKmGVvN+uQLfyqJAqYRWqVHsvBsUO2xDBCL8CJ/ZSukbd8vXaoYbpIssAmfLEzzBHEw== +"@types/validator@*", "@types/validator@^13.7.1": + version "13.7.1" + resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.7.1.tgz#cdab1b4779f6b1718a08de89d92d2603b71950cb" + integrity sha512-I6OUIZ5cYRk5lp14xSOAiXjWrfVoMZVjDuevBYgQDYzZIjsf2CAISpEcXOkFAtpAHbmWIDLcZObejqny/9xq5Q== "@types/webpack-sources@*": version "3.2.0" @@ -3394,13 +3386,6 @@ resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw== -"@types/yargs@^15.0.0": - version "15.0.9" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.9.tgz#524cd7998fe810cdb02f26101b699cccd156ff19" - integrity sha512-HmU8SeIRhZCWcnRskCs36Q1Q00KBV6Cqh/ora8WN1+22dY07AZdn6Gel8QZ3t26XYPImtcL8WV/eqjhVmMEw4g== - dependencies: - "@types/yargs-parser" "*" - "@types/yargs@^16.0.0": version "16.0.4" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977" @@ -3638,7 +3623,7 @@ resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== -abab@^2.0.3: +abab@^2.0.3, abab@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== @@ -3684,6 +3669,11 @@ acorn@^7.1.1, acorn@^7.4.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn@^8.2.4: + version "8.7.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" + integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== + after@0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" @@ -3747,7 +3737,7 @@ ajv@^5.0.0: fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.3.0" -ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5: +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -4008,18 +3998,6 @@ asn1.js@^5.2.0: minimalistic-assert "^1.0.0" safer-buffer "^2.1.0" -asn1@~0.2.3: - version "0.2.4" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" - integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= - assert@^1.1.1: version "1.5.0" resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" @@ -4119,16 +4097,6 @@ aws-sdk@^2.1044.0: uuid "3.3.2" xml2js "0.4.19" -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= - -aws4@^1.8.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" - integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== - axe-core@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.0.2.tgz#c7cf7378378a51fcd272d3c09668002a4990b1cb" @@ -4179,16 +4147,16 @@ babel-helper-get-function-arity@^6.24.1: babel-runtime "^6.22.0" babel-types "^6.24.1" -babel-jest@^26.2.2, babel-jest@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.3.tgz#d87d25cb0037577a0c89f82e5755c5d293c01056" - integrity sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA== +babel-jest@^27.4.5: + version "27.4.5" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.4.5.tgz#d38bd0be8ea71d8b97853a5fc9f76deeb095c709" + integrity sha512-3uuUTjXbgtODmSv/DXO9nZfD52IyC2OYTFaXGRzL0kpykzroaquCrD5+lZNafTvZlnNqZHt5pb0M08qVBZnsnA== dependencies: - "@jest/transform" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/babel__core" "^7.1.7" + "@jest/transform" "^27.4.5" + "@jest/types" "^27.4.2" + "@types/babel__core" "^7.1.14" babel-plugin-istanbul "^6.0.0" - babel-preset-jest "^26.6.2" + babel-preset-jest "^27.4.0" chalk "^4.0.0" graceful-fs "^4.2.4" slash "^3.0.0" @@ -4229,10 +4197,10 @@ babel-plugin-istanbul@^6.0.0: istanbul-lib-instrument "^4.0.0" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz#8185bd030348d254c6d7dd974355e6a28b21e62d" - integrity sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw== +babel-plugin-jest-hoist@^27.4.0: + version "27.4.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.4.0.tgz#d7831fc0f93573788d80dee7e682482da4c730d6" + integrity sha512-Jcu7qS4OX5kTWBc45Hz7BMmgXuJqRnhatqpUhnzGC3OBYpOmf2tv6jFNwZpwM7wU7MUuv2r9IPS/ZlYOuburVw== dependencies: "@babel/template" "^7.3.3" "@babel/types" "^7.3.3" @@ -4328,6 +4296,13 @@ babel-plugin-transform-inline-environment-variables@^0.4.3: resolved "https://registry.yarnpkg.com/babel-plugin-transform-inline-environment-variables/-/babel-plugin-transform-inline-environment-variables-0.4.3.tgz#a3b09883353be8b5e2336e3ff1ef8a5d93f9c489" integrity sha1-o7CYgzU76LXiM24/8e+KXZP5xIk= +babel-plugin-transform-typescript-metadata@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-typescript-metadata/-/babel-plugin-transform-typescript-metadata-0.3.2.tgz#7a327842d8c36ffe07ee1b5276434e56c297c9b7" + integrity sha512-mWEvCQTgXQf48yDqgN7CH50waTyYBeP2Lpqx4nNWab9sxEpdXVeKgfj1qYI2/TgUPQtNFZ85i3PemRtnXVYYJg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + babel-plugin-tsconfig-paths-module-resolver@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/babel-plugin-tsconfig-paths-module-resolver/-/babel-plugin-tsconfig-paths-module-resolver-1.0.3.tgz#ed5296034c82ac54a55b5e43e5d5789079545823" @@ -4354,12 +4329,12 @@ babel-preset-current-node-syntax@^1.0.0: "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-top-level-await" "^7.8.3" -babel-preset-jest@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz#747872b1171df032252426586881d62d31798fee" - integrity sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ== +babel-preset-jest@^27.4.0: + version "27.4.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.4.0.tgz#70d0e676a282ccb200fbabd7f415db5fdf393bca" + integrity sha512-NK4jGYpnBvNxcGo7/ZpZJr51jCGT+3bwwpVIDY2oNfTxJJldRtB4VAcYdgp1loDE50ODuTu+yBjpMAswv5tlpg== dependencies: - babel-plugin-jest-hoist "^26.6.2" + babel-plugin-jest-hoist "^27.4.0" babel-preset-current-node-syntax "^1.0.0" babel-runtime@^6.22.0, babel-runtime@^6.26.0: @@ -4454,13 +4429,6 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= - dependencies: - tweetnacl "^0.14.3" - big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -4915,10 +4883,10 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -camelcase@^6.0.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" - integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== +camelcase@^6.2.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.1.tgz#250fd350cfd555d0d2160b1d51510eaf8326e86e" + integrity sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA== camelize@1.0.0, camelize@^1.0.0: version "1.0.0" @@ -4939,23 +4907,11 @@ caniuse-lite@^1.0.30001280: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001280.tgz#066a506046ba4be34cde5f74a08db7a396718fb7" integrity sha512-kFXwYvHe5rix25uwueBxC569o53J6TpnGu0BEEn+6Lhl2vsnAumRFWEBhDft1fwyo6m1r4i+RqA4+163FpeFcA== -capture-exit@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" - integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g== - dependencies: - rsvp "^4.8.4" - capture-stack-trace@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d" integrity sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw== -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= - chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -5084,10 +5040,10 @@ ci-info@^1.5.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497" integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A== -ci-info@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" - integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== +ci-info@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.0.tgz#b4ed1fb6818dea4803a55c623041f9165d2066b2" + integrity sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw== cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" @@ -5097,10 +5053,10 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: inherits "^2.0.1" safe-buffer "^5.0.1" -cjs-module-lexer@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz#4186fcca0eae175970aee870b9fe2d6cf8d5655f" - integrity sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw== +cjs-module-lexer@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" + integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== class-utils@^0.3.5: version "0.3.6" @@ -5296,7 +5252,7 @@ colorspace@1.1.x: color "3.0.x" text-hex "1.0.x" -combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: +combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -5558,7 +5514,7 @@ core-js@^3.10.2, core-js@^3.6.4: resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.10.2.tgz#17cb038ce084522a717d873b63f2b3ee532e2cd5" integrity sha512-W+2oVYeNghuBr3yTzZFQ5rfmjZtYB/Ubg87R5YOmlGrIb+Uw9f7qjUbhsj+/EkXhcV7eOD3jiM4+sgraX3FZUw== -core-util-is@1.0.2, core-util-is@~1.0.0: +core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= @@ -5636,7 +5592,7 @@ cross-spawn@^5.0.1: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^6.0.0, cross-spawn@^6.0.5: +cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -5647,7 +5603,7 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -5750,7 +5706,7 @@ cssom@~0.3.6: resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== -cssstyle@^2.2.0: +cssstyle@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== @@ -5780,13 +5736,6 @@ damerau-levenshtein@^1.0.6: resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791" integrity sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug== -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= - dependencies: - assert-plus "^1.0.0" - dasherize@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/dasherize/-/dasherize-2.0.0.tgz#6d809c9cd0cf7bb8952d80fc84fa13d47ddb1308" @@ -5894,16 +5843,21 @@ decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= -decimal.js@^10.2.0: - version "10.2.1" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.1.tgz#238ae7b0f0c793d3e3cea410108b35a2c01426a3" - integrity sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw== +decimal.js@^10.2.1: + version "10.3.1" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" + integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= + deep-equal@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" @@ -6019,15 +5973,10 @@ dicer@0.2.5: readable-stream "1.1.x" streamsearch "0.1.2" -diff-sequences@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" - integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== - -diff-sequences@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.0.6.tgz#3305cb2e55a033924054695cc66019fd7f8e5723" - integrity sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ== +diff-sequences@^27.4.0: + version "27.4.0" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.4.0.tgz#d783920ad8d06ec718a060d00196dfef25b132a5" + integrity sha512-YqiQzkrsmHMH5uuh8OdQFU9/ZpADnwzml8z0O5HvRNda+5UZsaX/xN+AAxfR2hWq1Y7HZnAzO9J5lJXOuDz2Ww== diffie-hellman@^5.0.0: version "5.0.3" @@ -6256,14 +6205,6 @@ duplexify@^3.4.2, duplexify@^3.6.0: readable-stream "^2.0.0" stream-shift "^1.0.0" -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - ecdsa-sig-formatter@1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" @@ -6316,10 +6257,10 @@ elliptic@^6.5.3: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" -emittery@^0.7.1: - version "0.7.2" - resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82" - integrity sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ== +emittery@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860" + integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg== emoji-regex@*: version "10.0.0" @@ -6644,13 +6585,13 @@ escape-string-regexp@^2.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== -escodegen@^1.14.1: - version "1.14.3" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" - integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== +escodegen@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" + integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== dependencies: esprima "^4.0.1" - estraverse "^4.2.0" + estraverse "^5.2.0" esutils "^2.0.2" optionator "^0.8.1" optionalDependencies: @@ -6895,7 +6836,7 @@ esrever@^0.2.0: resolved "https://registry.yarnpkg.com/esrever/-/esrever-0.2.0.tgz#96e9d28f4f1b1a76784cd5d490eaae010e7407b8" integrity sha1-lunSj08bGnZ4TNXUkOquAQ50B7g= -estraverse@^4.1.1, estraverse@^4.2.0: +estraverse@^4.1.1: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== @@ -6941,11 +6882,6 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" -exec-sh@^0.3.2: - version "0.3.4" - resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5" - integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A== - execa@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" @@ -6959,34 +6895,6 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -execa@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" - integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== - dependencies: - cross-spawn "^7.0.0" - get-stream "^5.0.0" - human-signals "^1.1.1" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.0" - onetime "^5.1.0" - signal-exit "^3.0.2" - strip-final-newline "^2.0.0" - execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -7032,17 +6940,17 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -expect@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/expect/-/expect-26.6.2.tgz#c6b996bf26bf3fe18b67b2d0f51fc981ba934417" - integrity sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA== +expect@^27.4.2: + version "27.4.2" + resolved "https://registry.yarnpkg.com/expect/-/expect-27.4.2.tgz#4429b0f7e307771d176de9bdf23229b101db6ef6" + integrity sha512-BjAXIDC6ZOW+WBFNg96J22D27Nq5ohn+oGcuP2rtOtcjuxNoV9McpQ60PcQWhdFOSBIQdR72e+4HdnbZTFSTyg== dependencies: - "@jest/types" "^26.6.2" - ansi-styles "^4.0.0" - jest-get-type "^26.3.0" - jest-matcher-utils "^26.6.2" - jest-message-util "^26.6.2" - jest-regex-util "^26.0.0" + "@jest/types" "^27.4.2" + ansi-styles "^5.0.0" + jest-get-type "^27.4.0" + jest-matcher-utils "^27.4.2" + jest-message-util "^27.4.2" + jest-regex-util "^27.4.0" exports-loader@^0.6.4: version "0.6.4" @@ -7074,7 +6982,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.0, extend@^3.0.2, extend@~3.0.2: +extend@^3.0.0, extend@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -7093,16 +7001,6 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= - -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= - fast-deep-equal@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" @@ -7357,11 +7255,6 @@ for-in@^1.0.2: resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= - form-data@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" @@ -7371,15 +7264,6 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - formidable@^1.1.1: version "1.2.2" resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" @@ -7546,7 +7430,7 @@ fsevents@^1.2.7: bindings "^1.5.0" nan "^2.12.1" -fsevents@^2.1.2, fsevents@~2.3.2: +fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -7642,20 +7526,6 @@ get-stream@^3.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= -get-stream@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - -get-stream@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" - integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== - dependencies: - pump "^3.0.0" - get-stream@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" @@ -7674,13 +7544,6 @@ get-value@^2.0.3, get-value@^2.0.6: resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= - dependencies: - assert-plus "^1.0.0" - gifwrap@^0.9.2: version "0.9.2" resolved "https://registry.yarnpkg.com/gifwrap/-/gifwrap-0.9.2.tgz#348e286e67d7cf57942172e1e6f05a71cee78489" @@ -7720,7 +7583,7 @@ glob-stream@^6.1.0: to-absolute-glob "^2.0.0" unique-stream "^2.0.2" -glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7: +glob@7.2.0, glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -7850,11 +7713,6 @@ graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== -growly@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" - integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= - gulp-sort@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/gulp-sort/-/gulp-sort-2.0.0.tgz#c6762a2f1f0de0a3fc595a21599d3fac8dba1aca" @@ -7869,19 +7727,6 @@ gzip-size@^3.0.0: dependencies: duplexer "^0.1.1" -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= - -har-validator@~5.1.3: - version "5.1.5" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" - integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== - dependencies: - ajv "^6.12.3" - har-schema "^2.0.0" - has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" @@ -8262,14 +8107,14 @@ http-errors@~1.6.2: setprototypeof "1.1.0" statuses ">= 1.4.0 < 2" -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= +http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" + "@tootallnate/once" "1" + agent-base "6" + debug "4" https-browserify@^1.0.0: version "1.0.0" @@ -8284,11 +8129,6 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" -human-signals@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" - integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== - human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -8531,11 +8371,6 @@ ioredis@^4.27.0, ioredis@^4.28.0: redis-parser "^3.0.0" standard-as-callback "^2.1.0" -ip-regex@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" - integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= - is-absolute@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" @@ -8629,13 +8464,6 @@ is-ci@^1.0.10: dependencies: ci-info "^1.5.0" -is-ci@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" - integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== - dependencies: - ci-info "^2.0.0" - is-core-module@^2.2.0, is-core-module@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548" @@ -8685,11 +8513,6 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2: is-data-descriptor "^1.0.0" kind-of "^6.0.2" -is-docker@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.1.1.tgz#4125a88e44e450d384e09047ede71adc2d144156" - integrity sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw== - is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" @@ -8832,10 +8655,10 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-potential-custom-element-name@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" - integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== is-printable-key-event@^1.0.0: version "1.0.0" @@ -8911,7 +8734,7 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" -is-typedarray@^1.0.0, is-typedarray@~1.0.0: +is-typedarray@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= @@ -8955,13 +8778,6 @@ is-wsl@^1.1.0: resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= -is-wsl@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" - integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== - dependencies: - is-docker "^2.0.0" - isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -9007,11 +8823,6 @@ isomorphic.js@^0.2.4: resolved "https://registry.yarnpkg.com/isomorphic.js/-/isomorphic.js-0.2.4.tgz#24ca374163ae54a7ce3b86ce63b701b91aa84969" integrity sha512-Y4NjZceAwaPXctwsHgNsmfuPxR8lJ3f8X7QTAkhltrX4oGIv+eTlgHLXn4tWysC9zGTi929gapnPp+8F8cg7nA== -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= - istanbul-lib-coverage@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec" @@ -9068,120 +8879,138 @@ java-properties@^1.0.0: resolved "https://registry.yarnpkg.com/java-properties/-/java-properties-1.0.2.tgz#ccd1fa73907438a5b5c38982269d0e771fe78211" integrity sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ== -jest-changed-files@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.6.2.tgz#f6198479e1cc66f22f9ae1e22acaa0b429c042d0" - integrity sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ== +jest-changed-files@^27.4.2: + version "27.4.2" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.4.2.tgz#da2547ea47c6e6a5f6ed336151bd2075736eb4a5" + integrity sha512-/9x8MjekuzUQoPjDHbBiXbNEBauhrPU2ct7m8TfCg69ywt1y/N+yYwGh3gCpnqUS3klYWDU/lSNgv+JhoD2k1A== dependencies: - "@jest/types" "^26.6.2" - execa "^4.0.0" - throat "^5.0.0" + "@jest/types" "^27.4.2" + execa "^5.0.0" + throat "^6.0.1" -jest-cli@^26.0.0: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.6.3.tgz#43117cfef24bc4cd691a174a8796a532e135e92a" - integrity sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg== +jest-circus@^27.4.5: + version "27.4.5" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.4.5.tgz#70bfb78e0200cab9b84747bf274debacaa538467" + integrity sha512-eTNWa9wsvBwPykhMMShheafbwyakcdHZaEYh5iRrQ0PFJxkDP/e3U/FvzGuKWu2WpwUA3C3hPlfpuzvOdTVqnw== dependencies: - "@jest/core" "^26.6.3" - "@jest/test-result" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/environment" "^27.4.4" + "@jest/test-result" "^27.4.2" + "@jest/types" "^27.4.2" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^0.7.0" + expect "^27.4.2" + is-generator-fn "^2.0.0" + jest-each "^27.4.2" + jest-matcher-utils "^27.4.2" + jest-message-util "^27.4.2" + jest-runtime "^27.4.5" + jest-snapshot "^27.4.5" + jest-util "^27.4.2" + pretty-format "^27.4.2" + slash "^3.0.0" + stack-utils "^2.0.3" + throat "^6.0.1" + +jest-cli@^27.4.5: + version "27.4.5" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.4.5.tgz#8708f54c28d13681f3255ec9026a2b15b03d41e8" + integrity sha512-hrky3DSgE0u7sQxaCL7bdebEPHx5QzYmrGuUjaPLmPE8jx5adtvGuOlRspvMoVLTTDOHRnZDoRLYJuA+VCI7Hg== + dependencies: + "@jest/core" "^27.4.5" + "@jest/test-result" "^27.4.2" + "@jest/types" "^27.4.2" chalk "^4.0.0" exit "^0.1.2" graceful-fs "^4.2.4" import-local "^3.0.2" - is-ci "^2.0.0" - jest-config "^26.6.3" - jest-util "^26.6.2" - jest-validate "^26.6.2" + jest-config "^27.4.5" + jest-util "^27.4.2" + jest-validate "^27.4.2" prompts "^2.0.1" - yargs "^15.4.1" + yargs "^16.2.0" -jest-config@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.6.3.tgz#64f41444eef9eb03dc51d5c53b75c8c71f645349" - integrity sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg== +jest-config@^27.4.5: + version "27.4.5" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.4.5.tgz#77ed7f2ba7bcfd7d740ade711d0d13512e08a59e" + integrity sha512-t+STVJtPt+fpqQ8GBw850NtSQbnDOw/UzdPfzDaHQ48/AylQlW7LHj3dH+ndxhC1UxJ0Q3qkq7IH+nM1skwTwA== dependencies: "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^26.6.3" - "@jest/types" "^26.6.2" - babel-jest "^26.6.3" + "@jest/test-sequencer" "^27.4.5" + "@jest/types" "^27.4.2" + babel-jest "^27.4.5" chalk "^4.0.0" + ci-info "^3.2.0" deepmerge "^4.2.2" glob "^7.1.1" graceful-fs "^4.2.4" - jest-environment-jsdom "^26.6.2" - jest-environment-node "^26.6.2" - jest-get-type "^26.3.0" - jest-jasmine2 "^26.6.3" - jest-regex-util "^26.0.0" - jest-resolve "^26.6.2" - jest-util "^26.6.2" - jest-validate "^26.6.2" - micromatch "^4.0.2" - pretty-format "^26.6.2" + jest-circus "^27.4.5" + jest-environment-jsdom "^27.4.4" + jest-environment-node "^27.4.4" + jest-get-type "^27.4.0" + jest-jasmine2 "^27.4.5" + jest-regex-util "^27.4.0" + jest-resolve "^27.4.5" + jest-runner "^27.4.5" + jest-util "^27.4.2" + jest-validate "^27.4.2" + micromatch "^4.0.4" + pretty-format "^27.4.2" + slash "^3.0.0" -jest-diff@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" - integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== +jest-diff@^27.0.0, jest-diff@^27.4.2: + version "27.4.2" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.4.2.tgz#786b2a5211d854f848e2dcc1e324448e9481f36f" + integrity sha512-ujc9ToyUZDh9KcqvQDkk/gkbf6zSaeEg9AiBxtttXW59H/AcqEYp1ciXAtJp+jXWva5nAf/ePtSsgWwE5mqp4Q== dependencies: chalk "^4.0.0" - diff-sequences "^26.6.2" - jest-get-type "^26.3.0" - pretty-format "^26.6.2" + diff-sequences "^27.4.0" + jest-get-type "^27.4.0" + pretty-format "^27.4.2" -jest-diff@^27.0.0: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.3.1.tgz#d2775fea15411f5f5aeda2a5e02c2f36440f6d55" - integrity sha512-PCeuAH4AWUo2O5+ksW4pL9v5xJAcIKPUPfIhZBcG1RKv/0+dvaWTQK1Nrau8d67dp65fOqbeMdoil+6PedyEPQ== - dependencies: - chalk "^4.0.0" - diff-sequences "^27.0.6" - jest-get-type "^27.3.1" - pretty-format "^27.3.1" - -jest-docblock@^26.0.0: - version "26.0.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-26.0.0.tgz#3e2fa20899fc928cb13bd0ff68bd3711a36889b5" - integrity sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w== +jest-docblock@^27.4.0: + version "27.4.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.4.0.tgz#06c78035ca93cbbb84faf8fce64deae79a59f69f" + integrity sha512-7TBazUdCKGV7svZ+gh7C8esAnweJoG+SvcF6Cjqj4l17zA2q1cMwx2JObSioubk317H+cjcHgP+7fTs60paulg== dependencies: detect-newline "^3.0.0" -jest-each@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-26.6.2.tgz#02526438a77a67401c8a6382dfe5999952c167cb" - integrity sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A== +jest-each@^27.4.2: + version "27.4.2" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.4.2.tgz#19364c82a692d0d26557642098d1f4619c9ee7d3" + integrity sha512-53V2MNyW28CTruB3lXaHNk6PkiIFuzdOC9gR3C6j8YE/ACfrPnz+slB0s17AgU1TtxNzLuHyvNlLJ+8QYw9nBg== dependencies: - "@jest/types" "^26.6.2" + "@jest/types" "^27.4.2" chalk "^4.0.0" - jest-get-type "^26.3.0" - jest-util "^26.6.2" - pretty-format "^26.6.2" + jest-get-type "^27.4.0" + jest-util "^27.4.2" + pretty-format "^27.4.2" -jest-environment-jsdom@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz#78d09fe9cf019a357009b9b7e1f101d23bd1da3e" - integrity sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q== +jest-environment-jsdom@^27.4.4: + version "27.4.4" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.4.4.tgz#94f738e99514d7a880e8ed8e03e3a321d43b49db" + integrity sha512-cYR3ndNfHBqQgFvS1RL7dNqSvD//K56j/q1s2ygNHcfTCAp12zfIromO1w3COmXrxS8hWAh7+CmZmGCIoqGcGA== dependencies: - "@jest/environment" "^26.6.2" - "@jest/fake-timers" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/environment" "^27.4.4" + "@jest/fake-timers" "^27.4.2" + "@jest/types" "^27.4.2" "@types/node" "*" - jest-mock "^26.6.2" - jest-util "^26.6.2" - jsdom "^16.4.0" + jest-mock "^27.4.2" + jest-util "^27.4.2" + jsdom "^16.6.0" -jest-environment-node@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-26.6.2.tgz#824e4c7fb4944646356f11ac75b229b0035f2b0c" - integrity sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag== +jest-environment-node@^27.4.4: + version "27.4.4" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.4.4.tgz#42fe5e3b224cb69b99811ebf6f5eaa5a59618514" + integrity sha512-D+v3lbJ2GjQTQR23TK0kY3vFVmSeea05giInI41HHOaJnAwOnmUHTZgUaZL+VxUB43pIzoa7PMwWtCVlIUoVoA== dependencies: - "@jest/environment" "^26.6.2" - "@jest/fake-timers" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/environment" "^27.4.4" + "@jest/fake-timers" "^27.4.2" + "@jest/types" "^27.4.2" "@types/node" "*" - jest-mock "^26.6.2" - jest-util "^26.6.2" + jest-mock "^27.4.2" + jest-util "^27.4.2" jest-fetch-mock@^3.0.3: version "3.0.3" @@ -9191,100 +9020,94 @@ jest-fetch-mock@^3.0.3: cross-fetch "^3.0.4" promise-polyfill "^8.1.3" -jest-get-type@^26.3.0: - version "26.3.0" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" - integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== +jest-get-type@^27.4.0: + version "27.4.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.4.0.tgz#7503d2663fffa431638337b3998d39c5e928e9b5" + integrity sha512-tk9o+ld5TWq41DkK14L4wox4s2D9MtTpKaAVzXfr5CUKm5ZK2ExcaFE0qls2W71zE/6R2TxxrK9w2r6svAFDBQ== -jest-get-type@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.3.1.tgz#a8a2b0a12b50169773099eee60a0e6dd11423eff" - integrity sha512-+Ilqi8hgHSAdhlQ3s12CAVNd8H96ZkQBfYoXmArzZnOfAtVAJEiPDBirjByEblvG/4LPJmkL+nBqPO3A1YJAEg== - -jest-haste-map@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.6.2.tgz#dd7e60fe7dc0e9f911a23d79c5ff7fb5c2cafeaa" - integrity sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w== +jest-haste-map@^27.4.5: + version "27.4.5" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.4.5.tgz#c2921224a59223f91e03ec15703905978ef0cc1a" + integrity sha512-oJm1b5qhhPs78K24EDGifWS0dELYxnoBiDhatT/FThgB9yxqUm5F6li3Pv+Q+apMBmmPNzOBnZ7ZxWMB1Leq1Q== dependencies: - "@jest/types" "^26.6.2" + "@jest/types" "^27.4.2" "@types/graceful-fs" "^4.1.2" "@types/node" "*" anymatch "^3.0.3" fb-watchman "^2.0.0" graceful-fs "^4.2.4" - jest-regex-util "^26.0.0" - jest-serializer "^26.6.2" - jest-util "^26.6.2" - jest-worker "^26.6.2" - micromatch "^4.0.2" - sane "^4.0.3" + jest-regex-util "^27.4.0" + jest-serializer "^27.4.0" + jest-util "^27.4.2" + jest-worker "^27.4.5" + micromatch "^4.0.4" walker "^1.0.7" optionalDependencies: - fsevents "^2.1.2" + fsevents "^2.3.2" -jest-jasmine2@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz#adc3cf915deacb5212c93b9f3547cd12958f2edd" - integrity sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg== +jest-jasmine2@^27.4.5: + version "27.4.5" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.4.5.tgz#ff79d11561679ff6c89715b0cd6b1e8c0dfbc6dc" + integrity sha512-oUnvwhJDj2LhOiUB1kdnJjkx8C5PwgUZQb9urF77mELH9DGR4e2GqpWQKBOYXWs5+uTN9BGDqRz3Aeg5Wts7aw== dependencies: "@babel/traverse" "^7.1.0" - "@jest/environment" "^26.6.2" - "@jest/source-map" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/environment" "^27.4.4" + "@jest/source-map" "^27.4.0" + "@jest/test-result" "^27.4.2" + "@jest/types" "^27.4.2" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" - expect "^26.6.2" + expect "^27.4.2" is-generator-fn "^2.0.0" - jest-each "^26.6.2" - jest-matcher-utils "^26.6.2" - jest-message-util "^26.6.2" - jest-runtime "^26.6.3" - jest-snapshot "^26.6.2" - jest-util "^26.6.2" - pretty-format "^26.6.2" - throat "^5.0.0" + jest-each "^27.4.2" + jest-matcher-utils "^27.4.2" + jest-message-util "^27.4.2" + jest-runtime "^27.4.5" + jest-snapshot "^27.4.5" + jest-util "^27.4.2" + pretty-format "^27.4.2" + throat "^6.0.1" -jest-leak-detector@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz#7717cf118b92238f2eba65054c8a0c9c653a91af" - integrity sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg== +jest-leak-detector@^27.4.2: + version "27.4.2" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.4.2.tgz#7fc3120893a7a911c553f3f2bdff9faa4454abbb" + integrity sha512-ml0KvFYZllzPBJWDei3mDzUhyp/M4ubKebX++fPaudpe8OsxUE+m+P6ciVLboQsrzOCWDjE20/eXew9QMx/VGw== dependencies: - jest-get-type "^26.3.0" - pretty-format "^26.6.2" + jest-get-type "^27.4.0" + pretty-format "^27.4.2" -jest-matcher-utils@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz#8e6fd6e863c8b2d31ac6472eeb237bc595e53e7a" - integrity sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw== +jest-matcher-utils@^27.4.2: + version "27.4.2" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.4.2.tgz#d17c5038607978a255e0a9a5c32c24e984b6c60b" + integrity sha512-jyP28er3RRtMv+fmYC/PKG8wvAmfGcSNproVTW2Y0P/OY7/hWUOmsPfxN1jOhM+0u2xU984u2yEagGivz9OBGQ== dependencies: chalk "^4.0.0" - jest-diff "^26.6.2" - jest-get-type "^26.3.0" - pretty-format "^26.6.2" + jest-diff "^27.4.2" + jest-get-type "^27.4.0" + pretty-format "^27.4.2" -jest-message-util@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07" - integrity sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA== +jest-message-util@^27.4.2: + version "27.4.2" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.4.2.tgz#07f3f1bf207d69cf798ce830cc57f1a849f99388" + integrity sha512-OMRqRNd9E0DkBLZpFtZkAGYOXl6ZpoMtQJWTAREJKDOFa0M6ptB7L67tp+cszMBkvSgKOhNtQp2Vbcz3ZZKo/w== dependencies: - "@babel/code-frame" "^7.0.0" - "@jest/types" "^26.6.2" + "@babel/code-frame" "^7.12.13" + "@jest/types" "^27.4.2" "@types/stack-utils" "^2.0.0" chalk "^4.0.0" graceful-fs "^4.2.4" - micromatch "^4.0.2" - pretty-format "^26.6.2" + micromatch "^4.0.4" + pretty-format "^27.4.2" slash "^3.0.0" - stack-utils "^2.0.2" + stack-utils "^2.0.3" -jest-mock@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.6.2.tgz#d6cb712b041ed47fe0d9b6fc3474bc6543feb302" - integrity sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew== +jest-mock@^27.4.2: + version "27.4.2" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.4.2.tgz#184ff197a25491bfe4570c286daa5d62eb760b88" + integrity sha512-PDDPuyhoukk20JrQKeofK12hqtSka7mWH0QQuxSNgrdiPsrnYYLS6wbzu/HDlxZRzji5ylLRULeuI/vmZZDrYA== dependencies: - "@jest/types" "^26.6.2" + "@jest/types" "^27.4.2" "@types/node" "*" jest-pnp-resolver@^1.2.2: @@ -9292,161 +9115,172 @@ jest-pnp-resolver@^1.2.2: resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== -jest-regex-util@^26.0.0: - version "26.0.0" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" - integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A== +jest-regex-util@^27.4.0: + version "27.4.0" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.4.0.tgz#e4c45b52653128843d07ad94aec34393ea14fbca" + integrity sha512-WeCpMpNnqJYMQoOjm1nTtsgbR4XHAk1u00qDoNBQoykM280+/TmgA5Qh5giC1ecy6a5d4hbSsHzpBtu5yvlbEg== -jest-resolve-dependencies@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz#6680859ee5d22ee5dcd961fe4871f59f4c784fb6" - integrity sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg== +jest-resolve-dependencies@^27.4.5: + version "27.4.5" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.4.5.tgz#9398af854bdb12d6a9e5a8a536ee401f889a3ecf" + integrity sha512-elEVvkvRK51y037NshtEkEnukMBWvlPzZHiL847OrIljJ8yIsujD2GXRPqDXC4rEVKbcdsy7W0FxoZb4WmEs7w== dependencies: - "@jest/types" "^26.6.2" - jest-regex-util "^26.0.0" - jest-snapshot "^26.6.2" + "@jest/types" "^27.4.2" + jest-regex-util "^27.4.0" + jest-snapshot "^27.4.5" -jest-resolve@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.6.2.tgz#a3ab1517217f469b504f1b56603c5bb541fbb507" - integrity sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ== +jest-resolve@^27.4.5: + version "27.4.5" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.4.5.tgz#8dc44f5065fb8d58944c20f932cb7b9fe9760cca" + integrity sha512-xU3z1BuOz/hUhVUL+918KqUgK+skqOuUsAi7A+iwoUldK6/+PW+utK8l8cxIWT9AW7IAhGNXjSAh1UYmjULZZw== dependencies: - "@jest/types" "^26.6.2" + "@jest/types" "^27.4.2" chalk "^4.0.0" graceful-fs "^4.2.4" + jest-haste-map "^27.4.5" jest-pnp-resolver "^1.2.2" - jest-util "^26.6.2" - read-pkg-up "^7.0.1" - resolve "^1.18.1" + jest-util "^27.4.2" + jest-validate "^27.4.2" + resolve "^1.20.0" + resolve.exports "^1.1.0" slash "^3.0.0" -jest-runner@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.6.3.tgz#2d1fed3d46e10f233fd1dbd3bfaa3fe8924be159" - integrity sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ== +jest-runner@^27.4.5: + version "27.4.5" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.4.5.tgz#daba2ba71c8f34137dc7ac45616add35370a681e" + integrity sha512-/irauncTfmY1WkTaRQGRWcyQLzK1g98GYG/8QvIPviHgO1Fqz1JYeEIsSfF+9mc/UTA6S+IIHFgKyvUrtiBIZg== dependencies: - "@jest/console" "^26.6.2" - "@jest/environment" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/console" "^27.4.2" + "@jest/environment" "^27.4.4" + "@jest/test-result" "^27.4.2" + "@jest/transform" "^27.4.5" + "@jest/types" "^27.4.2" "@types/node" "*" chalk "^4.0.0" - emittery "^0.7.1" + emittery "^0.8.1" exit "^0.1.2" graceful-fs "^4.2.4" - jest-config "^26.6.3" - jest-docblock "^26.0.0" - jest-haste-map "^26.6.2" - jest-leak-detector "^26.6.2" - jest-message-util "^26.6.2" - jest-resolve "^26.6.2" - jest-runtime "^26.6.3" - jest-util "^26.6.2" - jest-worker "^26.6.2" + jest-docblock "^27.4.0" + jest-environment-jsdom "^27.4.4" + jest-environment-node "^27.4.4" + jest-haste-map "^27.4.5" + jest-leak-detector "^27.4.2" + jest-message-util "^27.4.2" + jest-resolve "^27.4.5" + jest-runtime "^27.4.5" + jest-util "^27.4.2" + jest-worker "^27.4.5" source-map-support "^0.5.6" - throat "^5.0.0" + throat "^6.0.1" -jest-runtime@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.6.3.tgz#4f64efbcfac398331b74b4b3c82d27d401b8fa2b" - integrity sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw== +jest-runtime@^27.4.5: + version "27.4.5" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.4.5.tgz#97703ad2a1799d4f50ab59049bd21a9ceaed2813" + integrity sha512-CIYqwuJQXHQtPd/idgrx4zgJ6iCb6uBjQq1RSAGQrw2S8XifDmoM1Ot8NRd80ooAm+ZNdHVwsktIMGlA1F1FAQ== dependencies: - "@jest/console" "^26.6.2" - "@jest/environment" "^26.6.2" - "@jest/fake-timers" "^26.6.2" - "@jest/globals" "^26.6.2" - "@jest/source-map" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/transform" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/yargs" "^15.0.0" + "@jest/console" "^27.4.2" + "@jest/environment" "^27.4.4" + "@jest/globals" "^27.4.4" + "@jest/source-map" "^27.4.0" + "@jest/test-result" "^27.4.2" + "@jest/transform" "^27.4.5" + "@jest/types" "^27.4.2" + "@types/yargs" "^16.0.0" chalk "^4.0.0" - cjs-module-lexer "^0.6.0" + cjs-module-lexer "^1.0.0" collect-v8-coverage "^1.0.0" + execa "^5.0.0" exit "^0.1.2" glob "^7.1.3" graceful-fs "^4.2.4" - jest-config "^26.6.3" - jest-haste-map "^26.6.2" - jest-message-util "^26.6.2" - jest-mock "^26.6.2" - jest-regex-util "^26.0.0" - jest-resolve "^26.6.2" - jest-snapshot "^26.6.2" - jest-util "^26.6.2" - jest-validate "^26.6.2" + jest-haste-map "^27.4.5" + jest-message-util "^27.4.2" + jest-mock "^27.4.2" + jest-regex-util "^27.4.0" + jest-resolve "^27.4.5" + jest-snapshot "^27.4.5" + jest-util "^27.4.2" + jest-validate "^27.4.2" slash "^3.0.0" strip-bom "^4.0.0" - yargs "^15.4.1" + yargs "^16.2.0" -jest-serializer@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.6.2.tgz#d139aafd46957d3a448f3a6cdabe2919ba0742d1" - integrity sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g== +jest-serializer@^27.4.0: + version "27.4.0" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.4.0.tgz#34866586e1cae2388b7d12ffa2c7819edef5958a" + integrity sha512-RDhpcn5f1JYTX2pvJAGDcnsNTnsV9bjYPU8xcV+xPwOXnUPOQwf4ZEuiU6G9H1UztH+OapMgu/ckEVwO87PwnQ== dependencies: "@types/node" "*" graceful-fs "^4.2.4" -jest-snapshot@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.6.2.tgz#f3b0af1acb223316850bd14e1beea9837fb39c84" - integrity sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og== +jest-snapshot@^27.4.5: + version "27.4.5" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.4.5.tgz#2ea909b20aac0fe62504bc161331f730b8a7ecc7" + integrity sha512-eCi/iM1YJFrJWiT9de4+RpWWWBqsHiYxFG9V9o/n0WXs6GpW4lUt4FAHAgFPTLPqCUVzrMQmSmTZSgQzwqR7IQ== dependencies: + "@babel/core" "^7.7.2" + "@babel/generator" "^7.7.2" + "@babel/parser" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/traverse" "^7.7.2" "@babel/types" "^7.0.0" - "@jest/types" "^26.6.2" + "@jest/transform" "^27.4.5" + "@jest/types" "^27.4.2" "@types/babel__traverse" "^7.0.4" - "@types/prettier" "^2.0.0" + "@types/prettier" "^2.1.5" + babel-preset-current-node-syntax "^1.0.0" chalk "^4.0.0" - expect "^26.6.2" + expect "^27.4.2" graceful-fs "^4.2.4" - jest-diff "^26.6.2" - jest-get-type "^26.3.0" - jest-haste-map "^26.6.2" - jest-matcher-utils "^26.6.2" - jest-message-util "^26.6.2" - jest-resolve "^26.6.2" + jest-diff "^27.4.2" + jest-get-type "^27.4.0" + jest-haste-map "^27.4.5" + jest-matcher-utils "^27.4.2" + jest-message-util "^27.4.2" + jest-resolve "^27.4.5" + jest-util "^27.4.2" natural-compare "^1.4.0" - pretty-format "^26.6.2" + pretty-format "^27.4.2" semver "^7.3.2" -jest-util@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" - integrity sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q== +jest-util@^27.4.2: + version "27.4.2" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.4.2.tgz#ed95b05b1adfd761e2cda47e0144c6a58e05a621" + integrity sha512-YuxxpXU6nlMan9qyLuxHaMMOzXAl5aGZWCSzben5DhLHemYQxCc4YK+4L3ZrCutT8GPQ+ui9k5D8rUJoDioMnA== dependencies: - "@jest/types" "^26.6.2" + "@jest/types" "^27.4.2" "@types/node" "*" chalk "^4.0.0" + ci-info "^3.2.0" graceful-fs "^4.2.4" - is-ci "^2.0.0" - micromatch "^4.0.2" + picomatch "^2.2.3" -jest-validate@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.6.2.tgz#23d380971587150467342911c3d7b4ac57ab20ec" - integrity sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ== +jest-validate@^27.4.2: + version "27.4.2" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.4.2.tgz#eecfcc1b1c9429aa007da08a2bae4e32a81bbbc3" + integrity sha512-hWYsSUej+Fs8ZhOm5vhWzwSLmVaPAxRy+Mr+z5MzeaHm9AxUpXdoVMEW4R86y5gOobVfBsMFLk4Rb+QkiEpx1A== dependencies: - "@jest/types" "^26.6.2" - camelcase "^6.0.0" + "@jest/types" "^27.4.2" + camelcase "^6.2.0" chalk "^4.0.0" - jest-get-type "^26.3.0" + jest-get-type "^27.4.0" leven "^3.1.0" - pretty-format "^26.6.2" + pretty-format "^27.4.2" -jest-watcher@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-26.6.2.tgz#a5b683b8f9d68dbcb1d7dae32172d2cca0592975" - integrity sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ== +jest-watcher@^27.4.2: + version "27.4.2" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.4.2.tgz#c9037edfd80354c9fe90de4b6f8b6e2b8e736744" + integrity sha512-NJvMVyyBeXfDezhWzUOCOYZrUmkSCiatpjpm+nFUid74OZEHk6aMLrZAukIiFDwdbqp6mTM6Ui1w4oc+8EobQg== dependencies: - "@jest/test-result" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/test-result" "^27.4.2" + "@jest/types" "^27.4.2" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" - jest-util "^26.6.2" + jest-util "^27.4.2" string-length "^4.0.1" -jest-worker@^26.2.1, jest-worker@^26.5.0, jest-worker@^26.6.2: +jest-worker@^26.2.1, jest-worker@^26.5.0: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== @@ -9455,6 +9289,15 @@ jest-worker@^26.2.1, jest-worker@^26.5.0, jest-worker@^26.6.2: merge-stream "^2.0.0" supports-color "^7.0.0" +jest-worker@^27.4.5: + version "27.4.5" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.4.5.tgz#d696e3e46ae0f24cff3fa7195ffba22889262242" + integrity sha512-f2s8kEdy15cv9r7q4KkzGXvlY0JTcmCbMHZBfSQDwW77REr45IDWwd0lksDFeVHH2jJ5pqb90T77XscrjeGzzg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + jimp@0.16.1: version "0.16.1" resolved "https://registry.yarnpkg.com/jimp/-/jimp-0.16.1.tgz#192f851a30e5ca11112a3d0aa53137659a78ca7a" @@ -9514,41 +9357,37 @@ js-yaml@4.1.0, js-yaml@^3.13.1, js-yaml@^3.14.1: argparse "^1.0.7" esprima "^4.0.0" -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= - -jsdom@^16.4.0: - version "16.4.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.4.0.tgz#36005bde2d136f73eee1a830c6d45e55408edddb" - integrity sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w== +jsdom@^16.6.0: + version "16.7.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" + integrity sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw== dependencies: - abab "^2.0.3" - acorn "^7.1.1" + abab "^2.0.5" + acorn "^8.2.4" acorn-globals "^6.0.0" cssom "^0.4.4" - cssstyle "^2.2.0" + cssstyle "^2.3.0" data-urls "^2.0.0" - decimal.js "^10.2.0" + decimal.js "^10.2.1" domexception "^2.0.1" - escodegen "^1.14.1" + escodegen "^2.0.0" + form-data "^3.0.0" html-encoding-sniffer "^2.0.1" - is-potential-custom-element-name "^1.0.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-potential-custom-element-name "^1.0.1" nwsapi "^2.2.0" - parse5 "5.1.1" - request "^2.88.2" - request-promise-native "^1.0.8" - saxes "^5.0.0" + parse5 "6.0.1" + saxes "^5.0.1" symbol-tree "^3.2.4" - tough-cookie "^3.0.1" + tough-cookie "^4.0.0" w3c-hr-time "^1.0.2" w3c-xmlserializer "^2.0.0" webidl-conversions "^6.1.0" whatwg-encoding "^1.0.5" whatwg-mimetype "^2.3.0" - whatwg-url "^8.0.0" - ws "^7.2.3" + whatwg-url "^8.5.0" + ws "^7.4.6" xml-name-validator "^3.0.0" jsesc@^2.5.1: @@ -9598,11 +9437,6 @@ json-schema-traverse@^1.0.0: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= - json-schema@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.3.0.tgz#90a9c5054bd065422c00241851ce8d59475b701b" @@ -9613,11 +9447,6 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= - json5@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" @@ -9681,16 +9510,6 @@ jsonwebtoken@8.5.1, jsonwebtoken@^8.5.0: ms "^2.1.1" semver "^5.6.0" -jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.2.3" - verror "1.10.0" - "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.1.0.tgz#642f1d7b88aa6d7eb9d8f2210e166478444fa891" @@ -10244,7 +10063,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.0.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5: +lodash@^4.0.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -10356,12 +10175,12 @@ make-dir@^3.0.0, make-dir@^3.0.2: dependencies: semver "^6.0.0" -makeerror@1.0.x: - version "1.0.11" - resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" - integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw= +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== dependencies: - tmpl "1.0.x" + tmpl "1.0.5" mammoth@^1.4.19: version "1.4.19" @@ -10531,7 +10350,7 @@ micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.2, micromatch@^4.0.4: +micromatch@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== @@ -10557,7 +10376,7 @@ mime-db@1.44.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== -mime-types@^2.1.12, mime-types@^2.1.18, mime-types@~2.1.19, mime-types@~2.1.24: +mime-types@^2.1.12, mime-types@^2.1.18, mime-types@~2.1.24: version "2.1.27" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== @@ -10611,7 +10430,7 @@ minimatch@^3.0.2, minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: +minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -10918,18 +10737,6 @@ node-modules-regexp@^1.0.0: resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= -node-notifier@^8.0.0: - version "8.0.1" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.1.tgz#f86e89bbc925f2b068784b31f382afdc6ca56be1" - integrity sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA== - dependencies: - growly "^1.3.0" - is-wsl "^2.2.0" - semver "^7.3.2" - shellwords "^0.1.1" - uuid "^8.3.0" - which "^2.0.2" - node-releases@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" @@ -11011,7 +10818,7 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" -npm-run-path@^4.0.0, npm-run-path@^4.0.1: +npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== @@ -11037,11 +10844,6 @@ nwsapi@^2.2.0: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - oauth@0.9.x: version "0.9.15" resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" @@ -11165,7 +10967,7 @@ one-time@^1.0.0: dependencies: fn.name "1.x.x" -onetime@^5.1.0, onetime@^5.1.2: +onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== @@ -11249,11 +11051,6 @@ p-any@1.1.0: dependencies: p-some "^2.0.0" -p-each-series@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48" - integrity sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ== - p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" @@ -11449,12 +11246,7 @@ parse5-htmlparser2-tree-adapter@^6.0.1: dependencies: parse5 "^6.0.1" -parse5@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" - integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== - -parse5@^6.0.1: +parse5@6.0.1, parse5@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== @@ -11870,22 +11662,12 @@ pretty-error@^2.1.1: lodash "^4.17.20" renderkid "^2.0.4" -pretty-format@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" - integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== +pretty-format@^27.0.0, pretty-format@^27.4.2: + version "27.4.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.4.2.tgz#e4ce92ad66c3888423d332b40477c87d1dac1fb8" + integrity sha512-p0wNtJ9oLuvgOQDEIZ9zQjZffK7KtyR6Si0jnXULIDwrlNF8Cuir3AZP0hHv0jmKuNN/edOnbMjnzd4uTcmWiw== dependencies: - "@jest/types" "^26.6.2" - ansi-regex "^5.0.0" - ansi-styles "^4.0.0" - react-is "^17.0.1" - -pretty-format@^27.0.0, pretty-format@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.3.1.tgz#7e9486365ccdd4a502061fa761d3ab9ca1b78df5" - integrity sha512-DR/c+pvFc52nLimLROYjnXPtolawm+uWDxr4FjuLDLUn+ktWnSN851KoHwHzzqq6rfCOjkzN8FLgDrSub6UDuA== - dependencies: - "@jest/types" "^27.2.5" + "@jest/types" "^27.4.2" ansi-regex "^5.0.1" ansi-styles "^5.0.0" react-is "^17.0.1" @@ -12133,7 +11915,7 @@ pseudomap@^1.0.2: resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= -psl@^1.1.28: +psl@^1.1.33: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== @@ -12200,11 +11982,6 @@ qs@^6.4.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ== -qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== - query-string@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.0.1.tgz#45bd149cf586aaa582dffc7ec7a8ad97dd02f75d" @@ -12553,15 +12330,6 @@ reactcss@^1.2.0: dependencies: lodash "^4.0.1" -read-pkg-up@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" - integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== - dependencies: - find-up "^4.1.0" - read-pkg "^5.2.0" - type-fest "^0.8.1" - read-pkg@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" @@ -12697,6 +12465,11 @@ referrer-policy@1.2.0: resolved "https://registry.yarnpkg.com/referrer-policy/-/referrer-policy-1.2.0.tgz#b99cfb8b57090dc454895ef897a4cc35ef67a98e" integrity sha512-LgQJIuS6nAy1Jd88DCQRemyE3mS+ispwlqMk3b0yjZ257fI1v9c+/p6SD5gP5FGyXUIgrNOAfmyioHwZtYv2VA== +reflect-metadata@^0.1.13: + version "0.1.13" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" + integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== + reflect.ownkeys@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" @@ -12858,48 +12631,6 @@ replace-ext@^1.0.0: resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a" integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw== -request-promise-core@1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f" - integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw== - dependencies: - lodash "^4.17.19" - -request-promise-native@^1.0.8: - version "1.0.9" - resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28" - integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g== - dependencies: - request-promise-core "1.1.4" - stealthy-require "^1.1.1" - tough-cookie "^2.3.3" - -request@^2.88.2: - version "2.88.2" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" - integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -13005,6 +12736,11 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= +resolve.exports@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" + integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== + resolve@^1.1.6, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.5.0: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" @@ -13144,7 +12880,7 @@ rst-selector-parser@^2.2.3: lodash.flattendeep "^4.4.0" nearley "^2.7.10" -rsvp@^4.8.2, rsvp@^4.8.4: +rsvp@^4.8.2: version "4.8.5" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== @@ -13192,26 +12928,11 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sane@^4.0.3: - version "4.1.0" - resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded" - integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA== - dependencies: - "@cnakazawa/watch" "^1.0.3" - anymatch "^2.0.0" - capture-exit "^2.0.0" - exec-sh "^0.3.2" - execa "^1.0.0" - fb-watchman "^2.0.0" - micromatch "^3.1.4" - minimist "^1.1.1" - walker "~1.0.5" - sanitizer@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/sanitizer/-/sanitizer-0.1.3.tgz#d4f0af7475d9a7baf2a9e5a611718baa178a39e1" @@ -13232,7 +12953,7 @@ sax@~1.1.1: resolved "https://registry.yarnpkg.com/sax/-/sax-1.1.6.tgz#5d616be8a5e607d54e114afae55b7eaf2fcc3240" integrity sha1-XWFr6KXmB9VOEUr65Vt+ry/MMkA= -saxes@^5.0.0: +saxes@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== @@ -13361,6 +13082,13 @@ sequelize-pool@^6.0.0: resolved "https://registry.yarnpkg.com/sequelize-pool/-/sequelize-pool-6.1.0.tgz#caaa0c1e324d3c2c3a399fed2c7998970925d668" integrity sha512-4YwEw3ZgK/tY/so+GfnSgXkdwIJJ1I32uZJztIEgZeAO6HMgj64OzySbWLgxj+tXhZCJnzRfkY9gINw8Ft8ZMg== +sequelize-typescript@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/sequelize-typescript/-/sequelize-typescript-2.1.1.tgz#92445632062db868b760cd20215406403da737a2" + integrity sha512-4am/5O6dlAvtR/akH2KizcECm4rRAjWr+oc5mo9vFVMez8hrbOhQlDNzk0H6VMOQASCN7yBF+qOnSEN60V6/vA== + dependencies: + glob "7.2.0" + sequelize@^6.9.0: version "6.9.0" resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-6.9.0.tgz#bac253fab04beb3c7de5de9e41e359d9666aab9d" @@ -13471,11 +13199,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shellwords@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" - integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== - shimmer@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" @@ -13809,21 +13532,6 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= -sshpk@^1.7.0: - version "1.16.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" - integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - ssri@^6.0.1: version "6.0.2" resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5" @@ -13843,10 +13551,10 @@ stack-trace@0.0.x: resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= -stack-utils@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.2.tgz#5cf48b4557becb4638d0bc4f21d23f5d19586593" - integrity sha512-0H7QK2ECz3fyZMzQ8rH0j2ykpfbnd20BFtfg/SqVC2+sCTtcw0aDTGB7dk+de4U4uUeuz6nOtJcrkFFLG1B0Rg== +stack-utils@^2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5" + integrity sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA== dependencies: escape-string-regexp "^2.0.0" @@ -13873,11 +13581,6 @@ static-extend@^0.1.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= -stealthy-require@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" - integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= - stoppable@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b" @@ -14169,7 +13872,7 @@ supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8.1.0: +supports-color@^8.0.0, supports-color@^8.1.0: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== @@ -14342,10 +14045,10 @@ thenify-all@^1.0.0: dependencies: any-promise "^1.0.0" -throat@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" - integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== +throat@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" + integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w== throng@^5.0.0: version "5.0.0" @@ -14451,7 +14154,7 @@ tmp@^0.2.1: dependencies: rimraf "^3.0.0" -tmpl@1.0.x: +tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== @@ -14545,22 +14248,14 @@ touch@^3.1.0: dependencies: nopt "~1.0.10" -tough-cookie@^2.3.3, tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== +tough-cookie@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" + integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg== dependencies: - psl "^1.1.28" - punycode "^2.1.1" - -tough-cookie@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2" - integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg== - dependencies: - ip-regex "^2.1.0" - psl "^1.1.28" + psl "^1.1.33" punycode "^2.1.1" + universalify "^0.1.2" tr46@^1.0.1: version "1.0.1" @@ -14569,10 +14264,10 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" -tr46@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.0.2.tgz#03273586def1595ae08fedb38d7733cee91d2479" - integrity sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg== +tr46@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" + integrity sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw== dependencies: punycode "^2.1.1" @@ -14587,9 +14282,9 @@ triple-beam@^1.2.0, triple-beam@^1.3.0: integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== tsconfig-paths@^3.11.0, tsconfig-paths@^3.9.0: - version "3.11.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz#954c1fe973da6339c78e06b03ce2e48810b65f36" - integrity sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA== + version "3.12.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz#19769aca6ee8f6a1a341e38c8fa45dd9fb18899b" + integrity sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg== dependencies: "@types/json5" "^0.0.29" json5 "^1.0.1" @@ -14637,11 +14332,6 @@ turndown@^7.1.1: dependencies: domino "^2.1.6" -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -14842,7 +14532,7 @@ unique-string@^2.0.0: dependencies: crypto-random-string "^2.0.0" -universalify@^0.1.0: +universalify@^0.1.0, universalify@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== @@ -15035,11 +14725,6 @@ uuid@3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== -uuid@^3.3.2: - version "3.4.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== - uuid@^8.1.0, uuid@^8.3.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -15050,10 +14735,10 @@ v8-compile-cache@^2.0.3, v8-compile-cache@^2.1.1: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132" integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q== -v8-to-istanbul@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.0.0.tgz#b4fe00e35649ef7785a9b7fcebcea05f37c332fc" - integrity sha512-fLL2rFuQpMtm9r8hrAV2apXX/WqHJ6+IC4/eQVdMDGBUgH/YMV4Gv3duk3kjmyg6uiQWBAA9nJwue4iJUOkHeA== +v8-to-istanbul@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.0.tgz#0aeb763894f1a0a1676adf8a8b7612a38902446c" + integrity sha512-/PRhfd8aTNp9Ggr62HPzXg2XasNFGy5PBt0Rp04du7/8GNNSgxFL6WBTkgMKSL9bFjH+8kKEG3f37FmxiTqUUA== dependencies: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" @@ -15087,15 +14772,6 @@ vary@^1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - vinyl-fs@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-3.0.3.tgz#c85849405f67428feabbbd5c5dbdd64f47d31bc7" @@ -15191,12 +14867,12 @@ walk-sync@^2.2.0: matcher-collection "^2.0.0" minimatch "^3.0.4" -walker@^1.0.7, walker@~1.0.5: - version "1.0.7" - resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" - integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs= +walker@^1.0.7: + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== dependencies: - makeerror "1.0.x" + makeerror "1.0.12" watchpack-chokidar2@^2.0.1: version "2.0.1" @@ -15365,13 +15041,13 @@ whatwg-url@^7.0.0: tr46 "^1.0.1" webidl-conversions "^4.0.2" -whatwg-url@^8.0.0: - version "8.4.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.4.0.tgz#50fb9615b05469591d2b2bd6dfaed2942ed72837" - integrity sha512-vwTUFf6V4zhcPkWp/4CQPr1TW9Ml6SF4lVyaIMBdJw5i6qUUJ1QWM4Z6YYVkfka0OUIzVo/0aNtGVGk256IKWw== +whatwg-url@^8.0.0, whatwg-url@^8.5.0: + version "8.7.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" + integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg== dependencies: - lodash.sortby "^4.7.0" - tr46 "^2.0.2" + lodash "^4.7.0" + tr46 "^2.1.0" webidl-conversions "^6.1.0" which-boxed-primitive@^1.0.2: @@ -15397,7 +15073,7 @@ which@^1.2.14, which@^1.2.9, which@^1.3.1: dependencies: isexe "^2.0.0" -which@^2.0.1, which@^2.0.2: +which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== @@ -15682,10 +15358,10 @@ write@1.0.3: dependencies: mkdirp "^0.5.1" -ws@^7.2.3, ws@^7.5.3: - version "7.5.5" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881" - integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w== +ws@^7.4.6, ws@^7.5.3: + version "7.5.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" + integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== ws@^8.2.3: version "8.2.3" @@ -15861,7 +15537,7 @@ yargs@^13.3.2: y18n "^4.0.0" yargs-parser "^13.1.2" -yargs@^15.0.0, yargs@^15.4.1: +yargs@^15.0.0: version "15.4.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==