Type server models (#6326)

* fix: type server models

* fix: make ParanoidModel generic

* fix: ApiKey

* fix: Attachment

* fix: AuthenticationProvider

* fix: Backlink

* fix: Collection

* fix: Comment

* fix: Document

* fix: FileOperation

* fix: Group

* fix: GroupPermission

* fix: GroupUser

* fix: Integration

* fix: IntegrationAuthentication

* fix: Notification

* fix: Pin

* fix: Revision

* fix: SearchQuery

* fix: Share

* fix: Star

* fix: Subscription

* fix: TypeError

* fix: Imports

* fix: Team

* fix: TeamDomain

* fix: User

* fix: UserAuthentication

* fix: UserPermission

* fix: View

* fix: WebhookDelivery

* fix: WebhookSubscription

* Remove type duplication

---------

Co-authored-by: Tom Moor <tom.moor@gmail.com>
This commit is contained in:
Apoorv Mishra
2024-01-12 22:33:05 +05:30
committed by GitHub
parent 8360c2dec9
commit 7e61a519f1
56 changed files with 337 additions and 171 deletions

View File

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

View File

@@ -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<Props> {
status: "pending",
});
let response, requestBody, requestHeaders, status;
let response, requestBody, requestHeaders;
let status: WebhookDeliveryStatus;
try {
requestBody = presentWebhook({
event,

View File

@@ -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() }),
]);
}
}

View File

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

View File

@@ -78,7 +78,6 @@ export default async function documentCreator({
editorVersion,
collectionId,
teamId: user.teamId,
userId: user.id,
createdAt,
updatedAt: updatedAt ?? createdAt,
lastModifiedById: user.id,

View File

@@ -40,10 +40,8 @@ describe("starCreator", () => {
});
await Star.create({
teamId: document.teamId,
documentId: document.id,
userId: user.id,
createdById: user.id,
index: "P",
});

View File

@@ -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",
});

View File

@@ -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",
});

View File

@@ -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<InferCreationAttributes<Team>>,
{
include: ["authenticationProviders"],
transaction,

View File

@@ -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,
})

View File

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

View File

@@ -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<InferCreationAttributes<User>>,
{
include: "authentications",
transaction,

View File

@@ -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,
});

View File

