Request validation for /api/collections.* (#5619)

This commit is contained in:
Apoorv Mishra
2023-08-06 22:24:13 +05:30
committed by GitHub
parent 0ddbd9c608
commit 401ae73a04
8 changed files with 481 additions and 260 deletions

View File

@@ -305,10 +305,11 @@ class Collection extends ParanoidModel {
@Column(DataType.UUID)
teamId: string;
static DEFAULT_SORT = {
field: "index",
direction: "asc",
};
static DEFAULT_SORT: { field: "title" | "index"; direction: "asc" | "desc" } =
{
field: "index",
direction: "asc",
};
/**
* Returns an array of unique userIds that are members of a collection,

View File

@@ -499,6 +499,31 @@ describe("#collections.add_group", () => {
expect(res.status).toEqual(200);
});
it("should fail with status 400 bad request when permission is null", async () => {
const user = await buildAdmin();
const collection = await buildCollection({
teamId: user.teamId,
userId: user.id,
permission: null,
});
const group = await buildGroup({
teamId: user.teamId,
});
const res = await server.post("/api/collections.add_group", {
body: {
token: user.getJwtToken(),
id: collection.id,
groupId: group.id,
permission: null,
},
});
const body = await res.json();
expect(res.status).toEqual(400);
expect(body.message).toEqual(
"permission: Expected 'read' | 'read_write' | 'admin', received null"
);
});
it("should require group in team", async () => {
const user = await buildUser();
const collection = await buildCollection({

View File

@@ -2,20 +2,18 @@ import fractionalIndex from "fractional-index";
import invariant from "invariant";
import Router from "koa-router";
import { Sequelize, Op, WhereOptions } from "sequelize";
import { randomElement } from "@shared/random";
import {
CollectionPermission,
FileOperationFormat,
FileOperationState,
FileOperationType,
} from "@shared/types";
import { colorPalette } from "@shared/utils/collections";
import collectionExporter from "@server/commands/collectionExporter";
import teamUpdater from "@server/commands/teamUpdater";
import { ValidationError } from "@server/errors";
import auth from "@server/middlewares/authentication";
import { rateLimiter } from "@server/middlewares/rateLimiter";
import { transaction } from "@server/middlewares/transaction";
import validate from "@server/middlewares/validate";
import {
Collection,
CollectionUser,
@@ -41,138 +39,128 @@ import { APIContext } from "@server/types";
import { RateLimiterStrategy } from "@server/utils/RateLimiter";
import { collectionIndexing } from "@server/utils/indexing";
import removeIndexCollision from "@server/utils/removeIndexCollision";
import {
assertUuid,
assertIn,
assertPresent,
assertHexColor,
assertIndexCharacters,
assertCollectionPermission,
assertBoolean,
} from "@server/validation";
import pagination from "./middlewares/pagination";
import pagination from "../middlewares/pagination";
import * as T from "./schema";
const router = new Router();
router.post("collections.create", auth(), async (ctx: APIContext) => {
const {
name,
color = randomElement(colorPalette),
description,
permission,
sharing,
icon,
sort = Collection.DEFAULT_SORT,
} = ctx.request.body;
let { index } = ctx.request.body;
assertPresent(name, "name is required");
router.post(
"collections.create",
auth(),
validate(T.CollectionsCreateSchema),
async (ctx: APIContext<T.CollectionsCreateReq>) => {
const { name, color, description, permission, sharing, icon, sort } =
ctx.input.body;
let { index } = ctx.input.body;
if (color) {
assertHexColor(color, "Invalid hex value (please use format #FFFFFF)");
}
const { user } = ctx.state.auth;
authorize(user, "createCollection", user.team);
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) {
assertIndexCharacters(index);
} else {
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"],
],
});
index = fractionalIndex(
null,
collections.length ? collections[0].index : null
);
}
index = fractionalIndex(
null,
collections.length ? collections[0].index : null
);
}
index = await removeIndexCollision(user.teamId, index);
const collection = await Collection.create({
name,
description,
icon,
color,
teamId: user.teamId,
createdById: user.id,
permission: permission ? permission : null,
sharing,
sort,
index,
});
await Event.create({
name: "collections.create",
collectionId: collection.id,
teamId: collection.teamId,
actorId: user.id,
data: {
index = await removeIndexCollision(user.teamId, index);
const collection = await Collection.create({
name,
},
ip: ctx.request.ip,
});
// we must reload the collection to get memberships for policy presenter
const reloaded = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(collection.id);
invariant(reloaded, "collection not found");
description,
icon,
color,
teamId: user.teamId,
createdById: user.id,
permission,
sharing,
sort,
index,
});
await Event.create({
name: "collections.create",
collectionId: collection.id,
teamId: collection.teamId,
actorId: user.id,
data: {
name,
},
ip: ctx.request.ip,
});
// we must reload the collection to get memberships for policy presenter
const reloaded = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(collection.id);
invariant(reloaded, "collection not found");
ctx.body = {
data: presentCollection(reloaded),
policies: presentPolicies(user, [reloaded]),
};
});
ctx.body = {
data: presentCollection(reloaded),
policies: presentPolicies(user, [reloaded]),
};
}
);
router.post("collections.info", auth(), async (ctx: APIContext) => {
const { id } = ctx.request.body;
assertPresent(id, "id is required");
const { user } = ctx.state.auth;
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(id);
router.post(
"collections.info",
auth(),
validate(T.CollectionsInfoSchema),
async (ctx: APIContext<T.CollectionsInfoReq>) => {
const { id } = ctx.input.body;
const { user } = ctx.state.auth;
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(id);
authorize(user, "read", collection);
authorize(user, "read", collection);
ctx.body = {
data: presentCollection(collection),
policies: presentPolicies(user, [collection]),
};
});
ctx.body = {
data: presentCollection(collection),
policies: presentPolicies(user, [collection]),
};
}
);
router.post("collections.documents", auth(), async (ctx: APIContext) => {
const { id } = ctx.request.body;
assertPresent(id, "id is required");
const { user } = ctx.state.auth;
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(id);
router.post(
"collections.documents",
auth(),
validate(T.CollectionsDocumentsSchema),
async (ctx: APIContext<T.CollectionsDocumentsReq>) => {
const { id } = ctx.input.body;
const { user } = ctx.state.auth;
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(id);
authorize(user, "readDocument", collection);
authorize(user, "readDocument", collection);
ctx.body = {
data: collection.documentStructure || [],
};
});
ctx.body = {
data: collection.documentStructure || [],
};
}
);
router.post(
"collections.import",
rateLimiter(RateLimiterStrategy.TenPerHour),
auth(),
validate(T.CollectionsImportSchema),
transaction(),
async (ctx: APIContext) => {
async (ctx: APIContext<T.CollectionsImportReq>) => {
const { transaction } = ctx.state;
const { attachmentId, format = FileOperationFormat.MarkdownZip } =
ctx.request.body;
assertUuid(attachmentId, "attachmentId is required");
const { attachmentId, format } = ctx.input.body;
const { user } = ctx.state.auth;
authorize(user, "importCollection", user.team);
@@ -180,8 +168,6 @@ router.post(
const attachment = await Attachment.findByPk(attachmentId);
authorize(user, "read", attachment);
assertIn(format, Object.values(FileOperationFormat), "Invalid format");
const fileOperation = await FileOperation.create(
{
type: FileOperationType.Import,
@@ -218,106 +204,105 @@ router.post(
}
);
router.post("collections.add_group", auth(), async (ctx: APIContext) => {
const {
id,
groupId,
permission = CollectionPermission.ReadWrite,
} = ctx.request.body;
assertUuid(id, "id is required");
assertUuid(groupId, "groupId is required");
assertCollectionPermission(permission);
router.post(
"collections.add_group",
auth(),
validate(T.CollectionsAddGroupSchema),
async (ctx: APIContext<T.CollectionsAddGroupsReq>) => {
const { id, groupId, permission } = ctx.input.body;
const { user } = ctx.state.auth;
const { user } = ctx.state.auth;
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(id);
authorize(user, "update", collection);
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(id);
authorize(user, "update", collection);
const group = await Group.findByPk(groupId);
authorize(user, "read", group);
const group = await Group.findByPk(groupId);
authorize(user, "read", group);
let membership = await CollectionGroup.findOne({
where: {
collectionId: id,
groupId,
},
});
if (!membership) {
membership = await CollectionGroup.create({
collectionId: id,
groupId,
permission,
createdById: user.id,
let membership = await CollectionGroup.findOne({
where: {
collectionId: id,
groupId,
},
});
} else if (permission) {
membership.permission = permission;
await membership.save();
if (!membership) {
membership = await CollectionGroup.create({
collectionId: id,
groupId,
permission,
createdById: user.id,
});
} else {
membership.permission = permission;
await membership.save();
}
await Event.create({
name: "collections.add_group",
collectionId: collection.id,
teamId: collection.teamId,
actorId: user.id,
modelId: groupId,
data: {
name: group.name,
},
ip: ctx.request.ip,
});
ctx.body = {
data: {
collectionGroupMemberships: [
presentCollectionGroupMembership(membership),
],
},
};
}
);
await Event.create({
name: "collections.add_group",
collectionId: collection.id,
teamId: collection.teamId,
actorId: user.id,
modelId: groupId,
data: {
name: group.name,
},
ip: ctx.request.ip,
});
router.post(
"collections.remove_group",
auth(),
validate(T.CollectionsRemoveGroupSchema),
async (ctx: APIContext<T.CollectionsRemoveGroupReq>) => {
const { id, groupId } = ctx.input.body;
const { user } = ctx.state.auth;
ctx.body = {
data: {
collectionGroupMemberships: [
presentCollectionGroupMembership(membership),
],
},
};
});
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(id);
authorize(user, "update", collection);
router.post("collections.remove_group", auth(), async (ctx: APIContext) => {
const { id, groupId } = ctx.request.body;
assertUuid(id, "id is required");
assertUuid(groupId, "groupId is required");
const group = await Group.findByPk(groupId);
authorize(user, "read", group);
const { user } = ctx.state.auth;
await collection.$remove("group", group);
await Event.create({
name: "collections.remove_group",
collectionId: collection.id,
teamId: collection.teamId,
actorId: user.id,
modelId: groupId,
data: {
name: group.name,
},
ip: ctx.request.ip,
});
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(id);
authorize(user, "update", collection);
const group = await Group.findByPk(groupId);
authorize(user, "read", group);
await collection.$remove("group", group);
await Event.create({
name: "collections.remove_group",
collectionId: collection.id,
teamId: collection.teamId,
actorId: user.id,
modelId: groupId,
data: {
name: group.name,
},
ip: ctx.request.ip,
});
ctx.body = {
success: true,
};
});
ctx.body = {
success: true,
};
}
);
router.post(
"collections.group_memberships",
auth(),
pagination(),
async (ctx: APIContext) => {
const { id, query, permission } = ctx.request.body;
assertUuid(id, "id is required");
validate(T.CollectionsGroupMembershipsSchema),
async (ctx: APIContext<T.CollectionsGroupMembershipsReq>) => {
const { id, query, permission } = ctx.input.body;
const { user } = ctx.state.auth;
const collection = await Collection.scope({
@@ -380,12 +365,11 @@ router.post(
"collections.add_user",
auth(),
transaction(),
async (ctx: APIContext) => {
validate(T.CollectionsAddUserSchema),
async (ctx: APIContext<T.CollectionsAddUserReq>) => {
const { auth, transaction } = ctx.state;
const actor = auth.user;
const { id, userId, permission } = ctx.request.body;
assertUuid(id, "id is required");
assertUuid(userId, "userId is required");
const { id, userId, permission } = ctx.input.body;
const collection = await Collection.scope({
method: ["withMembership", actor.id],
@@ -404,10 +388,6 @@ router.post(
lock: transaction.LOCK.UPDATE,
});
if (permission) {
assertCollectionPermission(permission);
}
if (!membership) {
membership = await CollectionUser.create(
{
@@ -455,12 +435,11 @@ router.post(
"collections.remove_user",
auth(),
transaction(),
async (ctx: APIContext) => {
validate(T.CollectionsRemoveUserSchema),
async (ctx: APIContext<T.CollectionsRemoveUserReq>) => {
const { auth, transaction } = ctx.state;
const actor = auth.user;
const { id, userId } = ctx.request.body;
assertUuid(id, "id is required");
assertUuid(userId, "userId is required");
const { id, userId } = ctx.input.body;
const collection = await Collection.scope({
method: ["withMembership", actor.id],
@@ -496,9 +475,9 @@ router.post(
"collections.memberships",
auth(),
pagination(),
async (ctx: APIContext) => {
const { id, query, permission } = ctx.request.body;
assertUuid(id, "id is required");
validate(T.CollectionsMembershipsSchema),
async (ctx: APIContext<T.CollectionsMembershipsReq>) => {
const { id, query, permission } = ctx.input.body;
const { user } = ctx.state.auth;
const collection = await Collection.scope({
@@ -520,7 +499,6 @@ router.post(
}
if (permission) {
assertCollectionPermission(permission);
where = { ...where, permission };
}
@@ -560,20 +538,13 @@ router.post(
"collections.export",
rateLimiter(RateLimiterStrategy.TenPerHour),
auth(),
validate(T.CollectionsExportSchema),
transaction(),
async (ctx: APIContext) => {
async (ctx: APIContext<T.CollectionsExportReq>) => {
const { transaction } = ctx.state;
const { id } = ctx.request.body;
const {
format = FileOperationFormat.MarkdownZip,
includeAttachments = true,
} = ctx.request.body;
assertUuid(id, "id is required");
assertIn(format, Object.values(FileOperationFormat), "Invalid format");
assertBoolean(includeAttachments, "includeAttachments must be a boolean");
const { id, format, includeAttachments } = ctx.input.body;
const { user } = ctx.state.auth;
const team = await Team.findByPk(user.teamId);
authorize(user, "createExport", team);
@@ -605,20 +576,15 @@ router.post(
"collections.export_all",
rateLimiter(RateLimiterStrategy.FivePerHour),
auth(),
validate(T.CollectionsExportAllSchema),
transaction(),
async (ctx: APIContext) => {
async (ctx: APIContext<T.CollectionsExportAllReq>) => {
const { transaction } = ctx.state;
const {
format = FileOperationFormat.MarkdownZip,
includeAttachments = true,
} = ctx.request.body;
const { format, includeAttachments } = ctx.input.body;
const { user } = ctx.state.auth;
const team = await Team.findByPk(user.teamId);
authorize(user, "createExport", team);
assertIn(format, Object.values(FileOperationFormat), "Invalid format");
assertBoolean(includeAttachments, "includeAttachments must be a boolean");
const fileOperation = await collectionExporter({
user,
team,
@@ -640,15 +606,12 @@ router.post(
router.post(
"collections.update",
auth(),
validate(T.CollectionsUpdateSchema),
transaction(),
async (ctx: APIContext) => {
async (ctx: APIContext<T.CollectionsUpdateReq>) => {
const { transaction } = ctx.state;
const { id, name, description, icon, permission, color, sort, sharing } =
ctx.request.body;
if (color) {
assertHexColor(color, "Invalid hex value (please use format #FFFFFF)");
}
ctx.input.body;
const { user } = ctx.state.auth;
const collection = await Collection.scope({
@@ -697,9 +660,6 @@ router.post(
}
if (permission !== undefined) {
if (permission) {
assertCollectionPermission(permission);
}
privacyChanged = permission !== collection.permission;
collection.permission = permission ? permission : null;
}
@@ -782,9 +742,10 @@ router.post(
router.post(
"collections.list",
auth(),
validate(T.CollectionsListSchema),
pagination(),
async (ctx: APIContext) => {
const { includeListOnly } = ctx.request.body;
async (ctx: APIContext<T.CollectionsListReq>) => {
const { includeListOnly } = ctx.input.body;
const { user } = ctx.state.auth;
const collectionIds = await user.collectionIds();
const where: WhereOptions<Collection> =
@@ -830,12 +791,12 @@ router.post(
router.post(
"collections.delete",
auth(),
validate(T.CollectionsDeleteSchema),
transaction(),
async (ctx: APIContext) => {
async (ctx: APIContext<T.CollectionsDeleteReq>) => {
const { transaction } = ctx.state;
const { id } = ctx.request.body;
const { id } = ctx.input.body;
const { user } = ctx.state.auth;
assertUuid(id, "id is required");
const collection = await Collection.scope({
method: ["withMembership", user.id],
@@ -886,14 +847,12 @@ router.post(
router.post(
"collections.move",
auth(),
validate(T.CollectionsMoveSchema),
transaction(),
async (ctx: APIContext) => {
async (ctx: APIContext<T.CollectionsMoveReq>) => {
const { transaction } = ctx.state;
const id = ctx.request.body.id;
let index = ctx.request.body.index;
assertPresent(index, "index is required");
assertIndexCharacters(index);
assertUuid(id, "id must be a uuid");
const { id } = ctx.input.body;
let { index } = ctx.input.body;
const { user } = ctx.state.auth;
const collection = await Collection.findByPk(id, {

View File

@@ -0,0 +1 @@
export { default } from "./collections";

View File

@@ -0,0 +1,225 @@
import { isUndefined } from "lodash";
import { z } from "zod";
import { randomElement } from "@shared/random";
import { CollectionPermission, FileOperationFormat } from "@shared/types";
import { colorPalette } from "@shared/utils/collections";
import { Collection } from "@server/models";
import { ValidateColor, ValidateIcon, ValidateIndex } from "@server/validation";
import BaseSchema from "../BaseSchema";
export const CollectionsCreateSchema = BaseSchema.extend({
body: z.object({
name: z.string(),
color: z
.string()
.regex(ValidateColor.regex, { message: ValidateColor.message })
.default(randomElement(colorPalette)),
description: z.string().nullish(),
permission: z
.nativeEnum(CollectionPermission)
.nullish()
.transform((val) => (isUndefined(val) ? null : val)),
sharing: z.boolean().default(true),
icon: z
.string()
.max(ValidateIcon.maxLength, {
message: `Must be ${ValidateIcon.maxLength} or fewer characters long`,
})
.optional(),
sort: z
.object({
field: z.union([z.literal("title"), z.literal("index")]),
direction: z.union([z.literal("asc"), z.literal("desc")]),
})
.default(Collection.DEFAULT_SORT),
index: z
.string()
.regex(ValidateIndex.regex, { message: ValidateIndex.message })
.max(ValidateIndex.maxLength, {
message: `Must be ${ValidateIndex.maxLength} or fewer characters long`,
})
.optional(),
}),
});
export type CollectionsCreateReq = z.infer<typeof CollectionsCreateSchema>;
export const CollectionsInfoSchema = BaseSchema.extend({
body: z.object({
id: z.string().uuid(),
}),
});
export type CollectionsInfoReq = z.infer<typeof CollectionsInfoSchema>;
export const CollectionsDocumentsSchema = BaseSchema.extend({
body: z.object({
id: z.string().uuid(),
}),
});
export type CollectionsDocumentsReq = z.infer<
typeof CollectionsDocumentsSchema
>;
export const CollectionsImportSchema = BaseSchema.extend({
body: z.object({
attachmentId: z.string().uuid(),
format: z
.nativeEnum(FileOperationFormat)
.default(FileOperationFormat.MarkdownZip),
}),
});
export type CollectionsImportReq = z.infer<typeof CollectionsImportSchema>;
export const CollectionsAddGroupSchema = BaseSchema.extend({
body: z.object({
id: z.string().uuid(),
groupId: z.string().uuid(),
permission: z
.nativeEnum(CollectionPermission)
.default(CollectionPermission.ReadWrite),
}),
});
export type CollectionsAddGroupsReq = z.infer<typeof CollectionsAddGroupSchema>;
export const CollectionsRemoveGroupSchema = BaseSchema.extend({
body: z.object({
id: z.string().uuid(),
groupId: z.string().uuid(),
}),
});
export type CollectionsRemoveGroupReq = z.infer<
typeof CollectionsRemoveGroupSchema
>;
export const CollectionsGroupMembershipsSchema = BaseSchema.extend({
body: z.object({
id: z.string().uuid(),
query: z.string().optional(),
permission: z.nativeEnum(CollectionPermission).optional(),
}),
});
export type CollectionsGroupMembershipsReq = z.infer<
typeof CollectionsGroupMembershipsSchema
>;
export const CollectionsAddUserSchema = BaseSchema.extend({
body: z.object({
id: z.string().uuid(),
userId: z.string().uuid(),
permission: z.nativeEnum(CollectionPermission).optional(),
}),
});
export type CollectionsAddUserReq = z.infer<typeof CollectionsAddUserSchema>;
export const CollectionsRemoveUserSchema = BaseSchema.extend({
body: z.object({
id: z.string().uuid(),
userId: z.string().uuid(),
}),
});
export type CollectionsRemoveUserReq = z.infer<
typeof CollectionsRemoveUserSchema
>;
export const CollectionsMembershipsSchema = BaseSchema.extend({
body: z.object({
id: z.string().uuid(),
query: z.string().optional(),
permission: z.nativeEnum(CollectionPermission).optional(),
}),
});
export type CollectionsMembershipsReq = z.infer<
typeof CollectionsMembershipsSchema
>;
export const CollectionsExportSchema = BaseSchema.extend({
body: z.object({
id: z.string().uuid(),
format: z
.nativeEnum(FileOperationFormat)
.default(FileOperationFormat.MarkdownZip),
includeAttachments: z.boolean().default(true),
}),
});
export type CollectionsExportReq = z.infer<typeof CollectionsExportSchema>;
export const CollectionsExportAllSchema = BaseSchema.extend({
body: z.object({
format: z
.nativeEnum(FileOperationFormat)
.default(FileOperationFormat.MarkdownZip),
includeAttachments: z.boolean().default(true),
}),
});
export type CollectionsExportAllReq = z.infer<
typeof CollectionsExportAllSchema
>;
export const CollectionsUpdateSchema = BaseSchema.extend({
body: z.object({
id: z.string().uuid(),
name: z.string().optional(),
description: z.string().nullish(),
icon: z
.string()
.max(ValidateIcon.maxLength, {
message: `Must be ${ValidateIcon.maxLength} or fewer characters long`,
})
.nullish(),
permission: z.nativeEnum(CollectionPermission).nullish(),
color: z
.string()
.regex(ValidateColor.regex, { message: ValidateColor.message })
.nullish(),
sort: z
.object({
field: z.union([z.literal("title"), z.literal("index")]),
direction: z.union([z.literal("asc"), z.literal("desc")]),
})
.optional(),
sharing: z.boolean().optional(),
}),
});
export type CollectionsUpdateReq = z.infer<typeof CollectionsUpdateSchema>;
export const CollectionsListSchema = BaseSchema.extend({
body: z.object({
includeListOnly: z.boolean().default(false),
}),
});
export type CollectionsListReq = z.infer<typeof CollectionsListSchema>;
export const CollectionsDeleteSchema = BaseSchema.extend({
body: z.object({
id: z.string().uuid(),
}),
});
export type CollectionsDeleteReq = z.infer<typeof CollectionsDeleteSchema>;
export const CollectionsMoveSchema = BaseSchema.extend({
body: z.object({
id: z.string().uuid(),
index: z
.string()
.regex(ValidateIndex.regex, { message: ValidateIndex.message })
.max(ValidateIndex.maxLength, {
message: `Must be ${ValidateIndex.maxLength} or fewer characters long`,
}),
}),
});
export type CollectionsMoveReq = z.infer<typeof CollectionsMoveSchema>;

View File

@@ -287,7 +287,7 @@ describe("#team.update", () => {
body: {
token: admin.getJwtToken(),
id: collection.id,
permission: "",
permission: null,
},
});

View File

@@ -187,6 +187,7 @@ export class ValidateDocumentId {
export class ValidateIndex {
public static regex = new RegExp("^[\x20-\x7E]+$");
public static message = "Must be between x20 to x7E ASCII";
public static maxLength = 100;
}
export class ValidateURL {
@@ -209,3 +210,12 @@ export class ValidateURL {
public static message = "Must be a valid url";
}
export class ValidateColor {
public static regex = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i;
public static message = "Must be a hex value (please use format #FFFFFF)";
}
export class ValidateIcon {
public static maxLength = 50;
}