feat: batch commit actions improvements (list view support, batch delete in 'your commits')

* feat(frontend): batch commit action support in list view

* feat(frontend): batch delete in 'your commits' page

* fix(server): authorizeResolver isPublic check always failed

* fix(server): fixing tests
This commit is contained in:
Kristaps Fabians Geikins
2022-09-26 13:16:44 +03:00
committed by GitHub
parent 025141a8b1
commit 61a2caaae9
23 changed files with 303 additions and 255 deletions
+1 -1
View File
@@ -84,7 +84,7 @@ function createCache(): InMemoryCache {
},
commits: {
keyArgs: false,
merge: buildAbstractCollectionMergeFunction('CommitCollectionUser', {
merge: buildAbstractCollectionMergeFunction('CommitCollection', {
checkIdentity: true
})
},
@@ -290,6 +290,15 @@ export type Commit = {
parents?: Maybe<Array<Maybe<Scalars['String']>>>;
referencedObject: Scalars['String'];
sourceApplication?: Maybe<Scalars['String']>;
/**
* Will throw an authorization error if active user isn't authorized to see it, for example,
* if a stream isn't public and the user doesn't have the appropriate rights.
*/
stream: Stream;
/** @deprecated Use the stream field instead */
streamId?: Maybe<Scalars['String']>;
/** @deprecated Use the stream field instead */
streamName?: Maybe<Scalars['String']>;
totalChildrenCount?: Maybe<Scalars['Int']>;
};
@@ -305,42 +314,10 @@ export type CommitActivityArgs = {
export type CommitCollection = {
__typename?: 'CommitCollection';
cursor?: Maybe<Scalars['String']>;
items?: Maybe<Array<Maybe<Commit>>>;
items?: Maybe<Array<Commit>>;
totalCount: Scalars['Int'];
};
export type CommitCollectionUser = {
__typename?: 'CommitCollectionUser';
cursor?: Maybe<Scalars['String']>;
items?: Maybe<Array<Maybe<CommitCollectionUserNode>>>;
totalCount: Scalars['Int'];
};
export type CommitCollectionUserNode = {
__typename?: 'CommitCollectionUserNode';
branchName?: Maybe<Scalars['String']>;
/**
* The total number of comments for this commit. To actually get the comments, use the comments query and pass in a resource array consisting of of this commit's id.
* E.g.,
* ```
* query{
* comments(streamId:"streamId" resources:[{resourceType: commit, resourceId:"commitId"}] ){
* ...
* }
* ```
*/
commentCount: Scalars['Int'];
createdAt?: Maybe<Scalars['DateTime']>;
id: Scalars['String'];
message?: Maybe<Scalars['String']>;
parents?: Maybe<Array<Maybe<Scalars['String']>>>;
referencedObject: Scalars['String'];
sourceApplication?: Maybe<Scalars['String']>;
streamId?: Maybe<Scalars['String']>;
streamName?: Maybe<Scalars['String']>;
totalChildrenCount?: Maybe<Scalars['Int']>;
};
export type CommitCreateInput = {
branchName: Scalars['String'];
message?: InputMaybe<Scalars['String']>;
@@ -1497,7 +1474,7 @@ export type User = {
* Get commits authored by the user. If requested for another user, then only commits
* from public streams will be returned.
*/
commits?: Maybe<CommitCollectionUser>;
commits?: Maybe<CommitCollection>;
company?: Maybe<Scalars['String']>;
/** Returns the apps you have created. */
createdApps?: Maybe<Array<Maybe<ServerApp>>>;
@@ -1699,7 +1676,7 @@ export type StreamWithBranchQueryVariables = Exact<{
}>;
export type StreamWithBranchQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, role?: string | null, branch?: { __typename?: 'Branch', id: string, name: string, description?: string | null, commits?: { __typename?: 'CommitCollection', totalCount: number, cursor?: string | null, items?: Array<{ __typename?: 'Commit', id: string, authorName?: string | null, authorId?: string | null, authorAvatar?: string | null, sourceApplication?: string | null, message?: string | null, referencedObject: string, createdAt?: string | null, commentCount: number } | null> | null } | null } | null } | null };
export type StreamWithBranchQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, role?: string | null, branch?: { __typename?: 'Branch', id: string, name: string, description?: string | null, commits?: { __typename?: 'CommitCollection', totalCount: number, cursor?: string | null, items?: Array<{ __typename?: 'Commit', id: string, authorName?: string | null, authorId?: string | null, authorAvatar?: string | null, sourceApplication?: string | null, message?: string | null, referencedObject: string, createdAt?: string | null, commentCount: number }> | null } | null } | null } | null };
export type BranchCreatedSubscriptionVariables = Exact<{
streamId: Scalars['String'];
@@ -1857,14 +1834,14 @@ export type StreamCommitsQueryVariables = Exact<{
}>;
export type StreamCommitsQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, role?: string | null, commits?: { __typename?: 'CommitCollection', totalCount: number, items?: Array<{ __typename?: 'Commit', id: string, authorId?: string | null, authorName?: string | null, authorAvatar?: string | null, createdAt?: string | null, message?: string | null, referencedObject: string, branchName?: string | null, sourceApplication?: string | null } | null> | null } | null } | null };
export type StreamCommitsQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, role?: string | null, commits?: { __typename?: 'CommitCollection', totalCount: number, items?: Array<{ __typename?: 'Commit', id: string, authorId?: string | null, authorName?: string | null, authorAvatar?: string | null, createdAt?: string | null, message?: string | null, referencedObject: string, branchName?: string | null, sourceApplication?: string | null }> | null } | null } | null };
export type StreamsQueryVariables = Exact<{
cursor?: InputMaybe<Scalars['String']>;
}>;
export type StreamsQuery = { __typename?: 'Query', streams?: { __typename?: 'StreamCollection', totalCount: number, cursor?: string | null, items?: Array<{ __typename?: 'Stream', id: string, name: string, description?: string | null, role?: string | null, isPublic: boolean, createdAt: string, updatedAt: string, commentCount: number, favoritedDate?: string | null, favoritesCount: number, collaborators: Array<{ __typename?: 'StreamCollaborator', id: string, name: string, company?: string | null, avatar?: string | null, role: string }>, commits?: { __typename?: 'CommitCollection', totalCount: number, items?: Array<{ __typename?: 'Commit', id: string, createdAt?: string | null, message?: string | null, authorId?: string | null, branchName?: string | null, authorName?: string | null, authorAvatar?: string | null, referencedObject: string } | null> | null } | null, branches?: { __typename?: 'BranchCollection', totalCount: number } | null }> | null } | null };
export type StreamsQuery = { __typename?: 'Query', streams?: { __typename?: 'StreamCollection', totalCount: number, cursor?: string | null, items?: Array<{ __typename?: 'Stream', id: string, name: string, description?: string | null, role?: string | null, isPublic: boolean, createdAt: string, updatedAt: string, commentCount: number, favoritedDate?: string | null, favoritesCount: number, collaborators: Array<{ __typename?: 'StreamCollaborator', id: string, name: string, company?: string | null, avatar?: string | null, role: string }>, commits?: { __typename?: 'CommitCollection', totalCount: number, items?: Array<{ __typename?: 'Commit', id: string, createdAt?: string | null, message?: string | null, authorId?: string | null, branchName?: string | null, authorName?: string | null, authorAvatar?: string | null, referencedObject: string }> | null } | null, branches?: { __typename?: 'BranchCollection', totalCount: number } | null }> | null } | null };
export type CommonStreamFieldsFragment = { __typename?: 'Stream', id: string, name: string, description?: string | null, role?: string | null, isPublic: boolean, createdAt: string, updatedAt: string, commentCount: number, favoritedDate?: string | null, favoritesCount: number, collaborators: Array<{ __typename?: 'StreamCollaborator', id: string, name: string, company?: string | null, avatar?: string | null, role: string }>, commits?: { __typename?: 'CommitCollection', totalCount: number } | null, branches?: { __typename?: 'BranchCollection', totalCount: number } | null };
@@ -1909,7 +1886,7 @@ export type StreamFirstCommitQueryVariables = Exact<{
}>;
export type StreamFirstCommitQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, commits?: { __typename?: 'CommitCollection', totalCount: number, items?: Array<{ __typename?: 'Commit', id: string, referencedObject: string } | null> | null } | null } | null };
export type StreamFirstCommitQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, commits?: { __typename?: 'CommitCollection', totalCount: number, items?: Array<{ __typename?: 'Commit', id: string, referencedObject: string }> | null } | null } | null };
export type StreamBranchFirstCommitQueryVariables = Exact<{
id: Scalars['String'];
@@ -1917,7 +1894,7 @@ export type StreamBranchFirstCommitQueryVariables = Exact<{
}>;
export type StreamBranchFirstCommitQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, branch?: { __typename?: 'Branch', commits?: { __typename?: 'CommitCollection', totalCount: number, items?: Array<{ __typename?: 'Commit', id: string, referencedObject: string } | null> | null } | null } | null } | null };
export type StreamBranchFirstCommitQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, branch?: { __typename?: 'Branch', commits?: { __typename?: 'CommitCollection', totalCount: number, items?: Array<{ __typename?: 'Commit', id: string, referencedObject: string }> | null } | null } | null } | null };
export type StreamSettingsQueryVariables = Exact<{
id: Scalars['String'];
@@ -1940,24 +1917,24 @@ export type DeleteStreamMutationVariables = Exact<{
export type DeleteStreamMutation = { __typename?: 'Mutation', streamDelete: boolean };
export type CommonUserFieldsFragment = { __typename?: 'User', id: string, email?: string | null, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, hasPendingVerification?: boolean | null, profiles?: Record<string, unknown> | null, role?: string | null, streams?: { __typename?: 'StreamCollection', totalCount: number } | null, commits?: { __typename?: 'CommitCollectionUser', totalCount: number, items?: Array<{ __typename?: 'CommitCollectionUserNode', id: string, createdAt?: string | null } | null> | null } | null };
export type CommonUserFieldsFragment = { __typename?: 'User', id: string, email?: string | null, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, hasPendingVerification?: boolean | null, profiles?: Record<string, unknown> | null, role?: string | null, streams?: { __typename?: 'StreamCollection', totalCount: number } | null, commits?: { __typename?: 'CommitCollection', totalCount: number, items?: Array<{ __typename?: 'Commit', id: string, createdAt?: string | null }> | null } | null };
export type UserFavoriteStreamsQueryVariables = Exact<{
cursor?: InputMaybe<Scalars['String']>;
}>;
export type UserFavoriteStreamsQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, email?: string | null, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, hasPendingVerification?: boolean | null, profiles?: Record<string, unknown> | null, role?: string | null, favoriteStreams?: { __typename?: 'StreamCollection', totalCount: number, cursor?: string | null, items?: Array<{ __typename?: 'Stream', id: string, name: string, description?: string | null, role?: string | null, isPublic: boolean, createdAt: string, updatedAt: string, commentCount: number, favoritedDate?: string | null, favoritesCount: number, collaborators: Array<{ __typename?: 'StreamCollaborator', id: string, name: string, company?: string | null, avatar?: string | null, role: string }>, commits?: { __typename?: 'CommitCollection', totalCount: number } | null, branches?: { __typename?: 'BranchCollection', totalCount: number } | null }> | null } | null, streams?: { __typename?: 'StreamCollection', totalCount: number } | null, commits?: { __typename?: 'CommitCollectionUser', totalCount: number, items?: Array<{ __typename?: 'CommitCollectionUserNode', id: string, createdAt?: string | null } | null> | null } | null } | null };
export type UserFavoriteStreamsQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, email?: string | null, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, hasPendingVerification?: boolean | null, profiles?: Record<string, unknown> | null, role?: string | null, favoriteStreams?: { __typename?: 'StreamCollection', totalCount: number, cursor?: string | null, items?: Array<{ __typename?: 'Stream', id: string, name: string, description?: string | null, role?: string | null, isPublic: boolean, createdAt: string, updatedAt: string, commentCount: number, favoritedDate?: string | null, favoritesCount: number, collaborators: Array<{ __typename?: 'StreamCollaborator', id: string, name: string, company?: string | null, avatar?: string | null, role: string }>, commits?: { __typename?: 'CommitCollection', totalCount: number } | null, branches?: { __typename?: 'BranchCollection', totalCount: number } | null }> | null } | null, streams?: { __typename?: 'StreamCollection', totalCount: number } | null, commits?: { __typename?: 'CommitCollection', totalCount: number, items?: Array<{ __typename?: 'Commit', id: string, createdAt?: string | null }> | null } | null } | null };
export type MainUserDataQueryVariables = Exact<{ [key: string]: never; }>;
export type MainUserDataQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, email?: string | null, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, hasPendingVerification?: boolean | null, profiles?: Record<string, unknown> | null, role?: string | null, streams?: { __typename?: 'StreamCollection', totalCount: number } | null, commits?: { __typename?: 'CommitCollectionUser', totalCount: number, items?: Array<{ __typename?: 'CommitCollectionUserNode', id: string, createdAt?: string | null } | null> | null } | null } | null };
export type MainUserDataQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, email?: string | null, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, hasPendingVerification?: boolean | null, profiles?: Record<string, unknown> | null, role?: string | null, streams?: { __typename?: 'StreamCollection', totalCount: number } | null, commits?: { __typename?: 'CommitCollection', totalCount: number, items?: Array<{ __typename?: 'Commit', id: string, createdAt?: string | null }> | null } | null } | null };
export type ExtraUserDataQueryVariables = Exact<{ [key: string]: never; }>;
export type ExtraUserDataQuery = { __typename?: 'Query', user?: { __typename?: 'User', totalOwnedStreamsFavorites: number, notificationPreferences: Record<string, unknown>, id: string, email?: string | null, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, hasPendingVerification?: boolean | null, profiles?: Record<string, unknown> | null, role?: string | null, streams?: { __typename?: 'StreamCollection', totalCount: number } | null, commits?: { __typename?: 'CommitCollectionUser', totalCount: number, items?: Array<{ __typename?: 'CommitCollectionUserNode', id: string, createdAt?: string | null } | null> | null } | null } | null };
export type ExtraUserDataQuery = { __typename?: 'Query', user?: { __typename?: 'User', totalOwnedStreamsFavorites: number, notificationPreferences: Record<string, unknown>, id: string, email?: string | null, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, hasPendingVerification?: boolean | null, profiles?: Record<string, unknown> | null, role?: string | null, streams?: { __typename?: 'StreamCollection', totalCount: number } | null, commits?: { __typename?: 'CommitCollection', totalCount: number, items?: Array<{ __typename?: 'Commit', id: string, createdAt?: string | null }> | null } | null } | null };
export type UserSearchQueryVariables = Exact<{
query: Scalars['String'];
@@ -1,6 +1,7 @@
import { ReactiveVar } from '@apollo/client/core'
import { isUndefined } from 'lodash'
import Vue, { VueConstructor } from 'vue'
import { LooseRequired } from 'vue/types/common'
export type Nullable<T> = T | null
@@ -25,6 +26,8 @@ export type GetReactiveVarType<V extends ReactiveVar<any>> = V extends ReactiveV
? T
: unknown
export type SetupProps<P = unknown> = Readonly<LooseRequired<P>>
// Copied from Vue typings & improved ergonomics
export type CombinedVueInstance<
Instance extends Vue = Vue,
@@ -18,6 +18,7 @@
v-if="allowSelect"
v-model="selectedState"
dense
hide-details
@change="onSelect"
/>
<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap">
@@ -44,13 +45,13 @@
</div>
<v-divider v-if="showStreamAndBranch" />
<div v-if="showStreamAndBranch" class="d-flex align-center caption px-5 py-2">
<div v-show="commit.streamName" class="text-truncate mr-2">
<div v-show="streamName" class="text-truncate mr-2">
<router-link
class="text-decoration-none d-inline-flex align-center"
:to="`/streams/${streamId}`"
>
<v-icon x-small class="primary--text mr-2">mdi-folder-outline</v-icon>
{{ commit.streamName }}
{{ streamName }}
</router-link>
</div>
<div class="text-right flex-grow-1 text-truncate">
@@ -85,6 +86,8 @@
</v-hover>
</template>
<script>
import { useSelectableCommit } from '@/main/lib/stream/composables/commitMultiActions'
export default {
components: {
PreviewImage: () => import('@/main/components/common/PreviewImage'),
@@ -106,27 +109,22 @@ export default {
default: false
}
},
setup(props, ctx) {
const { highlighted, selectedState, onSelect } = useSelectableCommit(props, ctx)
return { highlighted, selectedState, onSelect }
},
computed: {
highlighted() {
return this.highlight || this.selected
},
streamId() {
return (
this.commit.streamId ?? this.$route.params.streamId ?? this.$route.query.stream
this.commit.streamId ||
this.commit.stream?.id ||
this.$route.params.streamId ||
this.$route.query.stream
)
},
selectedState: {
get() {
return this.selected
},
set(val) {
this.$emit('update:selected', !!val)
}
}
},
methods: {
onSelect() {
this.$emit('select', { value: this.selected })
streamName() {
return this.commit.streamName || this.commit.stream?.name
}
}
}
@@ -2,10 +2,16 @@
<div>
<div
:class="`${background} d-flex px-2 py-3 mb-2 align-center rounded-lg`"
:style="`transition: all 0.2s ease-in-out; ${
highlight ? 'outline: 0.2rem solid #047EFB;' : ''
}`"
:style="`${highlighted ? 'outline: 0.2rem solid #047EFB;' : ''}`"
>
<v-checkbox
v-if="allowSelect"
v-model="selectedState"
dense
hide-details
class="mt-0 ml-2 pa-0"
@change="onSelect"
/>
<div class="flex-shrink-0">
<user-avatar :id="commit.authorId" :size="30" />
</div>
@@ -58,6 +64,7 @@
<script>
import { gql } from '@apollo/client/core'
import { limitedCommitActivityFieldsFragment } from '@/graphql/fragments/activity'
import { useSelectableCommit } from '@/main/lib/stream/composables/commitMultiActions'
export default {
components: {
@@ -102,8 +109,21 @@ export default {
highlight: {
type: Boolean,
default: false
},
allowSelect: {
type: Boolean,
default: false
},
selected: {
type: Boolean,
default: false
}
},
setup(props, ctx) {
const { highlighted, selectedState, onSelect } = useSelectableCommit(props, ctx)
return { highlighted, selectedState, onSelect }
},
apollo: {
activity: {
query: gql`
@@ -7,7 +7,9 @@
<prioritized-portal to="actions" identity="commits-multi-select" :priority="2">
<div class="d-flex align-center">
<v-btn small @click="clear">Clear selection</v-btn>
<v-btn small class="ml-2" color="primary" @click="initMove">Move To</v-btn>
<v-btn v-if="streamId" small class="ml-2" color="primary" @click="initMove">
Move To
</v-btn>
<v-btn small class="mx-2" color="red" @click="initDelete">Delete</v-btn>
</div>
</prioritized-portal>
@@ -23,6 +25,7 @@
</div>
</template>
<script lang="ts">
import { Optional } from '@/helpers/typeHelpers'
import PrioritizedPortal from '@/main/components/common/utility/PrioritizedPortal.vue'
import CommitsBatchActionsDialog from '@/main/dialogs/commit/CommitsBatchActionsDialog.vue'
import { BatchActionType } from '@/main/lib/stream/composables/commitMultiActions'
@@ -36,26 +39,28 @@ export default defineComponent({
},
props: {
streamId: {
type: String,
required: true
type: String as PropType<Optional<string>>,
default: undefined
},
selectedCommitIds: {
type: Array as PropType<string[]>,
required: true
},
branchName: {
type: String,
required: true
type: String as PropType<Optional<string>>,
default: undefined
}
},
setup(props, { emit }) {
const showDialog = ref(false)
const dialogType = ref(BatchActionType.Move)
const dialogType = ref(BatchActionType.Delete)
const count = computed(() => props.selectedCommitIds?.length || 0)
const clear = () => emit('clear')
const initMove = () => {
if (!props.streamId) return
showDialog.value = true
dialogType.value = BatchActionType.Move
}
@@ -8,14 +8,18 @@
Deleting commits is an irrevocable action! If you are sure about wanting to
delete the selected commits, please click on the button below!
</template>
<template v-else-if="type === BatchActionType.Move">
<template
v-else-if="
type === BatchActionType.Move && supportsBranchScopedActions && streamId
"
>
<div class="mb-4">
Please select the target branch to move all of the selected commits to:
</div>
<branch-select
v-model="targetBranch"
:stream-id="streamId"
:excluded-names="[branchName]"
:excluded-names="branchName ? [branchName] : []"
/>
</template>
</template>
@@ -27,7 +31,9 @@
Delete
</v-btn>
</template>
<template v-else-if="type === BatchActionType.Move">
<template
v-else-if="type === BatchActionType.Move && supportsBranchScopedActions"
>
<v-btn color="primary" :disabled="moveDisabled" @click="moveCommits">
Move
</v-btn>
@@ -49,7 +55,7 @@ import {
DeleteCommitsMutation,
DeleteCommitsMutationVariables
} from '@/graphql/generated/graphql'
import { Nullable } from '@/helpers/typeHelpers'
import { Nullable, Optional } from '@/helpers/typeHelpers'
import {
convertThrowIntoFetchResult,
getFirstErrorMessage
@@ -65,12 +71,12 @@ export default defineComponent({
},
props: {
streamId: {
type: String,
required: true
type: String as PropType<Optional<string>>,
default: undefined
},
branchName: {
type: String,
required: true
type: String as PropType<Optional<string>>,
default: undefined
},
selectedCommitIds: {
type: Array as PropType<string[]>,
@@ -89,6 +95,10 @@ export default defineComponent({
const apollo = useApolloClient().client
const { triggerNotification } = useGlobalToast()
const supportsBranchScopedActions = computed(
() => props.streamId && props.branchName
)
const targetBranch = ref(null as Nullable<string>)
const loading = ref(false)
@@ -156,7 +166,7 @@ export default defineComponent({
const moveCommits = async () => {
await invokeAction<MoveCommitsMutation, MoveCommitsMutationVariables>({
shouldQuit: () => moveDisabled.value,
shouldQuit: () => !supportsBranchScopedActions.value || moveDisabled.value,
getResult: (data) => data?.commitsMove,
document: MoveCommitsDocument,
variables: {
@@ -193,7 +203,8 @@ export default defineComponent({
deleteCommits,
targetBranch,
moveDisabled,
deleteDisabled
deleteDisabled,
supportsBranchScopedActions
}
}
})
@@ -1,4 +1,5 @@
import { ref, computed } from 'vue'
import { SetupProps } from '@/helpers/typeHelpers'
import { ref, computed, SetupContext } from 'vue'
export enum BatchActionType {
Move = 'move',
@@ -44,3 +45,24 @@ export function useCommitMultiActions() {
clearSelectedCommits
}
}
/**
* Use inside a component that represents a commit that can be selected (e.g. for batch actions)
*/
export function useSelectableCommit(
props: SetupProps<{ allowSelect: boolean; selected: boolean; highlight: boolean }>,
ctx: SetupContext
) {
const highlighted = computed(() => props.highlight || props.selected)
const selectedState = computed({
get: () => props.selected,
set: (newVal) => ctx.emit('update:selected', !!newVal)
})
const onSelect = () => ctx.emit('select', { value: props.selected })
return {
highlighted,
onSelect,
selectedState
}
}
@@ -1,6 +1,11 @@
<template>
<div>
<!-- Toolbar -->
<commit-multi-select-toolbar
v-if="hasSelectedCommits"
:selected-commit-ids="selectedCommitIds"
@clear="clearSelectedCommits"
@finish="onBatchCommitActionFinish"
/>
<prioritized-portal to="toolbar" identity="commits" :priority="0">
<div class="font-weight-bold">
Your Latest Commits
@@ -18,7 +23,12 @@
lg="4"
xl="3"
>
<commit-preview-card :commit="commit" :preview-height="180" />
<commit-preview-card
:commit="commit"
:preview-height="180"
:allow-select="isCommitOrStreamOwner(commit)"
:selected.sync="selectedCommitsState[commit.id]"
/>
</v-col>
<v-col cols="12" sm="6" md="6" lg="4" xl="3">
<infinite-loading spinner="waveDots" @infinite="infiniteHandler">
@@ -65,6 +75,9 @@ import { gql } from '@apollo/client/core'
import { useQuery } from '@vue/apollo-composable'
import { computed, defineComponent } from 'vue'
import PrioritizedPortal from '@/main/components/common/utility/PrioritizedPortal.vue'
import CommitMultiSelectToolbar from '@/main/components/stream/commit/CommitMultiSelectToolbar.vue'
import { useCommitMultiActions } from '@/main/lib/stream/composables/commitMultiActions'
import { Roles } from '@/helpers/mainConstants'
export default defineComponent({
name: 'TheCommits',
@@ -72,10 +85,15 @@ export default defineComponent({
InfiniteLoading: () => import('vue-infinite-loading'),
CommitPreviewCard: () => import('@/main/components/common/CommitPreviewCard'),
NoDataPlaceholder: () => import('@/main/components/common/NoDataPlaceholder'),
PrioritizedPortal
PrioritizedPortal,
CommitMultiSelectToolbar
},
setup() {
const { result, fetchMore: userFetchMore } = useQuery(gql`
const {
result,
fetchMore: userFetchMore,
refetch: userRefetch
} = useQuery(gql`
query ($cursor: String) {
user {
id
@@ -87,12 +105,16 @@ export default defineComponent({
id
referencedObject
message
streamName
streamId
authorId
createdAt
sourceApplication
branchName
commentCount
stream {
id
role
name
}
}
}
}
@@ -103,10 +125,32 @@ export default defineComponent({
(user.value?.commits.items || []).filter((c) => c.branchName !== 'globals')
)
const isCommitOrStreamOwner = (commit) => {
const userId = user.value.id
return commit.stream.role === Roles.Stream.Owner || commit.authorId === userId
}
const {
selectedCommitIds,
hasSelectedCommits,
clearSelectedCommits,
selectedCommitsState
} = useCommitMultiActions()
const onBatchCommitActionFinish = () => {
userRefetch()
}
return {
user,
commitItems,
userFetchMore
userFetchMore,
selectedCommitIds,
hasSelectedCommits,
clearSelectedCommits,
selectedCommitsState,
onBatchCommitActionFinish,
isCommitOrStreamOwner
}
},
methods: {
@@ -52,13 +52,14 @@
<v-col v-if="stream && stream.branch && listMode" cols="12" class="px-4">
<v-list v-if="stream.branch.commits.items.length > 0" class="transparent">
<list-item-commit
v-for="(item, index) in allCommits"
v-for="item in allCommits"
:key="item.id + 'list'"
:commit="item"
:stream-id="streamId"
:allow-select="isStreamOwner || isCommitOwner(item)"
:selected.sync="selectedCommitsState[item.id]"
show-received-receipts
class="mb-1 rounded"
:highlight="index === 0"
></list-item-commit>
</v-list>
</v-col>
@@ -42,20 +42,6 @@ extend type Commit {
commentCount: Int!
}
extend type CommitCollectionUserNode {
"""
The total number of comments for this commit. To actually get the comments, use the comments query and pass in a resource array consisting of of this commit's id.
E.g.,
```
query{
comments(streamId:"streamId" resources:[{resourceType: commit, resourceId:"commitId"}] ){
...
}
```
"""
commentCount: Int!
}
extend type Object {
"""
The total number of comments for this commit. To actually get the comments, use the comments query and pass in a resource array consisting of of this object's id.
@@ -10,7 +10,7 @@ extend type User {
Get commits authored by the user. If requested for another user, then only commits
from public streams will be returned.
"""
commits(limit: Int! = 25, cursor: String): CommitCollectionUser
commits(limit: Int! = 25, cursor: String): CommitCollection
}
type Branch {
@@ -34,19 +34,13 @@ type Commit {
authorId: String
authorAvatar: String
createdAt: DateTime
}
type CommitCollectionUserNode {
id: String!
referencedObject: String!
message: String
sourceApplication: String
totalChildrenCount: Int
branchName: String
parents: [String]
streamId: String
streamName: String
createdAt: DateTime
streamId: String @deprecated(reason: "Use the stream field instead")
streamName: String @deprecated(reason: "Use the stream field instead")
"""
Will throw an authorization error if active user isn't authorized to see it, for example,
if a stream isn't public and the user doesn't have the appropriate rights.
"""
stream: Stream! @hasRole(role: "server:user") @hasScope(scope: "streams:read")
}
type BranchCollection {
@@ -58,13 +52,7 @@ type BranchCollection {
type CommitCollection {
totalCount: Int!
cursor: String
items: [Commit]
}
type CommitCollectionUser {
totalCount: Int!
cursor: String
items: [CommitCollectionUserNode]
items: [Commit!]
}
extend type Mutation {
@@ -8,18 +8,19 @@ const crs = require('crypto-random-string')
const { gql } = require('apollo-server-express')
const { createBlobs } = require('@/modules/blobstorage/tests/helpers')
const { expect } = require('chai')
const { Users, Streams } = require('@/modules/core/dbSchema')
describe('Blobs graphql @blobstorage', () => {
/** @type {import('apollo-server-express').ApolloServer} */
let apollo
const user = {
name: 'Baron Von Blubba',
email: 'barron@bubble.bobble',
email: 'zebarron@bubble.bobble',
password: 'bubblesAreMyBlobs'
}
before(async () => {
await truncateTables(['blob_storage', Users.name, Streams.name])
user.id = await createUser(user)
await truncateTables(['blob_storage'])
apollo = buildApolloServer({
context: () =>
addLoadersToCtx({
@@ -108,14 +108,6 @@ module.exports = {
return await getResourceCommentCount({ resourceId: parent.id })
}
},
CommitCollectionUserNode: {
// urgh, i think we tripped our gql schemas in there a bit
async commentCount(parent, args, context) {
if (context.role === Roles.Server.ArchivedUser)
throw new ApolloForbiddenError('You are not authorized.')
return await getResourceCommentCount({ resourceId: parent.id })
}
},
Object: {
async commentCount(parent, args, context) {
if (context.role === Roles.Server.ArchivedUser)
@@ -284,6 +284,15 @@ export type Commit = {
parents?: Maybe<Array<Maybe<Scalars['String']>>>;
referencedObject: Scalars['String'];
sourceApplication?: Maybe<Scalars['String']>;
/**
* Will throw an authorization error if active user isn't authorized to see it, for example,
* if a stream isn't public and the user doesn't have the appropriate rights.
*/
stream: Stream;
/** @deprecated Use the stream field instead */
streamId?: Maybe<Scalars['String']>;
/** @deprecated Use the stream field instead */
streamName?: Maybe<Scalars['String']>;
totalChildrenCount?: Maybe<Scalars['Int']>;
};
@@ -299,42 +308,10 @@ export type CommitActivityArgs = {
export type CommitCollection = {
__typename?: 'CommitCollection';
cursor?: Maybe<Scalars['String']>;
items?: Maybe<Array<Maybe<Commit>>>;
items?: Maybe<Array<Commit>>;
totalCount: Scalars['Int'];
};
export type CommitCollectionUser = {
__typename?: 'CommitCollectionUser';
cursor?: Maybe<Scalars['String']>;
items?: Maybe<Array<Maybe<CommitCollectionUserNode>>>;
totalCount: Scalars['Int'];
};
export type CommitCollectionUserNode = {
__typename?: 'CommitCollectionUserNode';
branchName?: Maybe<Scalars['String']>;
/**
* The total number of comments for this commit. To actually get the comments, use the comments query and pass in a resource array consisting of of this commit's id.
* E.g.,
* ```
* query{
* comments(streamId:"streamId" resources:[{resourceType: commit, resourceId:"commitId"}] ){
* ...
* }
* ```
*/
commentCount: Scalars['Int'];
createdAt?: Maybe<Scalars['DateTime']>;
id: Scalars['String'];
message?: Maybe<Scalars['String']>;
parents?: Maybe<Array<Maybe<Scalars['String']>>>;
referencedObject: Scalars['String'];
sourceApplication?: Maybe<Scalars['String']>;
streamId?: Maybe<Scalars['String']>;
streamName?: Maybe<Scalars['String']>;
totalChildrenCount?: Maybe<Scalars['Int']>;
};
export type CommitCreateInput = {
branchName: Scalars['String'];
message?: InputMaybe<Scalars['String']>;
@@ -1471,7 +1448,7 @@ export type User = {
* Get commits authored by the user. If requested for another user, then only commits
* from public streams will be returned.
*/
commits?: Maybe<CommitCollectionUser>;
commits?: Maybe<CommitCollection>;
company?: Maybe<Scalars['String']>;
/** Returns the apps you have created. */
createdApps?: Maybe<Array<Maybe<ServerApp>>>;
@@ -1726,7 +1703,7 @@ export type ResolversTypes = {
BlobMetadata: ResolverTypeWrapper<BlobMetadata>;
BlobMetadataCollection: ResolverTypeWrapper<BlobMetadataCollection>;
Boolean: ResolverTypeWrapper<Scalars['Boolean']>;
Branch: ResolverTypeWrapper<Omit<Branch, 'author'> & { author?: Maybe<ResolversTypes['User']> }>;
Branch: ResolverTypeWrapper<Omit<Branch, 'author' | 'commits'> & { author?: Maybe<ResolversTypes['User']>, commits?: Maybe<ResolversTypes['CommitCollection']> }>;
BranchCollection: ResolverTypeWrapper<Omit<BranchCollection, 'items'> & { items?: Maybe<Array<ResolversTypes['Branch']>> }>;
BranchCreateInput: BranchCreateInput;
BranchDeleteInput: BranchDeleteInput;
@@ -1737,10 +1714,8 @@ export type ResolversTypes = {
CommentCreateInput: CommentCreateInput;
CommentEditInput: CommentEditInput;
CommentThreadActivityMessage: ResolverTypeWrapper<CommentThreadActivityMessage>;
Commit: ResolverTypeWrapper<Commit>;
CommitCollection: ResolverTypeWrapper<CommitCollection>;
CommitCollectionUser: ResolverTypeWrapper<CommitCollectionUser>;
CommitCollectionUserNode: ResolverTypeWrapper<CommitCollectionUserNode>;
Commit: ResolverTypeWrapper<Omit<Commit, 'stream'> & { stream: ResolversTypes['Stream'] }>;
CommitCollection: ResolverTypeWrapper<Omit<CommitCollection, 'items'> & { items?: Maybe<Array<ResolversTypes['Commit']>> }>;
CommitCreateInput: CommitCreateInput;
CommitDeleteInput: CommitDeleteInput;
CommitReceivedInput: CommitReceivedInput;
@@ -1791,7 +1766,7 @@ export type ResolversTypes = {
StreamUpdatePermissionInput: StreamUpdatePermissionInput;
String: ResolverTypeWrapper<Scalars['String']>;
Subscription: ResolverTypeWrapper<{}>;
User: ResolverTypeWrapper<Omit<User, 'favoriteStreams' | 'streams'> & { favoriteStreams?: Maybe<ResolversTypes['StreamCollection']>, streams?: Maybe<ResolversTypes['StreamCollection']> }>;
User: ResolverTypeWrapper<Omit<User, 'commits' | 'favoriteStreams' | 'streams'> & { commits?: Maybe<ResolversTypes['CommitCollection']>, favoriteStreams?: Maybe<ResolversTypes['StreamCollection']>, streams?: Maybe<ResolversTypes['StreamCollection']> }>;
UserDeleteInput: UserDeleteInput;
UserRoleInput: UserRoleInput;
UserSearchResultCollection: ResolverTypeWrapper<UserSearchResultCollection>;
@@ -1821,7 +1796,7 @@ export type ResolversParentTypes = {
BlobMetadata: BlobMetadata;
BlobMetadataCollection: BlobMetadataCollection;
Boolean: Scalars['Boolean'];
Branch: Omit<Branch, 'author'> & { author?: Maybe<ResolversParentTypes['User']> };
Branch: Omit<Branch, 'author' | 'commits'> & { author?: Maybe<ResolversParentTypes['User']>, commits?: Maybe<ResolversParentTypes['CommitCollection']> };
BranchCollection: Omit<BranchCollection, 'items'> & { items?: Maybe<Array<ResolversParentTypes['Branch']>> };
BranchCreateInput: BranchCreateInput;
BranchDeleteInput: BranchDeleteInput;
@@ -1832,10 +1807,8 @@ export type ResolversParentTypes = {
CommentCreateInput: CommentCreateInput;
CommentEditInput: CommentEditInput;
CommentThreadActivityMessage: CommentThreadActivityMessage;
Commit: Commit;
CommitCollection: CommitCollection;
CommitCollectionUser: CommitCollectionUser;
CommitCollectionUserNode: CommitCollectionUserNode;
Commit: Omit<Commit, 'stream'> & { stream: ResolversParentTypes['Stream'] };
CommitCollection: Omit<CommitCollection, 'items'> & { items?: Maybe<Array<ResolversParentTypes['Commit']>> };
CommitCreateInput: CommitCreateInput;
CommitDeleteInput: CommitDeleteInput;
CommitReceivedInput: CommitReceivedInput;
@@ -1882,7 +1855,7 @@ export type ResolversParentTypes = {
StreamUpdatePermissionInput: StreamUpdatePermissionInput;
String: Scalars['String'];
Subscription: {};
User: Omit<User, 'favoriteStreams' | 'streams'> & { favoriteStreams?: Maybe<ResolversParentTypes['StreamCollection']>, streams?: Maybe<ResolversParentTypes['StreamCollection']> };
User: Omit<User, 'commits' | 'favoriteStreams' | 'streams'> & { commits?: Maybe<ResolversParentTypes['CommitCollection']>, favoriteStreams?: Maybe<ResolversParentTypes['StreamCollection']>, streams?: Maybe<ResolversParentTypes['StreamCollection']> };
UserDeleteInput: UserDeleteInput;
UserRoleInput: UserRoleInput;
UserSearchResultCollection: UserSearchResultCollection;
@@ -2077,39 +2050,20 @@ export type CommitResolvers<ContextType = GraphQLContext, ParentType extends Res
parents?: Resolver<Maybe<Array<Maybe<ResolversTypes['String']>>>, ParentType, ContextType>;
referencedObject?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
sourceApplication?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
stream?: Resolver<ResolversTypes['Stream'], ParentType, ContextType>;
streamId?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
streamName?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
totalChildrenCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type CommitCollectionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['CommitCollection'] = ResolversParentTypes['CommitCollection']> = {
cursor?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
items?: Resolver<Maybe<Array<Maybe<ResolversTypes['Commit']>>>, ParentType, ContextType>;
items?: Resolver<Maybe<Array<ResolversTypes['Commit']>>, ParentType, ContextType>;
totalCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type CommitCollectionUserResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['CommitCollectionUser'] = ResolversParentTypes['CommitCollectionUser']> = {
cursor?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
items?: Resolver<Maybe<Array<Maybe<ResolversTypes['CommitCollectionUserNode']>>>, ParentType, ContextType>;
totalCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type CommitCollectionUserNodeResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['CommitCollectionUserNode'] = ResolversParentTypes['CommitCollectionUserNode']> = {
branchName?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
commentCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
createdAt?: Resolver<Maybe<ResolversTypes['DateTime']>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
message?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
parents?: Resolver<Maybe<Array<Maybe<ResolversTypes['String']>>>, ParentType, ContextType>;
referencedObject?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
sourceApplication?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
streamId?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
streamName?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
totalChildrenCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export interface DateTimeScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['DateTime'], any> {
name: 'DateTime';
}
@@ -2439,7 +2393,7 @@ export type UserResolvers<ContextType = GraphQLContext, ParentType extends Resol
authorizedApps?: Resolver<Maybe<Array<Maybe<ResolversTypes['ServerAppListItem']>>>, ParentType, ContextType>;
avatar?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
bio?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
commits?: Resolver<Maybe<ResolversTypes['CommitCollectionUser']>, ParentType, ContextType, RequireFields<UserCommitsArgs, 'limit'>>;
commits?: Resolver<Maybe<ResolversTypes['CommitCollection']>, ParentType, ContextType, RequireFields<UserCommitsArgs, 'limit'>>;
company?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
createdApps?: Resolver<Maybe<Array<Maybe<ResolversTypes['ServerApp']>>>, ParentType, ContextType>;
email?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
@@ -2516,8 +2470,6 @@ export type Resolvers<ContextType = GraphQLContext> = {
CommentThreadActivityMessage?: CommentThreadActivityMessageResolvers<ContextType>;
Commit?: CommitResolvers<ContextType>;
CommitCollection?: CommitCollectionResolvers<ContextType>;
CommitCollectionUser?: CommitCollectionUserResolvers<ContextType>;
CommitCollectionUserNode?: CommitCollectionUserNodeResolvers<ContextType>;
DateTime?: GraphQLScalarType;
EmailAddress?: GraphQLScalarType;
FileUpload?: FileUploadResolvers<ContextType>;
@@ -31,6 +31,10 @@ const {
batchMoveCommits,
batchDeleteCommits
} = require('@/modules/core/services/commit/batchCommitActions')
const {
validateStreamAccess
} = require('@/modules/core/services/streams/streamAccessService')
const { StreamInvalidAccessError } = require('@/modules/core/errors/stream')
// subscription events
const COMMIT_CREATED = 'COMMIT_CREATED'
@@ -40,6 +44,29 @@ const COMMIT_DELETED = 'COMMIT_DELETED'
/** @type {import('@/modules/core/graph/generated/graphql').Resolvers} */
module.exports = {
Query: {},
Commit: {
async stream(parent, _args, ctx) {
const { id: commitId } = parent
const stream = await ctx.loaders.commits.getCommitStream.load(commitId)
if (!stream) {
throw new StreamInvalidAccessError('Commit stream not found')
}
await validateStreamAccess(ctx.userId, stream.id)
return stream
},
async streamId(parent, _args, ctx) {
const { id: commitId } = parent
const stream = await ctx.loaders.commits.getCommitStream.load(commitId)
return stream?.id || null
},
async streamName(parent, _args, ctx) {
const { id: commitId } = parent
const stream = await ctx.loaders.commits.getCommitStream.load(commitId)
return stream?.name || null
}
},
Stream: {
async commits(parent, args) {
if (args.limit && args.limit > 100)
@@ -22,10 +22,10 @@ const {
const {
authorizeResolver,
validateScopes,
validateServerRole,
pubsub,
StreamPubsubEvents
StreamPubsubEvents,
validateScopes,
validateServerRole
} = require(`@/modules/shared`)
const { saveActivity } = require(`@/modules/activitystream/services`)
const { ActionTypes } = require('@/modules/activitystream/helpers/types')
@@ -102,7 +102,7 @@ const _deleteStream = async (parent, args, context) => {
*/
module.exports = {
Query: {
async stream(parent, args, context) {
async stream(_, args, context) {
const stream = await getStream({ streamId: args.id, userId: context.userId })
if (!stream) throw new ApolloError('Stream not found')
+13 -1
View File
@@ -17,6 +17,7 @@ import {
} from '@/modules/core/helpers/types'
import { Nullable } from '@/modules/shared/helpers/typeHelper'
import { ServerInviteRecord } from '@/modules/serverinvites/helpers/types'
import { getCommitStreams } from '@/modules/core/repositories/commits'
/**
* Build request-scoped dataloaders
@@ -78,9 +79,20 @@ export function buildRequestLoaders(ctx: AuthContext) {
if (!userId) return streamIds.map(() => null)
const results = await getStreamRoles(userId, streamIds.slice())
return streamIds.map((id) => results[id])
return streamIds.map((id) => results[id] || null)
})
},
commits: {
/**
* Get a commit's stream from DB
*/
getCommitStream: new DataLoader<string, Nullable<StreamRecord>>(
async (commitIds) => {
const results = await getCommitStreams(commitIds.slice())
return commitIds.map((id) => results[id] || null)
}
)
},
users: {
/**
* Get user from DB
@@ -2,10 +2,15 @@ import {
BranchCommits,
Branches,
Commits,
StreamCommits
StreamCommits,
Streams
} from '@/modules/core/dbSchema'
import { BranchCommitRecord, CommitRecord } from '@/modules/core/helpers/types'
import { uniqBy } from 'lodash'
import {
BranchCommitRecord,
CommitRecord,
StreamRecord
} from '@/modules/core/helpers/types'
import { keyBy, uniqBy } from 'lodash'
const CommitWithStreamBranchMetadataFields = [
...Commits.cols,
@@ -20,6 +25,23 @@ export type CommitWithStreamBranchMetadata = CommitRecord & {
branchName: string
}
/**
* Get commit associated streams. Results are streams keyed by commit IDs
*/
export async function getCommitStreams(commitIds: string[]) {
const q = Commits.knex()
.select<Array<StreamRecord & { commitId: string }>>([
...Streams.cols,
`${Commits.col.id} as commitId`
])
.whereIn(Commits.col.id, commitIds)
.innerJoin(StreamCommits.name, StreamCommits.col.commitId, Commits.col.id)
.innerJoin(Streams.name, Streams.col.id, StreamCommits.col.streamId)
const results = await q
return keyBy(results, (r) => r.commitId)
}
/**
* Get commits with their stream and branch IDs
*/
@@ -263,13 +263,17 @@ module.exports = {
'commits.createdAt',
{ branchName: 'branches.name' },
{ streamId: 'stream_commits.streamId' },
{ streamName: 'streams.name' }
{ streamName: 'streams.name' },
{ authorName: 'users.name' },
{ authorId: 'users.id' },
{ authorAvatar: 'users.avatar' }
])
.select()
.join('stream_commits', 'commits.id', 'stream_commits.commitId')
.join('streams', 'stream_commits.streamId', 'streams.id')
.join('branch_commits', 'commits.id', 'branch_commits.commitId')
.join('branches', 'branches.id', 'branch_commits.branchId')
.leftJoin('users', 'commits.author', 'users.id')
.where('author', userId)
if (publicOnly) query.andWhere('streams.isPublic', true)
@@ -16,17 +16,21 @@ const {
/**
* Validate that the user has the required permission level (or one above it) for the specified stream
* @param {string} userId
* @param {string} [userId] If falsy, will throw for non-public streams
* @param {string} streamId
* @param {string} expectedRole
* @param {string} [expectedRole] Defaults to reviewer
* @returns {Promise<boolean>}
*/
async function validateStreamAccess(userId, streamId, expectedRole) {
expectedRole = expectedRole || Roles.Stream.Reviewer
const streamRoles = Object.values(Roles.Stream)
if (!streamRoles.includes(expectedRole)) {
throw new LogicError('Unexpected stream role')
}
userId = userId || null
try {
await authorizeResolver(userId, streamId, expectedRole)
} catch (e) {
+3 -1
View File
@@ -142,6 +142,8 @@ async function validateScopes(scopes, scope) {
* @param {string} requiredRole
*/
async function authorizeResolver(userId, resourceId, requiredRole) {
userId = userId || null
if (!roles) roles = await knex('user_roles').select('*')
// TODO: Cache these results with a TTL of 1 mins or so, it's pointless to query the db every time we get a ping.
@@ -155,7 +157,7 @@ async function authorizeResolver(userId, resourceId, requiredRole) {
.select('isPublic')
.where({ id: resourceId })
.first()
if (isPublic && roles[requiredRole] < 200) return true
if (isPublic && role.weight < 200) return true
} catch (e) {
throw new ApolloError(
`Resource of type ${role.resourceTarget} with ${resourceId} not found`
@@ -278,6 +278,15 @@ export type Commit = {
parents?: Maybe<Array<Maybe<Scalars['String']>>>;
referencedObject: Scalars['String'];
sourceApplication?: Maybe<Scalars['String']>;
/**
* Will throw an authorization error if active user isn't authorized to see it, for example,
* if a stream isn't public and the user doesn't have the appropriate rights.
*/
stream: Stream;
/** @deprecated Use the stream field instead */
streamId?: Maybe<Scalars['String']>;
/** @deprecated Use the stream field instead */
streamName?: Maybe<Scalars['String']>;
totalChildrenCount?: Maybe<Scalars['Int']>;
};
@@ -293,42 +302,10 @@ export type CommitActivityArgs = {
export type CommitCollection = {
__typename?: 'CommitCollection';
cursor?: Maybe<Scalars['String']>;
items?: Maybe<Array<Maybe<Commit>>>;
items?: Maybe<Array<Commit>>;
totalCount: Scalars['Int'];
};
export type CommitCollectionUser = {
__typename?: 'CommitCollectionUser';
cursor?: Maybe<Scalars['String']>;
items?: Maybe<Array<Maybe<CommitCollectionUserNode>>>;
totalCount: Scalars['Int'];
};
export type CommitCollectionUserNode = {
__typename?: 'CommitCollectionUserNode';
branchName?: Maybe<Scalars['String']>;
/**
* The total number of comments for this commit. To actually get the comments, use the comments query and pass in a resource array consisting of of this commit's id.
* E.g.,
* ```
* query{
* comments(streamId:"streamId" resources:[{resourceType: commit, resourceId:"commitId"}] ){
* ...
* }
* ```
*/
commentCount: Scalars['Int'];
createdAt?: Maybe<Scalars['DateTime']>;
id: Scalars['String'];
message?: Maybe<Scalars['String']>;
parents?: Maybe<Array<Maybe<Scalars['String']>>>;
referencedObject: Scalars['String'];
sourceApplication?: Maybe<Scalars['String']>;
streamId?: Maybe<Scalars['String']>;
streamName?: Maybe<Scalars['String']>;
totalChildrenCount?: Maybe<Scalars['Int']>;
};
export type CommitCreateInput = {
branchName: Scalars['String'];
message?: InputMaybe<Scalars['String']>;
@@ -1465,7 +1442,7 @@ export type User = {
* Get commits authored by the user. If requested for another user, then only commits
* from public streams will be returned.
*/
commits?: Maybe<CommitCollectionUser>;
commits?: Maybe<CommitCollection>;
company?: Maybe<Scalars['String']>;
/** Returns the apps you have created. */
createdApps?: Maybe<Array<Maybe<ServerApp>>>;
@@ -1709,7 +1686,7 @@ export type ReadStreamBranchCommitsQueryVariables = Exact<{
}>;
export type ReadStreamBranchCommitsQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, role?: string | null, branch?: { __typename?: 'Branch', id: string, name: string, description?: string | null, commits?: { __typename?: 'CommitCollection', totalCount: number, cursor?: string | null, items?: Array<{ __typename?: 'Commit', id: string, authorName?: string | null, authorId?: string | null, authorAvatar?: string | null, sourceApplication?: string | null, message?: string | null, referencedObject: string, createdAt?: string | null, commentCount: number } | null> | null } | null } | null } | null };
export type ReadStreamBranchCommitsQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, role?: string | null, branch?: { __typename?: 'Branch', id: string, name: string, description?: string | null, commits?: { __typename?: 'CommitCollection', totalCount: number, cursor?: string | null, items?: Array<{ __typename?: 'Commit', id: string, authorName?: string | null, authorId?: string | null, authorAvatar?: string | null, sourceApplication?: string | null, message?: string | null, referencedObject: string, createdAt?: string | null, commentCount: number }> | null } | null } | null } | null };
export type MoveCommitsMutationVariables = Exact<{
input: CommitsMoveInput;