diff --git a/packages/api/src/entity/groups/group.ts b/packages/api/src/entity/groups/group.ts index bce8d2234..4aa22e1ee 100644 --- a/packages/api/src/entity/groups/group.ts +++ b/packages/api/src/entity/groups/group.ts @@ -3,17 +3,19 @@ import { CreateDateColumn, Entity, JoinColumn, + OneToMany, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm' import { User } from '../user' +import { GroupMembership } from './group_membership' @Entity() export class Group { @PrimaryGeneratedColumn('uuid') - id?: string + id!: string @Column('text') name!: string @@ -23,8 +25,11 @@ export class Group { createdBy!: User @CreateDateColumn() - createdAt?: Date + createdAt!: Date @UpdateDateColumn() - updatedAt?: Date + updatedAt!: Date + + @OneToMany(() => GroupMembership, (groupMembership) => groupMembership.group) + members!: GroupMembership[] } diff --git a/packages/api/src/entity/groups/group_membership.ts b/packages/api/src/entity/groups/group_membership.ts index 1cb1e537c..30b92d943 100644 --- a/packages/api/src/entity/groups/group_membership.ts +++ b/packages/api/src/entity/groups/group_membership.ts @@ -1,7 +1,9 @@ import { + Column, CreateDateColumn, Entity, JoinColumn, + ManyToOne, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn, @@ -20,7 +22,7 @@ export class GroupMembership { @JoinColumn() user!: User - @OneToOne(() => Group) + @ManyToOne(() => Group, (group) => group.members) @JoinColumn() group!: Group @@ -33,4 +35,7 @@ export class GroupMembership { @UpdateDateColumn() updatedAt?: Date + + @Column('boolean', { default: false }) + isAdmin!: boolean } diff --git a/packages/api/src/entity/profile.ts b/packages/api/src/entity/profile.ts index a30d057cf..b5f04b48d 100644 --- a/packages/api/src/entity/profile.ts +++ b/packages/api/src/entity/profile.ts @@ -33,4 +33,7 @@ export class Profile { @UpdateDateColumn() updatedAt!: Date + + @Column('boolean', { default: false }) + private!: boolean } diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index 946be9a8c..f9db0f7e3 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -275,6 +275,29 @@ export type CreateArticleSuccess = { user: User; }; +export type CreateGroupError = { + __typename?: 'CreateGroupError'; + errorCodes: Array; +}; + +export enum CreateGroupErrorCode { + BadRequest = 'BAD_REQUEST', + Unauthorized = 'UNAUTHORIZED' +} + +export type CreateGroupInput = { + expiresInDays?: InputMaybe; + maxMembers?: InputMaybe; + name: Scalars['String']; +}; + +export type CreateGroupResult = CreateGroupError | CreateGroupSuccess; + +export type CreateGroupSuccess = { + __typename?: 'CreateGroupSuccess'; + group: RecommendationGroup; +}; + export type CreateHighlightError = { __typename?: 'CreateHighlightError'; errorCodes: Array; @@ -824,6 +847,23 @@ export type GoogleSignupSuccess = { me: User; }; +export type GroupsError = { + __typename?: 'GroupsError'; + errorCodes: Array; +}; + +export enum GroupsErrorCode { + BadRequest = 'BAD_REQUEST', + Unauthorized = 'UNAUTHORIZED' +} + +export type GroupsResult = GroupsError | GroupsSuccess; + +export type GroupsSuccess = { + __typename?: 'GroupsSuccess'; + groups: Array; +}; + export type Highlight = { __typename?: 'Highlight'; annotation?: Maybe; @@ -1066,6 +1106,7 @@ export type Mutation = { addPopularRead: AddPopularReadResult; createArticle: CreateArticleResult; createArticleSavingRequest: CreateArticleSavingRequestResult; + createGroup: CreateGroupResult; createHighlight: CreateHighlightResult; createHighlightReply: CreateHighlightReplyResult; createLabel: CreateLabelResult; @@ -1140,6 +1181,11 @@ export type MutationCreateArticleSavingRequestArgs = { }; +export type MutationCreateGroupArgs = { + input: CreateGroupInput; +}; + + export type MutationCreateHighlightArgs = { input: CreateHighlightInput; }; @@ -1527,6 +1573,7 @@ export type Query = { getFollowers: GetFollowersResult; getFollowing: GetFollowingResult; getUserPersonalization: GetUserPersonalizationResult; + groups: GroupsResult; hello?: Maybe; integrations: IntegrationsResult; labels: LabelsResult; @@ -1696,6 +1743,17 @@ export type RecentSearchesSuccess = { searches: Array; }; +export type RecommendationGroup = { + __typename?: 'RecommendationGroup'; + admins: Array; + createdAt: Scalars['Date']; + id: Scalars['ID']; + inviteUrl: Scalars['String']; + members: Array; + name: Scalars['String']; + updatedAt: Scalars['Date']; +}; + export type Reminder = { __typename?: 'Reminder'; archiveUntil: Scalars['Boolean']; @@ -2909,6 +2967,11 @@ export type ResolversTypes = { CreateArticleSavingRequestResult: ResolversTypes['CreateArticleSavingRequestError'] | ResolversTypes['CreateArticleSavingRequestSuccess']; CreateArticleSavingRequestSuccess: ResolverTypeWrapper; CreateArticleSuccess: ResolverTypeWrapper; + CreateGroupError: ResolverTypeWrapper; + CreateGroupErrorCode: CreateGroupErrorCode; + CreateGroupInput: CreateGroupInput; + CreateGroupResult: ResolversTypes['CreateGroupError'] | ResolversTypes['CreateGroupSuccess']; + CreateGroupSuccess: ResolverTypeWrapper; CreateHighlightError: ResolverTypeWrapper; CreateHighlightErrorCode: CreateHighlightErrorCode; CreateHighlightInput: CreateHighlightInput; @@ -3023,6 +3086,10 @@ export type ResolversTypes = { GoogleSignupInput: GoogleSignupInput; GoogleSignupResult: ResolversTypes['GoogleSignupError'] | ResolversTypes['GoogleSignupSuccess']; GoogleSignupSuccess: ResolverTypeWrapper; + GroupsError: ResolverTypeWrapper; + GroupsErrorCode: GroupsErrorCode; + GroupsResult: ResolversTypes['GroupsError'] | ResolversTypes['GroupsSuccess']; + GroupsSuccess: ResolverTypeWrapper; Highlight: ResolverTypeWrapper; HighlightReply: ResolverTypeWrapper; HighlightStats: ResolverTypeWrapper; @@ -3090,6 +3157,7 @@ export type ResolversTypes = { RecentSearchesErrorCode: RecentSearchesErrorCode; RecentSearchesResult: ResolversTypes['RecentSearchesError'] | ResolversTypes['RecentSearchesSuccess']; RecentSearchesSuccess: ResolverTypeWrapper; + RecommendationGroup: ResolverTypeWrapper; Reminder: ResolverTypeWrapper; ReminderError: ResolverTypeWrapper; ReminderErrorCode: ReminderErrorCode; @@ -3331,6 +3399,10 @@ export type ResolversParentTypes = { CreateArticleSavingRequestResult: ResolversParentTypes['CreateArticleSavingRequestError'] | ResolversParentTypes['CreateArticleSavingRequestSuccess']; CreateArticleSavingRequestSuccess: CreateArticleSavingRequestSuccess; CreateArticleSuccess: CreateArticleSuccess; + CreateGroupError: CreateGroupError; + CreateGroupInput: CreateGroupInput; + CreateGroupResult: ResolversParentTypes['CreateGroupError'] | ResolversParentTypes['CreateGroupSuccess']; + CreateGroupSuccess: CreateGroupSuccess; CreateHighlightError: CreateHighlightError; CreateHighlightInput: CreateHighlightInput; CreateHighlightReplyError: CreateHighlightReplyError; @@ -3421,6 +3493,9 @@ export type ResolversParentTypes = { GoogleSignupInput: GoogleSignupInput; GoogleSignupResult: ResolversParentTypes['GoogleSignupError'] | ResolversParentTypes['GoogleSignupSuccess']; GoogleSignupSuccess: GoogleSignupSuccess; + GroupsError: GroupsError; + GroupsResult: ResolversParentTypes['GroupsError'] | ResolversParentTypes['GroupsSuccess']; + GroupsSuccess: GroupsSuccess; Highlight: Highlight; HighlightReply: HighlightReply; HighlightStats: HighlightStats; @@ -3475,6 +3550,7 @@ export type ResolversParentTypes = { RecentSearchesError: RecentSearchesError; RecentSearchesResult: ResolversParentTypes['RecentSearchesError'] | ResolversParentTypes['RecentSearchesSuccess']; RecentSearchesSuccess: RecentSearchesSuccess; + RecommendationGroup: RecommendationGroup; Reminder: Reminder; ReminderError: ReminderError; ReminderResult: ResolversParentTypes['ReminderError'] | ResolversParentTypes['ReminderSuccess']; @@ -3830,6 +3906,20 @@ export type CreateArticleSuccessResolvers; }; +export type CreateGroupErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type CreateGroupResultResolvers = { + __resolveType: TypeResolveFn<'CreateGroupError' | 'CreateGroupSuccess', ParentType, ContextType>; +}; + +export type CreateGroupSuccessResolvers = { + group?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type CreateHighlightErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -4234,6 +4324,20 @@ export type GoogleSignupSuccessResolvers; }; +export type GroupsErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type GroupsResultResolvers = { + __resolveType: TypeResolveFn<'GroupsError' | 'GroupsSuccess', ParentType, ContextType>; +}; + +export type GroupsSuccessResolvers = { + groups?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type HighlightResolvers = { annotation?: Resolver, ParentType, ContextType>; createdAt?: Resolver; @@ -4417,6 +4521,7 @@ export type MutationResolvers>; createArticle?: Resolver>; createArticleSavingRequest?: Resolver>; + createGroup?: Resolver>; createHighlight?: Resolver>; createHighlightReply?: Resolver>; createLabel?: Resolver>; @@ -4557,6 +4662,7 @@ export type QueryResolvers>; getFollowing?: Resolver>; getUserPersonalization?: Resolver; + groups?: Resolver; hello?: Resolver, ParentType, ContextType>; integrations?: Resolver; labels?: Resolver; @@ -4616,6 +4722,17 @@ export type RecentSearchesSuccessResolvers; }; +export type RecommendationGroupResolvers = { + admins?: Resolver, ParentType, ContextType>; + createdAt?: Resolver; + id?: Resolver; + inviteUrl?: Resolver; + members?: Resolver, ParentType, ContextType>; + name?: Resolver; + updatedAt?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type ReminderResolvers = { archiveUntil?: Resolver; id?: Resolver; @@ -5341,6 +5458,9 @@ export type Resolvers = { CreateArticleSavingRequestResult?: CreateArticleSavingRequestResultResolvers; CreateArticleSavingRequestSuccess?: CreateArticleSavingRequestSuccessResolvers; CreateArticleSuccess?: CreateArticleSuccessResolvers; + CreateGroupError?: CreateGroupErrorResolvers; + CreateGroupResult?: CreateGroupResultResolvers; + CreateGroupSuccess?: CreateGroupSuccessResolvers; CreateHighlightError?: CreateHighlightErrorResolvers; CreateHighlightReplyError?: CreateHighlightReplyErrorResolvers; CreateHighlightReplyResult?: CreateHighlightReplyResultResolvers; @@ -5422,6 +5542,9 @@ export type Resolvers = { GoogleSignupError?: GoogleSignupErrorResolvers; GoogleSignupResult?: GoogleSignupResultResolvers; GoogleSignupSuccess?: GoogleSignupSuccessResolvers; + GroupsError?: GroupsErrorResolvers; + GroupsResult?: GroupsResultResolvers; + GroupsSuccess?: GroupsSuccessResolvers; Highlight?: HighlightResolvers; HighlightReply?: HighlightReplyResolvers; HighlightStats?: HighlightStatsResolvers; @@ -5468,6 +5591,7 @@ export type Resolvers = { RecentSearchesError?: RecentSearchesErrorResolvers; RecentSearchesResult?: RecentSearchesResultResolvers; RecentSearchesSuccess?: RecentSearchesSuccessResolvers; + RecommendationGroup?: RecommendationGroupResolvers; Reminder?: ReminderResolvers; ReminderError?: ReminderErrorResolvers; ReminderResult?: ReminderResultResolvers; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 62eee676c..447b3a776 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -232,6 +232,27 @@ type CreateArticleSuccess { user: User! } +type CreateGroupError { + errorCodes: [CreateGroupErrorCode!]! +} + +enum CreateGroupErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +input CreateGroupInput { + expiresInDays: Int + maxMembers: Int + name: String! +} + +union CreateGroupResult = CreateGroupError | CreateGroupSuccess + +type CreateGroupSuccess { + group: RecommendationGroup! +} + type CreateHighlightError { errorCodes: [CreateHighlightErrorCode!]! } @@ -728,6 +749,21 @@ type GoogleSignupSuccess { me: User! } +type GroupsError { + errorCodes: [GroupsErrorCode!]! +} + +enum GroupsErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +union GroupsResult = GroupsError | GroupsSuccess + +type GroupsSuccess { + groups: [RecommendationGroup!]! +} + type Highlight { annotation: String createdAt: Date! @@ -948,6 +984,7 @@ type Mutation { addPopularRead(name: String!): AddPopularReadResult! createArticle(input: CreateArticleInput!): CreateArticleResult! createArticleSavingRequest(input: CreateArticleSavingRequestInput!): CreateArticleSavingRequestResult! + createGroup(input: CreateGroupInput!): CreateGroupResult! createHighlight(input: CreateHighlightInput!): CreateHighlightResult! createHighlightReply(input: CreateHighlightReplyInput!): CreateHighlightReplyResult! createLabel(input: CreateLabelInput!): CreateLabelResult! @@ -1115,6 +1152,7 @@ type Query { getFollowers(userId: ID): GetFollowersResult! getFollowing(userId: ID): GetFollowingResult! getUserPersonalization: GetUserPersonalizationResult! + groups: GroupsResult! hello: String integrations: IntegrationsResult! labels: LabelsResult! @@ -1181,6 +1219,16 @@ type RecentSearchesSuccess { searches: [RecentSearch!]! } +type RecommendationGroup { + admins: [User!]! + createdAt: Date! + id: ID! + inviteUrl: String! + members: [User!]! + name: String! + updatedAt: Date! +} + type Reminder { archiveUntil: Boolean! id: ID! diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index 2648160a7..3d7a05808 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -26,6 +26,7 @@ import { articleSavingRequestResolver, createArticleResolver, createArticleSavingRequestResolver, + createGroupResolver, createHighlightResolver, createLabelResolver, createNewsletterEmailResolver, @@ -54,6 +55,7 @@ import { getUserResolver, googleLoginResolver, googleSignupResolver, + groupsResolver, integrationsResolver, labelsResolver, logOutResolver, @@ -186,6 +188,7 @@ export const functionResolvers = { saveFilter: saveFilterResolver, deleteFilter: deleteFilterResolver, moveFilter: moveFilterResolver, + createGroup: createGroupResolver, }, Query: { me: getMeUserResolver, @@ -216,6 +219,7 @@ export const functionResolvers = { rules: rulesResolver, deviceTokens: deviceTokensResolver, filters: filtersResolver, + groups: groupsResolver, }, User: { async sharedArticles( @@ -634,4 +638,6 @@ export const functionResolvers = { ...resultResolveTypeResolver('Filters'), ...resultResolveTypeResolver('DeleteFilter'), ...resultResolveTypeResolver('MoveFilter'), + ...resultResolveTypeResolver('CreateGroup'), + ...resultResolveTypeResolver('Groups'), } diff --git a/packages/api/src/resolvers/index.ts b/packages/api/src/resolvers/index.ts index ee324050e..f1cf2d8e2 100644 --- a/packages/api/src/resolvers/index.ts +++ b/packages/api/src/resolvers/index.ts @@ -23,3 +23,4 @@ export * from './api_key' export * from './integrations' export * from './rules' export * from './filters' +export * from './recommendations' diff --git a/packages/api/src/resolvers/recommendations/index.ts b/packages/api/src/resolvers/recommendations/index.ts new file mode 100644 index 000000000..68cd8a04a --- /dev/null +++ b/packages/api/src/resolvers/recommendations/index.ts @@ -0,0 +1,118 @@ +import { + CreateGroupError, + CreateGroupErrorCode, + CreateGroupSuccess, + GroupsError, + GroupsErrorCode, + GroupsSuccess, + MutationCreateGroupArgs, +} from '../../generated/graphql' +import { + createGroup, + getInviteUrl, + getRecommendationGroups, +} from '../../services/create_group' +import { authorized, userDataToUser } from '../../utils/helpers' +import { getRepository } from '../../entity/utils' +import { User } from '../../entity/user' + +export const createGroupResolver = authorized< + CreateGroupSuccess, + CreateGroupError, + MutationCreateGroupArgs +>(async (_, { input }, { claims: { uid }, log }) => { + log.info('Creating group', { + input, + labels: { + source: 'resolver', + resolver: 'createGroupResolver', + uid, + }, + }) + + try { + const userData = await getRepository(User).findOne({ + where: { id: uid }, + relations: ['profile'], + }) + if (!userData) { + return { + errorCodes: [CreateGroupErrorCode.Unauthorized], + } + } + + const [group, invite] = await createGroup({ + admin: userData, + name: input.name, + maxMembers: input.maxMembers, + expiresInDays: input.expiresInDays, + }) + + const inviteUrl = getInviteUrl(invite) + const user = userDataToUser(userData) + + return { + group: { + ...group, + inviteUrl, + admins: [user], + members: [user], + }, + } + } catch (error) { + log.error('Error creating group', { + error, + labels: { + source: 'resolver', + resolver: 'createGroupResolver', + uid, + }, + }) + + return { + errorCodes: [CreateGroupErrorCode.BadRequest], + } + } +}) + +export const groupsResolver = authorized( + async (_, __, { claims: { uid }, log }) => { + log.info('Getting groups', { + labels: { + source: 'resolver', + resolver: 'groupsResolver', + uid, + }, + }) + + try { + const user = await getRepository(User).findOneBy({ + id: uid, + }) + if (!user) { + return { + errorCodes: [GroupsErrorCode.Unauthorized], + } + } + + const groups = await getRecommendationGroups(user) + + return { + groups, + } + } catch (error) { + log.error('Error getting groups', { + error, + labels: { + source: 'resolver', + resolver: 'groupsResolver', + uid, + }, + }) + + return { + errorCodes: [GroupsErrorCode.BadRequest], + } + } + } +) diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 13e67f122..0a5ed9910 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -2137,6 +2137,52 @@ const schema = gql` NOT_FOUND } + input CreateGroupInput { + name: String! @sanitize(maxLength: 140) + maxMembers: Int + expiresInDays: Int + } + + union CreateGroupResult = CreateGroupSuccess | CreateGroupError + + type CreateGroupSuccess { + group: RecommendationGroup! + } + + type RecommendationGroup { + id: ID! + name: String! + inviteUrl: String! + admins: [User!]! + members: [User!]! + createdAt: Date! + updatedAt: Date! + } + + type CreateGroupError { + errorCodes: [CreateGroupErrorCode!]! + } + + enum CreateGroupErrorCode { + UNAUTHORIZED + BAD_REQUEST + } + + union GroupsResult = GroupsSuccess | GroupsError + + type GroupsSuccess { + groups: [RecommendationGroup!]! + } + + type GroupsError { + errorCodes: [GroupsErrorCode!]! + } + + enum GroupsErrorCode { + UNAUTHORIZED + BAD_REQUEST + } + # Mutations type Mutation { googleLogin(input: GoogleLoginInput!): LoginResult! @@ -2214,6 +2260,7 @@ const schema = gql` saveFilter(input: SaveFilterInput!): SaveFilterResult! deleteFilter(id: ID!): DeleteFilterResult! moveFilter(input: MoveFilterInput!): MoveFilterResult! + createGroup(input: CreateGroupInput!): CreateGroupResult! } # FIXME: remove sort from feedArticles after all cached tabs are closed @@ -2269,6 +2316,7 @@ const schema = gql` rules(enabled: Boolean): RulesResult! deviceTokens: DeviceTokensResult! filters: FiltersResult! + groups: GroupsResult! } ` diff --git a/packages/api/src/services/create_group.ts b/packages/api/src/services/create_group.ts index a8897460b..62557049b 100644 --- a/packages/api/src/services/create_group.ts +++ b/packages/api/src/services/create_group.ts @@ -4,15 +4,28 @@ import { Invite } from '../entity/groups/invite' import { GroupMembership } from '../entity/groups/group_membership' import { nanoid } from 'nanoid' import { AppDataSource } from '../server' +import { RecommendationGroup, User as GraphqlUser } from '../generated/graphql' +import { getRepository } from '../entity/utils' +import { homePageURL } from '../env' +import { userDataToUser } from '../utils/helpers' export const createGroup = async (input: { admin: User name: string - maxMembers?: number - expiresInDays?: number + maxMembers?: number | null + expiresInDays?: number | null }): Promise<[Group, Invite]> => { const [group, invite] = await AppDataSource.transaction<[Group, Invite]>( async (t) => { + // Max number of groups a user can create + const maxGroups = 3 + const groupCount = await getRepository(Group).countBy({ + createdBy: { id: input.admin.id }, + }) + if (groupCount >= maxGroups) { + throw new Error('Max groups reached') + } + const group = await t.getRepository(Group).save({ name: input.name, createdBy: input.admin, @@ -28,7 +41,7 @@ export const createGroup = async (input: { group, code, createdBy: input.admin, - maxMembers: input.maxMembers || 50, + maxMembers: input.maxMembers || 12, expirationTime: expirationTime, }) // Add the admin to the group as its first user @@ -36,9 +49,45 @@ export const createGroup = async (input: { user: input.admin, group, invite, + isAdmin: true, }) return [group, invite] } ) return [group, invite] } + +export const getRecommendationGroups = async ( + user: User +): Promise => { + const groupMembers = await getRepository(GroupMembership).find({ + where: { user: { id: user.id } }, + relations: ['invite', 'group.members.user.profile'], + }) + + return groupMembers.map((gm) => { + const admins: GraphqlUser[] = [] + const members: GraphqlUser[] = [] + gm.group.members.forEach((m) => { + const user = userDataToUser(m.user) + if (m.isAdmin) { + admins.push(user) + } + members.push(user) + }) + + return { + id: gm.group.id, + name: gm.group.name, + createdAt: gm.group.createdAt, + updatedAt: gm.group.updatedAt, + inviteUrl: getInviteUrl(gm.invite), + admins, + members, + } + }) +} + +export const getInviteUrl = (invite: Invite) => { + return `${homePageURL()}/invite/${invite.code}` +} diff --git a/packages/db/migrations/0101.do.add_is_admin_to_group_membership.sql b/packages/db/migrations/0101.do.add_is_admin_to_group_membership.sql new file mode 100755 index 000000000..2967aa992 --- /dev/null +++ b/packages/db/migrations/0101.do.add_is_admin_to_group_membership.sql @@ -0,0 +1,9 @@ +-- Type: DO +-- Name: add_is_admin_to_group_membership +-- Description: Add is_admin field to group_membership table + +BEGIN; + +ALTER TABLE omnivore.group_membership ADD COLUMN is_admin BOOLEAN NOT NULL DEFAULT FALSE; + +COMMIT; diff --git a/packages/db/migrations/0101.undo.add_is_admin_to_group_membership.sql b/packages/db/migrations/0101.undo.add_is_admin_to_group_membership.sql new file mode 100755 index 000000000..07ed5affb --- /dev/null +++ b/packages/db/migrations/0101.undo.add_is_admin_to_group_membership.sql @@ -0,0 +1,9 @@ +-- Type: UNDO +-- Name: add_is_admin_to_group_membership +-- Description: Add is_admin field to group_membership table + +BEGIN; + +ALTER TABLE omnivore.group_membership DROP COLUMN IF EXISTS is_admin; + +COMMIT;