Compare commits

...

13 Commits

Author SHA1 Message Date
Tom Moor 9b383cbdf8 test 2024-08-24 10:21:35 -04:00
Tom Moor 114436a585 Merge main 2024-08-24 09:51:32 -04:00
Tom Moor a41a21726b tsc 2023-12-28 23:01:42 -05:00
Tom Moor 65be484d99 iteration 2023-10-26 08:59:04 -04:00
Tom Moor 1eae330775 Improve types 2023-10-26 08:59:04 -04:00
Tom Moor 3ac9a644ea No need to persist file operation update events 2023-10-26 08:59:04 -04:00
Tom Moor 0d30474af1 FileOperations 2023-10-26 08:59:03 -04:00
Tom Moor b1be4a9f15 fix: attachments.update event on redirect 2023-10-26 08:52:12 -04:00
Tom Moor 4555e3b957 attachments 2023-10-26 08:52:12 -04:00
Tom Moor ac919db914 Move logic to base model 2023-10-26 08:52:12 -04:00
Tom Moor c6e5e46544 test 2023-10-26 08:52:12 -04:00
Tom Moor 9abb279a32 Also hookup destroy 2023-10-26 08:52:12 -04:00
Tom Moor bb5a471df8 Working on event refactor 2023-10-26 08:52:12 -04:00
27 changed files with 482 additions and 413 deletions
+23 -53
View File
@@ -1,9 +1,9 @@
import { Transaction } from "sequelize";
import { v4 as uuidv4 } from "uuid";
import { AttachmentPreset } from "@shared/types";
import { Attachment, Event, User } from "@server/models";
import { Attachment, User } from "@server/models";
import AttachmentHelper from "@server/models/helpers/AttachmentHelper";
import FileStorage from "@server/storage/files";
import { APIContext } from "@server/types";
import { RequestInit } from "@server/utils/fetch";
type BaseProps = {
@@ -17,10 +17,8 @@ type BaseProps = {
source?: "import";
/** The preset to use for the attachment */
preset: AttachmentPreset;
/** The IP address of the user creating the attachment, if available. */
ip?: string;
/** The database transaction to use for the creation */
transaction?: Transaction;
/** The request context */
ctx: APIContext;
/** Options to pass to fetch when downloading the attachment */
fetchOptions?: RequestInit;
};
@@ -42,8 +40,7 @@ export default async function attachmentCreator({
user,
source,
preset,
ip,
transaction,
ctx,
fetchOptions,
...rest
}: Props): Promise<Attachment | undefined> {
@@ -64,20 +61,15 @@ export default async function attachmentCreator({
if (!res) {
return;
}
attachment = await Attachment.create(
{
id,
key,
acl,
size: res.contentLength,
contentType: res.contentType,
teamId: user.teamId,
userId: user.id,
},
{
transaction,
}
);
attachment = await Attachment.createWithCtx(ctx, {
id,
key,
acl,
size: res.contentLength,
contentType: res.contentType,
teamId: user.teamId,
userId: user.id,
});
} else {
const { buffer, type } = rest;
await FileStorage.store({
@@ -88,38 +80,16 @@ export default async function attachmentCreator({
acl,
});
attachment = await Attachment.create(
{
id,
key,
acl,
size: buffer.length,
contentType: type,
teamId: user.teamId,
userId: user.id,
},
{
transaction,
}
);
}
await Event.create(
{
name: "attachments.create",
data: {
name,
source,
},
modelId: attachment.id,
attachment = await Attachment.createWithCtx(ctx, {
id,
key,
acl,
size: buffer.length,
contentType: type,
teamId: user.teamId,
actorId: user.id,
ip,
},
{
transaction,
}
);
userId: user.id,
});
}
return attachment;
}
+17 -42
View File
@@ -1,4 +1,3 @@
import { Transaction } from "sequelize";
import { v4 as uuidv4 } from "uuid";
import {
FileOperationFormat,
@@ -6,8 +5,9 @@ import {
FileOperationState,
} from "@shared/types";
import { traceFunction } from "@server/logging/tracing";
import { Collection, Event, Team, User, FileOperation } from "@server/models";
import { Collection, Team, User, FileOperation } from "@server/models";
import { Buckets } from "@server/models/helpers/AttachmentHelper";
import { type APIContext } from "@server/types";
type Props = {
collection?: Collection;
@@ -15,8 +15,7 @@ type Props = {
user: User;
format?: FileOperationFormat;
includeAttachments?: boolean;
ip: string;
transaction: Transaction;
ctx: APIContext;
};
function getKeyForFileOp(
@@ -35,8 +34,7 @@ async function collectionExporter({
user,
format = FileOperationFormat.MarkdownZip,
includeAttachments = true,
ip,
transaction,
ctx,
}: Props) {
const collectionId = collection?.id;
const key = getKeyForFileOp(
@@ -44,43 +42,20 @@ async function collectionExporter({
format,
collection?.name || team.name
);
const fileOperation = await FileOperation.create(
{
type: FileOperationType.Export,
state: FileOperationState.Creating,
format,
key,
url: null,
size: 0,
collectionId,
userId: user.id,
teamId: user.teamId,
options: {
includeAttachments,
},
const fileOperation = await FileOperation.createWithCtx(ctx, {
type: FileOperationType.Export,
state: FileOperationState.Creating,
format,
key,
url: null,
size: 0,
collectionId,
options: {
includeAttachments,
},
{
transaction,
}
);
await Event.create(
{
name: "fileOperations.create",
teamId: user.teamId,
actorId: user.id,
modelId: fileOperation.id,
collectionId,
ip,
data: {
type: FileOperationType.Export,
format,
},
},
{
transaction,
}
);
userId: user.id,
teamId: user.teamId,
});
fileOperation.user = user;
+4 -5
View File
@@ -1,9 +1,9 @@
import { Transaction } from "sequelize";
import { Optional } from "utility-types";
import { Document, Event, User } from "@server/models";
import { DocumentHelper } from "@server/models/helpers/DocumentHelper";
import { ProsemirrorHelper } from "@server/models/helpers/ProsemirrorHelper";
import { TextHelper } from "@server/models/helpers/TextHelper";
import { APIContext } from "@server/types";
type Props = Optional<
Pick<
@@ -31,8 +31,7 @@ type Props = Optional<
publish?: boolean;
templateDocument?: Document | null;
user: User;
ip?: string;
transaction?: Transaction;
ctx: APIContext;
};
export default async function documentCreator({
@@ -58,9 +57,9 @@ export default async function documentCreator({
editorVersion,
publishedAt,
sourceMetadata,
ip,
transaction,
ctx,
}: Props): Promise<Document> {
const { transaction, ip } = ctx.context;
const templateId = templateDocument ? templateDocument.id : undefined;
if (state && templateDocument) {
+5 -10
View File
@@ -1,10 +1,9 @@
import { createContext } from "@server/context";
import { sequelize } from "@server/storage/database";
import { buildDocument, buildUser } from "@server/test/factories";
import documentDuplicator from "./documentDuplicator";
describe("documentDuplicator", () => {
const ip = "127.0.0.1";
it("should duplicate existing document", async () => {
const user = await buildUser();
const original = await buildDocument({
@@ -16,9 +15,8 @@ describe("documentDuplicator", () => {
documentDuplicator({
document: original,
collection: original.collection,
transaction,
user,
ip,
ctx: createContext(user, transaction),
})
);
@@ -43,9 +41,8 @@ describe("documentDuplicator", () => {
document: original,
collection: original.collection,
title: "New title",
transaction,
user,
ip,
ctx: createContext(user, transaction),
})
);
@@ -77,9 +74,8 @@ describe("documentDuplicator", () => {
document: original,
collection: original.collection,
user,
transaction,
recursive: true,
ip,
ctx: createContext(user, transaction),
})
);
@@ -97,10 +93,9 @@ describe("documentDuplicator", () => {
documentDuplicator({
document: original,
collection: original.collection,
transaction,
publish: false,
user,
ip,
ctx: createContext(user, transaction),
})
);
+7 -12
View File
@@ -1,7 +1,8 @@
import { Transaction, Op } from "sequelize";
import { Op } from "sequelize";
import { User, Collection, Document } from "@server/models";
import { DocumentHelper } from "@server/models/helpers/DocumentHelper";
import { ProsemirrorHelper } from "@server/models/helpers/ProsemirrorHelper";
import { APIContext } from "@server/types";
import documentCreator from "./documentCreator";
type Props = {
@@ -19,10 +20,8 @@ type Props = {
publish?: boolean;
/** Whether to duplicate child documents */
recursive?: boolean;
/** The database transaction to use for the creation */
transaction?: Transaction;
/** The IP address of the request */
ip: string;
/** The request context */
ctx: APIContext;
};
export default async function documentDuplicator({
@@ -33,16 +32,14 @@ export default async function documentDuplicator({
title,
publish,
recursive,
transaction,
ip,
ctx,
}: Props): Promise<Document[]> {
const newDocuments: Document[] = [];
const sharedProperties = {
user,
collectionId: collection?.id,
publish: publish ?? !!document.publishedAt,
ip,
transaction,
ctx,
};
const duplicated = await documentCreator({
@@ -76,9 +73,7 @@ export default async function documentDuplicator({
[Op.eq]: null,
},
},
{
transaction,
}
ctx
);
for (const childDocument of childDocuments) {
+123 -94
View File
@@ -1,28 +1,31 @@
import path from "path";
import fs from "fs-extra";
import { createContext } from "@server/context";
import Attachment from "@server/models/Attachment";
import { sequelize } from "@server/storage/database";
import { buildUser } from "@server/test/factories";
import documentImporter from "./documentImporter";
jest.mock("@server/storage/files");
describe("documentImporter", () => {
const ip = "127.0.0.1";
it("should convert Word Document to markdown", async () => {
const user = await buildUser();
const fileName = "images.docx";
const content = await fs.readFile(
path.resolve(__dirname, "..", "test", "fixtures", fileName)
);
const response = await documentImporter({
user,
mimeType:
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
fileName,
content,
ip,
});
const response = await sequelize.transaction((transaction) =>
documentImporter({
user,
mimeType:
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
fileName,
content,
ctx: createContext(user, transaction),
})
);
const attachments = await Attachment.count({
where: {
teamId: user.teamId,
@@ -40,13 +43,15 @@ describe("documentImporter", () => {
const content = await fs.readFile(
path.resolve(__dirname, "..", "test", "fixtures", fileName)
);
const response = await documentImporter({
user,
mimeType: "application/octet-stream",
fileName,
content,
ip,
});
const response = await sequelize.transaction((transaction) =>
documentImporter({
user,
mimeType: "application/octet-stream",
fileName,
content,
ctx: createContext(user, transaction),
})
);
const attachments = await Attachment.count({
where: {
teamId: user.teamId,
@@ -67,13 +72,15 @@ describe("documentImporter", () => {
let error;
try {
await documentImporter({
user,
mimeType: "application/octet-stream",
fileName,
content,
ip,
});
await sequelize.transaction((transaction) =>
documentImporter({
user,
mimeType: "application/octet-stream",
fileName,
content,
ctx: createContext(user, transaction),
})
);
} catch (err) {
error = err.message;
}
@@ -87,13 +94,15 @@ describe("documentImporter", () => {
const content = await fs.readFile(
path.resolve(__dirname, "..", "test", "fixtures", fileName)
);
const response = await documentImporter({
user,
mimeType: "application/octet-stream",
fileName,
content,
ip,
});
const response = await sequelize.transaction((transaction) =>
documentImporter({
user,
mimeType: "application/octet-stream",
fileName,
content,
ctx: createContext(user, transaction),
})
);
const attachments = await Attachment.count({
where: {
teamId: user.teamId,
@@ -112,13 +121,15 @@ describe("documentImporter", () => {
path.resolve(__dirname, "..", "test", "fixtures", fileName),
"utf8"
);
const response = await documentImporter({
user,
mimeType: "text/html",
fileName,
content,
ip,
});
const response = await sequelize.transaction((transaction) =>
documentImporter({
user,
mimeType: "text/html",
fileName,
content,
ctx: createContext(user, transaction),
})
);
expect(response.text).toContain("Text paragraph");
expect(response.title).toEqual("Heading 1");
});
@@ -129,13 +140,16 @@ describe("documentImporter", () => {
const content = await fs.readFile(
path.resolve(__dirname, "..", "test", "fixtures", fileName)
);
const response = await documentImporter({
user,
mimeType: "application/msword",
fileName,
content,
ip,
});
const response = await sequelize.transaction((transaction) =>
documentImporter({
user,
mimeType: "application/msword",
fileName,
content,
ctx: createContext(user, transaction),
})
);
expect(response.text).toContain("this is a test document");
expect(response.title).toEqual("Heading 1");
});
@@ -147,13 +161,15 @@ describe("documentImporter", () => {
path.resolve(__dirname, "..", "test", "fixtures", fileName),
"utf8"
);
const response = await documentImporter({
user,
mimeType: "text/plain",
fileName,
content,
ip,
});
const response = await sequelize.transaction((transaction) =>
documentImporter({
user,
mimeType: "text/plain",
fileName,
content,
ctx: createContext(user, transaction),
})
);
expect(response.text).toContain("This is a test paragraph");
expect(response.title).toEqual("Heading 1");
});
@@ -162,13 +178,16 @@ describe("documentImporter", () => {
const user = await buildUser();
const fileName = "markdown.md";
const content = `# Title`;
const response = await documentImporter({
user,
mimeType: "text/plain",
fileName,
content,
ip,
});
const response = await sequelize.transaction((transaction) =>
documentImporter({
user,
mimeType: "text/plain",
fileName,
content,
ctx: createContext(user, transaction),
})
);
expect(response.text).toEqual("");
expect(response.title).toEqual("Title");
});
@@ -180,13 +199,15 @@ describe("documentImporter", () => {
path.resolve(__dirname, "..", "test", "fixtures", fileName),
"utf8"
);
const response = await documentImporter({
user,
mimeType: "application/lol",
fileName,
content,
ip,
});
const response = await sequelize.transaction((transaction) =>
documentImporter({
user,
mimeType: "application/lol",
fileName,
content,
ctx: createContext(user, transaction),
})
);
expect(response.text).toContain("This is a test paragraph");
expect(response.title).toEqual("Heading 1");
});
@@ -200,13 +221,15 @@ describe("documentImporter", () => {
let error;
try {
await documentImporter({
user,
mimeType: "executable/zip",
fileName,
content,
ip,
});
await sequelize.transaction((transaction) =>
documentImporter({
user,
mimeType: "executable/zip",
fileName,
content,
ctx: createContext(user, transaction),
})
);
} catch (err) {
error = err.message;
}
@@ -228,13 +251,15 @@ describe("documentImporter", () => {
</body>
</html>
`;
const response = await documentImporter({
user,
mimeType: "text/html",
fileName,
content,
ip,
});
const response = await sequelize.transaction((transaction) =>
documentImporter({
user,
mimeType: "text/html",
fileName,
content,
ctx: createContext(user, transaction),
})
);
expect(response.text).toEqual("\\$100");
});
@@ -252,13 +277,15 @@ describe("documentImporter", () => {
</body>
</html>
`;
const response = await documentImporter({
user,
mimeType: "text/html",
fileName,
content,
ip,
});
const response = await sequelize.transaction((transaction) =>
documentImporter({
user,
mimeType: "text/html",
fileName,
content,
ctx: createContext(user, transaction),
})
);
expect(response.text).toEqual("`echo $foo`");
});
@@ -276,13 +303,15 @@ describe("documentImporter", () => {
</body>
</html>
`;
const response = await documentImporter({
user,
mimeType: "text/html",
fileName,
content,
ip,
});
const response = await sequelize.transaction((transaction) =>
documentImporter({
user,
mimeType: "text/html",
fileName,
content,
ctx: createContext(user, transaction),
})
);
expect(response.text).toEqual("```\necho $foo\n```");
});
});
+4 -11
View File
@@ -1,13 +1,13 @@
import emojiRegex from "emoji-regex";
import escapeRegExp from "lodash/escapeRegExp";
import truncate from "lodash/truncate";
import { Transaction } from "sequelize";
import parseTitle from "@shared/utils/parseTitle";
import { DocumentValidation } from "@shared/validations";
import { traceFunction } from "@server/logging/tracing";
import { User } from "@server/models";
import { ProsemirrorHelper } from "@server/models/helpers/ProsemirrorHelper";
import { TextHelper } from "@server/models/helpers/TextHelper";
import { APIContext } from "@server/types";
import { DocumentConverter } from "@server/utils/DocumentConverter";
import { InvalidRequestError } from "../errors";
@@ -16,8 +16,7 @@ type Props = {
mimeType: string;
fileName: string;
content: Buffer | string;
ip?: string;
transaction?: Transaction;
ctx: APIContext;
};
async function documentImporter({
@@ -25,8 +24,7 @@ async function documentImporter({
fileName,
content,
user,
ip,
transaction,
ctx,
}: Props): Promise<{
icon?: string;
text: string;
@@ -66,12 +64,7 @@ async function documentImporter({
// Remove any closed and immediately reopened formatting marks
text = text.replace(/\*\*\*\*/gi, "").replace(/____/gi, "");
text = await TextHelper.replaceImagesWithAttachments(
text,
user,
ip,
transaction
);
text = await TextHelper.replaceImagesWithAttachments(text, user, ctx);
// Sanity check text cannot possibly be longer than state so if it is, we can short-circuit here
if (text.length > DocumentValidation.maxStateLength) {
-30
View File
@@ -1,30 +0,0 @@
import { Transaction } from "sequelize";
import { FileOperation, Event, User } from "@server/models";
type Props = {
fileOperation: FileOperation;
user: User;
ip: string;
transaction: Transaction;
};
export default async function fileOperationDeleter({
fileOperation,
user,
ip,
transaction,
}: Props) {
await fileOperation.destroy({ transaction });
await Event.create(
{
name: "fileOperations.delete",
teamId: user.teamId,
actorId: user.id,
modelId: fileOperation.id,
ip,
},
{
transaction,
}
);
}
+17
View File
@@ -0,0 +1,17 @@
import { Transaction } from "sequelize";
import { User } from "@server/models";
import { APIContext } from "@server/types";
export function createContext(
user: User,
transaction?: Transaction,
ip?: string
) {
return {
context: {
ip: ip ?? user.lastActiveIp,
transaction,
auth: { user },
},
} as APIContext;
}
+10
View File
@@ -159,6 +159,16 @@ export default function auth(options: AuthenticationOptions = {}) {
}
}
Object.defineProperty(ctx, "context", {
get() {
return {
auth: ctx.state.auth,
transaction: ctx.state.transaction,
ip: ctx.request.ip,
};
},
});
return next();
};
}
+4
View File
@@ -24,6 +24,10 @@ class ApiKey extends ParanoidModel<
> {
static prefix = "ol_api_";
static eventNamespace = "api_keys";
static eventData = ["name"];
@Length({
min: ApiKeyValidation.minNameLength,
max: ApiKeyValidation.maxNameLength,
+2
View File
@@ -35,6 +35,8 @@ class Attachment extends IdModel<
InferAttributes<Attachment>,
Partial<InferCreationAttributes<Attachment>>
> {
static eventData = ["name"];
@Length({
max: 4096,
msg: "key must be 4096 characters or less",
+4
View File
@@ -51,6 +51,10 @@ class FileOperation extends ParanoidModel<
InferAttributes<FileOperation>,
Partial<InferCreationAttributes<FileOperation>>
> {
static eventNamespace = "fileOperations";
static eventData = ["type", "format"];
@Column(DataType.ENUM(...Object.values(FileOperationType)))
type: FileOperationType;
+147 -2
View File
@@ -3,13 +3,158 @@ import isEqual from "fast-deep-equal";
import isArray from "lodash/isArray";
import isObject from "lodash/isObject";
import pick from "lodash/pick";
import { FindOptions, NonAttribute } from "sequelize";
import { Model as SequelizeModel } from "sequelize-typescript";
import {
CreateOptions,
CreationAttributes,
FindOptions,
InstanceDestroyOptions,
InstanceUpdateOptions,
ModelStatic,
NonAttribute,
} from "sequelize";
import {
AfterCreate,
AfterDestroy,
AfterUpdate,
Model as SequelizeModel,
} from "sequelize-typescript";
import Logger from "@server/logging/Logger";
import { APIContext } from "@server/types";
class Model<
TModelAttributes extends {} = any,
TCreationAttributes extends {} = TModelAttributes
> extends SequelizeModel<TModelAttributes, TCreationAttributes> {
/**
* The namespace to use for events, if none is provided the table name is used.
*/
static eventNamespace: string | undefined;
/**
* The properties to include in the event data when this model is mutated.
*/
static eventData: string[] = [];
/**
* This is the same as calling `set` and then calling `save`.
*/
public updateWithCtx(ctx: APIContext, keys: Partial<TModelAttributes>) {
return this.update(keys, ctx.context as InstanceUpdateOptions);
}
/**
* Destroy the row corresponding to this instance. Depending on your setting for paranoid, the row will
* either be completely deleted, or have its deletedAt timestamp set to the current time.
*/
public destroyWithCtx(ctx: APIContext) {
return this.destroy(ctx.context as InstanceDestroyOptions);
}
/**
* Builds a new model instance and calls save on it.
*/
public static createWithCtx<M extends Model>(
this: ModelStatic<M>,
ctx: APIContext,
values?: CreationAttributes<M>
) {
return this.create(values, ctx.context as CreateOptions);
}
@AfterCreate
static async afterCreateEvent<T extends Model>(
model: T,
context: APIContext["context"]
) {
await this.insertEvent("create", model, context);
}
@AfterUpdate
static async afterUpdateEvent<T extends Model>(
model: T,
context: APIContext["context"]
) {
await this.insertEvent("update", model, context);
}
@AfterDestroy
static async afterDestroyEvent<T extends Model>(
model: T,
context: APIContext["context"]
) {
await this.insertEvent("delete", model, context);
}
/**
* Insert an event into the database recording a mutation to this model.
*
* @param name The name of the event.
* @param model The model that was mutated.
* @param context The API context.
*/
protected static async insertEvent<T extends Model>(
name: string,
model: T,
context: APIContext["context"]
) {
// If no eventData is defined, don't create an event
if (this.eventData.length === 0) {
return;
}
if (!context.transaction) {
Logger.warn("No transaction provided to insertEvent", {
modelId: model.id,
});
}
if (!context.ip) {
Logger.warn("No ip provided to insertEvent", {
modelId: model.id,
});
}
const models = this.sequelize!.models;
const namespace = this.eventNamespace || this.tableName;
return models.event.create(
{
name: `${namespace}.${name}`,
modelId: model.id,
collectionId:
"collectionId" in model
? model.collectionId
: model instanceof models.collection
? model.id
: undefined,
documentId:
"documentId" in model
? model.documentId
: model instanceof models.document
? model.id
: undefined,
userId:
"userId" in model
? model.userId
: model instanceof models.user
? model.id
: undefined,
teamId:
"teamId" in model
? model.teamId
: model instanceof models.team
? model.id
: context.auth?.user.teamId,
actorId: context.auth?.user.id,
ip: context.ip,
data: pick(model, this.eventData),
},
{
transaction: context.transaction,
}
);
}
/**
* Find all models in batches, calling the callback function for each batch.
*
+3 -5
View File
@@ -1,6 +1,5 @@
import escapeRegExp from "lodash/escapeRegExp";
import startCase from "lodash/startCase";
import { Transaction } from "sequelize";
import { AttachmentPreset } from "@shared/types";
import {
getCurrentDateAsString,
@@ -13,6 +12,7 @@ import env from "@server/env";
import { trace } from "@server/logging/tracing";
import { Attachment, User } from "@server/models";
import FileStorage from "@server/storage/files";
import { APIContext } from "@server/types";
import parseAttachmentIds from "@server/utils/parseAttachmentIds";
import parseImages from "@server/utils/parseImages";
@@ -91,8 +91,7 @@ export class TextHelper {
static async replaceImagesWithAttachments(
markdown: string,
user: User,
ip?: string,
transaction?: Transaction
ctx: APIContext
) {
let output = markdown;
const images = parseImages(markdown);
@@ -114,11 +113,10 @@ export class TextHelper {
url: image.src,
preset: AttachmentPreset.DocumentAttachment,
user,
ip,
transaction,
fetchOptions: {
timeout: timeoutPerImage,
},
ctx,
});
if (attachment) {
+3 -4
View File
@@ -1,6 +1,7 @@
import { SourceMetadata } from "@shared/types";
import documentCreator from "@server/commands/documentCreator";
import documentImporter from "@server/commands/documentImporter";
import { createContext } from "@server/context";
import { User } from "@server/models";
import { sequelize } from "@server/storage/database";
import FileStorage from "@server/storage/files";
@@ -48,8 +49,7 @@ export default class DocumentImportTask extends BaseTask<Props> {
fileName: sourceMetadata.fileName,
mimeType: sourceMetadata.mimeType,
content,
ip,
transaction,
ctx: createContext(user, transaction, ip),
});
return documentCreator({
@@ -62,8 +62,7 @@ export default class DocumentImportTask extends BaseTask<Props> {
collectionId,
parentDocumentId,
user,
ip,
transaction,
ctx: createContext(user, transaction, ip),
});
});
return { documentId: document.id };
@@ -34,7 +34,7 @@ export default class ErrorTimedOutFileOperationsTask extends BaseTask<Props> {
fileOperations.map(async (fileOperation) => {
fileOperation.state = FileOperationState.Error;
fileOperation.error = "Timed out";
await fileOperation.save();
await fileOperation.save({ hooks: false });
})
);
Logger.info("task", `Updated ${fileOperations.length} file operations`);
+15 -10
View File
@@ -4,9 +4,11 @@ import escapeRegExp from "lodash/escapeRegExp";
import mime from "mime-types";
import { v4 as uuidv4 } from "uuid";
import documentImporter from "@server/commands/documentImporter";
import { createContext } from "@server/context";
import Logger from "@server/logging/Logger";
import { FileOperation, User } from "@server/models";
import { Buckets } from "@server/models/helpers/AttachmentHelper";
import { sequelize } from "@server/storage/database";
import ImportHelper, { FileTreeNode } from "@server/utils/ImportHelper";
import ImportTask, { StructuredImportData } from "./ImportTask";
@@ -82,16 +84,19 @@ export default class ImportMarkdownZipTask extends ImportTask {
return;
}
const { title, icon, text } = await documentImporter({
mimeType: "text/markdown",
fileName: child.name,
content:
child.children.length > 0
? ""
: await fs.readFile(child.path, "utf8"),
user,
ip: user.lastActiveIp || undefined,
});
const { title, icon, text } = await sequelize.transaction(
async (transaction) =>
documentImporter({
mimeType: "text/markdown",
fileName: child.name,
content:
child.children.length > 0
? ""
: await fs.readFile(child.path, "utf8"),
user,
ctx: createContext(user, transaction),
})
);
const existingDocumentIndex = output.documents.findIndex(
(doc) =>
+24 -17
View File
@@ -5,8 +5,10 @@ import escapeRegExp from "lodash/escapeRegExp";
import mime from "mime-types";
import { v4 as uuidv4 } from "uuid";
import documentImporter from "@server/commands/documentImporter";
import { createContext } from "@server/context";
import Logger from "@server/logging/Logger";
import { FileOperation, User } from "@server/models";
import { sequelize } from "@server/storage/database";
import ImportHelper, { FileTreeNode } from "@server/utils/ImportHelper";
import ImportTask, { StructuredImportData } from "./ImportTask";
@@ -96,16 +98,19 @@ export default class ImportNotionTask extends ImportTask {
Logger.debug("task", `Processing ${name} as ${mimeType}`);
const { title, icon, text } = await documentImporter({
mimeType: mimeType || "text/markdown",
fileName: name,
content:
child.children.length > 0
? ""
: await fs.readFile(child.path, "utf8"),
user,
ip: user.lastActiveIp || undefined,
});
const { title, icon, text } = await sequelize.transaction(
async (transaction) =>
documentImporter({
mimeType: mimeType || "text/markdown",
fileName: name,
content:
child.children.length > 0
? ""
: await fs.readFile(child.path, "utf8"),
user,
ctx: createContext(user, transaction),
})
);
const existingDocumentIndex = output.documents.findIndex(
(doc) => doc.externalId === externalId
@@ -218,13 +223,15 @@ export default class ImportNotionTask extends ImportTask {
mimeType === "text/plain" ||
mimeType === "text/html"
) {
const { text } = await documentImporter({
mimeType,
fileName: name,
content: await fs.readFile(node.path, "utf8"),
user,
ip: user.lastActiveIp || undefined,
});
const { text } = await sequelize.transaction(async (transaction) =>
documentImporter({
mimeType,
fileName: name,
content: await fs.readFile(node.path, "utf8"),
user,
ctx: createContext(user, transaction),
})
);
description = text;
} else if (node.children.length > 0) {
+12 -8
View File
@@ -13,6 +13,7 @@ import {
import { CollectionValidation } from "@shared/validations";
import attachmentCreator from "@server/commands/attachmentCreator";
import documentCreator from "@server/commands/documentCreator";
import { createContext } from "@server/context";
import { serializer } from "@server/editor";
import { InternalError, ValidationError } from "@server/errors";
import Logger from "@server/logging/Logger";
@@ -182,10 +183,15 @@ export default abstract class ImportTask extends BaseTask<Props> {
state: FileOperationState,
error?: Error
) {
await fileOperation.update({
state,
error: error ? truncate(error.message, { length: 255 }) : undefined,
});
await fileOperation.update(
{
state,
error: error ? truncate(error.message, { length: 255 }) : undefined,
},
{
hooks: false,
}
);
await Event.schedule({
name: "fileOperations.update",
modelId: fileOperation.id,
@@ -466,8 +472,7 @@ export default abstract class ImportTask extends BaseTask<Props> {
parentDocumentId: item.parentDocumentId,
importId: fileOperation.id,
user,
ip,
transaction,
ctx: createContext(user, transaction),
});
documents.set(item.id, document);
@@ -501,8 +506,7 @@ export default abstract class ImportTask extends BaseTask<Props> {
type: item.mimeType,
buffer: await item.buffer(),
user,
ip,
transaction,
ctx: createContext(user, transaction),
});
if (attachment) {
attachments.set(item.id, attachment);
+15 -40
View File
@@ -3,7 +3,7 @@ import { UserRole } from "@shared/types";
import auth from "@server/middlewares/authentication";
import { transaction } from "@server/middlewares/transaction";
import validate from "@server/middlewares/validate";
import { ApiKey, Event } from "@server/models";
import { ApiKey } from "@server/models";
import { authorize } from "@server/policies";
import { presentApiKey } from "@server/presenters";
import { APIContext, AuthenticationType } from "@server/types";
@@ -20,32 +20,16 @@ router.post(
async (ctx: APIContext<T.APIKeysCreateReq>) => {
const { name, expiresAt } = ctx.input.body;
const { user } = ctx.state.auth;
const { transaction } = ctx.state;
authorize(user, "createApiKey", user.team);
const key = await ApiKey.create(
{
name,
userId: user.id,
expiresAt,
},
{ transaction }
);
await Event.createFromContext(
ctx,
{
name: "api_keys.create",
modelId: key.id,
data: {
name,
},
},
{ transaction }
);
const apiKey = await ApiKey.createWithCtx(ctx, {
name,
userId: user.id,
expiresAt,
});
ctx.body = {
data: presentApiKey(key),
data: presentApiKey(apiKey),
};
}
);
@@ -56,18 +40,20 @@ router.post(
pagination(),
async (ctx: APIContext) => {
const { user } = ctx.state.auth;
const keys = await ApiKey.findAll({
const { pagination } = ctx.state;
const apiKeys = await ApiKey.findAll({
where: {
userId: user.id,
},
order: [["createdAt", "DESC"]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
offset: pagination.offset,
limit: pagination.limit,
});
ctx.body = {
pagination: ctx.state.pagination,
data: keys.map(presentApiKey),
pagination,
data: apiKeys.map(presentApiKey),
};
}
);
@@ -88,18 +74,7 @@ router.post(
});
authorize(user, "delete", key);
await key.destroy({ transaction });
await Event.createFromContext(
ctx,
{
name: "api_keys.delete",
modelId: key.id,
data: {
name: key.name,
},
},
{ transaction }
);
await key.destroyWithCtx(ctx);
ctx.body = {
success: true,
+11 -25
View File
@@ -64,31 +64,17 @@ router.post(
userId: user.id,
});
const attachment = await Attachment.create(
{
id: modelId,
key,
acl,
size,
expiresAt: AttachmentHelper.presetToExpiry(preset),
contentType,
documentId,
teamId: user.teamId,
userId: user.id,
},
{ transaction }
);
await Event.createFromContext(
ctx,
{
name: "attachments.create",
data: {
name,
},
modelId,
},
{ transaction }
);
const attachment = await Attachment.createWithCtx(ctx, {
id: modelId,
key,
acl,
size,
expiresAt: AttachmentHelper.presetToExpiry(preset),
contentType,
documentId,
teamId: user.teamId,
userId: user.id,
});
const presignedPost = await FileStorage.getPresignedPost(
key,
+15 -23
View File
@@ -166,7 +166,6 @@ router.post(
async (ctx: APIContext<T.CollectionsImportReq>) => {
const { transaction } = ctx.state;
const { attachmentId, permission, format } = ctx.input.body;
const { user } = ctx.state.auth;
authorize(user, "importCollection", user.team);
@@ -175,23 +174,18 @@ router.post(
});
authorize(user, "read", attachment);
const fileOperation = await FileOperation.create(
{
type: FileOperationType.Import,
state: FileOperationState.Creating,
format,
size: attachment.size,
key: attachment.key,
userId: user.id,
teamId: user.teamId,
options: {
permission,
},
const fileOperation = await FileOperation.createWithCtx(ctx, {
type: FileOperationType.Import,
state: FileOperationState.Creating,
format,
size: attachment.size,
key: attachment.key,
userId: user.id,
teamId: user.teamId,
options: {
permission,
},
{
transaction,
}
);
});
await Event.createFromContext(
ctx,
@@ -568,8 +562,8 @@ router.post(
validate(T.CollectionsExportSchema),
transaction(),
async (ctx: APIContext<T.CollectionsExportReq>) => {
const { transaction } = ctx.state;
const { id, format, includeAttachments } = ctx.input.body;
const { transaction } = ctx.state;
const { user } = ctx.state.auth;
const team = await Team.findByPk(user.teamId, { transaction });
@@ -586,8 +580,7 @@ router.post(
team,
format,
includeAttachments,
ip: ctx.request.ip,
transaction,
ctx,
});
ctx.body = {
@@ -606,9 +599,9 @@ router.post(
validate(T.CollectionsExportAllSchema),
transaction(),
async (ctx: APIContext<T.CollectionsExportAllReq>) => {
const { transaction } = ctx.state;
const { format, includeAttachments } = ctx.input.body;
const { user } = ctx.state.auth;
const { transaction } = ctx.state;
const team = await Team.findByPk(user.teamId, { transaction });
authorize(user, "createExport", team);
@@ -617,8 +610,7 @@ router.post(
team,
format,
includeAttachments,
ip: ctx.request.ip,
transaction,
ctx,
});
ctx.body = {
+3 -11
View File
@@ -1118,10 +1118,9 @@ router.post(
document,
title,
publish,
transaction,
recursive,
parentDocumentId,
ip: ctx.request.ip,
ctx,
});
ctx.body = {
@@ -1375,7 +1374,6 @@ router.post(
collectionId,
parentDocumentId,
publish,
ip: ctx.request.ip,
});
const response: DocumentImportTaskResponse = await job.finished();
if ("error" in response) {
@@ -1470,12 +1468,7 @@ router.post(
const document = await documentCreator({
title,
text: await TextHelper.replaceImagesWithAttachments(
text,
user,
ctx.request.ip,
transaction
),
text: await TextHelper.replaceImagesWithAttachments(text, user, ctx),
icon,
color,
createdAt,
@@ -1487,8 +1480,7 @@ router.post(
fullWidth,
user,
editorVersion,
ip: ctx.request.ip,
transaction,
ctx,
});
document.collection = collection;
@@ -282,6 +282,7 @@ describe("#fileOperations.delete", () => {
expect(
await Event.count({
where: {
name: "fileOperations.delete",
teamId: team.id,
},
})
@@ -1,7 +1,6 @@
import Router from "koa-router";
import { WhereOptions } from "sequelize";
import { UserRole } from "@shared/types";
import fileOperationDeleter from "@server/commands/fileOperationDeleter";
import { ValidationError } from "@server/errors";
import auth from "@server/middlewares/authentication";
import { transaction } from "@server/middlewares/transaction";
@@ -116,15 +115,11 @@ router.post(
const fileOperation = await FileOperation.unscoped().findByPk(id, {
rejectOnEmpty: true,
transaction,
lock: transaction.LOCK.UPDATE,
});
authorize(user, "delete", fileOperation);
await fileOperationDeleter({
fileOperation,
user,
ip: ctx.request.ip,
transaction,
});
await fileOperation.destroyWithCtx(ctx);
ctx.body = {
success: true,
+10 -3
View File
@@ -49,8 +49,8 @@ export type AuthenticationResult = AccountProvisionerResult & {
export type Authentication = {
user: User;
token: string;
type: AuthenticationType;
token?: string;
type?: AuthenticationType;
};
export type Pagination = {
@@ -77,8 +77,15 @@ export interface APIContext<ReqT = BaseReq, ResT = BaseRes>
DefaultContext & IRouterParamContext<AppState>,
ResT
> {
/** Typed and validated version of request, consisting of validated body, query, etc */
/** Typed and validated version of request, consisting of validated body, query, etc. */
input: ReqT;
/** The current request's context, which is passed to database mutations. */
context: {
transaction?: Transaction;
auth: Authentication;
ip?: string;
};
}
type BaseEvent<T extends Model> = {