forked from cerc-io/snowballtools-base
Implement functionality to add project members (#48)
* Add mutation for adding project member * Add GQL client method for add project member * Handle review comments * Integrate add project member GQL client method * Handle cascades for project members and user organizations * Add null type to user.name and domain.redirectTo --------- Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
@@ -9,7 +9,7 @@ import { Organization } from './entity/Organization';
|
||||
import { UserOrganization } from './entity/UserOrganization';
|
||||
import { Project } from './entity/Project';
|
||||
import { Deployment, Environment } from './entity/Deployment';
|
||||
import { ProjectMember } from './entity/ProjectMember';
|
||||
import { Permission, ProjectMember } from './entity/ProjectMember';
|
||||
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
||||
import { Domain } from './entity/Domain';
|
||||
|
||||
@@ -198,6 +198,38 @@ export class Database {
|
||||
}
|
||||
}
|
||||
|
||||
async addProjectMember (projectId: string, data: {
|
||||
email: string,
|
||||
permissions: Permission[]
|
||||
}): Promise<boolean> {
|
||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
||||
const userRepository = this.dataSource.getRepository(User);
|
||||
|
||||
let user = await userRepository.findOneBy({
|
||||
email: data.email
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
user = await userRepository.save({
|
||||
email: data.email,
|
||||
isVerified: false
|
||||
});
|
||||
}
|
||||
|
||||
const newProjectMember = await projectMemberRepository.save({
|
||||
project: {
|
||||
id: projectId
|
||||
},
|
||||
permissions: data.permissions,
|
||||
isPending: true,
|
||||
member: {
|
||||
id: user.id
|
||||
}
|
||||
});
|
||||
|
||||
return Boolean(newProjectMember);
|
||||
}
|
||||
|
||||
async addEnvironmentVariablesByProjectId (projectId: string, environmentVariables: {
|
||||
environments: string[];
|
||||
key: string;
|
||||
@@ -338,13 +370,18 @@ export class Database {
|
||||
|
||||
async deleteProjectById (projectId: string): Promise<boolean> {
|
||||
const projectRepository = this.dataSource.getRepository(Project);
|
||||
const deleteResult = await projectRepository.softDelete({ id: projectId });
|
||||
const project = await projectRepository.findOneOrFail({
|
||||
where: {
|
||||
id: projectId
|
||||
},
|
||||
relations: {
|
||||
projectMembers: true
|
||||
}
|
||||
});
|
||||
|
||||
if (deleteResult.affected) {
|
||||
return deleteResult.affected > 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
const deleteResult = await projectRepository.softRemove(project);
|
||||
|
||||
return Boolean(deleteResult);
|
||||
}
|
||||
|
||||
async deleteDomainById (domainId: string): Promise<boolean> {
|
||||
|
||||
@@ -35,7 +35,7 @@ export class Domain {
|
||||
name!: string;
|
||||
|
||||
@Column('int', { nullable: true })
|
||||
redirectToId!: number;
|
||||
redirectToId!: number | null;
|
||||
|
||||
@ManyToOne(() => Domain)
|
||||
@JoinColumn({ name: 'redirectToId' })
|
||||
|
||||
@@ -3,8 +3,10 @@ import {
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn
|
||||
UpdateDateColumn,
|
||||
OneToMany
|
||||
} from 'typeorm';
|
||||
import { UserOrganization } from './UserOrganization';
|
||||
|
||||
@Entity()
|
||||
export class Organization {
|
||||
@@ -19,4 +21,9 @@ export class Organization {
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
|
||||
@OneToMany(() => UserOrganization, userOrganization => userOrganization.organization, {
|
||||
cascade: ['soft-remove']
|
||||
})
|
||||
userOrganizations!: UserOrganization[];
|
||||
}
|
||||
|
||||
@@ -63,9 +63,11 @@ export class Project {
|
||||
@DeleteDateColumn()
|
||||
deletedAt?: Date;
|
||||
|
||||
@OneToMany(() => ProjectMember, projectMember => projectMember.project)
|
||||
projectMembers!: ProjectMember[];
|
||||
|
||||
@OneToMany(() => Deployment, (deployment) => deployment.project)
|
||||
deployments!: Deployment[];
|
||||
|
||||
@OneToMany(() => ProjectMember, projectMember => projectMember.project, {
|
||||
cascade: ['soft-remove']
|
||||
})
|
||||
projectMembers!: ProjectMember[];
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import {
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
JoinColumn
|
||||
JoinColumn,
|
||||
Unique,
|
||||
DeleteDateColumn
|
||||
} from 'typeorm';
|
||||
|
||||
import { Project } from './Project';
|
||||
@@ -17,15 +19,16 @@ export enum Permission {
|
||||
}
|
||||
|
||||
@Entity()
|
||||
@Unique(['project', 'member'])
|
||||
export class ProjectMember {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.projectMembers, { onDelete: 'CASCADE' })
|
||||
@ManyToOne(() => User, (user) => user.projectMembers)
|
||||
@JoinColumn({ name: 'userId' })
|
||||
member!: User;
|
||||
|
||||
@ManyToOne(() => Project, (project) => project.projectMembers, { onDelete: 'CASCADE' })
|
||||
@ManyToOne(() => Project, (project) => project.projectMembers)
|
||||
@JoinColumn({ name: 'projectId' })
|
||||
project!: Project;
|
||||
|
||||
@@ -34,9 +37,15 @@ export class ProjectMember {
|
||||
})
|
||||
permissions!: Permission[];
|
||||
|
||||
@Column('boolean', { default: false })
|
||||
isPending!: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt?: Date;
|
||||
}
|
||||
|
||||
@@ -3,17 +3,21 @@ import {
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
OneToMany
|
||||
OneToMany,
|
||||
Unique
|
||||
} from 'typeorm';
|
||||
|
||||
import { ProjectMember } from './ProjectMember';
|
||||
import { UserOrganization } from './UserOrganization';
|
||||
|
||||
@Entity()
|
||||
@Unique(['email'])
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column('varchar', { length: 255 })
|
||||
name!: string;
|
||||
@Column('varchar', { length: 255, nullable: true })
|
||||
name!: string | null;
|
||||
|
||||
@Column()
|
||||
email!: string;
|
||||
@@ -21,12 +25,22 @@ export class User {
|
||||
@Column('varchar', { nullable: true })
|
||||
gitHubToken!: string | null;
|
||||
|
||||
@Column('boolean', { default: false })
|
||||
isVerified!: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@CreateDateColumn()
|
||||
updatedAt!: Date;
|
||||
|
||||
@OneToMany(() => ProjectMember, projectMember => projectMember.project)
|
||||
@OneToMany(() => ProjectMember, projectMember => projectMember.project, {
|
||||
cascade: ['soft-remove']
|
||||
})
|
||||
projectMembers!: ProjectMember[];
|
||||
|
||||
@OneToMany(() => UserOrganization, UserOrganization => UserOrganization.member, {
|
||||
cascade: ['soft-remove']
|
||||
})
|
||||
userOrganizations!: UserOrganization[];
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
JoinColumn
|
||||
JoinColumn,
|
||||
DeleteDateColumn
|
||||
} from 'typeorm';
|
||||
|
||||
import { User } from './User';
|
||||
@@ -22,11 +23,11 @@ export class UserOrganization {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@ManyToOne(() => User, { onDelete: 'CASCADE' })
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'userId' })
|
||||
member!: User;
|
||||
|
||||
@ManyToOne(() => Organization, { onDelete: 'CASCADE' })
|
||||
@ManyToOne(() => Organization)
|
||||
@JoinColumn({ name: 'organizationId' })
|
||||
organization!: Organization;
|
||||
|
||||
@@ -40,4 +41,7 @@ export class UserOrganization {
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt?: Date;
|
||||
}
|
||||
|
||||
@@ -111,6 +111,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
|
||||
}
|
||||
},
|
||||
|
||||
// TODO: Return error in GQL response
|
||||
Mutation: {
|
||||
removeProjectMember: async (_: any, { projectMemberId }: { projectMemberId: string }, context: any) => {
|
||||
try {
|
||||
@@ -124,7 +125,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
|
||||
assert(memberProject);
|
||||
|
||||
if (isUserOwner(String(context.userId), String(memberProject.owner.id))) {
|
||||
return db.removeProjectMemberById(projectMemberId);
|
||||
return await db.removeProjectMemberById(projectMemberId);
|
||||
} else {
|
||||
throw new Error('Invalid operation: not authorized');
|
||||
}
|
||||
@@ -141,7 +142,23 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
|
||||
}
|
||||
}) => {
|
||||
try {
|
||||
return db.updateProjectMemberById(projectMemberId, data);
|
||||
return await db.updateProjectMemberById(projectMemberId, data);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
addProjectMember: async (_: any, { projectId, data }: {
|
||||
projectId: string,
|
||||
data: {
|
||||
email: string,
|
||||
permissions: Permission[]
|
||||
}
|
||||
}) => {
|
||||
try {
|
||||
// TODO: Send invitation
|
||||
return await db.addProjectMember(projectId, data);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
@@ -150,7 +167,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
|
||||
|
||||
addEnvironmentVariables: async (_: any, { projectId, environmentVariables }: { projectId: string, environmentVariables: { environments: string[], key: string, value: string}[] }) => {
|
||||
try {
|
||||
return db.addEnvironmentVariablesByProjectId(projectId, environmentVariables);
|
||||
return await db.addEnvironmentVariablesByProjectId(projectId, environmentVariables);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
@@ -162,7 +179,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
|
||||
value: string
|
||||
}}) => {
|
||||
try {
|
||||
return db.updateEnvironmentVariable(environmentVariableId, environmentVariable);
|
||||
return await db.updateEnvironmentVariable(environmentVariableId, environmentVariable);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
@@ -171,7 +188,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
|
||||
|
||||
removeEnvironmentVariable: async (_: any, { environmentVariableId }: { environmentVariableId: string}) => {
|
||||
try {
|
||||
return db.deleteEnvironmentVariable(environmentVariableId);
|
||||
return await db.deleteEnvironmentVariable(environmentVariableId);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
@@ -180,7 +197,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
|
||||
|
||||
updateDeploymentToProd: async (_: any, { deploymentId }: { deploymentId: string }) => {
|
||||
try {
|
||||
return db.updateDeploymentById(deploymentId, {
|
||||
return await db.updateDeploymentById(deploymentId, {
|
||||
environment: Environment.Production
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -200,8 +217,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
|
||||
|
||||
redeployToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => {
|
||||
try {
|
||||
await db.redeployToProdById(context.userId, deploymentId);
|
||||
return true;
|
||||
return await db.redeployToProdById(context.userId, deploymentId);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
@@ -210,7 +226,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
|
||||
|
||||
deleteProject: async (_: any, { projectId }: { projectId: string }) => {
|
||||
try {
|
||||
return db.deleteProjectById(projectId);
|
||||
return await db.deleteProjectById(projectId);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
@@ -219,8 +235,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
|
||||
|
||||
deleteDomain: async (_: any, { domainId }: { domainId: string }) => {
|
||||
try {
|
||||
await db.deleteDomainById(domainId);
|
||||
return true;
|
||||
return await db.deleteDomainById(domainId);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
@@ -229,7 +244,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
|
||||
|
||||
rollbackDeployment: async (_: any, { projectId, deploymentId }: {deploymentId: string, projectId: string }) => {
|
||||
try {
|
||||
return db.rollbackDeploymentById(projectId, deploymentId);
|
||||
return await db.rollbackDeploymentById(projectId, deploymentId);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
@@ -238,8 +253,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
|
||||
|
||||
addDomain: async (_: any, { projectId, domainDetails }: { projectId: string, domainDetails: { name: string } }) => {
|
||||
try {
|
||||
await db.addDomainByProjectId(projectId, domainDetails);
|
||||
return true;
|
||||
return await db.addDomainByProjectId(projectId, domainDetails);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
@@ -248,8 +262,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
|
||||
|
||||
updateDomain: async (_: any, { domainId, domainDetails }: { domainId: string, domainDetails: DeepPartial<Domain>}) => {
|
||||
try {
|
||||
await db.updateDomainById(domainId, domainDetails);
|
||||
return true;
|
||||
return await db.updateDomainById(domainId, domainDetails);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
|
||||
@@ -28,10 +28,11 @@ enum DomainStatus {
|
||||
|
||||
type User {
|
||||
id: String!
|
||||
name: String!
|
||||
name: String
|
||||
email: String!
|
||||
organizations: [Organization!]
|
||||
projects: [Project!]
|
||||
isVerified: Boolean!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
gitHubToken: String
|
||||
@@ -77,6 +78,7 @@ type ProjectMember {
|
||||
id: String!
|
||||
member: User!
|
||||
permissions: [Permission!]!
|
||||
isPending: Boolean!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
@@ -134,6 +136,7 @@ type AuthResult {
|
||||
type Mutation {
|
||||
removeProjectMember(projectMemberId: String!): Boolean!
|
||||
updateProjectMember(projectMemberId: String!, data: UpdateProjectMemberInput): Boolean!
|
||||
addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean!
|
||||
addEnvironmentVariables(projectId: String!, environmentVariables: [AddEnvironmentVariableInput!]): Boolean!
|
||||
removeEnvironmentVariable(environmentVariableId: String!): Boolean!
|
||||
updateEnvironmentVariable(environmentVariableId: String!, environmentVariable: UpdateEnvironmentVariableInput!): Boolean!
|
||||
@@ -177,6 +180,11 @@ input UpdateEnvironmentVariableInput {
|
||||
value: String
|
||||
}
|
||||
|
||||
input AddProjectMemberInput {
|
||||
email: String!
|
||||
permissions: [Permission!]
|
||||
}
|
||||
|
||||
input UpdateProjectMemberInput {
|
||||
permissions: [Permission]
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ export const projectMemberToGqlType = (dbProjectMember: ProjectMember): any => {
|
||||
return {
|
||||
id: dbProjectMember.id,
|
||||
member: dbProjectMember.member,
|
||||
isPending: dbProjectMember.isPending,
|
||||
permissions: dbProjectMember.permissions,
|
||||
createdAt: dbProjectMember.createdAt,
|
||||
updatedAt: dbProjectMember.updatedAt
|
||||
|
||||
+40
-9
@@ -2,46 +2,77 @@
|
||||
{
|
||||
"memberIndex": 1,
|
||||
"projectIndex": 0,
|
||||
"permissions": ["View"]
|
||||
"permissions": [
|
||||
"View"
|
||||
],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 0,
|
||||
"permissions": ["View", "Edit"]
|
||||
"permissions": [
|
||||
"View",
|
||||
"Edit"
|
||||
],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 1,
|
||||
"permissions": ["View"]
|
||||
"permissions": [
|
||||
"View"
|
||||
],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 0,
|
||||
"projectIndex": 2,
|
||||
"permissions": ["View"]
|
||||
"permissions": [
|
||||
"View"
|
||||
],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 1,
|
||||
"projectIndex": 2,
|
||||
"permissions": ["View", "Edit"]
|
||||
"permissions": [
|
||||
"View",
|
||||
"Edit"
|
||||
],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 0,
|
||||
"projectIndex": 3,
|
||||
"permissions": ["View"]
|
||||
"permissions": [
|
||||
"View"
|
||||
],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 3,
|
||||
"permissions": ["View", "Edit"]
|
||||
"permissions": [
|
||||
"View",
|
||||
"Edit"
|
||||
],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 1,
|
||||
"projectIndex": 4,
|
||||
"permissions": ["View"]
|
||||
"permissions": [
|
||||
"View"
|
||||
],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 4,
|
||||
"permissions": ["View", "Edit"]
|
||||
"permissions": [
|
||||
"View",
|
||||
"Edit"
|
||||
],
|
||||
"isPending": false
|
||||
}
|
||||
]
|
||||
|
||||
+6
-3
@@ -1,14 +1,17 @@
|
||||
[
|
||||
{
|
||||
"name": "Saugat Yadav",
|
||||
"email": "saugaty@airfoil.studio"
|
||||
"email": "saugaty@airfoil.studio",
|
||||
"isVerified": true
|
||||
},
|
||||
{
|
||||
"name": "Gideon Low",
|
||||
"email": "gideonl@airfoil.studio"
|
||||
"email": "gideonl@airfoil.studio",
|
||||
"isVerified": true
|
||||
},
|
||||
{
|
||||
"name": "Sushan Yadav",
|
||||
"email": "sushany@airfoil.studio"
|
||||
"email": "sushany@airfoil.studio",
|
||||
"isVerified": true
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user