spike: db transaction as middleware (#4301)

This commit is contained in:
Tom Moor
2022-10-26 20:38:37 -04:00
committed by GitHub
parent 51b3371bf5
commit c916d4f594
2 changed files with 136 additions and 80 deletions

View File

@@ -0,0 +1,25 @@
import { Context, Next } from "koa";
import { Transaction } from "sequelize";
import { sequelize } from "@server/database/sequelize";
export type TransactionContext = Context & {
state: Context["state"] & {
transaction: Transaction;
};
};
/**
* Middleware that wraps a route in a database transaction, useful for mutations
* The transaction is available on the context as `ctx.state.transaction` and
* should be passed to all database calls within the route.
*
* @returns The middleware function.
*/
export function transaction() {
return async function transactionMiddleware(ctx: Context, next: Next) {
await sequelize.transaction(async (t: Transaction) => {
ctx.state.transaction = t;
return next();
});
};
}

View File

@@ -12,6 +12,10 @@ import { sequelize } from "@server/database/sequelize";
import { AuthorizationError, ValidationError } from "@server/errors";
import auth from "@server/middlewares/authentication";
import { rateLimiter } from "@server/middlewares/rateLimiter";
import {
TransactionContext,
transaction,
} from "@server/middlewares/transaction";
import {
Collection,
CollectionUser,
@@ -357,96 +361,123 @@ router.post(
}
);
router.post("collections.add_user", auth(), async (ctx) => {
const { id, userId, permission } = ctx.request.body;
assertUuid(id, "id is required");
assertUuid(userId, "userId is required");
router.post(
"collections.add_user",
auth(),
transaction(),
async (ctx: TransactionContext) => {
const { transaction } = ctx.state;
const { id, userId, permission } = ctx.request.body;
assertUuid(id, "id is required");
assertUuid(userId, "userId is required");
const collection = await Collection.scope({
method: ["withMembership", ctx.state.user.id],
}).findByPk(id);
authorize(ctx.state.user, "update", collection);
const collection = await Collection.scope({
method: ["withMembership", ctx.state.user.id],
}).findByPk(id);
authorize(ctx.state.user, "update", collection);
const user = await User.findByPk(userId);
authorize(ctx.state.user, "read", user);
const user = await User.findByPk(userId);
authorize(ctx.state.user, "read", user);
let membership = await CollectionUser.findOne({
where: {
collectionId: id,
userId,
},
});
if (userId === ctx.state.user.id) {
throw AuthorizationError("You cannot add yourself to a collection");
}
if (permission) {
assertCollectionPermission(permission);
}
if (!membership) {
membership = await CollectionUser.create({
collectionId: id,
userId,
permission: permission || user.defaultCollectionPermission,
createdById: ctx.state.user.id,
let membership = await CollectionUser.findOne({
where: {
collectionId: id,
userId,
},
transaction,
lock: transaction.LOCK.UPDATE,
});
} else if (permission) {
membership.permission = permission;
await membership.save();
if (userId === ctx.state.user.id) {
throw AuthorizationError("You cannot add yourself to a collection");
}
if (permission) {
assertCollectionPermission(permission);
}
if (!membership) {
membership = await CollectionUser.create(
{
collectionId: id,
userId,
permission: permission || user.defaultCollectionPermission,
createdById: ctx.state.user.id,
},
{
transaction,
}
);
} else if (permission) {
membership.permission = permission;
await membership.save({ transaction });
}
await Event.create(
{
name: "collections.add_user",
userId,
collectionId: collection.id,
teamId: collection.teamId,
actorId: ctx.state.user.id,
data: {
name: user.name,
},
ip: ctx.request.ip,
},
{
transaction,
}
);
ctx.body = {
data: {
users: [presentUser(user)],
memberships: [presentMembership(membership)],
},
};
}
);
await Event.create({
name: "collections.add_user",
userId,
collectionId: collection.id,
teamId: collection.teamId,
actorId: ctx.state.user.id,
data: {
name: user.name,
},
ip: ctx.request.ip,
});
router.post(
"collections.remove_user",
auth(),
transaction(),
async (ctx: TransactionContext) => {
const { transaction } = ctx.state;
const { id, userId } = ctx.request.body;
assertUuid(id, "id is required");
assertUuid(userId, "userId is required");
ctx.body = {
data: {
users: [presentUser(user)],
memberships: [presentMembership(membership)],
},
};
});
const collection = await Collection.scope({
method: ["withMembership", ctx.state.user.id],
}).findByPk(id);
authorize(ctx.state.user, "update", collection);
router.post("collections.remove_user", auth(), async (ctx) => {
const { id, userId } = ctx.request.body;
assertUuid(id, "id is required");
assertUuid(userId, "userId is required");
const user = await User.findByPk(userId);
authorize(ctx.state.user, "read", user);
const collection = await Collection.scope({
method: ["withMembership", ctx.state.user.id],
}).findByPk(id);
authorize(ctx.state.user, "update", collection);
await collection.$remove("user", user, { transaction });
await Event.create(
{
name: "collections.remove_user",
userId,
collectionId: collection.id,
teamId: collection.teamId,
actorId: ctx.state.user.id,
data: {
name: user.name,
},
ip: ctx.request.ip,
},
{ transaction }
);
const user = await User.findByPk(userId);
authorize(ctx.state.user, "read", user);
await collection.$remove("user", user);
await Event.create({
name: "collections.remove_user",
userId,
collectionId: collection.id,
teamId: collection.teamId,
actorId: ctx.state.user.id,
data: {
name: user.name,
},
ip: ctx.request.ip,
});
ctx.body = {
success: true,
};
});
ctx.body = {
success: true,
};
}
);
router.post("collections.memberships", auth(), pagination(), async (ctx) => {
const { id, query, permission } = ctx.request.body;