Add externalId property on groups (#8127)

* Add 'externalId' property on groups

* Remove clientside Field decorator

* Allow querying by externalId
This commit is contained in:
Tom Moor
2024-12-26 16:44:04 +00:00
committed by GitHub
parent 3d5a167f7f
commit 68a469daa7
9 changed files with 87 additions and 11 deletions

View File

@@ -10,6 +10,9 @@ class Group extends Model {
@observable
name: string;
@observable
externalId: string | undefined;
@observable
memberCount: number;

View File

@@ -3,6 +3,7 @@ import { Event, Group, type User } from "@server/models";
type Props = {
name: string;
externalId: string | undefined;
actor: User;
ip: string;
transaction?: Transaction;
@@ -10,6 +11,7 @@ type Props = {
export default async function groupCreator({
name,
externalId,
actor,
ip,
transaction,
@@ -17,6 +19,7 @@ export default async function groupCreator({
const group = await Group.create(
{
name,
externalId,
teamId: actor.teamId,
createdById: actor.id,
},

View File

@@ -3,7 +3,8 @@ import { Event, type Group, type User } from "@server/models";
type Props = {
group: Group;
name: string;
name: string | undefined;
externalId: string | undefined;
actor: User;
ip: string;
transaction?: Transaction;
@@ -12,11 +13,17 @@ type Props = {
export default async function groupUpdater({
group,
name,
externalId,
actor,
ip,
transaction,
}: Props): Promise<Group> {
group.name = name;
if (name) {
group.name = name;
}
if (externalId) {
group.externalId = externalId;
}
if (group.changed()) {
await group.save({ transaction });

View File

@@ -0,0 +1,24 @@
"use strict";
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.transaction(async transaction => {
await queryInterface.addColumn("groups", "externalId", {
type: Sequelize.STRING,
}, { transaction });
await queryInterface.addIndex("groups", ["externalId"], { transaction });
await queryInterface.addIndex("group_permissions", ["documentId"], { transaction });
await queryInterface.addIndex("group_permissions", ["sourceId"], { transaction });
});
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.transaction(async transaction => {
await queryInterface.removeIndex("group_permissions", ["sourceId"], { transaction });
await queryInterface.removeIndex("group_permissions", ["documentId"], { transaction });
await queryInterface.removeIndex("groups", ["externalId"], { transaction });
await queryInterface.removeColumn("groups", "externalId", { transaction });
});
},
};

View File

@@ -65,6 +65,9 @@ class Group extends ParanoidModel<
@Column
name: string;
@Column
externalId: string;
static filterByMember(userId: string | undefined) {
return userId
? this.scope({ method: ["withMembership", userId] })

View File

@@ -4,6 +4,7 @@ export default async function presentGroup(group: Group) {
return {
id: group.id,
name: group.name,
externalId: group.externalId,
memberCount: await group.memberCount,
createdAt: group.createdAt,
updatedAt: group.updatedAt,

View File

@@ -12,11 +12,13 @@ describe("#groups.create", () => {
body: {
token: user.getJwtToken(),
name,
externalId: "123",
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.name).toEqual(name);
expect(body.data.externalId).toEqual("123");
});
});
@@ -67,12 +69,14 @@ describe("#groups.update", () => {
teamId: user.teamId,
});
});
it("allows admin to edit a group", async () => {
const res = await server.post("/api/groups.update", {
body: {
token: user.getJwtToken(),
id: group.id,
name: "Test",
externalId: "123",
},
});
const events = await Event.findAll({
@@ -84,7 +88,9 @@ describe("#groups.update", () => {
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.name).toBe("Test");
expect(body.data.externalId).toBe("123");
});
it("does not create an event if the update is a noop", async () => {
const res = await server.post("/api/groups.update", {
body: {
@@ -103,6 +109,7 @@ describe("#groups.update", () => {
expect(res.status).toEqual(200);
expect(body.data.name).toBe(group.name);
});
it("fails with validation error when name already taken", async () => {
await buildGroup({
teamId: user.teamId,
@@ -275,6 +282,23 @@ describe("#groups.list", () => {
expect(body.data.groups.length).toEqual(1);
expect(body.data.groups[0].id).toEqual(group.id);
});
it("should allow to find a group by its externalId", async () => {
const user = await buildUser();
const group = await buildGroup({ teamId: user.teamId, externalId: "123" });
await buildGroup({ teamId: user.teamId });
const res = await server.post("/api/groups.list", {
body: {
externalId: "123",
token: user.getJwtToken(),
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.groups.length).toEqual(1);
expect(body.data.groups[0].id).toEqual(group.id);
});
});
describe("#groups.info", () => {

View File

@@ -31,7 +31,7 @@ router.post(
pagination(),
validate(T.GroupsListSchema),
async (ctx: APIContext<T.GroupsListReq>) => {
const { sort, direction, query, userId, name } = ctx.input.body;
const { sort, direction, query, userId, externalId, name } = ctx.input.body;
const { user } = ctx.state.auth;
authorize(user, "listGroups", user.team);
@@ -55,6 +55,13 @@ router.post(
};
}
if (externalId) {
where = {
...where,
externalId,
};
}
const groups = await Group.filterByMember(userId).findAll({
where,
order: [[sort, direction]],
@@ -110,18 +117,19 @@ router.post(
router.post(
"groups.create",
rateLimiter(RateLimiterStrategy.TenPerHour),
rateLimiter(RateLimiterStrategy.TenPerMinute),
auth(),
validate(T.GroupsCreateSchema),
transaction(),
async (ctx: APIContext<T.GroupsCreateReq>) => {
const { name } = ctx.input.body;
const { name, externalId } = ctx.input.body;
const { user } = ctx.state.auth;
const { transaction } = ctx.state;
authorize(user, "createGroup", user.team);
const group = await groupCreator({
name,
externalId,
actor: user,
ip: ctx.request.ip,
transaction,
@@ -140,7 +148,7 @@ router.post(
validate(T.GroupsUpdateSchema),
transaction(),
async (ctx: APIContext<T.GroupsUpdateReq>) => {
const { id, name } = ctx.input.body;
const { id, name, externalId } = ctx.input.body;
const { user } = ctx.state.auth;
const { transaction } = ctx.state;
@@ -150,6 +158,7 @@ router.post(
group = await groupUpdater({
group,
name,
externalId,
actor: user,
ip: ctx.request.ip,
transaction,

View File

@@ -13,7 +13,6 @@ export const GroupsListSchema = z.object({
.string()
.optional()
.transform((val) => (val !== "ASC" ? "DESC" : val)),
/** Groups sorting column */
sort: z
.string()
@@ -21,13 +20,12 @@ export const GroupsListSchema = z.object({
message: "Invalid sort parameter",
})
.default("updatedAt"),
/** Only list groups where this user is a member */
userId: z.string().uuid().optional(),
/** Find group matching externalId */
externalId: z.string().optional(),
/** @deprecated Find group with matching name */
name: z.string().optional(),
/** Find group matching query */
query: z.string().optional(),
}),
@@ -45,6 +43,8 @@ export const GroupsCreateSchema = z.object({
body: z.object({
/** Group name */
name: z.string(),
/** Optionally link this group to an external source. */
externalId: z.string().optional(),
}),
});
@@ -53,7 +53,9 @@ export type GroupsCreateReq = z.infer<typeof GroupsCreateSchema>;
export const GroupsUpdateSchema = z.object({
body: BaseIdSchema.extend({
/** Group name */
name: z.string(),
name: z.string().optional(),
/** Optionally link this group to an external source. */
externalId: z.string().optional(),
}),
});