Skip to content

Commit

Permalink
Add Group Membership model
Browse files Browse the repository at this point in the history
  • Loading branch information
PopDaph committed Jul 25, 2024
1 parent fdb6d79 commit 9e9467e
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 0 deletions.
2 changes: 2 additions & 0 deletions front/admin/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
} from "@app/lib/models/workspace";
import { ContentFragmentModel } from "@app/lib/resources/storage/models/content_fragment";
import { FileModel } from "@app/lib/resources/storage/models/files";
import { GroupMembershipModel } from "@app/lib/resources/storage/models/group_memberships";
import { GroupModel } from "@app/lib/resources/storage/models/groups";
import { KeyModel } from "@app/lib/resources/storage/models/keys";
// Labs - Can be removed at all times if a solution is dropped
Expand Down Expand Up @@ -92,6 +93,7 @@ async function main() {
await MembershipModel.sync({ alter: true });
await MembershipInvitation.sync({ alter: true });
await GroupModel.sync({ alter: true });
await GroupMembershipModel.sync({ alter: true });

await App.sync({ alter: true });
await Dataset.sync({ alter: true });
Expand Down
49 changes: 49 additions & 0 deletions front/lib/resources/group_resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ import type {

import type { Authenticator } from "@app/lib/auth";
import { BaseResource } from "@app/lib/resources/base_resource";
import { MembershipResource } from "@app/lib/resources/membership_resource";
import { GroupMembershipModel } from "@app/lib/resources/storage/models/group_memberships";
import { GroupModel } from "@app/lib/resources/storage/models/groups";
import type { ReadonlyAttributesType } from "@app/lib/resources/storage/types";
import { getResourceIdFromSId, makeSId } from "@app/lib/resources/string_ids";
import { UserResource } from "@app/lib/resources/user_resource";
import { renderLightWorkspaceType } from "@app/lib/workspace";

// Attributes are marked as read-only to reflect the stateless nature of our Resource.
// This design will be moved up to BaseResource once we transition away from Sequelize.
Expand Down Expand Up @@ -177,6 +181,51 @@ export class GroupResource extends BaseResource<GroupModel> {
return new this(GroupModel, group.get());
}

static async addMember(
auth: Authenticator,
groupId: string,
userId: string,
transaction?: Transaction
): Promise<void> {
const owner = auth.getNonNullableWorkspace();
const [group, user] = await Promise.all([
this.fetchById(auth, groupId),
UserResource.fetchById(userId),
]);

if (!group) {
throw new Error("Group not found.");
}
if (group.type !== "regular") {
throw new Error("Cannot add members to non-regular groups.");
}
if (!user) {
throw new Error("User not found.");
}

const workspace = renderLightWorkspaceType({ workspace: owner });
const membership =
await MembershipResource.getActiveMembershipOfUserInWorkspace({
user,
workspace,
transaction,
});

if (!membership) {
throw new Error("User is not a member of the workspace.");
}

await GroupMembershipModel.create(
{
groupId: group.id,
userId: user.id,
workspaceId: owner.id,
startAt: new Date(),
},
{ transaction }
);
}

toJSON() {
return {
id: this.id,
Expand Down
112 changes: 112 additions & 0 deletions front/lib/resources/storage/models/group_memberships.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type {
CreationOptional,
ForeignKey,
InferAttributes,
InferCreationAttributes,
} from "sequelize";
import { DataTypes, Model, Op } from "sequelize";

import { User } from "@app/lib/models/user";
import { Workspace } from "@app/lib/models/workspace";
import { frontSequelize } from "@app/lib/resources/storage";
import { GroupModel } from "@app/lib/resources/storage/models/groups";

export class GroupMembershipModel extends Model<
InferAttributes<GroupMembershipModel>,
InferCreationAttributes<GroupMembershipModel>
> {
declare id: CreationOptional<number>;
declare createdAt: CreationOptional<Date>;
declare updatedAt: CreationOptional<Date>;

declare startAt: Date;
declare endAt: Date | null;

declare groupId: ForeignKey<GroupModel["id"]>;
declare userId: ForeignKey<User["id"]>;
declare workspaceId: ForeignKey<Workspace["id"]>;

static async checkOverlap(
userId: number,
groupId: number,
startAt: Date,
endAt: Date | null,
excludeId?: number
) {
const where: any = {
userId,
groupId,
[Op.or]: [{ endAt: null }, { endAt: { [Op.gt]: startAt } }],
startAt: { [Op.lt]: endAt || new Date(8640000000000000) }, // Max date if endAt is null
};

if (excludeId) {
where.id = { [Op.ne]: excludeId };
}

const overlapping = await this.findOne({ where });
return !!overlapping;
}
}
GroupMembershipModel.init(
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
},
startAt: {
type: DataTypes.DATE,
allowNull: false,
},
endAt: {
type: DataTypes.DATE,
allowNull: true,
},
},
{
modelName: "group_memberships",
sequelize: frontSequelize,
indexes: [{ fields: ["userId", "groupId"] }],
validate: {
async noOverlap() {
if (
await GroupMembershipModel.checkOverlap(
this.userId as number,
this.groupId as number,
this.startAt as Date,
this.endAt as Date | null,
this.id as number
)
) {
throw new Error("Overlapping group membership period");
}
},
},
}
);
User.hasMany(GroupMembershipModel, {
foreignKey: { allowNull: false },
onDelete: "CASCADE",
});
GroupModel.hasMany(GroupMembershipModel, {
foreignKey: { allowNull: false },
onDelete: "CASCADE",
});
Workspace.hasMany(GroupMembershipModel, {
foreignKey: { allowNull: false },
onDelete: "CASCADE",
});
GroupMembershipModel.belongsTo(User);
GroupMembershipModel.belongsTo(GroupModel);
GroupMembershipModel.belongsTo(Workspace);
13 changes: 13 additions & 0 deletions front/migrations/db/migration_47.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- Migration created on Jul 25, 2024
CREATE TABLE IF NOT EXISTS "group_memberships" (
"id" SERIAL ,
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"startAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"endAt" TIMESTAMP WITH TIME ZONE,
"userId" INTEGER NOT NULL REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
"groupId" INTEGER NOT NULL REFERENCES "groups" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
"workspaceId" INTEGER NOT NULL REFERENCES "workspaces" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY ("id")
);
CREATE INDEX "group_memberships_user_id_group_id" ON "group_memberships" ("userId", "groupId");

0 comments on commit 9e9467e

Please sign in to comment.