mirror of
https://github.com/outline/outline.git
synced 2026-04-23 10:39:14 -05:00
fix: Index calculation does not take into account user scope (#7294)
* fix: Index calculation does not take into account user scope * Standardize sort index maxlength to 256
This commit is contained in:
@@ -197,8 +197,8 @@ class Collection extends ParanoidModel<
|
||||
color: string | null;
|
||||
|
||||
@Length({
|
||||
max: 100,
|
||||
msg: `index must be 100 characters or less`,
|
||||
max: 256,
|
||||
msg: `index must be 256 characters or less`,
|
||||
})
|
||||
@Column
|
||||
index: string | null;
|
||||
@@ -425,13 +425,18 @@ class Collection extends ParanoidModel<
|
||||
/**
|
||||
* Find the first collection that the specified user has access to.
|
||||
*
|
||||
* @param user User object
|
||||
* @param user User to find the collection for
|
||||
* @param options Additional options for the query
|
||||
* @returns collection First collection in the sidebar order
|
||||
*/
|
||||
static async findFirstCollectionForUser(user: User) {
|
||||
static async findFirstCollectionForUser(
|
||||
user: User,
|
||||
options: FindOptions = {}
|
||||
) {
|
||||
const id = await user.collectionIds();
|
||||
return this.findOne({
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
id,
|
||||
},
|
||||
order: [
|
||||
@@ -439,6 +444,7 @@ class Collection extends ParanoidModel<
|
||||
Sequelize.literal('"collection"."index" collate "C"'),
|
||||
["updatedAt", "DESC"],
|
||||
],
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ForeignKey,
|
||||
BelongsTo,
|
||||
Table,
|
||||
Length,
|
||||
} from "sequelize-typescript";
|
||||
import Collection from "./Collection";
|
||||
import Document from "./Document";
|
||||
@@ -19,6 +20,10 @@ class Pin extends IdModel<
|
||||
InferAttributes<Pin>,
|
||||
Partial<InferCreationAttributes<Pin>>
|
||||
> {
|
||||
@Length({
|
||||
max: 256,
|
||||
msg: `index must be 256 characters or less`,
|
||||
})
|
||||
@Column
|
||||
index: string | null;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
BelongsTo,
|
||||
ForeignKey,
|
||||
Table,
|
||||
Length,
|
||||
} from "sequelize-typescript";
|
||||
import Collection from "./Collection";
|
||||
import Document from "./Document";
|
||||
@@ -18,6 +19,10 @@ class Star extends IdModel<
|
||||
InferAttributes<Star>,
|
||||
Partial<InferCreationAttributes<Star>>
|
||||
> {
|
||||
@Length({
|
||||
max: 256,
|
||||
msg: `index must be 256 characters or less`,
|
||||
})
|
||||
@Column
|
||||
index: string | null;
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
Table,
|
||||
DataType,
|
||||
Scopes,
|
||||
AllowNull,
|
||||
AfterCreate,
|
||||
AfterUpdate,
|
||||
Length,
|
||||
} from "sequelize-typescript";
|
||||
import { CollectionPermission, DocumentPermission } from "@shared/types";
|
||||
import Collection from "./Collection";
|
||||
@@ -73,7 +73,10 @@ class UserMembership extends IdModel<
|
||||
permission: CollectionPermission | DocumentPermission;
|
||||
|
||||
/** The visible sort order in "shared with me" */
|
||||
@AllowNull
|
||||
@Length({
|
||||
max: 256,
|
||||
msg: `index must be 256 characters or less`,
|
||||
})
|
||||
@Column
|
||||
index: string | null;
|
||||
|
||||
|
||||
@@ -60,29 +60,16 @@ router.post(
|
||||
const { user } = ctx.state.auth;
|
||||
authorize(user, "createCollection", user.team);
|
||||
|
||||
if (!index) {
|
||||
const collections = await Collection.findAll({
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
deletedAt: null,
|
||||
},
|
||||
attributes: ["id", "index", "updatedAt"],
|
||||
limit: 1,
|
||||
order: [
|
||||
// using LC_COLLATE:"C" because we need byte order to drive the sorting
|
||||
Sequelize.literal('"collection"."index" collate "C"'),
|
||||
["updatedAt", "DESC"],
|
||||
],
|
||||
if (index) {
|
||||
index = await removeIndexCollision(user.teamId, index, { transaction });
|
||||
} else {
|
||||
const first = await Collection.findFirstCollectionForUser(user, {
|
||||
attributes: ["id", "index"],
|
||||
transaction,
|
||||
});
|
||||
|
||||
index = fractionalIndex(
|
||||
null,
|
||||
collections.length ? collections[0].index : null
|
||||
);
|
||||
index = fractionalIndex(null, first ? first.index : null);
|
||||
}
|
||||
|
||||
index = await removeIndexCollision(user.teamId, index);
|
||||
const collection = Collection.build({
|
||||
name,
|
||||
content: data,
|
||||
@@ -891,7 +878,7 @@ router.post(
|
||||
});
|
||||
authorize(user, "move", collection);
|
||||
|
||||
index = await removeIndexCollision(user.teamId, index);
|
||||
index = await removeIndexCollision(user.teamId, index, { transaction });
|
||||
await collection.update(
|
||||
{
|
||||
index,
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import fractionalIndex from "fractional-index";
|
||||
import { Op, Sequelize } from "sequelize";
|
||||
import { Op, Sequelize, type FindOptions } from "sequelize";
|
||||
import Collection from "@server/models/Collection";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param teamId The team id whose collections has to be fetched
|
||||
* @param index the index for which collision has to be checked
|
||||
* @param options Additional options to be passed to the query
|
||||
* @returns An index, if there is collision returns a new index otherwise the same index
|
||||
*/
|
||||
export default async function removeIndexCollision(
|
||||
teamId: string,
|
||||
index: string
|
||||
index: string,
|
||||
options: FindOptions = {}
|
||||
) {
|
||||
const collection = await Collection.findOne({
|
||||
where: {
|
||||
@@ -18,6 +20,7 @@ export default async function removeIndexCollision(
|
||||
deletedAt: null,
|
||||
index,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
||||
if (!collection) {
|
||||
@@ -38,6 +41,7 @@ export default async function removeIndexCollision(
|
||||
Sequelize.literal('"collection"."index" collate "C"'),
|
||||
["updatedAt", "DESC"],
|
||||
],
|
||||
...options,
|
||||
});
|
||||
const nextCollectionIndex = nextCollection.length
|
||||
? nextCollection[0].index
|
||||
|
||||
Reference in New Issue
Block a user