Compare commits

...

1 Commits

Author SHA1 Message Date
Tom Moor 919b83fa2a Convert stars, towards #7920 2024-11-10 22:13:48 -05:00
10 changed files with 58 additions and 218 deletions
+6 -10
View File
@@ -1,24 +1,21 @@
import { Star, Event } from "@server/models";
import { sequelize } from "@server/storage/database";
import { buildDocument, buildUser } from "@server/test/factories";
import { withAPIContext } from "@server/test/support";
import starCreator from "./starCreator";
describe("starCreator", () => {
const ip = "127.0.0.1";
it("should create star", async () => {
it("should create star for document", async () => {
const user = await buildUser();
const document = await buildDocument({
userId: user.id,
teamId: user.teamId,
});
const star = await sequelize.transaction(async (transaction) =>
const star = await withAPIContext(user, (ctx) =>
starCreator({
ctx,
documentId: document.id,
user,
ip,
transaction,
})
);
@@ -45,12 +42,11 @@ describe("starCreator", () => {
index: "P",
});
const star = await sequelize.transaction(async (transaction) =>
const star = await withAPIContext(user, (ctx) =>
starCreator({
ctx,
documentId: document.id,
user,
ip,
transaction,
})
);
+8 -26
View File
@@ -1,6 +1,7 @@
import fractionalIndex from "fractional-index";
import { Sequelize, Transaction, WhereOptions } from "sequelize";
import { Star, User, Event } from "@server/models";
import { Sequelize, WhereOptions } from "sequelize";
import { Star, User } from "@server/models";
import { APIContext } from "@server/types";
type Props = {
/** The user creating the star */
@@ -11,9 +12,8 @@ type Props = {
collectionId?: string;
/** The sorted index for the star in the sidebar If no index is provided then it will be at the end */
index?: string;
/** The IP address of the user creating the star */
ip: string;
transaction: Transaction;
/** The request context */
ctx: APIContext;
};
/**
@@ -27,8 +27,7 @@ export default async function starCreator({
user,
documentId,
collectionId,
ip,
transaction,
ctx,
...rest
}: Props): Promise<Star> {
let { index } = rest;
@@ -47,14 +46,14 @@ export default async function starCreator({
Sequelize.literal('"star"."index" collate "C"'),
["updatedAt", "DESC"],
],
transaction,
transaction: ctx.state.transaction,
});
// create a star at the beginning of the list
index = fractionalIndex(null, stars.length ? stars[0].index : null);
}
const [star, isCreated] = await Star.findOrCreate({
const [star] = await Star.findOrCreateWithCtx(ctx, {
where: documentId
? {
userId: user.id,
@@ -67,24 +66,7 @@ export default async function starCreator({
defaults: {
index,
},
transaction,
});
if (isCreated) {
await Event.create(
{
name: "stars.create",
teamId: user.teamId,
modelId: star.id,
userId: user.id,
actorId: user.id,
documentId,
collectionId,
ip,
},
{ transaction }
);
}
return star;
}
-40
View File
@@ -1,40 +0,0 @@
import { Event, Star } from "@server/models";
import { buildDocument, buildUser } from "@server/test/factories";
import starDestroyer from "./starDestroyer";
describe("starDestroyer", () => {
const ip = "127.0.0.1";
it("should destroy existing star", async () => {
const user = await buildUser();
const document = await buildDocument({
userId: user.id,
teamId: user.teamId,
});
const star = await Star.create({
documentId: document.id,
userId: user.id,
index: "P",
});
await starDestroyer({
star,
user,
ip,
});
const count = await Star.count({
where: {
userId: user.id,
},
});
expect(count).toEqual(0);
const event = await Event.findLatest({
teamId: user.teamId,
});
expect(event!.name).toEqual("stars.delete");
expect(event!.modelId).toEqual(star.id);
});
});
-43
View File
@@ -1,43 +0,0 @@
import { Transaction } from "sequelize";
import { Event, Star, User } from "@server/models";
type Props = {
/** The user destroying the star */
user: User;
/** The star to destroy */
star: Star;
/** The IP address of the user creating the star */
ip: string;
/** Optional existing transaction */
transaction?: Transaction;
};
/**
* This command destroys a document star. This just removes the star itself and
* does not touch the document
*
* @param Props The properties of the star to destroy
* @returns void
*/
export default async function starDestroyer({
user,
star,
ip,
transaction,
}: Props): Promise<Star> {
await star.destroy({ transaction });
await Event.create(
{
name: "stars.delete",
modelId: star.id,
teamId: user.teamId,
actorId: user.id,
userId: star.userId,
documentId: star.documentId,
ip,
},
{ transaction }
);
return star;
}
-37
View File
@@ -1,37 +0,0 @@
import { Event, Star } from "@server/models";
import { buildDocument, buildUser } from "@server/test/factories";
import starUpdater from "./starUpdater";
describe("starUpdater", () => {
const ip = "127.0.0.1";
it("should update (move) existing star", async () => {
const user = await buildUser();
const document = await buildDocument({
userId: user.id,
teamId: user.teamId,
});
let star = await Star.create({
documentId: document.id,
userId: user.id,
index: "P",
});
star = await starUpdater({
star,
index: "h",
user,
ip,
});
const event = await Event.findLatest({
teamId: user.teamId,
});
expect(star.documentId).toEqual(document.id);
expect(star.userId).toEqual(user.id);
expect(star.index).toEqual("h");
expect(event!.name).toEqual("stars.update");
expect(event!.modelId).toEqual(star.id);
});
});
-47
View File
@@ -1,47 +0,0 @@
import { Transaction } from "sequelize";
import { Event, Star, User } from "@server/models";
type Props = {
/** The user updating the star */
user: User;
/** The existing star */
star: Star;
/** The index to star the document at */
index: string;
/** The IP address of the user creating the star */
ip: string;
/** Optional existing transaction */
transaction?: Transaction;
};
/**
* This command updates a "starred" document. A star can only be moved to a new
* index (reordered) once created.
*
* @param Props The properties of the star to update
* @returns Star The updated star
*/
export default async function starUpdater({
user,
star,
index,
ip,
transaction,
}: Props): Promise<Star> {
star.index = index;
await star.save({ transaction });
await Event.create(
{
name: "stars.update",
modelId: star.id,
userId: star.userId,
teamId: user.teamId,
actorId: user.id,
documentId: star.documentId,
ip,
},
{ transaction }
);
return star;
}
+2
View File
@@ -19,6 +19,8 @@ class Star extends IdModel<
InferAttributes<Star>,
Partial<InferCreationAttributes<Star>>
> {
static eventNamespace = "stars";
@Length({
max: 256,
msg: `index must be 256 characters or less`,
+36 -2
View File
@@ -4,10 +4,12 @@ import isArray from "lodash/isArray";
import isObject from "lodash/isObject";
import pick from "lodash/pick";
import {
Attributes,
CreateOptions,
CreationAttributes,
DataTypes,
FindOptions,
FindOrCreateOptions,
InstanceDestroyOptions,
InstanceUpdateOptions,
ModelStatic,
@@ -18,6 +20,7 @@ import {
AfterCreate,
AfterDestroy,
AfterUpdate,
AfterUpsert,
BeforeCreate,
Model as SequelizeModel,
} from "sequelize-typescript";
@@ -47,8 +50,9 @@ class Model<
* This is the same as calling `set` and then calling `save`.
*/
public updateWithCtx(ctx: APIContext, keys: Partial<TModelAttributes>) {
this.set(keys);
this.cacheChangeset();
return this.update(keys, ctx.context as InstanceUpdateOptions);
return this.save(ctx.context as SaveOptions);
}
/**
@@ -59,6 +63,21 @@ class Model<
return this.destroy(ctx.context as InstanceDestroyOptions);
}
/**
* Find a row that matches the query, or build and save the row if none is found
* The successful result of the promise will be (instance, created) - Make sure to use `.then(([...]))`
*/
public static findOrCreateWithCtx<M extends Model>(
this: ModelStatic<M>,
ctx: APIContext,
options: FindOrCreateOptions<Attributes<M>, CreationAttributes<M>>
) {
return this.findOrCreate({
...options,
...ctx.context,
});
}
/**
* Builds a new model instance and calls save on it.
*/
@@ -83,6 +102,14 @@ class Model<
await this.insertEvent("create", model, context);
}
@AfterUpsert
static async afterUpsertEvent<T extends Model>(
model: T,
context: APIContext["context"]
) {
await this.insertEvent("create", model, context);
}
@AfterUpdate
static async afterUpdateEvent<T extends Model>(
model: T,
@@ -267,7 +294,14 @@ class Model<
* Cache the current changeset for later use.
*/
protected cacheChangeset() {
this.previousChangeset = this.changeset;
const previous = this.changeset;
if (
Object.keys(previous.attributes).length > 0 ||
Object.keys(previous.previous).length > 0
) {
this.previousChangeset = previous;
}
}
/**
+4 -13
View File
@@ -1,8 +1,6 @@
import Router from "koa-router";
import { Sequelize } from "sequelize";
import starCreator from "@server/commands/starCreator";
import starDestroyer from "@server/commands/starDestroyer";
import starUpdater from "@server/commands/starUpdater";
import auth from "@server/middlewares/authentication";
import { transaction } from "@server/middlewares/transaction";
import validate from "@server/middlewares/validate";
@@ -46,12 +44,11 @@ router.post(
}
const star = await starCreator({
ctx,
user,
documentId,
collectionId,
ip: ctx.request.ip,
index,
transaction,
});
ctx.body = {
@@ -130,19 +127,13 @@ router.post(
const { user } = ctx.state.auth;
const { transaction } = ctx.state;
let star = await Star.findByPk(id, {
const star = await Star.findByPk(id, {
transaction,
lock: transaction.LOCK.UPDATE,
});
authorize(user, "update", star);
star = await starUpdater({
user,
star,
ip: ctx.request.ip,
index,
transaction,
});
await star.updateWithCtx(ctx, { index });
ctx.body = {
data: presentStar(star),
@@ -167,7 +158,7 @@ router.post(
});
authorize(user, "delete", star);
await starDestroyer({ user, star, ip: ctx.request.ip, transaction });
await star.destroyWithCtx(ctx);
ctx.body = {
success: true,
+2
View File
@@ -1,6 +1,7 @@
import { faker } from "@faker-js/faker";
import { Transaction } from "sequelize";
import sharedEnv from "@shared/env";
import { createContext } from "@server/context";
import env from "@server/env";
import { User } from "@server/models";
import onerror from "@server/onerror";
@@ -45,6 +46,7 @@ export function withAPIContext<T>(
transaction,
};
return fn({
...createContext(user, transaction),
state,
request: {
ip: faker.internet.ip(),