@@ -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<ApiKey>,
Partial<InferCreationAttributes<ApiKey>>
> {
static prefix = "ol_api_";
@Length({

View File

@@ -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<Attachment>,
Partial<InferCreationAttributes<Attachment>>
> {
@Length({
max: 4096,
msg: "key must be 4096 characters or less",

View File

@@ -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<AuthenticationProvider>,
Partial<InferCreationAttributes<AuthenticationProvider>>
> {
@IsUUID(4)
@PrimaryKey
@Default(DataType.UUIDV4)
@@ -97,7 +105,9 @@ class AuthenticationProvider extends Model {
}
}
disable = async (options?: SaveOptions<AuthenticationProvider>) => {
disable: (
options?: InstanceUpdateOptions<InferAttributes<AuthenticationProvider>>
) => Promise<AuthenticationProvider> = async (options) => {
const res = await (
this.constructor as typeof AuthenticationProvider
).findAndCountAll({
@@ -124,7 +134,9 @@ class AuthenticationProvider extends Model {
}
};
enable = (options?: SaveOptions<AuthenticationProvider>) =>
enable: (
options?: InstanceUpdateOptions<InferAttributes<AuthenticationProvider>>
) => Promise<AuthenticationProvider> = (options) =>
this.update(
{
enabled: true,

View File

@@ -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<Backlink>,
Partial<InferCreationAttributes<Backlink>>
> {
@BelongsTo(() => User, "userId")
user: User;

View File

@@ -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(),

View File

@@ -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<Collection>,
Partial<InferCreationAttributes<Collection>>
> {
@SimpleLength({
min: 10,
max: 10,

View File

@@ -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<Comment>,
Partial<InferCreationAttributes<Comment>>
> {
@TextLength({
max: CommentValidation.maxLength,
msg: `Comment must be less than ${CommentValidation.maxLength} characters`,

View File

@@ -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<Document>,
Partial<InferCreationAttributes<Document>>
> {
@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

View File

@@ -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<Event>,
Partial<InferCreationAttributes<Event>>
> {
@IsUUID(4)
@Column(DataType.UUID)
modelId: string | null;

View File

@@ -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<FileOperation>,
Partial<InferCreationAttributes<FileOperation>>
> {
@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

View File

@@ -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<Group>,
Partial<InferCreationAttributes<Group>>
> {
@Length({ min: 0, max: 255, msg: "name must be be 255 characters or less" })
@NotContainsUrl
@Column

View File

@@ -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<GroupPermission>,
Partial<InferCreationAttributes<GroupPermission>>
> {
@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;

View File

@@ -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<GroupUser>,
Partial<InferCreationAttributes<GroupUser>>
> {
@BelongsTo(() => User, "userId")
user: User;

View File

@@ -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<T = unknown> extends IdModel {
class Integration<T = unknown> extends IdModel<
InferAttributes<Integration<T>>,
Partial<InferCreationAttributes<Integration<T>>>
> {
@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<T>;
@@ -67,11 +68,11 @@ class Integration<T = unknown> 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;

View File

@@ -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<IntegrationAuthentication>,
Partial<InferCreationAttributes<IntegrationAuthentication>>
> {
@Column(DataType.STRING)
service: IntegrationService;

View File

@@ -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<Notification>,
Partial<InferCreationAttributes<Notification>>
> {
@IsUUID(4)
@PrimaryKey
@Default(DataType.UUIDV4)
@@ -92,7 +99,7 @@ class Notification extends Model {
@AllowNull
@Column
emailedAt: Date;
emailedAt?: Date | null;
@AllowNull
@Column

View File

@@ -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<Pin>,
Partial<InferCreationAttributes<Pin>>
> {
@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;

View File

@@ -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<Revision>,
Partial<InferCreationAttributes<Revision>>
> {
@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<Revision>
options?: SaveOptions<InferAttributes<Revision>>
) {
const revision = this.buildFromDocument(document);
return revision.save(options);

View File

@@ -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<SearchQuery>,
Partial<InferCreationAttributes<SearchQuery>>
> {
@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;

View File

@@ -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<Share>,
Partial<InferCreationAttributes<Share>>
> {
@Column
published: boolean;

View File

@@ -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<Star>,
Partial<InferCreationAttributes<Star>>
> {
@Column
index: string | null;

View File

@@ -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<Subscription>,
Partial<InferCreationAttributes<Subscription>>
> {
@BelongsTo(() => User, "userId")
user: User;

View File

@@ -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<Team>,
Partial<InferCreationAttributes<Team>>
> {
@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;
}

View File

@@ -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<TeamDomain>,
Partial<InferCreationAttributes<TeamDomain>>
> {
@NotIn({
args: env.isCloudHosted ? [emailProviders] : [],
msg: "You chose a restricted domain, please try another.",

View File

@@ -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<User>,
Partial<InferCreationAttributes<User>>
> {
@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<User>) => {
demote: (
to: UserRole,
options?: InstanceUpdateOptions<InferAttributes<Model>>
) => Promise<void> = 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<User>) =>
promote: (
options?: InstanceUpdateOptions<InferAttributes<User>>
) => Promise<User> = (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;
}

View File

@@ -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<UserAuthentication>,
Partial<InferCreationAttributes<UserAuthentication>>
> {
@Column(DataType.ARRAY(DataType.STRING))
scopes: string[];

View File

@@ -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<UserPermission>,
Partial<InferCreationAttributes<UserPermission>>
> {
@Default(CollectionPermission.ReadWrite)
@IsIn([Object.values(CollectionPermission)])
@Column(DataType.STRING)

View File

@@ -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<View>,
Partial<InferCreationAttributes<View>>
> {
@Column
lastEditingAt: Date | null;
@@ -55,7 +63,7 @@ class View extends IdModel {
userId: string;
documentId: string;
},
options?: FindOrCreateOptions
options?: FindOrCreateOptions<InferAttributes<View>>
) {
const [model, created] = await this.findOrCreate({
...options,

View File

@@ -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<WebhookDelivery>,
Partial<InferCreationAttributes<WebhookDelivery>>
> {
@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;

View File

@@ -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<WebhookSubscription>,
Partial<InferCreationAttributes<WebhookSubscription>>
> {
@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<WebhookSubscription>
*/
public async disable(options?: SaveOptions<WebhookSubscription>) {
public async disable(
options?: InstanceUpdateOptions<InferAttributes<WebhookSubscription>>
) {
return this.update({ enabled: false }, options);
}

View File

@@ -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<TModelAttributes, TCreationAttributes> {
@IsUUID(4)
@PrimaryKey
@Default(DataType.UUIDV4)

View File

@@ -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<TModelAttributes, TCreationAttributes> {
/**
* Find all models in batches, calling the callback function for each batch.
*

View File

@@ -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<TModelAttributes, TCreationAttributes> {
@DeletedAt
deletedAt: Date | null;
}

View File

@@ -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();

View File

@@ -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", {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<Model>,
InferCreationAttributes<Model>
>;
}
) {
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,

View File

@@ -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<Subscription> = {}) {
}
return Subscription.create({
enabled: true,
event: "documents.update",
...overrides,
});
@@ -139,7 +139,7 @@ export function buildTeam(overrides: Record<string, any> = {}) {
},
],
...overrides,
},
} as Partial<InferCreationAttributes<Team>>,
{
include: "authenticationProviders",
}
@@ -200,7 +200,7 @@ export async function buildUser(overrides: Partial<User> = {}) {
]
: [],
...overrides,
},
} as Partial<InferCreationAttributes<User>>,
{
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",

View File

@@ -343,6 +343,8 @@ export type ViewEvent = BaseEvent & {
};
};
export type WebhookDeliveryStatus = "pending" | "success" | "failed";
export type WebhookSubscriptionEvent = BaseEvent & {
name:
| "webhookSubscriptions.create"

View File

@@ -62,7 +62,7 @@ export type PublicEnv = {
APP_NAME: string;
ROOT_SHARE_ID?: string;
analytics: {
service?: IntegrationService;
service?: IntegrationService | UserCreatableIntegrationService;
settings?: IntegrationSettings<IntegrationType.Analytics>;
};
};
@@ -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",