Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: remove feature flag owner #10319

Merged
merged 3 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codegen.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"mappers": {
"AddFeatureFlagSuccess": "./types/AddFeatureFlagSuccess#AddFeatureFlagSuccessSource",
"ApplyFeatureFlagSuccess": "./types/ApplyFeatureFlagSuccess#ApplyFeatureFlagSuccessSource",
"RemoveFeatureFlagOwnerSuccess": "./types/RemoveFeatureFlagOwnerSuccess#RemoveFeatureFlagOwnerSuccessSource",
"DeleteFeatureFlagSuccess": "./types/DeleteFeatureFlagSuccess#DeleteFeatureFlagSuccessSource",
"UpdateFeatureFlagSuccess": "./types/UpdateFeatureFlagSuccess#UpdateFeatureFlagSuccessSource",
"ChangeEmailDomainSuccess": "./types/ChangeEmailDomainSuccess#ChangeEmailDomainSuccessSource",
Expand Down
107 changes: 107 additions & 0 deletions packages/server/graphql/private/mutations/removeFeatureFlagOwner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import getKysely from '../../../postgres/getKysely'
import getUsersByDomain from '../../../postgres/queries/getUsersByDomain'
import {getUsersByEmails} from '../../../postgres/queries/getUsersByEmails'
import {getUserId} from '../../../utils/authorization'
import standardError from '../../../utils/standardError'
import {MutationResolvers} from '../resolverTypes'

const removeFeatureFlagOwner: MutationResolvers['removeFeatureFlagOwner'] = async (
_source,
{flagName, subjects},
{authToken}
) => {
const pg = getKysely()

const viewerId = getUserId(authToken)

const subjectKeys = Object.keys(subjects)

if (subjectKeys.length === 0) {
return standardError(new Error('At least one subject type must be provided'), {
userId: viewerId
})
}

const featureFlag = await pg
.selectFrom('FeatureFlag')
.select(['id', 'scope'])
.where('featureName', '=', flagName)
.executeTakeFirst()

if (!featureFlag) {
return standardError(new Error('Feature flag not found'), {userId: viewerId})
}

const {id: featureFlagId, scope} = featureFlag

const userIds: string[] = []
const teamIds: string[] = []
const orgIds: string[] = []

if (scope === 'User') {
if (subjects.emails) {
const users = await getUsersByEmails(subjects.emails)
userIds.push(...users.map((user) => user.id))
}

if (subjects.domains) {
for (const domain of subjects.domains) {
const domainUsers = await getUsersByDomain(domain)
userIds.push(...domainUsers.map((user) => user.id))
}
}

if (subjects.userIds) {
userIds.push(...subjects.userIds)
}
} else if (scope === 'Team') {
if (subjects.teamIds) {
teamIds.push(...subjects.teamIds)
}
} else if (scope === 'Organization') {
if (subjects.orgIds) {
orgIds.push(...subjects.orgIds)
}
}

let deletedCount = 0

if (scope === 'User' && userIds.length > 0) {
const result = await pg
.deleteFrom('FeatureFlagOwner')
.where('featureFlagId', '=', featureFlagId)
.where('userId', 'in', userIds)
.executeTakeFirst()
deletedCount = Number(result?.numDeletedRows ?? 0)
} else if (scope === 'Team' && teamIds.length > 0) {
const result = await pg
.deleteFrom('FeatureFlagOwner')
.where('featureFlagId', '=', featureFlagId)
.where('teamId', 'in', teamIds)
.executeTakeFirst()
deletedCount = Number(result?.numDeletedRows ?? 0)
} else if (scope === 'Organization' && orgIds.length > 0) {
const result = await pg
.deleteFrom('FeatureFlagOwner')
.where('featureFlagId', '=', featureFlagId)
.where('orgId', 'in', orgIds)
.executeTakeFirst()
deletedCount = Number(result?.numDeletedRows ?? 0)
}

if (deletedCount === 0) {
return standardError(
new Error('No feature flag owners were removed. Check the scope and subjects provided.')
)
}

return {
featureFlagId,
removedCount: deletedCount,
userIds: scope === 'User' ? userIds : [],
teamIds: scope === 'Team' ? teamIds : [],
orgIds: scope === 'Organization' ? orgIds : []
nickoferrall marked this conversation as resolved.
Show resolved Hide resolved
}
}

export default removeFeatureFlagOwner
14 changes: 14 additions & 0 deletions packages/server/graphql/private/typeDefs/Mutation.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ type Mutation {
subjects: SubjectsInput!
): ApplyFeatureFlagPayload!

"""
Remove a feature flag from specified subjects (users, teams, or organizations)
"""
removeFeatureFlagOwner(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 Might just be me, but I find this name confusing: removing the owner of the feature flag sounds backwards. If I take your candy, did I remove you as the owner from that candy?
I would just call it removeFeatureFlag.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha yeah, but as there's featureFlag and featureFlagOwner, I'd prefer to explicitly say we're removing the owner. There's already a deleteFeatureFlag mutation, which could get confusing

"""
The name of the feature flag to remove
"""
flagName: String!
"""
The subjects from which the feature flag will be removed
"""
subjects: SubjectsInput!
): RemoveFeatureFlagOwnerPayload!

"""
Delete an existing feature flag
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""
Successful result of removing a feature flag owner
"""
type RemoveFeatureFlagOwnerSuccess {
"""
The feature flag that was removed
"""
featureFlag: FeatureFlag!
"""
The feature flag was removed from the following users
"""
users: [User!]
"""
The feature flag was removed from the following teams
"""
teams: [Team!]
"""
The feature flag was removed from the following organizations
"""
organizations: [Organization!]
"""
The number of subjects the feature flag was removed from
"""
removedCount: Int!
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""
Return value for removeFeatureFlagOwner, which could be an error or success
"""
union RemoveFeatureFlagOwnerPayload = ErrorPayload | RemoveFeatureFlagOwnerSuccess
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import isValid from '../../isValid'
import {RemoveFeatureFlagOwnerSuccessResolvers} from '../resolverTypes'

export type RemoveFeatureFlagOwnerSuccessSource = {
featureFlagId: string
userIds: string[] | null
teamIds: string[] | null
orgIds: string[] | null
removedCount: number
}

const RemoveFeatureFlagOwnerSuccess: RemoveFeatureFlagOwnerSuccessResolvers = {
featureFlag: async ({featureFlagId}, _args, {dataLoader}) => {
return dataLoader.get('featureFlags').loadNonNull(featureFlagId)
},
users: async ({userIds}, _args, {dataLoader}) => {
if (!userIds) return null
return (await dataLoader.get('users').loadMany(userIds)).filter(isValid)
},
teams: async ({teamIds}, _args, {dataLoader}) => {
if (!teamIds) return null
return (await dataLoader.get('teams').loadMany(teamIds)).filter(isValid)
},
organizations: async ({orgIds}, _args, {dataLoader}) => {
if (!orgIds) return null
return (await dataLoader.get('organizations').loadMany(orgIds)).filter(isValid)
},
removedCount: ({removedCount}) => removedCount
}

export default RemoveFeatureFlagOwnerSuccess
Loading