diff --git a/plugins/slack/server/auth/slack.ts b/plugins/slack/server/auth/slack.ts index bfd52597c..d780208a9 100644 --- a/plugins/slack/server/auth/slack.ts +++ b/plugins/slack/server/auth/slack.ts @@ -200,7 +200,8 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) { const { user } = ctx.state.auth; assertPresent(code || error, "code is required"); - const collectionId = state; + // FIX ME! What about having zod like schema in place here? + const collectionId = state as string; assertUuid(collectionId, "collectionId must be an uuid"); if (error) { diff --git a/plugins/webhooks/server/tasks/DeliverWebhookTask.ts b/plugins/webhooks/server/tasks/DeliverWebhookTask.ts index 827b150df..aab3c27fd 100644 --- a/plugins/webhooks/server/tasks/DeliverWebhookTask.ts +++ b/plugins/webhooks/server/tasks/DeliverWebhookTask.ts @@ -60,6 +60,7 @@ import { TeamEvent, UserEvent, ViewEvent, + WebhookDeliveryStatus, WebhookSubscriptionEvent, } from "@server/types"; import fetch from "@server/utils/fetch"; @@ -572,7 +573,8 @@ export default class DeliverWebhookTask extends BaseTask { status: "pending", }); - let response, requestBody, requestHeaders, status; + let response, requestBody, requestHeaders; + let status: WebhookDeliveryStatus; try { requestBody = presentWebhook({ event, diff --git a/server/collaboration/ViewsExtension.ts b/server/collaboration/ViewsExtension.ts index fc4096779..e68cd04e2 100644 --- a/server/collaboration/ViewsExtension.ts +++ b/server/collaboration/ViewsExtension.ts @@ -43,7 +43,7 @@ export class ViewsExtension implements Extension { ); await Promise.all([ View.touch(documentId, context.user.id, true), - context.user.update({ lastViewedAt: new Date() }), + context.user.update({ lastActiveAt: new Date() }), ]); } } diff --git a/server/commands/commentDestroyer.test.ts b/server/commands/commentDestroyer.test.ts index 51929f860..dfe8d6000 100644 --- a/server/commands/commentDestroyer.test.ts +++ b/server/commands/commentDestroyer.test.ts @@ -1,5 +1,5 @@ import { Comment, Event } from "@server/models"; -import { buildDocument, buildUser } from "@server/test/factories"; +import { buildComment, buildDocument, buildUser } from "@server/test/factories"; import commentDestroyer from "./commentDestroyer"; describe("commentDestroyer", () => { @@ -12,24 +12,9 @@ describe("commentDestroyer", () => { teamId: user.teamId, }); - const comment = await Comment.create({ - teamId: document.teamId, + const comment = await buildComment({ + userId: user.id, documentId: document.id, - data: { - type: "doc", - content: [ - { - type: "paragraph", - content: [ - { - type: "text", - text: "test", - }, - ], - }, - ], - }, - createdById: user.id, }); await commentDestroyer({ diff --git a/server/commands/documentCreator.ts b/server/commands/documentCreator.ts index cd8f88fd1..8424d0a25 100644 --- a/server/commands/documentCreator.ts +++ b/server/commands/documentCreator.ts @@ -78,7 +78,6 @@ export default async function documentCreator({ editorVersion, collectionId, teamId: user.teamId, - userId: user.id, createdAt, updatedAt: updatedAt ?? createdAt, lastModifiedById: user.id, diff --git a/server/commands/starCreator.test.ts b/server/commands/starCreator.test.ts index b037b00fb..f5917f10f 100644 --- a/server/commands/starCreator.test.ts +++ b/server/commands/starCreator.test.ts @@ -40,10 +40,8 @@ describe("starCreator", () => { }); await Star.create({ - teamId: document.teamId, documentId: document.id, userId: user.id, - createdById: user.id, index: "P", }); diff --git a/server/commands/starDestroyer.test.ts b/server/commands/starDestroyer.test.ts index 078296b57..1ff9ded95 100644 --- a/server/commands/starDestroyer.test.ts +++ b/server/commands/starDestroyer.test.ts @@ -13,10 +13,8 @@ describe("starDestroyer", () => { }); const star = await Star.create({ - teamId: document.teamId, documentId: document.id, userId: user.id, - createdById: user.id, index: "P", }); diff --git a/server/commands/starUpdater.test.ts b/server/commands/starUpdater.test.ts index 44ad8c457..08db69d1f 100644 --- a/server/commands/starUpdater.test.ts +++ b/server/commands/starUpdater.test.ts @@ -13,10 +13,8 @@ describe("starUpdater", () => { }); let star = await Star.create({ - teamId: document.teamId, documentId: document.id, userId: user.id, - createdById: user.id, index: "P", }); diff --git a/server/commands/teamCreator.ts b/server/commands/teamCreator.ts index d0668fce6..20daafe03 100644 --- a/server/commands/teamCreator.ts +++ b/server/commands/teamCreator.ts @@ -1,4 +1,4 @@ -import { Transaction } from "sequelize"; +import { InferCreationAttributes, Transaction } from "sequelize"; import slugify from "slugify"; import { RESERVED_SUBDOMAINS } from "@shared/utils/domains"; import { traceFunction } from "@server/logging/tracing"; @@ -50,7 +50,7 @@ async function teamCreator({ name, avatarUrl, authenticationProviders, - }, + } as Partial>, { include: ["authenticationProviders"], transaction, diff --git a/server/commands/teamPermanentDeleter.test.ts b/server/commands/teamPermanentDeleter.test.ts index 0d59b05d6..1f19978a7 100644 --- a/server/commands/teamPermanentDeleter.test.ts +++ b/server/commands/teamPermanentDeleter.test.ts @@ -70,7 +70,8 @@ describe("teamPermanentDeleter", () => { expect( await Collection.unscoped().count({ where: { - id: document.collectionId, + // buildDocument() above guarantees this to be non-null + id: document.collectionId!, }, paranoid: false, }) diff --git a/server/commands/userInviter.ts b/server/commands/userInviter.ts index a7f114092..567e51562 100644 --- a/server/commands/userInviter.ts +++ b/server/commands/userInviter.ts @@ -58,7 +58,6 @@ export default async function userInviter({ teamId: user.teamId, name: invite.name, email: invite.email, - service: null, isAdmin: invite.role === UserRole.Admin, isViewer: invite.role === UserRole.Viewer, invitedById: user.id, diff --git a/server/commands/userProvisioner.ts b/server/commands/userProvisioner.ts index f0101be89..7e3197e5b 100644 --- a/server/commands/userProvisioner.ts +++ b/server/commands/userProvisioner.ts @@ -1,3 +1,4 @@ +import { InferCreationAttributes } from "sequelize"; import InviteAcceptedEmail from "@server/emails/templates/InviteAcceptedEmail"; import { DomainNotAllowedError, @@ -236,9 +237,8 @@ export default async function userProvisioner({ isViewer: isAdmin === true ? false : defaultUserRole === "viewer", teamId, avatarUrl, - service: null, authentications: authentication ? [authentication] : [], - }, + } as Partial>, { include: "authentications", transaction, diff --git a/server/middlewares/authentication.ts b/server/middlewares/authentication.ts index 1259947dc..fb3249237 100644 --- a/server/middlewares/authentication.ts +++ b/server/middlewares/authentication.ts @@ -101,7 +101,7 @@ export default function auth(options: AuthenticationOptions = {}) { if (user.isSuspended) { const suspendingAdmin = await User.findOne({ where: { - id: user.suspendedById, + id: user.suspendedById!, }, paranoid: false, }); diff --git a/server/models/ApiKey.ts b/server/models/ApiKey.ts index 8c5a49e31..6b6a06398 100644 --- a/server/models/ApiKey.ts +++ b/server/models/ApiKey.ts @@ -1,4 +1,5 @@ import randomstring from "randomstring"; +import { InferAttributes, InferCreationAttributes } from "sequelize"; import { Column, Table, @@ -14,7 +15,10 @@ import Length from "./validators/Length"; @Table({ tableName: "apiKeys", modelName: "apiKey" }) @Fix -class ApiKey extends ParanoidModel { +class ApiKey extends ParanoidModel< + InferAttributes, + Partial> +> { static prefix = "ol_api_"; @Length({ diff --git a/server/models/Attachment.ts b/server/models/Attachment.ts index 26e794cc2..22f11e49f 100644 --- a/server/models/Attachment.ts +++ b/server/models/Attachment.ts @@ -1,7 +1,11 @@ import { createReadStream } from "fs"; import path from "path"; import { File } from "formidable"; -import { QueryTypes } from "sequelize"; +import { + InferAttributes, + InferCreationAttributes, + QueryTypes, +} from "sequelize"; import { BeforeDestroy, BelongsTo, @@ -25,7 +29,10 @@ import Length from "./validators/Length"; @Table({ tableName: "attachments", modelName: "attachment" }) @Fix -class Attachment extends IdModel { +class Attachment extends IdModel< + InferAttributes, + Partial> +> { @Length({ max: 4096, msg: "key must be 4096 characters or less", diff --git a/server/models/AuthenticationProvider.ts b/server/models/AuthenticationProvider.ts index 48288eec2..9e0bfda2e 100644 --- a/server/models/AuthenticationProvider.ts +++ b/server/models/AuthenticationProvider.ts @@ -1,4 +1,9 @@ -import { Op, SaveOptions } from "sequelize"; +import { + InferAttributes, + InferCreationAttributes, + InstanceUpdateOptions, + Op, +} from "sequelize"; import { BelongsTo, Column, @@ -8,11 +13,11 @@ import { ForeignKey, HasMany, Table, - Model, IsUUID, PrimaryKey, } from "sequelize-typescript"; import env from "@server/env"; +import Model from "@server/models/base/Model"; import AzureClient from "@server/utils/azure"; import GoogleClient from "@server/utils/google"; import OIDCClient from "@server/utils/oidc"; @@ -28,7 +33,10 @@ import Length from "./validators/Length"; updatedAt: false, }) @Fix -class AuthenticationProvider extends Model { +class AuthenticationProvider extends Model< + InferAttributes, + Partial> +> { @IsUUID(4) @PrimaryKey @Default(DataType.UUIDV4) @@ -97,7 +105,9 @@ class AuthenticationProvider extends Model { } } - disable = async (options?: SaveOptions) => { + disable: ( + options?: InstanceUpdateOptions> + ) => Promise = async (options) => { const res = await ( this.constructor as typeof AuthenticationProvider ).findAndCountAll({ @@ -124,7 +134,9 @@ class AuthenticationProvider extends Model { } }; - enable = (options?: SaveOptions) => + enable: ( + options?: InstanceUpdateOptions> + ) => Promise = (options) => this.update( { enabled: true, diff --git a/server/models/Backlink.ts b/server/models/Backlink.ts index 833f53607..39df9c1e3 100644 --- a/server/models/Backlink.ts +++ b/server/models/Backlink.ts @@ -1,3 +1,4 @@ +import { InferAttributes, InferCreationAttributes } from "sequelize"; import { DataType, BelongsTo, @@ -12,7 +13,10 @@ import Fix from "./decorators/Fix"; @Table({ tableName: "backlinks", modelName: "backlink" }) @Fix -class Backlink extends IdModel { +class Backlink extends IdModel< + InferAttributes, + Partial> +> { @BelongsTo(() => User, "userId") user: User; diff --git a/server/models/Collection.test.ts b/server/models/Collection.test.ts index d7d7407fc..d1e22631c 100644 --- a/server/models/Collection.test.ts +++ b/server/models/Collection.test.ts @@ -223,11 +223,10 @@ describe("#updateDocument", () => { const document = await buildDocument({ collectionId: collection.id }); await collection.reload(); - const newDocument = await Document.create({ + const newDocument = await buildDocument({ parentDocumentId: document.id, collectionId: collection.id, teamId: collection.teamId, - userId: collection.createdById, lastModifiedById: collection.createdById, createdById: collection.createdById, title: "Child document", @@ -277,11 +276,10 @@ describe("#removeDocument", () => { await collection.reload(); // Add a child for testing - const newDocument = await Document.create({ + const newDocument = await buildDocument({ parentDocumentId: document.id, collectionId: collection.id, teamId: collection.teamId, - userId: collection.createdById, lastModifiedById: collection.createdById, createdById: collection.createdById, title: "Child document", @@ -306,11 +304,10 @@ describe("#removeDocument", () => { await collection.reload(); // Add a child for testing - const newDocument = await Document.create({ + const newDocument = await buildDocument({ parentDocumentId: document.id, collectionId: collection.id, teamId: collection.teamId, - userId: collection.createdById, lastModifiedById: collection.createdById, createdById: collection.createdById, publishedAt: new Date(), diff --git a/server/models/Collection.ts b/server/models/Collection.ts index 4c4d46ba2..a93b6301c 100644 --- a/server/models/Collection.ts +++ b/server/models/Collection.ts @@ -10,6 +10,8 @@ import { Op, FindOptions, NonNullFindOptions, + InferAttributes, + InferCreationAttributes, } from "sequelize"; import { Sequelize, @@ -159,7 +161,10 @@ import NotContainsUrl from "./validators/NotContainsUrl"; })) @Table({ tableName: "collections", modelName: "collection" }) @Fix -class Collection extends ParanoidModel { +class Collection extends ParanoidModel< + InferAttributes, + Partial> +> { @SimpleLength({ min: 10, max: 10, diff --git a/server/models/Comment.ts b/server/models/Comment.ts index 8a65f5eb4..4ac2af5cd 100644 --- a/server/models/Comment.ts +++ b/server/models/Comment.ts @@ -1,3 +1,4 @@ +import { InferAttributes, InferCreationAttributes } from "sequelize"; import { DataType, BelongsTo, @@ -38,7 +39,10 @@ import TextLength from "./validators/TextLength"; })) @Table({ tableName: "comments", modelName: "comment" }) @Fix -class Comment extends ParanoidModel { +class Comment extends ParanoidModel< + InferAttributes, + Partial> +> { @TextLength({ max: CommentValidation.maxLength, msg: `Comment must be less than ${CommentValidation.maxLength} characters`, diff --git a/server/models/Document.ts b/server/models/Document.ts index 67ee3092a..80ce0ebad 100644 --- a/server/models/Document.ts +++ b/server/models/Document.ts @@ -3,7 +3,13 @@ import compact from "lodash/compact"; import isNil from "lodash/isNil"; import uniq from "lodash/uniq"; import randomstring from "randomstring"; -import type { Identifier, NonNullFindOptions, SaveOptions } from "sequelize"; +import type { + Identifier, + InferAttributes, + InferCreationAttributes, + NonNullFindOptions, + SaveOptions, +} from "sequelize"; import { Sequelize, Transaction, @@ -186,7 +192,10 @@ type AdditionalFindOptions = { })) @Table({ tableName: "documents", modelName: "document" }) @Fix -class Document extends ParanoidModel { +class Document extends ParanoidModel< + InferAttributes, + Partial> +> { @SimpleLength({ min: 10, max: 10, @@ -208,7 +217,7 @@ class Document extends ParanoidModel { @IsNumeric @Column(DataType.SMALLINT) - version: number; + version?: number | null; @Default(false) @Column @@ -261,7 +270,7 @@ class Document extends ParanoidModel { msg: `Document collaborative state is too large, you must create a new document`, }) @Column(DataType.BLOB) - state: Uint8Array; + state?: Uint8Array | null; /** Whether this document is part of onboarding. */ @Default(false) @@ -387,14 +396,13 @@ class Document extends ParanoidModel { // ensure documents have a title model.title = model.title || ""; - if (model.previous("title") && model.previous("title") !== model.title) { + const previousTitle = model.previous("title"); + if (previousTitle && previousTitle !== model.title) { if (!model.previousTitles) { model.previousTitles = []; } - model.previousTitles = uniq( - model.previousTitles.concat(model.previous("title")) - ); + model.previousTitles = uniq(model.previousTitles.concat(previousTitle)); } // add the current user as a collaborator on this doc diff --git a/server/models/Event.ts b/server/models/Event.ts index 53503ebb5..a6742f15d 100644 --- a/server/models/Event.ts +++ b/server/models/Event.ts @@ -1,4 +1,9 @@ -import type { SaveOptions, WhereOptions } from "sequelize"; +import type { + InferAttributes, + InferCreationAttributes, + SaveOptions, + WhereOptions, +} from "sequelize"; import { ForeignKey, AfterSave, @@ -22,7 +27,10 @@ import Fix from "./decorators/Fix"; @Table({ tableName: "events", modelName: "event", updatedAt: false }) @Fix -class Event extends IdModel { +class Event extends IdModel< + InferAttributes, + Partial> +> { @IsUUID(4) @Column(DataType.UUID) modelId: string | null; diff --git a/server/models/FileOperation.ts b/server/models/FileOperation.ts index 8d945efc8..32c821419 100644 --- a/server/models/FileOperation.ts +++ b/server/models/FileOperation.ts @@ -1,4 +1,9 @@ -import { Op, WhereOptions } from "sequelize"; +import { + InferAttributes, + InferCreationAttributes, + Op, + WhereOptions, +} from "sequelize"; import { ForeignKey, DefaultScope, @@ -36,7 +41,10 @@ import Fix from "./decorators/Fix"; })) @Table({ tableName: "file_operations", modelName: "file_operation" }) @Fix -class FileOperation extends ParanoidModel { +class FileOperation extends ParanoidModel< + InferAttributes, + Partial> +> { @Column(DataType.ENUM(...Object.values(FileOperationType))) type: FileOperationType; @@ -50,7 +58,7 @@ class FileOperation extends ParanoidModel { key: string; @Column - url: string; + url?: string | null; @Column error: string | null; @@ -118,7 +126,7 @@ class FileOperation extends ParanoidModel { @ForeignKey(() => Collection) @Column(DataType.UUID) - collectionId: string; + collectionId?: string | null; /** * Count the number of export file operations for a given team after a point diff --git a/server/models/Group.ts b/server/models/Group.ts index 91b1df91d..ef5a43dd9 100644 --- a/server/models/Group.ts +++ b/server/models/Group.ts @@ -1,4 +1,4 @@ -import { Op } from "sequelize"; +import { InferAttributes, InferCreationAttributes, Op } from "sequelize"; import { AfterDestroy, BelongsTo, @@ -69,7 +69,10 @@ import NotContainsUrl from "./validators/NotContainsUrl"; }, }) @Fix -class Group extends ParanoidModel { +class Group extends ParanoidModel< + InferAttributes, + Partial> +> { @Length({ min: 0, max: 255, msg: "name must be be 255 characters or less" }) @NotContainsUrl @Column diff --git a/server/models/GroupPermission.ts b/server/models/GroupPermission.ts index 441263531..802eadd88 100644 --- a/server/models/GroupPermission.ts +++ b/server/models/GroupPermission.ts @@ -1,4 +1,4 @@ -import { Op } from "sequelize"; +import { InferAttributes, InferCreationAttributes, Op } from "sequelize"; import { BelongsTo, Column, @@ -40,7 +40,10 @@ import Fix from "./decorators/Fix"; })) @Table({ tableName: "group_permissions", modelName: "group_permission" }) @Fix -class GroupPermission extends ParanoidModel { +class GroupPermission extends ParanoidModel< + InferAttributes, + Partial> +> { @Default(CollectionPermission.ReadWrite) @IsIn([Object.values(CollectionPermission)]) @Column(DataType.STRING) @@ -71,6 +74,10 @@ class GroupPermission extends ParanoidModel { @BelongsTo(() => User, "createdById") createdBy: User; + + @ForeignKey(() => User) + @Column(DataType.UUID) + createdById: string; } export default GroupPermission; diff --git a/server/models/GroupUser.ts b/server/models/GroupUser.ts index 520b18929..ce267122b 100644 --- a/server/models/GroupUser.ts +++ b/server/models/GroupUser.ts @@ -1,3 +1,4 @@ +import { InferAttributes, InferCreationAttributes } from "sequelize"; import { DefaultScope, BelongsTo, @@ -37,7 +38,10 @@ import Fix from "./decorators/Fix"; })) @Table({ tableName: "group_users", modelName: "group_user", paranoid: true }) @Fix -class GroupUser extends Model { +class GroupUser extends Model< + InferAttributes, + Partial> +> { @BelongsTo(() => User, "userId") user: User; diff --git a/server/models/Integration.ts b/server/models/Integration.ts index 4906cc8bd..05254250f 100644 --- a/server/models/Integration.ts +++ b/server/models/Integration.ts @@ -1,3 +1,4 @@ +import { InferAttributes, InferCreationAttributes } from "sequelize"; import { ForeignKey, BelongsTo, @@ -8,7 +9,10 @@ import { IsIn, } from "sequelize-typescript"; import { IntegrationType, IntegrationService } from "@shared/types"; -import type { IntegrationSettings } from "@shared/types"; +import type { + IntegrationSettings, + UserCreatableIntegrationService, +} from "@shared/types"; import Collection from "./Collection"; import IntegrationAuthentication from "./IntegrationAuthentication"; import Team from "./Team"; @@ -16,12 +20,6 @@ import User from "./User"; import IdModel from "./base/IdModel"; import Fix from "./decorators/Fix"; -export enum UserCreatableIntegrationService { - Diagrams = "diagrams", - Grist = "grist", - GoogleAnalytics = "google-analytics", -} - @Scopes(() => ({ withAuthentication: { include: [ @@ -35,14 +33,17 @@ export enum UserCreatableIntegrationService { })) @Table({ tableName: "integrations", modelName: "integration" }) @Fix -class Integration extends IdModel { +class Integration extends IdModel< + InferAttributes>, + Partial>> +> { @IsIn([Object.values(IntegrationType)]) @Column(DataType.STRING) type: IntegrationType; @IsIn([Object.values(IntegrationService)]) @Column(DataType.STRING) - service: IntegrationService; + service: IntegrationService | UserCreatableIntegrationService; @Column(DataType.JSONB) settings: IntegrationSettings; @@ -67,11 +68,11 @@ class Integration extends IdModel { teamId: string; @BelongsTo(() => Collection, "collectionId") - collection: Collection; + collection?: Collection | null; @ForeignKey(() => Collection) @Column(DataType.UUID) - collectionId: string; + collectionId?: string | null; @BelongsTo(() => IntegrationAuthentication, "authenticationId") authentication: IntegrationAuthentication; diff --git a/server/models/IntegrationAuthentication.ts b/server/models/IntegrationAuthentication.ts index 207258aff..f444aabe7 100644 --- a/server/models/IntegrationAuthentication.ts +++ b/server/models/IntegrationAuthentication.ts @@ -1,3 +1,4 @@ +import { InferAttributes, InferCreationAttributes } from "sequelize"; import { DataType, Table, @@ -17,7 +18,10 @@ import Fix from "./decorators/Fix"; @Table({ tableName: "authentications", modelName: "authentication" }) @Fix -class IntegrationAuthentication extends IdModel { +class IntegrationAuthentication extends IdModel< + InferAttributes, + Partial> +> { @Column(DataType.STRING) service: IntegrationService; diff --git a/server/models/Notification.ts b/server/models/Notification.ts index 52cb5db35..f8645ecf4 100644 --- a/server/models/Notification.ts +++ b/server/models/Notification.ts @@ -1,9 +1,12 @@ import crypto from "crypto"; -import type { SaveOptions } from "sequelize"; +import type { + InferAttributes, + InferCreationAttributes, + SaveOptions, +} from "sequelize"; import { Table, ForeignKey, - Model, Column, PrimaryKey, IsUUID, @@ -18,6 +21,7 @@ import { } from "sequelize-typescript"; import { NotificationEventType } from "@shared/types"; import env from "@server/env"; +import Model from "@server/models/base/Model"; import Collection from "./Collection"; import Comment from "./Comment"; import Document from "./Document"; @@ -83,7 +87,10 @@ import Fix from "./decorators/Fix"; updatedAt: false, }) @Fix -class Notification extends Model { +class Notification extends Model< + InferAttributes, + Partial> +> { @IsUUID(4) @PrimaryKey @Default(DataType.UUIDV4) @@ -92,7 +99,7 @@ class Notification extends Model { @AllowNull @Column - emailedAt: Date; + emailedAt?: Date | null; @AllowNull @Column diff --git a/server/models/Pin.ts b/server/models/Pin.ts index 80f2c0309..91005a7fb 100644 --- a/server/models/Pin.ts +++ b/server/models/Pin.ts @@ -1,3 +1,4 @@ +import { InferAttributes, InferCreationAttributes } from "sequelize"; import { DataType, Column, @@ -14,7 +15,10 @@ import Fix from "./decorators/Fix"; @Table({ tableName: "pins", modelName: "pin" }) @Fix -class Pin extends IdModel { +class Pin extends IdModel< + InferAttributes, + Partial> +> { @Column index: string | null; @@ -28,11 +32,11 @@ class Pin extends IdModel { createdById: string; @BelongsTo(() => Collection, "collectionId") - collection: Collection; + collection?: Collection | null; @ForeignKey(() => Collection) @Column(DataType.UUID) - collectionId: string; + collectionId?: string | null; @BelongsTo(() => Document, "documentId") document: Document; diff --git a/server/models/Revision.ts b/server/models/Revision.ts index ff01926d1..b812c99bd 100644 --- a/server/models/Revision.ts +++ b/server/models/Revision.ts @@ -1,4 +1,9 @@ -import { Op, SaveOptions } from "sequelize"; +import { + InferAttributes, + InferCreationAttributes, + Op, + SaveOptions, +} from "sequelize"; import { DataType, BelongsTo, @@ -28,10 +33,13 @@ import Length from "./validators/Length"; })) @Table({ tableName: "revisions", modelName: "revision" }) @Fix -class Revision extends IdModel { +class Revision extends IdModel< + InferAttributes, + Partial> +> { @IsNumeric @Column(DataType.SMALLINT) - version: number; + version?: number | null; @SimpleLength({ max: 255, @@ -133,7 +141,7 @@ class Revision extends IdModel { */ static createFromDocument( document: Document, - options?: SaveOptions + options?: SaveOptions> ) { const revision = this.buildFromDocument(document); return revision.save(options); diff --git a/server/models/SearchQuery.ts b/server/models/SearchQuery.ts index 2bff820f4..4c1967f53 100644 --- a/server/models/SearchQuery.ts +++ b/server/models/SearchQuery.ts @@ -1,7 +1,7 @@ +import { InferAttributes, InferCreationAttributes } from "sequelize"; import { Table, ForeignKey, - Model, Column, PrimaryKey, IsUUID, @@ -10,8 +10,10 @@ import { DataType, Default, } from "sequelize-typescript"; -import Team from "./Team"; -import User from "./User"; +import Share from "@server/models/Share"; +import Team from "@server/models/Team"; +import User from "@server/models/User"; +import Model from "@server/models/base/Model"; import Fix from "./decorators/Fix"; @Table({ @@ -20,7 +22,10 @@ import Fix from "./decorators/Fix"; updatedAt: false, }) @Fix -class SearchQuery extends Model { +class SearchQuery extends Model< + InferAttributes, + Partial> +> { @IsUUID(4) @PrimaryKey @Default(DataType.UUIDV4) @@ -69,11 +74,18 @@ class SearchQuery extends Model { // associations @BelongsTo(() => User, "userId") - user: User; + user?: User | null; @ForeignKey(() => User) @Column(DataType.UUID) - userId: string; + userId?: string | null; + + @BelongsTo(() => Share, "shareId") + share?: Share | null; + + @ForeignKey(() => Share) + @Column(DataType.UUID) + shareId?: string | null; @BelongsTo(() => Team, "teamId") team: Team; diff --git a/server/models/Share.ts b/server/models/Share.ts index 1f13c2f64..2657756c5 100644 --- a/server/models/Share.ts +++ b/server/models/Share.ts @@ -1,4 +1,8 @@ -import { type SaveOptions } from "sequelize"; +import { + InferAttributes, + InferCreationAttributes, + type SaveOptions, +} from "sequelize"; import { ForeignKey, BelongsTo, @@ -69,7 +73,10 @@ import Length from "./validators/Length"; })) @Table({ tableName: "shares", modelName: "share" }) @Fix -class Share extends IdModel { +class Share extends IdModel< + InferAttributes, + Partial> +> { @Column published: boolean; diff --git a/server/models/Star.ts b/server/models/Star.ts index 55ffe5a54..009a8a53a 100644 --- a/server/models/Star.ts +++ b/server/models/Star.ts @@ -1,3 +1,4 @@ +import { InferAttributes, InferCreationAttributes } from "sequelize"; import { Column, DataType, @@ -13,7 +14,10 @@ import Fix from "./decorators/Fix"; @Table({ tableName: "stars", modelName: "star" }) @Fix -class Star extends IdModel { +class Star extends IdModel< + InferAttributes, + Partial> +> { @Column index: string | null; diff --git a/server/models/Subscription.ts b/server/models/Subscription.ts index 4ca5338dd..3b70a3a6b 100644 --- a/server/models/Subscription.ts +++ b/server/models/Subscription.ts @@ -1,3 +1,4 @@ +import { InferAttributes, InferCreationAttributes } from "sequelize"; import { Column, DataType, @@ -23,7 +24,10 @@ import Fix from "./decorators/Fix"; })) @Table({ tableName: "subscriptions", modelName: "subscription" }) @Fix -class Subscription extends ParanoidModel { +class Subscription extends ParanoidModel< + InferAttributes, + Partial> +> { @BelongsTo(() => User, "userId") user: User; diff --git a/server/models/Team.ts b/server/models/Team.ts index 77d05306b..2c894ddb4 100644 --- a/server/models/Team.ts +++ b/server/models/Team.ts @@ -3,7 +3,11 @@ import fs from "fs"; import path from "path"; import { URL } from "url"; import util from "util"; -import { type SaveOptions } from "sequelize"; +import { + InferAttributes, + InferCreationAttributes, + type SaveOptions, +} from "sequelize"; import { Op } from "sequelize"; import { Column, @@ -65,7 +69,10 @@ const readFile = util.promisify(fs.readFile); })) @Table({ tableName: "teams", modelName: "team" }) @Fix -class Team extends ParanoidModel { +class Team extends ParanoidModel< + InferAttributes, + Partial> +> { @NotContainsUrl @Length({ min: 2, max: 255, msg: "name must be between 2 to 255 characters" }) @Column @@ -275,7 +282,6 @@ class Team extends ParanoidModel { parentDocumentId: null, collectionId: collection.id, teamId: collection.teamId, - userId: collection.createdById, lastModifiedById: collection.createdById, createdById: collection.createdById, title, @@ -366,14 +372,9 @@ class Team extends ParanoidModel { @AfterUpdate static deletePreviousAvatar = async (model: Team) => { - if ( - model.previous("avatarUrl") && - model.previous("avatarUrl") !== model.avatarUrl - ) { - const attachmentIds = parseAttachmentIds( - model.previous("avatarUrl"), - true - ); + const previousAvatarUrl = model.previous("avatarUrl"); + if (previousAvatarUrl && previousAvatarUrl !== model.avatarUrl) { + const attachmentIds = parseAttachmentIds(previousAvatarUrl, true); if (!attachmentIds.length) { return; } diff --git a/server/models/TeamDomain.ts b/server/models/TeamDomain.ts index f96f63383..1447d26f7 100644 --- a/server/models/TeamDomain.ts +++ b/server/models/TeamDomain.ts @@ -1,4 +1,5 @@ import emailProviders from "email-providers"; +import { InferAttributes, InferCreationAttributes } from "sequelize"; import { Column, Table, @@ -21,7 +22,10 @@ import Length from "./validators/Length"; @Table({ tableName: "team_domains", modelName: "team_domain" }) @Fix -class TeamDomain extends IdModel { +class TeamDomain extends IdModel< + InferAttributes, + Partial> +> { @NotIn({ args: env.isCloudHosted ? [emailProviders] : [], msg: "You chose a restricted domain, please try another.", diff --git a/server/models/User.ts b/server/models/User.ts index bfa8afdfc..1c6249c66 100644 --- a/server/models/User.ts +++ b/server/models/User.ts @@ -8,6 +8,9 @@ import { SaveOptions, Op, FindOptions, + InferAttributes, + InferCreationAttributes, + InstanceUpdateOptions, } from "sequelize"; import { Table, @@ -40,6 +43,7 @@ import { } from "@shared/types"; import { stringToColor } from "@shared/utils/color"; import env from "@server/env"; +import Model from "@server/models/base/Model"; import DeleteAttachmentTask from "@server/queues/tasks/DeleteAttachmentTask"; import parseAttachmentIds from "@server/utils/parseAttachmentIds"; import { ValidationError } from "../errors"; @@ -117,7 +121,10 @@ export enum UserFlag { })) @Table({ tableName: "users", modelName: "user" }) @Fix -class User extends ParanoidModel { +class User extends ParanoidModel< + InferAttributes, + Partial> +> { @IsEmail @Length({ max: 255, msg: "User email must be 255 characters or less" }) @Column @@ -519,7 +526,10 @@ class User extends ParanoidModel { ], }); - demote = async (to: UserRole, options?: SaveOptions) => { + demote: ( + to: UserRole, + options?: InstanceUpdateOptions> + ) => Promise = async (to, options) => { const res = await (this.constructor as typeof User).findAndCountAll({ where: { teamId: this.teamId, @@ -568,7 +578,9 @@ class User extends ParanoidModel { } }; - promote = (options?: SaveOptions) => + promote: ( + options?: InstanceUpdateOptions> + ) => Promise = (options) => this.update( { isAdmin: true, @@ -605,14 +617,9 @@ class User extends ParanoidModel { @AfterUpdate static deletePreviousAvatar = async (model: User) => { - if ( - model.previous("avatarUrl") && - model.previous("avatarUrl") !== model.avatarUrl - ) { - const attachmentIds = parseAttachmentIds( - model.previous("avatarUrl"), - true - ); + const previousAvatarUrl = model.previous("avatarUrl"); + if (previousAvatarUrl && previousAvatarUrl !== model.avatarUrl) { + const attachmentIds = parseAttachmentIds(previousAvatarUrl, true); if (!attachmentIds.length) { return; } diff --git a/server/models/UserAuthentication.ts b/server/models/UserAuthentication.ts index c1940d74f..d6b402e01 100644 --- a/server/models/UserAuthentication.ts +++ b/server/models/UserAuthentication.ts @@ -1,6 +1,10 @@ import { addMinutes, subMinutes } from "date-fns"; import invariant from "invariant"; -import { SaveOptions } from "sequelize"; +import { + InferAttributes, + InferCreationAttributes, + SaveOptions, +} from "sequelize"; import { BeforeCreate, BelongsTo, @@ -22,7 +26,10 @@ import Fix from "./decorators/Fix"; @Table({ tableName: "user_authentications", modelName: "user_authentication" }) @Fix -class UserAuthentication extends IdModel { +class UserAuthentication extends IdModel< + InferAttributes, + Partial> +> { @Column(DataType.ARRAY(DataType.STRING)) scopes: string[]; diff --git a/server/models/UserPermission.ts b/server/models/UserPermission.ts index 5afabe7ca..8576dae18 100644 --- a/server/models/UserPermission.ts +++ b/server/models/UserPermission.ts @@ -1,4 +1,4 @@ -import { Op } from "sequelize"; +import { InferAttributes, InferCreationAttributes, Op } from "sequelize"; import { Column, ForeignKey, @@ -39,7 +39,10 @@ import Fix from "./decorators/Fix"; })) @Table({ tableName: "user_permissions", modelName: "user_permission" }) @Fix -class UserPermission extends IdModel { +class UserPermission extends IdModel< + InferAttributes, + Partial> +> { @Default(CollectionPermission.ReadWrite) @IsIn([Object.values(CollectionPermission)]) @Column(DataType.STRING) diff --git a/server/models/View.ts b/server/models/View.ts index eb5877700..53faf871e 100644 --- a/server/models/View.ts +++ b/server/models/View.ts @@ -1,4 +1,9 @@ -import { FindOrCreateOptions, Op } from "sequelize"; +import { + FindOrCreateOptions, + InferAttributes, + InferCreationAttributes, + Op, +} from "sequelize"; import { BelongsTo, Column, @@ -26,7 +31,10 @@ import Fix from "./decorators/Fix"; })) @Table({ tableName: "views", modelName: "view" }) @Fix -class View extends IdModel { +class View extends IdModel< + InferAttributes, + Partial> +> { @Column lastEditingAt: Date | null; @@ -55,7 +63,7 @@ class View extends IdModel { userId: string; documentId: string; }, - options?: FindOrCreateOptions + options?: FindOrCreateOptions> ) { const [model, created] = await this.findOrCreate({ ...options, diff --git a/server/models/WebhookDelivery.ts b/server/models/WebhookDelivery.ts index 7e3ffe9b9..8b9e49b1d 100644 --- a/server/models/WebhookDelivery.ts +++ b/server/models/WebhookDelivery.ts @@ -1,3 +1,4 @@ +import { InferAttributes, InferCreationAttributes } from "sequelize"; import { Column, Table, @@ -7,6 +8,7 @@ import { DataType, IsIn, } from "sequelize-typescript"; +import { type WebhookDeliveryStatus } from "@server/types"; import WebhookSubscription from "./WebhookSubscription"; import IdModel from "./base/IdModel"; import Fix from "./decorators/Fix"; @@ -16,14 +18,17 @@ import Fix from "./decorators/Fix"; modelName: "webhook_delivery", }) @Fix -class WebhookDelivery extends IdModel { +class WebhookDelivery extends IdModel< + InferAttributes, + Partial> +> { @NotEmpty @IsIn([["pending", "success", "failed"]]) @Column(DataType.STRING) - status: "pending" | "success" | "failed"; + status: WebhookDeliveryStatus; @Column(DataType.INTEGER) - statusCode: number; + statusCode?: number | null; @Column(DataType.JSONB) requestBody: unknown; diff --git a/server/models/WebhookSubscription.ts b/server/models/WebhookSubscription.ts index acf94c1f1..f598edbc4 100644 --- a/server/models/WebhookSubscription.ts +++ b/server/models/WebhookSubscription.ts @@ -1,6 +1,10 @@ import crypto from "crypto"; import isEmpty from "lodash/isEmpty"; -import { SaveOptions } from "sequelize"; +import { + InferAttributes, + InferCreationAttributes, + InstanceUpdateOptions, +} from "sequelize"; import { Column, Table, @@ -39,7 +43,10 @@ import Length from "./validators/Length"; modelName: "webhook_subscription", }) @Fix -class WebhookSubscription extends ParanoidModel { +class WebhookSubscription extends ParanoidModel< + InferAttributes, + Partial> +> { @NotEmpty @Length({ max: 255, msg: "Webhook name be less than 255 characters" }) @Column @@ -110,7 +117,9 @@ class WebhookSubscription extends ParanoidModel { * @param options Save options * @returns Promise */ - public async disable(options?: SaveOptions) { + public async disable( + options?: InstanceUpdateOptions> + ) { return this.update({ enabled: false }, options); } diff --git a/server/models/base/IdModel.ts b/server/models/base/IdModel.ts index 5fc2b6468..f76dbeba7 100644 --- a/server/models/base/IdModel.ts +++ b/server/models/base/IdModel.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/ban-types */ import { CreatedAt, UpdatedAt, @@ -9,7 +10,10 @@ import { } from "sequelize-typescript"; import Model from "./Model"; -class IdModel extends Model { +class IdModel< + TModelAttributes extends {} = any, + TCreationAttributes extends {} = TModelAttributes +> extends Model { @IsUUID(4) @PrimaryKey @Default(DataType.UUIDV4) diff --git a/server/models/base/Model.ts b/server/models/base/Model.ts index 52eb415a0..51b0b977a 100644 --- a/server/models/base/Model.ts +++ b/server/models/base/Model.ts @@ -1,7 +1,11 @@ +/* eslint-disable @typescript-eslint/ban-types */ import { FindOptions } from "sequelize"; import { Model as SequelizeModel } from "sequelize-typescript"; -class Model extends SequelizeModel { +class Model< + TModelAttributes extends {} = any, + TCreationAttributes extends {} = TModelAttributes +> extends SequelizeModel { /** * Find all models in batches, calling the callback function for each batch. * diff --git a/server/models/base/ParanoidModel.ts b/server/models/base/ParanoidModel.ts index 335240c35..7ebd8147e 100644 --- a/server/models/base/ParanoidModel.ts +++ b/server/models/base/ParanoidModel.ts @@ -1,7 +1,11 @@ +/* eslint-disable @typescript-eslint/ban-types */ import { DeletedAt } from "sequelize-typescript"; import IdModel from "./IdModel"; -class ParanoidModel extends IdModel { +class ParanoidModel< + TModelAttributes extends {} = any, + TCreationAttributes extends {} = TModelAttributes +> extends IdModel { @DeletedAt deletedAt: Date | null; } diff --git a/server/queues/tasks/RevisionCreatedNotificationsTask.test.ts b/server/queues/tasks/RevisionCreatedNotificationsTask.test.ts index 37581efad..dc0730ef4 100644 --- a/server/queues/tasks/RevisionCreatedNotificationsTask.test.ts +++ b/server/queues/tasks/RevisionCreatedNotificationsTask.test.ts @@ -112,7 +112,6 @@ describe("revisions.create", () => { userId: subscriber.id, documentId: document.id, event: "documents.update", - enabled: true, }); const task = new RevisionCreatedNotificationsTask(); @@ -303,7 +302,6 @@ describe("revisions.create", () => { userId: subscriber.id, documentId: document.id, event: "documents.update", - enabled: true, }); const task = new RevisionCreatedNotificationsTask(); @@ -343,7 +341,6 @@ describe("revisions.create", () => { userId: subscriber.id, documentId: document.id, event: "documents.update", - enabled: true, }); await subscription.destroy(); @@ -391,7 +388,6 @@ describe("revisions.create", () => { userId: subscriber.id, documentId: document.id, event: "documents.update", - enabled: true, }); const task = new RevisionCreatedNotificationsTask(); diff --git a/server/routes/api/comments/comments.test.ts b/server/routes/api/comments/comments.test.ts index 7406e0b13..4dcbcea4f 100644 --- a/server/routes/api/comments/comments.test.ts +++ b/server/routes/api/comments/comments.test.ts @@ -26,7 +26,6 @@ describe("#comments.list", () => { }); const comment = await buildComment({ userId: user.id, - teamId: team.id, documentId: document.id, }); const res = await server.post("/api/comments.list", { @@ -58,7 +57,6 @@ describe("#comments.list", () => { }); const comment = await buildComment({ userId: user.id, - teamId: team.id, documentId: document.id, }); const res = await server.post("/api/comments.list", { @@ -99,12 +97,10 @@ describe("#comments.list", () => { }); const comment1 = await buildComment({ userId: user.id, - teamId: team.id, documentId: document1.id, }); const comment2 = await buildComment({ userId: user.id, - teamId: team.id, documentId: document2.id, }); const res = await server.post("/api/comments.list", { @@ -143,7 +139,6 @@ describe("#comments.create", () => { const comment = await buildComment({ userId: user.id, - teamId: team.id, documentId: document.id, }); @@ -222,7 +217,6 @@ describe("#comments.info", () => { }); const comment = await buildComment({ userId: user.id, - teamId: team.id, documentId: document.id, }); const res = await server.post("/api/comments.info", { diff --git a/server/routes/api/documents/documents.ts b/server/routes/api/documents/documents.ts index e8d16da63..ab1b3bd5f 100644 --- a/server/routes/api/documents/documents.ts +++ b/server/routes/api/documents/documents.ts @@ -906,7 +906,6 @@ router.post( editorVersion: original.editorVersion, collectionId: original.collectionId, teamId: original.teamId, - userId: user.id, publishedAt: new Date(), lastModifiedById: user.id, createdById: user.id, diff --git a/server/routes/api/integrations/integrations.test.ts b/server/routes/api/integrations/integrations.test.ts index 7ef32d690..9a86b0024 100644 --- a/server/routes/api/integrations/integrations.test.ts +++ b/server/routes/api/integrations/integrations.test.ts @@ -1,8 +1,10 @@ -import { IntegrationService, IntegrationType } from "@shared/types"; -import { IntegrationAuthentication, User } from "@server/models"; -import Integration, { +import { + IntegrationService, UserCreatableIntegrationService, -} from "@server/models/Integration"; + IntegrationType, +} from "@shared/types"; +import { IntegrationAuthentication, User } from "@server/models"; +import Integration from "@server/models/Integration"; import { buildAdmin, buildTeam, diff --git a/server/routes/api/integrations/schema.ts b/server/routes/api/integrations/schema.ts index 2bc947186..e0b1c5d65 100644 --- a/server/routes/api/integrations/schema.ts +++ b/server/routes/api/integrations/schema.ts @@ -1,7 +1,9 @@ import { z } from "zod"; -import { IntegrationType } from "@shared/types"; +import { + IntegrationType, + UserCreatableIntegrationService, +} from "@shared/types"; import { Integration } from "@server/models"; -import { UserCreatableIntegrationService } from "@server/models/Integration"; import { BaseSchema } from "../schema"; export const IntegrationsListSchema = BaseSchema.extend({ diff --git a/server/scripts/20221029000000-crdt-to-text.ts b/server/scripts/20221029000000-crdt-to-text.ts index e1b9c5777..5651d1aa4 100644 --- a/server/scripts/20221029000000-crdt-to-text.ts +++ b/server/scripts/20221029000000-crdt-to-text.ts @@ -37,7 +37,8 @@ export default async function main(exit = false) { for (const document of documents) { const ydoc = new Y.Doc(); - Y.applyUpdate(ydoc, document.state); + // The where clause above ensures that state is non-null + Y.applyUpdate(ydoc, document.state!); const node = Node.fromJSON( schema, yDocToProsemirrorJSON(ydoc, "default") diff --git a/server/storage/database.ts b/server/storage/database.ts index 118635af0..e1cced297 100644 --- a/server/storage/database.ts +++ b/server/storage/database.ts @@ -1,7 +1,9 @@ import path from "path"; -import { Model, Sequelize } from "sequelize-typescript"; +import { InferAttributes, InferCreationAttributes } from "sequelize"; +import { Sequelize } from "sequelize-typescript"; import { Umzug, SequelizeStorage, MigrationError } from "umzug"; import env from "@server/env"; +import Model from "@server/models/base/Model"; import Logger from "../logging/Logger"; import * as models from "../models"; @@ -12,7 +14,12 @@ const url = env.DATABASE_CONNECTION_POOL_URL || env.DATABASE_URL; export function createDatabaseInstance( url: string, - models: { [key: string]: typeof Model } + models: { + [key: string]: typeof Model< + InferAttributes, + InferCreationAttributes + >; + } ) { return new Sequelize(url, { logging: (msg) => @@ -28,7 +35,7 @@ export function createDatabaseInstance( } : false, }, - models: Object.values(models) as any, + models: Object.values(models), pool: { max: poolMax, min: poolMin, diff --git a/server/test/factories.ts b/server/test/factories.ts index ba78a6f94..6636e87db 100644 --- a/server/test/factories.ts +++ b/server/test/factories.ts @@ -2,6 +2,7 @@ import { faker } from "@faker-js/faker"; import isNil from "lodash/isNil"; import isNull from "lodash/isNull"; import randomstring from "randomstring"; +import { InferCreationAttributes } from "sequelize"; import { v4 as uuidv4 } from "uuid"; import { CollectionPermission, @@ -122,7 +123,6 @@ export async function buildSubscription(overrides: Partial = {}) { } return Subscription.create({ - enabled: true, event: "documents.update", ...overrides, }); @@ -139,7 +139,7 @@ export function buildTeam(overrides: Record = {}) { }, ], ...overrides, - }, + } as Partial>, { include: "authenticationProviders", } @@ -200,7 +200,7 @@ export async function buildUser(overrides: Partial = {}) { ] : [], ...overrides, - }, + } as Partial>, { include: "authentications", } @@ -377,7 +377,7 @@ export async function buildDocument( publishedAt: isNull(overrides.collectionId) ? null : new Date(), lastModifiedById: overrides.userId, createdById: overrides.userId, - editorVersion: 2, + editorVersion: "12.0.0", ...overrides, }, { @@ -398,11 +398,9 @@ export async function buildDocument( export async function buildComment(overrides: { userId: string; - teamId: string; documentId: string; }) { const comment = await Comment.create({ - teamId: overrides.teamId, documentId: overrides.documentId, data: { type: "doc", diff --git a/server/types.ts b/server/types.ts index d8deb1123..0358edd2b 100644 --- a/server/types.ts +++ b/server/types.ts @@ -343,6 +343,8 @@ export type ViewEvent = BaseEvent & { }; }; +export type WebhookDeliveryStatus = "pending" | "success" | "failed"; + export type WebhookSubscriptionEvent = BaseEvent & { name: | "webhookSubscriptions.create" diff --git a/shared/types.ts b/shared/types.ts index 0e0985b5c..e878559d6 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -62,7 +62,7 @@ export type PublicEnv = { APP_NAME: string; ROOT_SHARE_ID?: string; analytics: { - service?: IntegrationService; + service?: IntegrationService | UserCreatableIntegrationService; settings?: IntegrationSettings; }; }; @@ -87,6 +87,12 @@ export enum IntegrationService { GoogleAnalytics = "google-analytics", } +export enum UserCreatableIntegrationService { + Diagrams = "diagrams", + Grist = "grist", + GoogleAnalytics = "google-analytics", +} + export enum CollectionPermission { Read = "read", ReadWrite = "read_write",