mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 17:51:57 +00:00
codeintel: Support bulk deletion of LSIF uploads (#42395)
This commit is contained in:
parent
9fa0f29637
commit
d3268e7b9f
@ -95,7 +95,7 @@ interface FilteredConnectionProps<C extends Connection<N>, N, NP = {}, HP = {}>
|
||||
queryConnection: (args: FilteredConnectionQueryArguments) => Observable<C>
|
||||
|
||||
/** Called when the queryConnection Observable emits. */
|
||||
onUpdate?: (value: C | ErrorLike | undefined, query: string) => void
|
||||
onUpdate?: (value: C | ErrorLike | undefined, query: string, activeValues: FilteredConnectionArgs) => void
|
||||
|
||||
/**
|
||||
* Set to true when the GraphQL response is expected to emit an `PageInfo.endCursor` value when
|
||||
@ -359,7 +359,11 @@ export class FilteredConnection<
|
||||
}
|
||||
}
|
||||
if (this.props.onUpdate) {
|
||||
this.props.onUpdate(connectionOrError, this.state.query)
|
||||
this.props.onUpdate(
|
||||
connectionOrError,
|
||||
this.state.query,
|
||||
this.buildArgs(this.state.activeValues)
|
||||
)
|
||||
}
|
||||
this.setState({ connectionOrError, ...rest })
|
||||
},
|
||||
@ -560,6 +564,7 @@ export class FilteredConnection<
|
||||
}
|
||||
|
||||
private onChange: React.ChangeEventHandler<HTMLInputElement> = event => {
|
||||
this.props.onInputChange?.(event)
|
||||
this.queryInputChanges.next(event.currentTarget.value)
|
||||
}
|
||||
|
||||
@ -576,10 +581,8 @@ export class FilteredConnection<
|
||||
this.showMoreClicks.next()
|
||||
}
|
||||
|
||||
private buildArgs = (
|
||||
values: Map<string, FilteredConnectionFilterValue>
|
||||
): { [name: string]: string | number | boolean } => {
|
||||
let args: { [name: string]: string | number | boolean } = {}
|
||||
private buildArgs = (values: Map<string, FilteredConnectionFilterValue>): FilteredConnectionArgs => {
|
||||
let args: FilteredConnectionArgs = {}
|
||||
for (const key of values.keys()) {
|
||||
const value = values.get(key)
|
||||
if (value === undefined) {
|
||||
@ -601,3 +604,7 @@ export const resetFilteredConnectionURLQuery = (parameters: URLSearchParams): vo
|
||||
parameters.delete('first')
|
||||
parameters.delete('after')
|
||||
}
|
||||
|
||||
export interface FilteredConnectionArgs {
|
||||
[name: string]: string | number | boolean
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import React, { FunctionComponent } from 'react'
|
||||
import { mdiChevronRight } from '@mdi/js'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { Link, H3, Icon } from '@sourcegraph/wildcard'
|
||||
import { Link, H3, Icon, Checkbox } from '@sourcegraph/wildcard'
|
||||
|
||||
import { LsifUploadFields } from '../../../../graphql-operations'
|
||||
import { CodeIntelState } from '../../shared/components/CodeIntelState'
|
||||
@ -19,14 +19,25 @@ import styles from './CodeIntelUploadNode.module.scss'
|
||||
export interface CodeIntelUploadNodeProps {
|
||||
node: LsifUploadFields
|
||||
now?: () => Date
|
||||
selection: Set<string> | 'all'
|
||||
onCheckboxToggle: (id: string, checked: boolean) => void
|
||||
}
|
||||
|
||||
export const CodeIntelUploadNode: FunctionComponent<React.PropsWithChildren<CodeIntelUploadNodeProps>> = ({
|
||||
node,
|
||||
now,
|
||||
selection,
|
||||
onCheckboxToggle,
|
||||
}) => (
|
||||
<>
|
||||
<span className={styles.separator} />
|
||||
<Checkbox
|
||||
label=""
|
||||
id="disabledFieldsetCheck"
|
||||
disabled={selection === 'all'}
|
||||
checked={selection === 'all' ? true : selection.has(node.id)}
|
||||
onChange={input => onCheckboxToggle(node.id, input.target.checked)}
|
||||
/>
|
||||
|
||||
<div className={classNames(styles.information, 'd-flex flex-column')}>
|
||||
<div className="m-0">
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
import { ApolloError, MutationFunctionOptions, FetchResult, useMutation } from '@apollo/client'
|
||||
|
||||
import { gql, getDocumentNode } from '@sourcegraph/http-client'
|
||||
|
||||
import { DeleteLsifUploadsResult, DeleteLsifUploadsVariables } from '../../../../graphql-operations'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type DeleteLsifUploadsResults = Promise<FetchResult<DeleteLsifUploadsResult, Record<string, any>, Record<string, any>>>
|
||||
|
||||
interface UseDeleteLsifUploadsResult {
|
||||
handleDeleteLsifUploads: (
|
||||
options?: MutationFunctionOptions<DeleteLsifUploadsResult, DeleteLsifUploadsVariables> | undefined
|
||||
) => DeleteLsifUploadsResults
|
||||
deletesError: ApolloError | undefined
|
||||
}
|
||||
|
||||
const DELETE_LSIF_UPLOAD = gql`
|
||||
mutation DeleteLsifUploads($query: String, $state: LSIFUploadState, $isLatestForRepo: Boolean, $repository: ID) {
|
||||
deleteLSIFUploads(query: $query, state: $state, isLatestForRepo: $isLatestForRepo, repository: $repository) {
|
||||
alwaysNil
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const useDeleteLsifUploads = (): UseDeleteLsifUploadsResult => {
|
||||
const [handleDeleteLsifUploads, { error }] = useMutation<DeleteLsifUploadsResult, DeleteLsifUploadsVariables>(
|
||||
getDocumentNode(DELETE_LSIF_UPLOAD)
|
||||
)
|
||||
|
||||
return {
|
||||
handleDeleteLsifUploads,
|
||||
deletesError: error,
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: [info] minmax(auto, 1fr) [state] min-content [caret] min-content [end];
|
||||
grid-template-columns: min-content [info] minmax(auto, 1fr) [state] min-content [caret] min-content [end];
|
||||
row-gap: 1rem;
|
||||
column-gap: 1rem;
|
||||
align-items: center;
|
||||
|
||||
@ -3,10 +3,12 @@ import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 're
|
||||
import { useApolloClient } from '@apollo/client'
|
||||
import classNames from 'classnames'
|
||||
import { RouteComponentProps, useLocation } from 'react-router'
|
||||
import { of } from 'rxjs'
|
||||
import { of, Subject } from 'rxjs'
|
||||
|
||||
import { ErrorAlert } from '@sourcegraph/branded/src/components/alerts'
|
||||
import { isErrorLike } from '@sourcegraph/common'
|
||||
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { Container, PageHeader, useObservable } from '@sourcegraph/wildcard'
|
||||
import { Button, Container, PageHeader, useObservable } from '@sourcegraph/wildcard'
|
||||
|
||||
import {
|
||||
FilteredConnection,
|
||||
@ -14,7 +16,7 @@ import {
|
||||
FilteredConnectionQueryArguments,
|
||||
} from '../../../../components/FilteredConnection'
|
||||
import { PageTitle } from '../../../../components/PageTitle'
|
||||
import { LsifUploadFields, LSIFUploadState } from '../../../../graphql-operations'
|
||||
import { DeleteLsifUploadsVariables, LsifUploadFields, LSIFUploadState } from '../../../../graphql-operations'
|
||||
import { FlashMessage } from '../../configuration/components/FlashMessage'
|
||||
import { queryCommitGraphMetadata as defaultQueryCommitGraphMetadata } from '../../indexes/hooks/queryCommitGraphMetadata'
|
||||
import { CodeIntelUploadNode, CodeIntelUploadNodeProps } from '../components/CodeIntelUploadNode'
|
||||
@ -22,6 +24,8 @@ import { CommitGraphMetadata } from '../components/CommitGraphMetadata'
|
||||
import { EmptyUploads } from '../components/EmptyUploads'
|
||||
import { queryLsifUploadsByRepository as defaultQueryLsifUploadsByRepository } from '../hooks/queryLsifUploadsByRepository'
|
||||
import { queryLsifUploadsList as defaultQueryLsifUploadsList } from '../hooks/queryLsifUploadsList'
|
||||
import { useDeleteLsifUpload } from '../hooks/useDeleteLsifUpload'
|
||||
import { useDeleteLsifUploads } from '../hooks/useDeleteLsifUploads'
|
||||
|
||||
import styles from './CodeIntelUploadsPage.module.scss'
|
||||
|
||||
@ -134,6 +138,29 @@ export const CodeIntelUploadsPage: FunctionComponent<React.PropsWithChildren<Cod
|
||||
}
|
||||
}, [location.state])
|
||||
|
||||
const [args, setArgs] = useState<DeleteLsifUploadsVariables>()
|
||||
|
||||
// selection has the same type as CodeIntelUploadNode's prop because there is no CodeIntelUploadNodeProps
|
||||
const [selection, setSelection] = useState<CodeIntelUploadNodeProps['selection']>(new Set())
|
||||
const onCheckboxToggle = useCallback<CodeIntelUploadNodeProps['onCheckboxToggle']>((id, checked) => {
|
||||
setSelection(selection => {
|
||||
if (selection === 'all') {
|
||||
return selection
|
||||
}
|
||||
if (checked) {
|
||||
selection.add(id)
|
||||
} else {
|
||||
selection.delete(id)
|
||||
}
|
||||
return new Set(selection)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const { handleDeleteLsifUpload, deleteError } = useDeleteLsifUpload()
|
||||
const { handleDeleteLsifUploads, deletesError } = useDeleteLsifUploads()
|
||||
|
||||
const deletes = useMemo(() => new Subject<undefined>(), [])
|
||||
|
||||
return (
|
||||
<div className="code-intel-uploads">
|
||||
<PageTitle title="Code graph data uploads" />
|
||||
@ -164,6 +191,61 @@ export const CodeIntelUploadsPage: FunctionComponent<React.PropsWithChildren<Cod
|
||||
)}
|
||||
|
||||
<Container>
|
||||
<div className="mb-3">
|
||||
<Button
|
||||
className="mr-2"
|
||||
variant="primary"
|
||||
disabled={selection !== 'all' && selection.size === 0}
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
onClick={async () => {
|
||||
if (selection === 'all') {
|
||||
if (args === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
!confirm(
|
||||
`Delete all uploads matching the filter criteria?\n\n${Object.entries(args)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join('\n')}`
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
await handleDeleteLsifUploads({
|
||||
variables: args,
|
||||
update: cache => cache.modify({ fields: { node: () => {} } }),
|
||||
})
|
||||
|
||||
deletes.next()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for (const id of selection) {
|
||||
await handleDeleteLsifUpload({
|
||||
variables: { id },
|
||||
update: cache => cache.modify({ fields: { node: () => {} } }),
|
||||
})
|
||||
}
|
||||
|
||||
deletes.next()
|
||||
}}
|
||||
>
|
||||
Delete {selection === 'all' ? 'matching' : selection.size === 0 ? '' : selection.size}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setSelection(selection => (selection === 'all' ? new Set() : 'all'))}
|
||||
>
|
||||
{selection === 'all' ? 'Deselect' : 'Select matching'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isErrorLike(deleteError) && <ErrorAlert prefix="Error deleting LSIF upload" error={deleteError} />}
|
||||
{isErrorLike(deletesError) && <ErrorAlert prefix="Error deleting LSIF uploads" error={deletesError} />}
|
||||
|
||||
<div className="list-group position-relative">
|
||||
<FilteredConnection<LsifUploadFields, Omit<CodeIntelUploadNodeProps, 'node'>>
|
||||
listComponent="div"
|
||||
@ -172,13 +254,25 @@ export const CodeIntelUploadsPage: FunctionComponent<React.PropsWithChildren<Cod
|
||||
noun="upload"
|
||||
pluralNoun="uploads"
|
||||
nodeComponent={CodeIntelUploadNode}
|
||||
nodeComponentProps={{ now }}
|
||||
queryConnection={queryLsifUploads}
|
||||
nodeComponentProps={{ now, selection, onCheckboxToggle }}
|
||||
queryConnection={args => {
|
||||
setArgs({
|
||||
query: args.query ?? null,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
|
||||
state: (args as any).state ?? null,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
|
||||
isLatestForRepo: (args as any).isLatestForRepo ?? null,
|
||||
repository: repo?.id ?? null,
|
||||
})
|
||||
setSelection(new Set())
|
||||
return queryLsifUploads(args)
|
||||
}}
|
||||
history={history}
|
||||
location={props.location}
|
||||
cursorPaging={true}
|
||||
filters={filters}
|
||||
emptyElement={<EmptyUploads />}
|
||||
updates={deletes}
|
||||
/>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
@ -50,6 +50,7 @@ type UploadsServiceResolver interface {
|
||||
LSIFUploads(ctx context.Context, args *uploadsgraphql.LSIFUploadsQueryArgs) (resolvers.LSIFUploadConnectionResolver, error)
|
||||
LSIFUploadsByRepo(ctx context.Context, args *uploadsgraphql.LSIFRepositoryUploadsQueryArgs) (resolvers.LSIFUploadConnectionResolver, error)
|
||||
DeleteLSIFUpload(ctx context.Context, args *struct{ ID graphql.ID }) (*resolvers.EmptyResponse, error)
|
||||
DeleteLSIFUploads(ctx context.Context, args *uploadsgraphql.DeleteLSIFUploadsArgs) (*resolvers.EmptyResponse, error)
|
||||
}
|
||||
type PoliciesServiceResolver interface {
|
||||
CodeIntelligenceConfigurationPolicies(ctx context.Context, args *policiesgraphql.CodeIntelligenceConfigurationPoliciesArgs) (policiesgraphql.CodeIntelligenceConfigurationPolicyConnectionResolver, error)
|
||||
|
||||
@ -71,6 +71,32 @@ extend type Mutation {
|
||||
"""
|
||||
deleteLSIFUpload(id: ID!): EmptyResponse
|
||||
|
||||
"""
|
||||
Deletes LSIF uploads by filter criteria.
|
||||
"""
|
||||
deleteLSIFUploads(
|
||||
"""
|
||||
An (optional) search query that filters the state, repository name,
|
||||
commit, root, and indexer properties.
|
||||
"""
|
||||
query: String
|
||||
|
||||
"""
|
||||
The upload state.
|
||||
"""
|
||||
state: LSIFUploadState
|
||||
|
||||
"""
|
||||
When specified, only deletes uploads that are latest for the given repository.
|
||||
"""
|
||||
isLatestForRepo: Boolean
|
||||
|
||||
"""
|
||||
The repository.
|
||||
"""
|
||||
repository: ID
|
||||
): EmptyResponse
|
||||
|
||||
"""
|
||||
Deletes an LSIF index.
|
||||
"""
|
||||
|
||||
@ -70,6 +70,11 @@ func (r *Resolver) DeleteLSIFUpload(ctx context.Context, args *struct{ ID graphq
|
||||
return r.resolver.UploadRootResolver().DeleteLSIFUpload(ctx, args)
|
||||
}
|
||||
|
||||
// 🚨 SECURITY: Only site admins may modify code intelligence upload data
|
||||
func (r *Resolver) DeleteLSIFUploads(ctx context.Context, args *uploadsgraphql.DeleteLSIFUploadsArgs) (_ *sharedresolvers.EmptyResponse, err error) {
|
||||
return r.resolver.UploadRootResolver().DeleteLSIFUploads(ctx, args)
|
||||
}
|
||||
|
||||
// 🚨 SECURITY: dbstore layer handles authz for GetIndexByID
|
||||
func (r *Resolver) LSIFIndexByID(ctx context.Context, id graphql.ID) (_ sharedresolvers.LSIFIndexResolver, err error) {
|
||||
return r.resolver.AutoIndexingRootResolver().LSIFIndexByID(ctx, id)
|
||||
|
||||
@ -120,6 +120,13 @@ type GetUploadsOptions struct {
|
||||
InCommitGraph bool
|
||||
}
|
||||
|
||||
type DeleteUploadsOptions struct {
|
||||
State string
|
||||
Term string
|
||||
VisibleAtTip bool
|
||||
RepositoryID int
|
||||
}
|
||||
|
||||
type GetConfigurationPoliciesOptions struct {
|
||||
// RepositoryID indicates that only configuration policies that apply to the
|
||||
// specified repository (directly or via pattern) should be returned. This value
|
||||
|
||||
@ -56,6 +56,7 @@ type operations struct {
|
||||
addUploadPart *observation.Operation
|
||||
markQueued *observation.Operation
|
||||
markFailed *observation.Operation
|
||||
deleteUploads *observation.Operation
|
||||
|
||||
// Dumps
|
||||
findClosestDumps *observation.Operation
|
||||
@ -139,6 +140,7 @@ func newOperations(observationContext *observation.Context) *operations {
|
||||
addUploadPart: op("AddUploadPart"),
|
||||
markQueued: op("MarkQueued"),
|
||||
markFailed: op("MarkFailed"),
|
||||
deleteUploads: op("DeleteUploads"),
|
||||
|
||||
writeVisibleUploads: op("writeVisibleUploads"),
|
||||
persistNearestUploads: op("persistNearestUploads"),
|
||||
|
||||
@ -61,6 +61,7 @@ type Store interface {
|
||||
DeleteUploadsStuckUploading(ctx context.Context, uploadedBefore time.Time) (_ int, err error)
|
||||
DeleteUploadsWithoutRepository(ctx context.Context, now time.Time) (_ map[int]int, err error)
|
||||
DeleteUploadByID(ctx context.Context, id int) (_ bool, err error)
|
||||
DeleteUploads(ctx context.Context, opts types.DeleteUploadsOptions) (err error)
|
||||
|
||||
// Uploads (uploading)
|
||||
InsertUpload(ctx context.Context, upload types.Upload) (int, error)
|
||||
|
||||
@ -28,10 +28,10 @@ import (
|
||||
|
||||
// GetUploads returns a list of uploads and the total count of records matching the given conditions.
|
||||
func (s *store) GetUploads(ctx context.Context, opts types.GetUploadsOptions) (uploads []types.Upload, totalCount int, err error) {
|
||||
ctx, trace, endObservation := s.operations.getUploads.With(ctx, &err, observation.Args{LogFields: buildLogFields(opts)})
|
||||
ctx, trace, endObservation := s.operations.getUploads.With(ctx, &err, observation.Args{LogFields: buildGetUploadsLogFields(opts)})
|
||||
defer endObservation(1, observation.Args{})
|
||||
|
||||
tableExpr, conds, cte := buildConditionsAndCte(opts)
|
||||
tableExpr, conds, cte := buildGetConditionsAndCte(opts)
|
||||
authzConds, err := database.AuthzQueryConds(ctx, database.NewDBWith(s.logger, s.db))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
@ -679,6 +679,59 @@ const deleteUploadByIDQuery = `
|
||||
UPDATE lsif_uploads u SET state = CASE WHEN u.state = 'completed' THEN 'deleting' ELSE 'deleted' END WHERE id = %s RETURNING repository_id
|
||||
`
|
||||
|
||||
// DeleteUploads deletes uploads by filter criteria. The associated repositories will be marked as dirty
|
||||
// so that their commit graphs will be updated in the background.
|
||||
func (s *store) DeleteUploads(ctx context.Context, opts types.DeleteUploadsOptions) (err error) {
|
||||
ctx, _, endObservation := s.operations.deleteUploads.With(ctx, &err, observation.Args{LogFields: buildDeleteUploadsLogFields(opts)})
|
||||
defer endObservation(1, observation.Args{})
|
||||
|
||||
conds := buildDeleteConditions(opts)
|
||||
authzConds, err := database.AuthzQueryConds(ctx, database.NewDBWith(s.logger, s.db))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conds = append(conds, authzConds)
|
||||
|
||||
tx, err := s.transact(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { err = tx.Done(err) }()
|
||||
|
||||
unset, _ := tx.db.SetLocal(ctx, "codeintel.lsif_uploads_audit.reason", "direct delete by filter criteria request")
|
||||
defer unset(ctx)
|
||||
|
||||
query := sqlf.Sprintf(
|
||||
deleteUploadsQuery,
|
||||
sqlf.Join(conds, " AND "),
|
||||
)
|
||||
repoIDs, err := basestore.ScanInts(s.db.Query(ctx, query))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var dirtyErr error
|
||||
for _, repoID := range repoIDs {
|
||||
if err := tx.SetRepositoryAsDirty(ctx, repoID); err != nil {
|
||||
dirtyErr = err
|
||||
}
|
||||
}
|
||||
if dirtyErr != nil {
|
||||
err = dirtyErr
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteUploadsQuery = `
|
||||
-- source: internal/codeintel/stores/dbstore/uploads.go:DeleteUploads
|
||||
UPDATE lsif_uploads u
|
||||
SET state = CASE WHEN u.state = 'completed' THEN 'deleting' ELSE 'deleted' END
|
||||
FROM repo
|
||||
WHERE repo.id = u.repository_id AND %s
|
||||
RETURNING repository_id
|
||||
`
|
||||
|
||||
// UpdateUploadRetention updates the last data retention scan timestamp on the upload
|
||||
// records with the given protected identifiers and sets the expired field on the upload
|
||||
// records with the given expired identifiers.
|
||||
@ -1931,7 +1984,7 @@ func nilTimeToString(t *time.Time) string {
|
||||
return t.String()
|
||||
}
|
||||
|
||||
func buildConditionsAndCte(opts types.GetUploadsOptions) (*sqlf.Query, []*sqlf.Query, []cteDefinition) {
|
||||
func buildGetConditionsAndCte(opts types.GetUploadsOptions) (*sqlf.Query, []*sqlf.Query, []cteDefinition) {
|
||||
conds := make([]*sqlf.Query, 0, 12)
|
||||
|
||||
allowDeletedUploads := (opts.AllowDeletedUpload && opts.State == "") || opts.State == "deleted"
|
||||
@ -2047,6 +2100,26 @@ func buildConditionsAndCte(opts types.GetUploadsOptions) (*sqlf.Query, []*sqlf.Q
|
||||
return sourceTableExpr, conds, cteDefinitions
|
||||
}
|
||||
|
||||
func buildDeleteConditions(opts types.DeleteUploadsOptions) []*sqlf.Query {
|
||||
conds := []*sqlf.Query{}
|
||||
if opts.RepositoryID != 0 {
|
||||
conds = append(conds, sqlf.Sprintf("u.repository_id = %s", opts.RepositoryID))
|
||||
}
|
||||
conds = append(conds, sqlf.Sprintf("repo.deleted_at IS NULL"))
|
||||
conds = append(conds, sqlf.Sprintf("u.state != 'deleted'"))
|
||||
if opts.Term != "" {
|
||||
conds = append(conds, makeSearchCondition(opts.Term))
|
||||
}
|
||||
if opts.State != "" {
|
||||
conds = append(conds, makeStateCondition(opts.State))
|
||||
}
|
||||
if opts.VisibleAtTip {
|
||||
conds = append(conds, sqlf.Sprintf("EXISTS ("+visibleAtTipSubselectQuery+")"))
|
||||
}
|
||||
|
||||
return conds
|
||||
}
|
||||
|
||||
// makeSearchCondition returns a disjunction of LIKE clauses against all searchable columns of an upload.
|
||||
func makeSearchCondition(term string) *sqlf.Query {
|
||||
searchableColumns := []string{
|
||||
@ -2098,7 +2171,7 @@ func buildCTEPrefix(cteDefinitions []cteDefinition) *sqlf.Query {
|
||||
return sqlf.Sprintf("WITH\n%s", sqlf.Join(cteQueries, ",\n"))
|
||||
}
|
||||
|
||||
func buildLogFields(opts types.GetUploadsOptions) []log.Field {
|
||||
func buildGetUploadsLogFields(opts types.GetUploadsOptions) []log.Field {
|
||||
return []log.Field{
|
||||
log.Int("repositoryID", opts.RepositoryID),
|
||||
log.String("state", opts.State),
|
||||
@ -2116,3 +2189,11 @@ func buildLogFields(opts types.GetUploadsOptions) []log.Field {
|
||||
log.Int("offset", opts.Offset),
|
||||
}
|
||||
}
|
||||
|
||||
func buildDeleteUploadsLogFields(opts types.DeleteUploadsOptions) []log.Field {
|
||||
return []log.Field{
|
||||
log.String("state", opts.State),
|
||||
log.String("term", opts.Term),
|
||||
log.Bool("visibleAtTip", opts.VisibleAtTip),
|
||||
}
|
||||
}
|
||||
|
||||
@ -638,6 +638,55 @@ func TestDeleteUploadsStuckUploading(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteUploads(t *testing.T) {
|
||||
logger := logtest.Scoped(t)
|
||||
db := database.NewDB(logger, dbtest.NewDB(logger, t))
|
||||
store := New(db, &observation.TestContext)
|
||||
|
||||
t1 := time.Unix(1587396557, 0).UTC()
|
||||
t2 := t1.Add(time.Minute * 1)
|
||||
t3 := t1.Add(time.Minute * 2)
|
||||
t4 := t1.Add(time.Minute * 3)
|
||||
t5 := t1.Add(time.Minute * 4)
|
||||
|
||||
insertUploads(t, db,
|
||||
types.Upload{ID: 1, Commit: makeCommit(1111), UploadedAt: t1, State: "queued"}, // will not be deleted
|
||||
types.Upload{ID: 2, Commit: makeCommit(1112), UploadedAt: t2, State: "uploading"}, // will be deleted
|
||||
types.Upload{ID: 3, Commit: makeCommit(1113), UploadedAt: t3, State: "uploading"}, // will be deleted
|
||||
types.Upload{ID: 4, Commit: makeCommit(1114), UploadedAt: t4, State: "completed"}, // will not be deleted
|
||||
types.Upload{ID: 5, Commit: makeCommit(1115), UploadedAt: t5, State: "uploading"}, // will be deleted
|
||||
)
|
||||
|
||||
err := store.DeleteUploads(context.Background(), types.DeleteUploadsOptions{
|
||||
State: "uploading",
|
||||
Term: "",
|
||||
VisibleAtTip: false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error deleting uploads: %s", err)
|
||||
}
|
||||
|
||||
uploads, totalCount, err := store.GetUploads(context.Background(), types.GetUploadsOptions{Limit: 5})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error getting uploads: %s", err)
|
||||
}
|
||||
|
||||
var ids []int
|
||||
for _, upload := range uploads {
|
||||
ids = append(ids, upload.ID)
|
||||
}
|
||||
sort.Ints(ids)
|
||||
|
||||
expectedIDs := []int{1, 4}
|
||||
|
||||
if totalCount != len(expectedIDs) {
|
||||
t.Errorf("unexpected total count. want=%d have=%d", len(expectedIDs), totalCount)
|
||||
}
|
||||
if diff := cmp.Diff(expectedIDs, ids); diff != "" {
|
||||
t.Errorf("unexpected upload ids (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHardDeleteUploadsByIDs(t *testing.T) {
|
||||
logger := logtest.Scoped(t)
|
||||
db := database.NewDB(logger, dbtest.NewDB(logger, t))
|
||||
|
||||
@ -48,6 +48,9 @@ type MockStore struct {
|
||||
// DeleteUploadByIDFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method DeleteUploadByID.
|
||||
DeleteUploadByIDFunc *StoreDeleteUploadByIDFunc
|
||||
// DeleteUploadsFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method DeleteUploads.
|
||||
DeleteUploadsFunc *StoreDeleteUploadsFunc
|
||||
// DeleteUploadsStuckUploadingFunc is an instance of a mock function
|
||||
// object controlling the behavior of the method
|
||||
// DeleteUploadsStuckUploading.
|
||||
@ -238,6 +241,11 @@ func NewMockStore() *MockStore {
|
||||
return
|
||||
},
|
||||
},
|
||||
DeleteUploadsFunc: &StoreDeleteUploadsFunc{
|
||||
defaultHook: func(context.Context, types.DeleteUploadsOptions) (r0 error) {
|
||||
return
|
||||
},
|
||||
},
|
||||
DeleteUploadsStuckUploadingFunc: &StoreDeleteUploadsStuckUploadingFunc{
|
||||
defaultHook: func(context.Context, time.Time) (r0 int, r1 error) {
|
||||
return
|
||||
@ -505,6 +513,11 @@ func NewStrictMockStore() *MockStore {
|
||||
panic("unexpected invocation of MockStore.DeleteUploadByID")
|
||||
},
|
||||
},
|
||||
DeleteUploadsFunc: &StoreDeleteUploadsFunc{
|
||||
defaultHook: func(context.Context, types.DeleteUploadsOptions) error {
|
||||
panic("unexpected invocation of MockStore.DeleteUploads")
|
||||
},
|
||||
},
|
||||
DeleteUploadsStuckUploadingFunc: &StoreDeleteUploadsStuckUploadingFunc{
|
||||
defaultHook: func(context.Context, time.Time) (int, error) {
|
||||
panic("unexpected invocation of MockStore.DeleteUploadsStuckUploading")
|
||||
@ -760,6 +773,9 @@ func NewMockStoreFrom(i store.Store) *MockStore {
|
||||
DeleteUploadByIDFunc: &StoreDeleteUploadByIDFunc{
|
||||
defaultHook: i.DeleteUploadByID,
|
||||
},
|
||||
DeleteUploadsFunc: &StoreDeleteUploadsFunc{
|
||||
defaultHook: i.DeleteUploads,
|
||||
},
|
||||
DeleteUploadsStuckUploadingFunc: &StoreDeleteUploadsStuckUploadingFunc{
|
||||
defaultHook: i.DeleteUploadsStuckUploading,
|
||||
},
|
||||
@ -1570,6 +1586,110 @@ func (c StoreDeleteUploadByIDFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
|
||||
// StoreDeleteUploadsFunc describes the behavior when the DeleteUploads
|
||||
// method of the parent MockStore instance is invoked.
|
||||
type StoreDeleteUploadsFunc struct {
|
||||
defaultHook func(context.Context, types.DeleteUploadsOptions) error
|
||||
hooks []func(context.Context, types.DeleteUploadsOptions) error
|
||||
history []StoreDeleteUploadsFuncCall
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// DeleteUploads delegates to the next hook function in the queue and stores
|
||||
// the parameter and result values of this invocation.
|
||||
func (m *MockStore) DeleteUploads(v0 context.Context, v1 types.DeleteUploadsOptions) error {
|
||||
r0 := m.DeleteUploadsFunc.nextHook()(v0, v1)
|
||||
m.DeleteUploadsFunc.appendCall(StoreDeleteUploadsFuncCall{v0, v1, r0})
|
||||
return r0
|
||||
}
|
||||
|
||||
// SetDefaultHook sets function that is called when the DeleteUploads method
|
||||
// of the parent MockStore instance is invoked and the hook queue is empty.
|
||||
func (f *StoreDeleteUploadsFunc) SetDefaultHook(hook func(context.Context, types.DeleteUploadsOptions) error) {
|
||||
f.defaultHook = hook
|
||||
}
|
||||
|
||||
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||
// DeleteUploads method of the parent MockStore instance invokes the hook at
|
||||
// the front of the queue and discards it. After the queue is empty, the
|
||||
// default hook function is invoked for any future action.
|
||||
func (f *StoreDeleteUploadsFunc) PushHook(hook func(context.Context, types.DeleteUploadsOptions) error) {
|
||||
f.mutex.Lock()
|
||||
f.hooks = append(f.hooks, hook)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetDefaultReturn calls SetDefaultHook with a function that returns the
|
||||
// given values.
|
||||
func (f *StoreDeleteUploadsFunc) SetDefaultReturn(r0 error) {
|
||||
f.SetDefaultHook(func(context.Context, types.DeleteUploadsOptions) error {
|
||||
return r0
|
||||
})
|
||||
}
|
||||
|
||||
// PushReturn calls PushHook with a function that returns the given values.
|
||||
func (f *StoreDeleteUploadsFunc) PushReturn(r0 error) {
|
||||
f.PushHook(func(context.Context, types.DeleteUploadsOptions) error {
|
||||
return r0
|
||||
})
|
||||
}
|
||||
|
||||
func (f *StoreDeleteUploadsFunc) nextHook() func(context.Context, types.DeleteUploadsOptions) error {
|
||||
f.mutex.Lock()
|
||||
defer f.mutex.Unlock()
|
||||
|
||||
if len(f.hooks) == 0 {
|
||||
return f.defaultHook
|
||||
}
|
||||
|
||||
hook := f.hooks[0]
|
||||
f.hooks = f.hooks[1:]
|
||||
return hook
|
||||
}
|
||||
|
||||
func (f *StoreDeleteUploadsFunc) appendCall(r0 StoreDeleteUploadsFuncCall) {
|
||||
f.mutex.Lock()
|
||||
f.history = append(f.history, r0)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// History returns a sequence of StoreDeleteUploadsFuncCall objects
|
||||
// describing the invocations of this function.
|
||||
func (f *StoreDeleteUploadsFunc) History() []StoreDeleteUploadsFuncCall {
|
||||
f.mutex.Lock()
|
||||
history := make([]StoreDeleteUploadsFuncCall, len(f.history))
|
||||
copy(history, f.history)
|
||||
f.mutex.Unlock()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
// StoreDeleteUploadsFuncCall is an object that describes an invocation of
|
||||
// method DeleteUploads on an instance of MockStore.
|
||||
type StoreDeleteUploadsFuncCall struct {
|
||||
// Arg0 is the value of the 1st argument passed to this method
|
||||
// invocation.
|
||||
Arg0 context.Context
|
||||
// Arg1 is the value of the 2nd argument passed to this method
|
||||
// invocation.
|
||||
Arg1 types.DeleteUploadsOptions
|
||||
// Result0 is the value of the 1st result returned from this method
|
||||
// invocation.
|
||||
Result0 error
|
||||
}
|
||||
|
||||
// Args returns an interface slice containing the arguments of this
|
||||
// invocation.
|
||||
func (c StoreDeleteUploadsFuncCall) Args() []interface{} {
|
||||
return []interface{}{c.Arg0, c.Arg1}
|
||||
}
|
||||
|
||||
// Results returns an interface slice containing the results of this
|
||||
// invocation.
|
||||
func (c StoreDeleteUploadsFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0}
|
||||
}
|
||||
|
||||
// StoreDeleteUploadsStuckUploadingFunc describes the behavior when the
|
||||
// DeleteUploadsStuckUploading method of the parent MockStore instance is
|
||||
// invoked.
|
||||
|
||||
@ -450,6 +450,13 @@ func (s *Service) DeleteUploadByID(ctx context.Context, id int) (_ bool, err err
|
||||
return s.store.DeleteUploadByID(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteUploads(ctx context.Context, opts types.DeleteUploadsOptions) (err error) {
|
||||
ctx, _, endObservation := s.operations.deleteUploadByID.With(ctx, &err, observation.Args{})
|
||||
defer endObservation(1, observation.Args{})
|
||||
|
||||
return s.store.DeleteUploads(ctx, opts)
|
||||
}
|
||||
|
||||
// numAncestors is the number of ancestors to query from gitserver when trying to find the closest
|
||||
// ancestor we have data for. Setting this value too low (relative to a repository's commit rate)
|
||||
// will cause requests for an unknown commit return too few results; setting this value too high
|
||||
|
||||
@ -40,6 +40,12 @@ type GetUploadsOptions struct {
|
||||
InCommitGraph bool
|
||||
}
|
||||
|
||||
type DeleteUploadsOptions struct {
|
||||
State string
|
||||
Term string
|
||||
VisibleAtTip bool
|
||||
}
|
||||
|
||||
type DependencyReferenceCountUpdateType int
|
||||
|
||||
const (
|
||||
|
||||
@ -18,6 +18,7 @@ type UploadService interface {
|
||||
GetUploads(ctx context.Context, opts types.GetUploadsOptions) (uploads []types.Upload, totalCount int, err error)
|
||||
GetUploadsByIDs(ctx context.Context, ids ...int) (_ []types.Upload, err error)
|
||||
DeleteUploadByID(ctx context.Context, id int) (_ bool, err error)
|
||||
DeleteUploads(ctx context.Context, opts types.DeleteUploadsOptions) (err error)
|
||||
}
|
||||
|
||||
type AutoIndexingService interface {
|
||||
|
||||
@ -878,6 +878,9 @@ type MockUploadService struct {
|
||||
// DeleteUploadByIDFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method DeleteUploadByID.
|
||||
DeleteUploadByIDFunc *UploadServiceDeleteUploadByIDFunc
|
||||
// DeleteUploadsFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method DeleteUploads.
|
||||
DeleteUploadsFunc *UploadServiceDeleteUploadsFunc
|
||||
// GetAuditLogsForUploadFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method GetAuditLogsForUpload.
|
||||
GetAuditLogsForUploadFunc *UploadServiceGetAuditLogsForUploadFunc
|
||||
@ -908,6 +911,11 @@ func NewMockUploadService() *MockUploadService {
|
||||
return
|
||||
},
|
||||
},
|
||||
DeleteUploadsFunc: &UploadServiceDeleteUploadsFunc{
|
||||
defaultHook: func(context.Context, types.DeleteUploadsOptions) (r0 error) {
|
||||
return
|
||||
},
|
||||
},
|
||||
GetAuditLogsForUploadFunc: &UploadServiceGetAuditLogsForUploadFunc{
|
||||
defaultHook: func(context.Context, int) (r0 []types.UploadLog, r1 error) {
|
||||
return
|
||||
@ -950,6 +958,11 @@ func NewStrictMockUploadService() *MockUploadService {
|
||||
panic("unexpected invocation of MockUploadService.DeleteUploadByID")
|
||||
},
|
||||
},
|
||||
DeleteUploadsFunc: &UploadServiceDeleteUploadsFunc{
|
||||
defaultHook: func(context.Context, types.DeleteUploadsOptions) error {
|
||||
panic("unexpected invocation of MockUploadService.DeleteUploads")
|
||||
},
|
||||
},
|
||||
GetAuditLogsForUploadFunc: &UploadServiceGetAuditLogsForUploadFunc{
|
||||
defaultHook: func(context.Context, int) ([]types.UploadLog, error) {
|
||||
panic("unexpected invocation of MockUploadService.GetAuditLogsForUpload")
|
||||
@ -991,6 +1004,9 @@ func NewMockUploadServiceFrom(i UploadService) *MockUploadService {
|
||||
DeleteUploadByIDFunc: &UploadServiceDeleteUploadByIDFunc{
|
||||
defaultHook: i.DeleteUploadByID,
|
||||
},
|
||||
DeleteUploadsFunc: &UploadServiceDeleteUploadsFunc{
|
||||
defaultHook: i.DeleteUploads,
|
||||
},
|
||||
GetAuditLogsForUploadFunc: &UploadServiceGetAuditLogsForUploadFunc{
|
||||
defaultHook: i.GetAuditLogsForUpload,
|
||||
},
|
||||
@ -1122,6 +1138,111 @@ func (c UploadServiceDeleteUploadByIDFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
|
||||
// UploadServiceDeleteUploadsFunc describes the behavior when the
|
||||
// DeleteUploads method of the parent MockUploadService instance is invoked.
|
||||
type UploadServiceDeleteUploadsFunc struct {
|
||||
defaultHook func(context.Context, types.DeleteUploadsOptions) error
|
||||
hooks []func(context.Context, types.DeleteUploadsOptions) error
|
||||
history []UploadServiceDeleteUploadsFuncCall
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// DeleteUploads delegates to the next hook function in the queue and stores
|
||||
// the parameter and result values of this invocation.
|
||||
func (m *MockUploadService) DeleteUploads(v0 context.Context, v1 types.DeleteUploadsOptions) error {
|
||||
r0 := m.DeleteUploadsFunc.nextHook()(v0, v1)
|
||||
m.DeleteUploadsFunc.appendCall(UploadServiceDeleteUploadsFuncCall{v0, v1, r0})
|
||||
return r0
|
||||
}
|
||||
|
||||
// SetDefaultHook sets function that is called when the DeleteUploads method
|
||||
// of the parent MockUploadService instance is invoked and the hook queue is
|
||||
// empty.
|
||||
func (f *UploadServiceDeleteUploadsFunc) SetDefaultHook(hook func(context.Context, types.DeleteUploadsOptions) error) {
|
||||
f.defaultHook = hook
|
||||
}
|
||||
|
||||
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||
// DeleteUploads method of the parent MockUploadService instance invokes the
|
||||
// hook at the front of the queue and discards it. After the queue is empty,
|
||||
// the default hook function is invoked for any future action.
|
||||
func (f *UploadServiceDeleteUploadsFunc) PushHook(hook func(context.Context, types.DeleteUploadsOptions) error) {
|
||||
f.mutex.Lock()
|
||||
f.hooks = append(f.hooks, hook)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetDefaultReturn calls SetDefaultHook with a function that returns the
|
||||
// given values.
|
||||
func (f *UploadServiceDeleteUploadsFunc) SetDefaultReturn(r0 error) {
|
||||
f.SetDefaultHook(func(context.Context, types.DeleteUploadsOptions) error {
|
||||
return r0
|
||||
})
|
||||
}
|
||||
|
||||
// PushReturn calls PushHook with a function that returns the given values.
|
||||
func (f *UploadServiceDeleteUploadsFunc) PushReturn(r0 error) {
|
||||
f.PushHook(func(context.Context, types.DeleteUploadsOptions) error {
|
||||
return r0
|
||||
})
|
||||
}
|
||||
|
||||
func (f *UploadServiceDeleteUploadsFunc) nextHook() func(context.Context, types.DeleteUploadsOptions) error {
|
||||
f.mutex.Lock()
|
||||
defer f.mutex.Unlock()
|
||||
|
||||
if len(f.hooks) == 0 {
|
||||
return f.defaultHook
|
||||
}
|
||||
|
||||
hook := f.hooks[0]
|
||||
f.hooks = f.hooks[1:]
|
||||
return hook
|
||||
}
|
||||
|
||||
func (f *UploadServiceDeleteUploadsFunc) appendCall(r0 UploadServiceDeleteUploadsFuncCall) {
|
||||
f.mutex.Lock()
|
||||
f.history = append(f.history, r0)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// History returns a sequence of UploadServiceDeleteUploadsFuncCall objects
|
||||
// describing the invocations of this function.
|
||||
func (f *UploadServiceDeleteUploadsFunc) History() []UploadServiceDeleteUploadsFuncCall {
|
||||
f.mutex.Lock()
|
||||
history := make([]UploadServiceDeleteUploadsFuncCall, len(f.history))
|
||||
copy(history, f.history)
|
||||
f.mutex.Unlock()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
// UploadServiceDeleteUploadsFuncCall is an object that describes an
|
||||
// invocation of method DeleteUploads on an instance of MockUploadService.
|
||||
type UploadServiceDeleteUploadsFuncCall struct {
|
||||
// Arg0 is the value of the 1st argument passed to this method
|
||||
// invocation.
|
||||
Arg0 context.Context
|
||||
// Arg1 is the value of the 2nd argument passed to this method
|
||||
// invocation.
|
||||
Arg1 types.DeleteUploadsOptions
|
||||
// Result0 is the value of the 1st result returned from this method
|
||||
// invocation.
|
||||
Result0 error
|
||||
}
|
||||
|
||||
// Args returns an interface slice containing the arguments of this
|
||||
// invocation.
|
||||
func (c UploadServiceDeleteUploadsFuncCall) Args() []interface{} {
|
||||
return []interface{}{c.Arg0, c.Arg1}
|
||||
}
|
||||
|
||||
// Results returns an interface slice containing the results of this
|
||||
// invocation.
|
||||
func (c UploadServiceDeleteUploadsFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0}
|
||||
}
|
||||
|
||||
// UploadServiceGetAuditLogsForUploadFunc describes the behavior when the
|
||||
// GetAuditLogsForUpload method of the parent MockUploadService instance is
|
||||
// invoked.
|
||||
|
||||
@ -12,6 +12,7 @@ type operations struct {
|
||||
lsifUploadByID *observation.Operation
|
||||
lsifUploadsByRepo *observation.Operation
|
||||
deleteLsifUpload *observation.Operation
|
||||
deleteLsifUploads *observation.Operation
|
||||
|
||||
// Commit Graph
|
||||
commitGraph *observation.Operation
|
||||
@ -38,6 +39,7 @@ func newOperations(observationContext *observation.Context) *operations {
|
||||
lsifUploadByID: op("LSIFUploadByID"),
|
||||
lsifUploadsByRepo: op("LSIFUploadsByRepo"),
|
||||
deleteLsifUpload: op("DeleteLSIFUpload"),
|
||||
deleteLsifUploads: op("DeleteLSIFUploads"),
|
||||
|
||||
// Commit Graph
|
||||
commitGraph: op("CommitGraph"),
|
||||
|
||||
@ -18,6 +18,7 @@ type RootResolver interface {
|
||||
LSIFUploads(ctx context.Context, args *LSIFUploadsQueryArgs) (sharedresolvers.LSIFUploadConnectionResolver, error)
|
||||
LSIFUploadsByRepo(ctx context.Context, args *LSIFRepositoryUploadsQueryArgs) (sharedresolvers.LSIFUploadConnectionResolver, error)
|
||||
DeleteLSIFUpload(ctx context.Context, args *struct{ ID graphql.ID }) (*sharedresolvers.EmptyResponse, error)
|
||||
DeleteLSIFUploads(ctx context.Context, args *DeleteLSIFUploadsArgs) (*sharedresolvers.EmptyResponse, error)
|
||||
}
|
||||
|
||||
type rootResolver struct {
|
||||
@ -96,6 +97,13 @@ type LSIFRepositoryUploadsQueryArgs struct {
|
||||
RepositoryID graphql.ID
|
||||
}
|
||||
|
||||
type DeleteLSIFUploadsArgs struct {
|
||||
Query *string
|
||||
State *string
|
||||
IsLatestForRepo *bool
|
||||
Repository *graphql.ID
|
||||
}
|
||||
|
||||
// 🚨 SECURITY: dbstore layer handles authz for GetUploads
|
||||
func (r *rootResolver) LSIFUploads(ctx context.Context, args *LSIFUploadsQueryArgs) (_ sharedresolvers.LSIFUploadConnectionResolver, err error) {
|
||||
// Delegate behavior to LSIFUploadsByRepo with no specified repository identifier
|
||||
@ -145,3 +153,23 @@ func (r *rootResolver) DeleteLSIFUpload(ctx context.Context, args *struct{ ID gr
|
||||
|
||||
return &sharedresolvers.EmptyResponse{}, nil
|
||||
}
|
||||
|
||||
// 🚨 SECURITY: Only site admins may modify code intelligence upload data
|
||||
func (r *rootResolver) DeleteLSIFUploads(ctx context.Context, args *DeleteLSIFUploadsArgs) (_ *sharedresolvers.EmptyResponse, err error) {
|
||||
ctx, _, endObservation := r.operations.deleteLsifUploads.With(ctx, &err, observation.Args{})
|
||||
endObservation.OnCancel(ctx, 1, observation.Args{})
|
||||
|
||||
if err := backend.CheckCurrentUserIsSiteAdmin(ctx, r.autoindexSvc.GetUnsafeDB()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts, err := makeDeleteUploadsOptions(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := r.uploadSvc.DeleteUploads(ctx, opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &sharedresolvers.EmptyResponse{}, nil
|
||||
}
|
||||
|
||||
@ -120,6 +120,26 @@ func makeGetUploadsOptions(args *LSIFRepositoryUploadsQueryArgs) (types.GetUploa
|
||||
}, nil
|
||||
}
|
||||
|
||||
// makeDeleteUploadsOptions translates the given GraphQL arguments into options defined by the
|
||||
// store.DeleteUploads operations.
|
||||
func makeDeleteUploadsOptions(args *DeleteLSIFUploadsArgs) (types.DeleteUploadsOptions, error) {
|
||||
var repository int
|
||||
if args.Repository != nil {
|
||||
var err error
|
||||
repository, err = resolveRepositoryID(*args.Repository)
|
||||
if err != nil {
|
||||
return types.DeleteUploadsOptions{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return types.DeleteUploadsOptions{
|
||||
State: strings.ToLower(derefString(args.State, "")),
|
||||
Term: derefString(args.Query, ""),
|
||||
VisibleAtTip: derefBool(args.IsLatestForRepo, false),
|
||||
RepositoryID: repository,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// resolveRepositoryByID gets a repository's internal identifier from a GraphQL identifier.
|
||||
func resolveRepositoryID(id graphql.ID) (int, error) {
|
||||
if id == "" {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user