Add addOnAssignments to UserType in GraphQL API
What does this MR do and why?
Addresses part of issue: https://gitlab.com/gitlab-org/gitlab/-/issues/421974+
Add the add_on_assignments
field to the GraphQL API's UserType
for EE. We need to be able to query this value alongside the user record to render the add-on assignment UI for the group owner. It's intended that this API will be used to list many users, so ensure that batch loading works correctly for this field.
This MR will be followed up by some of the work in Create add on eligible users GraphQL API (!129203 - merged) to list the eligible add on users.
GraphQL Queries
Navigate to http://localhost:3000/-/graphql-explorer
Create Assignment
Click to expand
mutation userAddOnAssignmentCreate($input: UserAddOnAssignmentCreateInput!, $addOnPurchaseIds: [GitlabSubscriptionsAddOnPurchaseID!]!) {
userAddOnAssignmentCreate(input: $input) {
errors
clientMutationId
addOnPurchase {
id
name
purchasedQuantity
assignedQuantity
}
user {
name
addOnAssignments(addOnPurchaseIds: $addOnPurchaseIds) {
nodes {
addOnPurchase {
id
name
}
}
}
}
}
}
{
"input": {
"userId": "gid://gitlab/User/1",
"addOnPurchaseId": "gid://gitlab/GitlabSubscriptions::AddOnPurchase/1"
},
"addOnPurchaseIds": ["gid://gitlab/GitlabSubscriptions::AddOnPurchase/1"]
}
Remove Assignment
Click to expand
mutation userAddOnAssignmentRemove($input: UserAddOnAssignmentRemoveInput!, $addOnPurchaseIds: [GitlabSubscriptionsAddOnPurchaseID!]!) {
userAddOnAssignmentRemove(input: $input) {
errors
clientMutationId
addOnPurchase {
id
name
purchasedQuantity
assignedQuantity
}
user {
name
addOnAssignments(addOnPurchaseIds: $addOnPurchaseIds) {
nodes {
addOnPurchase {
id
name
}
}
}
}
}
}
{
"input": {
"userId": "gid://gitlab/User/1",
"addOnPurchaseId": "gid://gitlab/GitlabSubscriptions::AddOnPurchase/1"
},
"addOnPurchaseIds": ["gid://gitlab/GitlabSubscriptions::AddOnPurchase/1"]
}
How to set up and validate locally
- Enable the
hamilton_seat_management
FFFeature.enable(:hamilton_seat_management)
- Create an add on purchase
GitlabSubscriptions::AddOnPurchase.create!(add_on: GitlabSubscriptions::AddOn.first, expires_on: 1.year.from_now, namespace_id: <NAMESPACE-ID>, quantity: 10, purchase_xid: 'A-12345')
- Assign the add on using the create assignment mutation
- View the add-on assignments in the response
- Remove the assignment using the remove assignment mutation
- View the add-on assignments in the response
SQL Queries
The following queries are executed when loading the users list via the GraphQL API and requesting this new attribute:
SELECT "users".* FROM "users" ORDER BY "users"."created_at" DESC, "users"."id" DESC LIMIT 101
SELECT "subscription_user_add_on_assignments".* FROM "subscription_user_add_on_assignments" INNER JOIN "subscription_add_on_purchases" ON "subscription_add_on_purchases"."id" = "subscription_user_add_on_assignments"."add_on_purchase_id" WHERE "subscription_user_add_on_assignments"."user_id" IN (109, 108, 107, 106, 105, 104, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4) AND "subscription_add_on_purchases"."id" IN (1, 2) AND (expires_on >= '2023-08-28')
SELECT "subscription_add_on_purchases".* FROM "subscription_add_on_purchases" WHERE "subscription_add_on_purchases"."id" IN (1, 2)
SELECT "subscription_add_ons".* FROM "subscription_add_ons" WHERE "subscription_add_ons"."id" = 1
SELECT "namespaces".* FROM "namespaces" WHERE "namespaces"."id" IN (73, 510)
# --------- the following are loaded as a result of the `Preloaders::GroupPolicyPreloader` --------
SELECT MAX("members"."access_level") AS "maximum_access_level", "hierarchy"."id" AS "hierarchy_id" FROM ((SELECT "members"."id", "members"."access_level", "members"."source_id", "members"."source_type", "members"."user_id", "members"."notification_level", "members"."type", "members"."created_at", "members"."updated_at", "members"."created_by_id", "members"."invite_email", "members"."invite_token", "members"."invite_accepted_at", "members"."requested_at", "members"."expires_at", "members"."ldap", "members"."override", "members"."state", "members"."invite_email_success", "members"."member_namespace_id", "members"."member_role_id", "members"."expiry_notified_at" FROM "members" LEFT OUTER JOIN "users" ON "users"."id" = "members"."user_id" WHERE "members"."type" = 'GroupMember' AND "members"."source_type" = 'Namespace' AND "users"."state" = 'active' AND "members"."state" = 0 AND "members"."requested_at" IS NULL AND "members"."invite_token" IS NULL AND (members.access_level > 5) AND "members"."user_id" = 89)
UNION
(SELECT "members"."id", LEAST("group_group_links"."group_access", "members"."access_level") AS access_level, "group_group_links"."shared_group_id" AS source_id, "members"."source_type", "members"."user_id", "members"."notification_level", "members"."type", "members"."created_at", "members"."updated_at", "members"."created_by_id", "members"."invite_email", "members"."invite_token", "members"."invite_accepted_at", "members"."requested_at", "members"."expires_at", "members"."ldap", "members"."override", "members"."state", "members"."invite_email_success", "members"."member_namespace_id", "members"."member_role_id", "members"."expiry_notified_at" FROM "members" LEFT OUTER JOIN "users" ON "users"."id" = "members"."user_id" INNER JOIN group_group_links ON members.source_id = group_group_links.shared_with_group_id WHERE "members"."type" = 'GroupMember' AND "members"."source_type" = 'Namespace' AND "users"."state" = 'active' AND "members"."state" = 0 AND "members"."requested_at" IS NULL AND "members"."invite_token" IS NULL AND (members.access_level > 5) AND "members"."user_id" = 89)) members INNER JOIN (SELECT id, unnest(traversal_ids) as traversal_id FROM "namespaces" WHERE "namespaces"."id" IN (73, 73, 510, 510, 510, 22)) as hierarchy ON members.source_id = hierarchy.traversal_id WHERE "members"."type" = 'GroupMember' AND "members"."source_type" = 'Namespace' GROUP BY "hierarchy"."id"
SELECT namespaces.*, root_query.id as source_id FROM "namespaces" INNER JOIN (SELECT id, traversal_ids[1] as root_id FROM "namespaces" WHERE "namespaces"."type" = 'Group' AND "namespaces"."id" IN (73, 73, 510, 510, 510, 22)) as root_query ON root_query.root_id = namespaces.id
SELECT "ip_restrictions".* FROM "ip_restrictions" WHERE "ip_restrictions"."group_id" IN (22, 73, 510)
SELECT "saml_providers"."id", "saml_providers"."group_id", "saml_providers"."enabled", "saml_providers"."certificate_fingerprint", "saml_providers"."sso_url", "saml_providers"."enforced_sso", "saml_providers"."enforced_group_managed_accounts", "saml_providers"."prohibited_outer_forks", "saml_providers"."default_membership_role", "saml_providers"."git_check_enforced" FROM "saml_providers" WHERE "saml_providers"."group_id" IN (22, 73, 510)
SELECT VERSION()
WITH "base_ancestors_cte" AS MATERIALIZED (SELECT "namespaces"."traversal_ids" FROM "namespaces" WHERE "namespaces"."type" = 'Group' AND "namespaces"."id" IN (73, 73, 510, 510, 510, 22)) SELECT "namespaces"."id", "namespaces"."parent_id", "gitlab_subscriptions"."hosted_plan_id" FROM "namespaces" INNER JOIN (SELECT DISTINCT unnest("base_ancestors_cte"."traversal_ids") FROM base_ancestors_cte) AS ancestors(ancestor_id) ON namespaces.id = ancestors.ancestor_id LEFT OUTER JOIN gitlab_subscriptions ON gitlab_subscriptions.namespace_id=namespaces.id WHERE "namespaces"."type" = 'Group'
SELECT "plans".* FROM "plans" WHERE "plans"."id" IN (1, 6)
The main new addition here is the one below (link to query plan). This feature is not used in production yet, so this query plan was generated locally.
SELECT
"subscription_user_add_on_assignments".*
FROM
"subscription_user_add_on_assignments"
INNER JOIN "subscription_add_on_purchases" ON "subscription_add_on_purchases"."id" = "subscription_user_add_on_assignments"."add_on_purchase_id"
WHERE
"subscription_user_add_on_assignments"."user_id" IN (109, 108, 107, 106, 105, 104, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4)
AND "subscription_add_on_purchases"."id" IN (1, 2)
AND (expires_on >= '2023-08-28')
MR acceptance checklist
This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.
-
I have evaluated the MR acceptance checklist for this MR.