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 e8372b7
Show file tree
Hide file tree
Showing 4 changed files with 169 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
42 changes: 42 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,44 @@ export class GroupResource extends BaseResource<GroupModel> {
return new this(GroupModel, group.get());
}

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

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

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: this.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 whereOptions: 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) {
whereOptions.id = { [Op.ne]: excludeId };
}

const overlapping = await this.findOne({ where: whereOptions });
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: "RESTRICT",
});
GroupModel.hasMany(GroupMembershipModel, {
foreignKey: { allowNull: false },
onDelete: "RESTRICT",
});
Workspace.hasMany(GroupMembershipModel, {
foreignKey: { allowNull: false },
onDelete: "RESTRICT",
});
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 RESTRICT ON UPDATE CASCADE,
"groupId" INTEGER NOT NULL REFERENCES "groups" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
"workspaceId" INTEGER NOT NULL REFERENCES "workspaces" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
PRIMARY KEY ("id")
);
CREATE INDEX "group_memberships_user_id_group_id" ON "group_memberships" ("userId", "groupId");

0 comments on commit e8372b7

Please sign in to comment.