import createFlash from '~/flash'; import createDefaultClient from '~/lib/graphql'; import axios from '~/lib/utils/axios_utils'; import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; import { s__ } from '~/locale'; import { STATUSES } from '../../constants'; import { i18n, NEW_NAME_FIELD } from '../constants'; import { isAvailableForImport } from '../utils'; import bulkImportSourceGroupItemFragment from './fragments/bulk_import_source_group_item.fragment.graphql'; import bulkImportSourceGroupProgressFragment from './fragments/bulk_import_source_group_progress.fragment.graphql'; import addValidationErrorMutation from './mutations/add_validation_error.mutation.graphql'; import removeValidationErrorMutation from './mutations/remove_validation_error.mutation.graphql'; import setImportProgressMutation from './mutations/set_import_progress.mutation.graphql'; import setImportTargetMutation from './mutations/set_import_target.mutation.graphql'; import updateImportStatusMutation from './mutations/update_import_status.mutation.graphql'; import availableNamespacesQuery from './queries/available_namespaces.query.graphql'; import bulkImportSourceGroupQuery from './queries/bulk_import_source_group.query.graphql'; import groupAndProjectQuery from './queries/group_and_project.query.graphql'; import { SourceGroupsManager } from './services/source_groups_manager'; import { StatusPoller } from './services/status_poller'; import typeDefs from './typedefs.graphql'; export const clientTypenames = { BulkImportSourceGroupConnection: 'ClientBulkImportSourceGroupConnection', BulkImportSourceGroup: 'ClientBulkImportSourceGroup', AvailableNamespace: 'ClientAvailableNamespace', BulkImportPageInfo: 'ClientBulkImportPageInfo', BulkImportTarget: 'ClientBulkImportTarget', BulkImportProgress: 'ClientBulkImportProgress', BulkImportValidationError: 'ClientBulkImportValidationError', }; function makeGroup(data) { const result = { __typename: clientTypenames.BulkImportSourceGroup, ...data, }; const NESTED_OBJECT_FIELDS = { import_target: clientTypenames.BulkImportTarget, last_import_target: clientTypenames.BulkImportTarget, progress: clientTypenames.BulkImportProgress, }; Object.entries(NESTED_OBJECT_FIELDS).forEach(([field, type]) => { if (!data[field]) { return; } result[field] = { __typename: type, ...data[field], }; }); return result; } async function checkImportTargetIsValid({ client, newName, targetNamespace, sourceGroupId }) { const { data: { existingGroup, existingProject }, } = await client.query({ query: groupAndProjectQuery, fetchPolicy: 'no-cache', variables: { fullPath: `${targetNamespace}/${newName}`, }, }); const variables = { field: NEW_NAME_FIELD, sourceGroupId, }; if (!existingGroup && !existingProject) { client.mutate({ mutation: removeValidationErrorMutation, variables, }); } else { client.mutate({ mutation: addValidationErrorMutation, variables: { ...variables, message: i18n.NAME_ALREADY_EXISTS, }, }); } } const localProgressId = (id) => `not-started-${id}`; const nextName = (name) => `${name}-1`; export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGroupsManager }) { const groupsManager = new GroupsManager({ sourceUrl, }); let statusPoller; return { Query: { async bulkImportSourceGroup(_, { id }, { client, getCacheKey }) { return client.readFragment({ fragment: bulkImportSourceGroupItemFragment, fragmentName: 'BulkImportSourceGroupItem', id: getCacheKey({ __typename: clientTypenames.BulkImportSourceGroup, id, }), }); }, async bulkImportSourceGroups(_, vars, { client }) { if (!statusPoller) { statusPoller = new StatusPoller({ updateImportStatus: ({ id, status_name: status }) => client.mutate({ mutation: updateImportStatusMutation, variables: { id, status }, }), pollPath: endpoints.jobs, }); statusPoller.startPolling(); } return Promise.all([ axios.get(endpoints.status, { params: { page: vars.page, per_page: vars.perPage, filter: vars.filter, }, }), client.query({ query: availableNamespacesQuery }), ]).then( ([ { headers, data }, { data: { availableNamespaces }, }, ]) => { const pagination = parseIntPagination(normalizeHeaders(headers)); const response = { __typename: clientTypenames.BulkImportSourceGroupConnection, nodes: data.importable_data.map((group) => { const { jobId, importState: cachedImportState } = groupsManager.getImportStateFromStorageByGroupId(group.id) ?? {}; const status = cachedImportState?.status ?? STATUSES.NONE; const importTarget = status === STATUSES.FINISHED && cachedImportState.importTarget ? { target_namespace: cachedImportState.importTarget.target_namespace, new_name: nextName(cachedImportState.importTarget.new_name), } : cachedImportState?.importTarget ?? { new_name: group.full_path, target_namespace: availableNamespaces[0]?.full_path ?? '', }; return makeGroup({ ...group, validation_errors: [], progress: { id: jobId ?? localProgressId(group.id), status, }, import_target: importTarget, last_import_target: cachedImportState?.importTarget ?? null, }); }), pageInfo: { __typename: clientTypenames.BulkImportPageInfo, ...pagination, }, }; setTimeout(() => { response.nodes.forEach((group) => { if (isAvailableForImport(group)) { checkImportTargetIsValid({ client, newName: group.import_target.new_name, targetNamespace: group.import_target.target_namespace, sourceGroupId: group.id, }); } }); }); return response; }, ); }, availableNamespaces: () => axios.get(endpoints.availableNamespaces).then(({ data }) => data.map((namespace) => ({ __typename: clientTypenames.AvailableNamespace, ...namespace, })), ), }, Mutation: { setImportTarget(_, { targetNamespace, newName, sourceGroupId }, { client }) { checkImportTargetIsValid({ client, sourceGroupId, targetNamespace, newName, }); return makeGroup({ id: sourceGroupId, import_target: { target_namespace: targetNamespace, new_name: newName, id: sourceGroupId, }, }); }, async setImportProgress(_, { sourceGroupId, status, jobId, importTarget }) { if (jobId) { groupsManager.updateImportProgress(jobId, status); } return makeGroup({ id: sourceGroupId, progress: { id: jobId ?? localProgressId(sourceGroupId), status, }, last_import_target: { __typename: clientTypenames.BulkImportTarget, ...importTarget, }, }); }, async updateImportStatus(_, { id, status: newStatus }, { client, getCacheKey }) { groupsManager.updateImportProgress(id, newStatus); const progressItem = client.readFragment({ fragment: bulkImportSourceGroupProgressFragment, fragmentName: 'BulkImportSourceGroupProgress', id: getCacheKey({ __typename: clientTypenames.BulkImportProgress, id, }), }); const isInProgress = Boolean(progressItem); const { status: currentStatus } = progressItem ?? {}; if (newStatus === STATUSES.FINISHED && isInProgress && currentStatus !== newStatus) { const groups = groupsManager.getImportedGroupsByJobId(id); groups.forEach(async ({ id: groupId, importTarget }) => { client.mutate({ mutation: setImportTargetMutation, variables: { sourceGroupId: groupId, targetNamespace: importTarget.target_namespace, newName: nextName(importTarget.new_name), }, }); }); } return { __typename: clientTypenames.BulkImportProgress, id, status: newStatus, }; }, async addValidationError(_, { sourceGroupId, field, message }, { client }) { const { data: { bulkImportSourceGroup: { validation_errors: validationErrors, ...group }, }, } = await client.query({ query: bulkImportSourceGroupQuery, variables: { id: sourceGroupId }, }); return { ...group, validation_errors: [ ...validationErrors.filter(({ field: f }) => f !== field), { __typename: clientTypenames.BulkImportValidationError, field, message, }, ], }; }, async removeValidationError(_, { sourceGroupId, field }, { client }) { const { data: { bulkImportSourceGroup: { validation_errors: validationErrors, ...group }, }, } = await client.query({ query: bulkImportSourceGroupQuery, variables: { id: sourceGroupId }, }); return { ...group, validation_errors: validationErrors.filter(({ field: f }) => f !== field), }; }, async importGroups(_, { sourceGroupIds }, { client }) { const groups = await Promise.all( sourceGroupIds.map((id) => client .query({ query: bulkImportSourceGroupQuery, variables: { id }, }) .then(({ data }) => data.bulkImportSourceGroup), ), ); const GROUPS_BEING_SCHEDULED = sourceGroupIds.map((sourceGroupId) => makeGroup({ id: sourceGroupId, progress: { id: localProgressId(sourceGroupId), status: STATUSES.SCHEDULING, }, }), ); const defaultErrorMessage = s__('BulkImport|Importing the group failed'); axios .post(endpoints.createBulkImport, { bulk_import: groups.map((group) => ({ source_type: 'group_entity', source_full_path: group.full_path, destination_namespace: group.import_target.target_namespace, destination_name: group.import_target.new_name, })), }) .then(({ data: { id: jobId } }) => { groupsManager.createImportState(jobId, { status: STATUSES.CREATED, groups, }); return { status: STATUSES.CREATED, jobId }; }) .catch((e) => { const message = e?.response?.data?.error ?? defaultErrorMessage; createFlash({ message }); return { status: STATUSES.NONE }; }) .then((newStatus) => sourceGroupIds.forEach((sourceGroupId, idx) => client.mutate({ mutation: setImportProgressMutation, variables: { sourceGroupId, ...newStatus, importTarget: groups[idx].import_target }, }), ), ) .catch(() => createFlash({ message: defaultErrorMessage })); return GROUPS_BEING_SCHEDULED; }, }, }; } export const createApolloClient = ({ sourceUrl, endpoints }) => createDefaultClient( createResolvers({ sourceUrl, endpoints }), { assumeImmutableResults: true }, typeDefs, );