Merge pull request #1723 from specklesystems/gergo/adminFacelift

Admin Facelift Backend
This commit is contained in:
Gergő Jedlicska
2023-08-01 15:45:06 +02:00
committed by GitHub
19 changed files with 416 additions and 33 deletions
+5 -5
View File
@@ -7,11 +7,11 @@
# BIND_ADDRESS="127.0.0.1"
PORT=3000
CANONICAL_URL="http://localhost:3000"
CANONICAL_URL="http://127.0.0.1:3000"
SESSION_SECRET="-> FILL IN <-"
# Redis connection: default for local development environment
REDIS_URL="redis://localhost:6379"
REDIS_URL="redis://127.0.0.1:6379"
############################################################
# Frontend 2.0 settings
@@ -20,7 +20,7 @@ REDIS_URL="redis://localhost:6379"
# Whether server is meant to be used with Frontend 2.0
USE_FRONTEND_2=false
FRONTEND_ORIGIN="http://localhost:8081"
FRONTEND_ORIGIN="http://127.0.0.1:8081"
# URL of a project on any FE2 speckle server that will be pulled in and used as the onboarding stream
ONBOARDING_STREAM_URL=https://latest.speckle.systems/projects/843d07eb10
@@ -64,8 +64,8 @@ S3_CREATE_BUCKET="true"
# Emails
############################################################
EMAIL=true
EMAIL_HOST="127.0.0.1"
EMAIL_FROM="no-reply@example.org"
EMAIL_HOST="localhost"
EMAIL_PORT="1025"
# EMAIL_HOST="-> FILL IN <-"
@@ -126,4 +126,4 @@ STRATEGY_LOCAL=true
# FRONTEND_HOST=localhost
# FRONTEND_PORT=8081
SPECKLE_AUTOMATE_URL="http://localhost:3030"
SPECKLE_AUTOMATE_URL="http://127.0.0.1:3030"
+1 -1
View File
@@ -3,5 +3,5 @@
###############################################
PORT=0
POSTGRES_URL=postgres://speckle:speckle@localhost/speckle2_test
POSTGRES_URL=postgres://speckle:speckle@127.0.0.1/speckle2_test
POSTGRES_USER=''
@@ -0,0 +1,34 @@
type AdminUserList {
items: [LimitedUser!]!
cursor: String
totalCount: Int!
}
type ServerStatistics {
totalProjectCount: Int!
totalUserCount: Int!
totalPendingInvites: Int!
}
type AdminQueries {
userList(
limit: Int! = 25
cursor: String = null
query: String = null
role: ServerRole = null
): AdminUserList! @hasScope(scope: "users:read")
projectList(
query: String
orderBy: String
visibility: String
limit: Int! = 25
cursor: String = null
): ProjectCollection!
serverStatistics: ServerStatistics! @hasScope(scope: "server:stats")
}
extend type Query {
admin: AdminQueries! @hasServerRole(role: SERVER_ADMIN)
}
@@ -22,7 +22,9 @@ extend type Query {
orderBy: String
visibility: String
limit: Int = 25
): StreamCollection @hasRole(role: "server:admin")
): StreamCollection
@hasRole(role: "server:admin")
@deprecated(reason: "use admin.projectList instead")
"""
All of the discoverable streams of the server
@@ -28,6 +28,7 @@ extend type Query {
offset: Int! = 0
query: String = null
): AdminUsersListCollection
@deprecated(reason: "use admin.UserList instead")
@hasRole(role: "server:admin")
@hasScope(scope: "users:read")
@@ -1,5 +1,5 @@
extend type Query {
serverStats: ServerStats!
serverStats: ServerStats! @deprecated(reason: "use admin.serverStatistics instead")
}
type ServerStats {
+2
View File
@@ -24,6 +24,8 @@ generates:
ModelMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn'
VersionMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn'
CommentMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn'
AdminQueries: '@/modules/core/helpers/graphTypes#GraphQLEmptyReturn'
ServerStatistics: '@/modules/core/helpers/graphTypes#GraphQLEmptyReturn'
CommentReplyAuthorCollection: '@/modules/comments/helpers/graphTypes#CommentReplyAuthorCollectionGraphQLReturn'
Comment: '@/modules/comments/helpers/graphTypes#CommentGraphQLReturn'
PendingStreamCollaborator: '@/modules/serverinvites/helpers/graphTypes#PendingStreamCollaboratorGraphQLReturn'
@@ -3,6 +3,7 @@ import { ForbiddenError } from '@/modules/shared/errors'
import { getDirective } from '@graphql-tools/utils'
import { mapSchema } from '@graphql-tools/utils'
import { MapperKind } from '@graphql-tools/utils'
import { Roles } from '@speckle/shared'
import { defaultFieldResolver } from 'graphql'
/**
@@ -50,7 +51,8 @@ export const isOwner: GraphqlDirectiveBuilder = () => {
const authUserId = context.userId
if (info.parentType?.name === 'User') {
if (parentId !== authUserId) {
// allow admins to query private user fields
if (parentId !== authUserId && context.role !== Roles.Server.Admin) {
throw new ForbiddenError(
`You must be authenticated as the user whose '${fieldName}' value you wish to retrieve`
)
@@ -1,5 +1,5 @@
import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql';
import { StreamGraphQLReturn, CommitGraphQLReturn, ProjectGraphQLReturn, VersionGraphQLReturn, ModelGraphQLReturn, ModelsTreeItemGraphQLReturn, LimitedUserGraphQLReturn, MutationsObjectGraphQLReturn } from '@/modules/core/helpers/graphTypes';
import { StreamGraphQLReturn, CommitGraphQLReturn, ProjectGraphQLReturn, VersionGraphQLReturn, ModelGraphQLReturn, ModelsTreeItemGraphQLReturn, LimitedUserGraphQLReturn, MutationsObjectGraphQLReturn, GraphQLEmptyReturn } from '@/modules/core/helpers/graphTypes';
import { StreamAccessRequestGraphQLReturn } from '@/modules/accessrequests/helpers/graphTypes';
import { CommentReplyAuthorCollectionGraphQLReturn, CommentGraphQLReturn } from '@/modules/comments/helpers/graphTypes';
import { PendingStreamCollaboratorGraphQLReturn } from '@/modules/serverinvites/helpers/graphTypes';
@@ -61,6 +61,37 @@ export type ActivityCollection = {
totalCount: Scalars['Int'];
};
export type AdminQueries = {
__typename?: 'AdminQueries';
projectList: ProjectCollection;
serverStatistics: ServerStatistics;
userList: AdminUserList;
};
export type AdminQueriesProjectListArgs = {
cursor?: InputMaybe<Scalars['String']>;
limit?: Scalars['Int'];
orderBy?: InputMaybe<Scalars['String']>;
query?: InputMaybe<Scalars['String']>;
visibility?: InputMaybe<Scalars['String']>;
};
export type AdminQueriesUserListArgs = {
cursor?: InputMaybe<Scalars['String']>;
limit?: Scalars['Int'];
query?: InputMaybe<Scalars['String']>;
role?: InputMaybe<ServerRole>;
};
export type AdminUserList = {
__typename?: 'AdminUserList';
cursor?: Maybe<Scalars['String']>;
items: Array<LimitedUser>;
totalCount: Scalars['Int'];
};
export type AdminUsersListCollection = {
__typename?: 'AdminUsersListCollection';
items: Array<AdminUsersListItem>;
@@ -1593,11 +1624,16 @@ export type Query = {
_?: Maybe<Scalars['String']>;
/** Gets the profile of the authenticated user or null if not authenticated */
activeUser?: Maybe<User>;
/** All the streams of the server. Available to admins only. */
admin: AdminQueries;
/**
* All the streams of the server. Available to admins only.
* @deprecated use admin.projectList instead
*/
adminStreams?: Maybe<StreamCollection>;
/**
* Get all (or search for specific) users, registered or invited, from the server in a paginated view.
* The query looks for matches in name, company and email.
* @deprecated use admin.UserList instead
*/
adminUsers?: Maybe<AdminUsersListCollection>;
/** Gets a specific app from the server. */
@@ -1627,6 +1663,7 @@ export type Query = {
*/
projectInvite?: Maybe<PendingStreamCollaborator>;
serverInfo: ServerInfo;
/** @deprecated use admin.serverStatistics instead */
serverStats: ServerStats;
/**
* Returns a specific stream. Will throw an authorization error if active user isn't authorized
@@ -1881,6 +1918,13 @@ export enum ServerRole {
ServerUser = 'SERVER_USER'
}
export type ServerStatistics = {
__typename?: 'ServerStatistics';
totalPendingInvites: Scalars['Int'];
totalProjectCount: Scalars['Int'];
totalUserCount: Scalars['Int'];
};
export type ServerStats = {
__typename?: 'ServerStats';
/** An array of objects currently structured as { created_month: Date, count: int }. */
@@ -2712,6 +2756,8 @@ export type ResolversTypes = {
ActiveUserMutations: ResolverTypeWrapper<MutationsObjectGraphQLReturn>;
Activity: ResolverTypeWrapper<Activity>;
ActivityCollection: ResolverTypeWrapper<ActivityCollection>;
AdminQueries: ResolverTypeWrapper<GraphQLEmptyReturn>;
AdminUserList: ResolverTypeWrapper<Omit<AdminUserList, 'items'> & { items: Array<ResolversTypes['LimitedUser']> }>;
AdminUsersListCollection: ResolverTypeWrapper<Omit<AdminUsersListCollection, 'items'> & { items: Array<ResolversTypes['AdminUsersListItem']> }>;
AdminUsersListItem: ResolverTypeWrapper<Omit<AdminUsersListItem, 'invitedUser' | 'registeredUser'> & { invitedUser?: Maybe<ResolversTypes['ServerInvite']>, registeredUser?: Maybe<ResolversTypes['User']> }>;
ApiToken: ResolverTypeWrapper<ApiToken>;
@@ -2821,6 +2867,7 @@ export type ResolversTypes = {
ServerInvite: ResolverTypeWrapper<Omit<ServerInvite, 'invitedBy'> & { invitedBy: ResolversTypes['LimitedUser'] }>;
ServerInviteCreateInput: ServerInviteCreateInput;
ServerRole: ServerRole;
ServerStatistics: ResolverTypeWrapper<GraphQLEmptyReturn>;
ServerStats: ResolverTypeWrapper<ServerStats>;
SmartTextEditorValue: ResolverTypeWrapper<SmartTextEditorValue>;
SortDirection: SortDirection;
@@ -2870,6 +2917,8 @@ export type ResolversParentTypes = {
ActiveUserMutations: MutationsObjectGraphQLReturn;
Activity: Activity;
ActivityCollection: ActivityCollection;
AdminQueries: GraphQLEmptyReturn;
AdminUserList: Omit<AdminUserList, 'items'> & { items: Array<ResolversParentTypes['LimitedUser']> };
AdminUsersListCollection: Omit<AdminUsersListCollection, 'items'> & { items: Array<ResolversParentTypes['AdminUsersListItem']> };
AdminUsersListItem: Omit<AdminUsersListItem, 'invitedUser' | 'registeredUser'> & { invitedUser?: Maybe<ResolversParentTypes['ServerInvite']>, registeredUser?: Maybe<ResolversParentTypes['User']> };
ApiToken: ApiToken;
@@ -2969,6 +3018,7 @@ export type ResolversParentTypes = {
ServerInfoUpdateInput: ServerInfoUpdateInput;
ServerInvite: Omit<ServerInvite, 'invitedBy'> & { invitedBy: ResolversParentTypes['LimitedUser'] };
ServerInviteCreateInput: ServerInviteCreateInput;
ServerStatistics: GraphQLEmptyReturn;
ServerStats: ServerStats;
SmartTextEditorValue: SmartTextEditorValue;
Stream: StreamGraphQLReturn;
@@ -3069,6 +3119,20 @@ export type ActivityCollectionResolvers<ContextType = GraphQLContext, ParentType
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type AdminQueriesResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AdminQueries'] = ResolversParentTypes['AdminQueries']> = {
projectList?: Resolver<ResolversTypes['ProjectCollection'], ParentType, ContextType, RequireFields<AdminQueriesProjectListArgs, 'cursor' | 'limit'>>;
serverStatistics?: Resolver<ResolversTypes['ServerStatistics'], ParentType, ContextType>;
userList?: Resolver<ResolversTypes['AdminUserList'], ParentType, ContextType, RequireFields<AdminQueriesUserListArgs, 'cursor' | 'limit' | 'query' | 'role'>>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type AdminUserListResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AdminUserList'] = ResolversParentTypes['AdminUserList']> = {
cursor?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
items?: Resolver<Array<ResolversTypes['LimitedUser']>, ParentType, ContextType>;
totalCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type AdminUsersListCollectionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AdminUsersListCollection'] = ResolversParentTypes['AdminUsersListCollection']> = {
items?: Resolver<Array<ResolversTypes['AdminUsersListItem']>, ParentType, ContextType>;
totalCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
@@ -3579,6 +3643,7 @@ export type ProjectVersionsUpdatedMessageResolvers<ContextType = GraphQLContext,
export type QueryResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query']> = {
_?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
activeUser?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType>;
admin?: Resolver<ResolversTypes['AdminQueries'], ParentType, ContextType>;
adminStreams?: Resolver<Maybe<ResolversTypes['StreamCollection']>, ParentType, ContextType, RequireFields<QueryAdminStreamsArgs, 'limit' | 'offset'>>;
adminUsers?: Resolver<Maybe<ResolversTypes['AdminUsersListCollection']>, ParentType, ContextType, RequireFields<QueryAdminUsersArgs, 'limit' | 'offset' | 'query'>>;
app?: Resolver<Maybe<ResolversTypes['ServerApp']>, ParentType, ContextType, RequireFields<QueryAppArgs, 'id'>>;
@@ -3673,6 +3738,13 @@ export type ServerInviteResolvers<ContextType = GraphQLContext, ParentType exten
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type ServerStatisticsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['ServerStatistics'] = ResolversParentTypes['ServerStatistics']> = {
totalPendingInvites?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
totalProjectCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
totalUserCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type ServerStatsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['ServerStats'] = ResolversParentTypes['ServerStats']> = {
commitHistory?: Resolver<Maybe<Array<Maybe<ResolversTypes['JSONObject']>>>, ParentType, ContextType>;
objectHistory?: Resolver<Maybe<Array<Maybe<ResolversTypes['JSONObject']>>>, ParentType, ContextType>;
@@ -3911,6 +3983,8 @@ export type Resolvers<ContextType = GraphQLContext> = {
ActiveUserMutations?: ActiveUserMutationsResolvers<ContextType>;
Activity?: ActivityResolvers<ContextType>;
ActivityCollection?: ActivityCollectionResolvers<ContextType>;
AdminQueries?: AdminQueriesResolvers<ContextType>;
AdminUserList?: AdminUserListResolvers<ContextType>;
AdminUsersListCollection?: AdminUsersListCollectionResolvers<ContextType>;
AdminUsersListItem?: AdminUsersListItemResolvers<ContextType>;
ApiToken?: ApiTokenResolvers<ContextType>;
@@ -3968,6 +4042,7 @@ export type Resolvers<ContextType = GraphQLContext> = {
ServerAppListItem?: ServerAppListItemResolvers<ContextType>;
ServerInfo?: ServerInfoResolvers<ContextType>;
ServerInvite?: ServerInviteResolvers<ContextType>;
ServerStatistics?: ServerStatisticsResolvers<ContextType>;
ServerStats?: ServerStatsResolvers<ContextType>;
SmartTextEditorValue?: SmartTextEditorValueResolvers<ContextType>;
Stream?: StreamResolvers<ContextType>;
@@ -0,0 +1,42 @@
import { Resolvers } from '@/modules/core/graph/generated/graphql'
import { mapServerRoleToValue } from '@/modules/core/helpers/graphTypes'
import { adminProjectList, adminUserList } from '@/modules/core/services/admin'
import { getTotalStreamCount, getTotalUserCount } from '@/modules/stats/services'
export = {
Query: {
admin: () => ({})
},
AdminQueries: {
async userList(_parent, { limit, cursor, query, role }) {
return await adminUserList({
limit,
cursor,
query,
role: role ? mapServerRoleToValue(role) : null
})
},
async projectList(_parent, args) {
return await adminProjectList({
query: args.query ?? null,
orderBy: args.orderBy ?? null,
visibility: args.visibility ?? null,
limit: args.limit,
cursor: args.cursor
})
},
serverStatistics: () => ({})
},
ServerStatistics: {
async totalProjectCount() {
return await getTotalStreamCount()
},
async totalUserCount() {
return await getTotalUserCount()
},
async totalPendingInvites() {
return 0
}
}
} as Resolvers
@@ -96,6 +96,12 @@ export type ModelsTreeItemGraphQLReturn = Omit<ModelsTreeItem, 'model' | 'childr
*/
export type MutationsObjectGraphQLReturn = Record<string, never>
/**
* Use this to override the generated graphql type, in cases like graphql resolver
* collection objects
*/
export type GraphQLEmptyReturn = Record<string, never>
/**
* Map GQL StreamRole enum to the value types we use in the backend
*/
@@ -1,17 +1,22 @@
import { ServerAcl, Users, knex } from '@/modules/core/dbSchema'
import { LimitedUserRecord, UserRecord } from '@/modules/core/helpers/types'
import { Nullable } from '@/modules/shared/helpers/typeHelper'
import { isArray } from 'lodash'
import { clamp, isArray } from 'lodash'
import { metaHelpers } from '@/modules/core/helpers/meta'
import { UserValidationError } from '@/modules/core/errors/user'
import { Roles } from '@speckle/shared'
import { Knex } from 'knex'
import { Roles, ServerRoles } from '@speckle/shared'
export type UserWithOptionalRole<User extends LimitedUserRecord = UserRecord> = User & {
/**
* Available, if query joined this data from server_acl
* (this can be the server role or stream role depending on how and where this was retrieved)
*/
role?: string
role?: ServerRoles
}
export type UserWithRole<User extends LimitedUserRecord = UserRecord> = User & {
role: ServerRoles
}
export type GetUserParams = Partial<{
@@ -56,6 +61,62 @@ export async function getUsers(
return (await q).map((u) => (skipClean ? u : sanitizeUserRecord(u)))
}
type UserQuery = {
query: string | null
role: ServerRoles | null
}
const getUsersBaseQuery = (q: Knex.QueryBuilder, { query, role }: UserQuery) => {
if (query) {
q.where((queryBuilder) => {
queryBuilder
.where('email', 'ILIKE', `%${query}%`)
.orWhere('name', 'ILIKE', `%${query}%`)
.orWhere('company', 'ILIKE', `%${query}%`)
})
}
if (role) q.where({ role })
return q
}
/**
* List users
*/
export async function listUsers({
limit,
cursor,
query,
role
}: {
limit: number
cursor: Date | null
} & UserQuery): Promise<UserWithRole[]> {
const sanitizedLimit = clamp(limit, 1, 200)
const q = Users.knex<UserWithRole[]>()
.orderBy(Users.col.createdAt, 'desc')
.limit(sanitizedLimit)
.columns([
...Object.values(Users.col),
// Getting first role from grouped results
knex.raw(`(array_agg("server_acl"."role"))[1] as role`)
])
.leftJoin(ServerAcl.name, ServerAcl.col.userId, Users.col.id)
.groupBy(Users.col.id)
if (cursor) q.where(Users.col.createdAt, '<', cursor)
const users: UserWithRole[] = await getUsersBaseQuery(q, { query, role })
return users.map((u) => sanitizeUserRecord(u))
}
export async function countUsers(args: UserQuery): Promise<number> {
// const result = await getUsersBaseQuery(Users.knex(), args).countDistinct(Users.col.id)
const q = Users.knex()
.leftJoin(ServerAcl.name, ServerAcl.col.userId, Users.col.id)
.countDistinct(Users.col.id)
const result = await getUsersBaseQuery(q, args)
// .groupBy(Users.col.id)
// const result = await q
return parseInt(result[0]['count'])
}
/**
* Get user by ID
*/
@@ -0,0 +1,75 @@
import { StreamRecord, UserRecord } from '@/modules/core/helpers/types'
import { listUsers, countUsers } from '@/modules/core/repositories/users'
import { getStreams } from '@/modules/core/services/streams'
import { BaseError } from '@/modules/shared/errors/base'
import { ServerRoles } from '@speckle/shared'
type HasCursor = {
cursor: string | null
}
type Collection<T> = HasCursor & {
items: T[]
totalCount: number
}
type AdminUserListArgs = HasCursor & {
limit: number
query: string | null
role: ServerRoles | null
}
export class CursorParsingError extends BaseError {
static defaultMessage = 'Invalid cursor provided'
static code = 'INVALID_CURSOR_VALUE'
}
export const parseCursorToDate = (cursor: string): Date => {
const timestamp = Date.parse(Buffer.from(cursor, 'base64').toString('utf-8'))
if (isNaN(timestamp)) throw new CursorParsingError()
return new Date(timestamp)
}
export const convertDateToCursor = (date: Date): string =>
Buffer.from(date.toISOString()).toString('base64')
export const adminUserList = async (
args: AdminUserListArgs
): Promise<Collection<UserRecord>> => {
const parsedCursor = args.cursor ? parseCursorToDate(args.cursor) : null
const [items, totalCount] = await Promise.all([
listUsers({
role: args.role,
cursor: parsedCursor,
limit: args.limit,
query: args.query ?? null
}),
countUsers(args)
])
const cursor = items.length ? convertDateToCursor(items.slice(-1)[0].createdAt) : null
return { totalCount, items, cursor }
}
type AdminProjectListArgs = HasCursor & {
query: string | null
orderBy: string | null
visibility: string | null
limit: number
}
export const adminProjectList = async (
args: AdminProjectListArgs
): Promise<Collection<StreamRecord>> => {
const parsedCursor = args.cursor ? parseCursorToDate(args.cursor) : null
const { streams, totalCount, cursorDate } = await getStreams({
...args,
searchQuery: args.query,
cursor: parsedCursor
})
const cursor = cursorDate ? convertDateToCursor(cursorDate) : null
return {
cursor,
items: streams,
totalCount
}
}
@@ -73,16 +73,8 @@ module.exports = {
return await deleteStreamFromDb(streamId)
},
async getStreams({ offset, limit, orderBy, visibility, searchQuery }) {
const query = knex
.column(
'streams.*',
knex.raw('coalesce(sum(pg_column_size(objects.data)),0) as size')
)
.select()
.from('streams')
.leftJoin('objects', 'streams.id', 'objects.streamId')
.groupBy('streams.id')
async getStreams({ cursor, limit, orderBy, visibility, searchQuery }) {
const query = knex.select().from('streams')
const countQuery = Streams.knex()
@@ -116,9 +108,12 @@ module.exports = {
const [columnName, order] = orderBy.split(',')
const rows = await query.orderBy(`${columnName}`, order).offset(offset).limit(limit)
if (cursor) query.where(columnName, order === 'desc' ? '<' : '>', cursor)
return { streams: rows, totalCount: count }
const rows = await query.orderBy(`${columnName}`, order).limit(limit)
const cursorDate = rows.length ? rows.slice(-1)[0][columnName] : null
return { streams: rows, totalCount: count, cursorDate }
},
async getStreamUsers({ streamId }) {
@@ -607,11 +607,6 @@ describe('GraphQL API Core @core-api', () => {
expect(streamResults.body.data.adminStreams.totalCount).to.equal(10)
expect(streamResults.body.data.adminStreams.items.length).to.equal(2)
streamResults = await sendRequest(userA.token, {
query: '{ adminStreams(offset: 5) { totalCount items { id name } } }'
})
expect(streamResults.body.data.adminStreams.items.length).to.equal(5)
streamResults = await sendRequest(userA.token, {
query: '{ adminStreams( query: "Admin" ) { totalCount items { id name } } }'
})
@@ -52,6 +52,37 @@ export type ActivityCollection = {
totalCount: Scalars['Int'];
};
export type AdminQueries = {
__typename?: 'AdminQueries';
projectList: ProjectCollection;
serverStatistics: ServerStatistics;
userList: AdminUserList;
};
export type AdminQueriesProjectListArgs = {
cursor?: InputMaybe<Scalars['String']>;
limit?: Scalars['Int'];
orderBy?: InputMaybe<Scalars['String']>;
query?: InputMaybe<Scalars['String']>;
visibility?: InputMaybe<Scalars['String']>;
};
export type AdminQueriesUserListArgs = {
cursor?: InputMaybe<Scalars['String']>;
limit?: Scalars['Int'];
query?: InputMaybe<Scalars['String']>;
role?: InputMaybe<ServerRole>;
};
export type AdminUserList = {
__typename?: 'AdminUserList';
cursor?: Maybe<Scalars['String']>;
items: Array<LimitedUser>;
totalCount: Scalars['Int'];
};
export type AdminUsersListCollection = {
__typename?: 'AdminUsersListCollection';
items: Array<AdminUsersListItem>;
@@ -1584,11 +1615,16 @@ export type Query = {
_?: Maybe<Scalars['String']>;
/** Gets the profile of the authenticated user or null if not authenticated */
activeUser?: Maybe<User>;
/** All the streams of the server. Available to admins only. */
admin: AdminQueries;
/**
* All the streams of the server. Available to admins only.
* @deprecated use admin.projectList instead
*/
adminStreams?: Maybe<StreamCollection>;
/**
* Get all (or search for specific) users, registered or invited, from the server in a paginated view.
* The query looks for matches in name, company and email.
* @deprecated use admin.UserList instead
*/
adminUsers?: Maybe<AdminUsersListCollection>;
/** Gets a specific app from the server. */
@@ -1618,6 +1654,7 @@ export type Query = {
*/
projectInvite?: Maybe<PendingStreamCollaborator>;
serverInfo: ServerInfo;
/** @deprecated use admin.serverStatistics instead */
serverStats: ServerStats;
/**
* Returns a specific stream. Will throw an authorization error if active user isn't authorized
@@ -1872,6 +1909,13 @@ export enum ServerRole {
ServerUser = 'SERVER_USER'
}
export type ServerStatistics = {
__typename?: 'ServerStatistics';
totalPendingInvites: Scalars['Int'];
totalProjectCount: Scalars['Int'];
totalUserCount: Scalars['Int'];
};
export type ServerStats = {
__typename?: 'ServerStats';
/** An array of objects currently structured as { created_month: Date, count: int }. */
@@ -13,6 +13,9 @@ const {
module.exports = {
Query: {
/**
* @deprecated('Use admin.serverStatistics')
*/
async serverStats(parent, args, context) {
await validateServerRole(context, 'server:admin')
await validateScopes(context.scopes, 'server:stats')
@@ -21,6 +21,8 @@ module.exports = {
},
async getTotalUserCount() {
// returns -1 for small tables, no good
// const fastQuery = "SELECT reltuples::bigint AS estimate FROM pg_catalog.pg_class WHERE relname = 'users'"
const query = 'SELECT COUNT(*) FROM users'
const result = await knex.raw(query)
return parseInt(result.rows[0].count)
@@ -52,6 +52,37 @@ export type ActivityCollection = {
totalCount: Scalars['Int'];
};
export type AdminQueries = {
__typename?: 'AdminQueries';
projectList: ProjectCollection;
serverStatistics: ServerStatistics;
userList: AdminUserList;
};
export type AdminQueriesProjectListArgs = {
cursor?: InputMaybe<Scalars['String']>;
limit?: Scalars['Int'];
orderBy?: InputMaybe<Scalars['String']>;
query?: InputMaybe<Scalars['String']>;
visibility?: InputMaybe<Scalars['String']>;
};
export type AdminQueriesUserListArgs = {
cursor?: InputMaybe<Scalars['String']>;
limit?: Scalars['Int'];
query?: InputMaybe<Scalars['String']>;
role?: InputMaybe<ServerRole>;
};
export type AdminUserList = {
__typename?: 'AdminUserList';
cursor?: Maybe<Scalars['String']>;
items: Array<LimitedUser>;
totalCount: Scalars['Int'];
};
export type AdminUsersListCollection = {
__typename?: 'AdminUsersListCollection';
items: Array<AdminUsersListItem>;
@@ -1584,11 +1615,16 @@ export type Query = {
_?: Maybe<Scalars['String']>;
/** Gets the profile of the authenticated user or null if not authenticated */
activeUser?: Maybe<User>;
/** All the streams of the server. Available to admins only. */
admin: AdminQueries;
/**
* All the streams of the server. Available to admins only.
* @deprecated use admin.projectList instead
*/
adminStreams?: Maybe<StreamCollection>;
/**
* Get all (or search for specific) users, registered or invited, from the server in a paginated view.
* The query looks for matches in name, company and email.
* @deprecated use admin.UserList instead
*/
adminUsers?: Maybe<AdminUsersListCollection>;
/** Gets a specific app from the server. */
@@ -1618,6 +1654,7 @@ export type Query = {
*/
projectInvite?: Maybe<PendingStreamCollaborator>;
serverInfo: ServerInfo;
/** @deprecated use admin.serverStatistics instead */
serverStats: ServerStats;
/**
* Returns a specific stream. Will throw an authorization error if active user isn't authorized
@@ -1872,6 +1909,13 @@ export enum ServerRole {
ServerUser = 'SERVER_USER'
}
export type ServerStatistics = {
__typename?: 'ServerStatistics';
totalPendingInvites: Scalars['Int'];
totalProjectCount: Scalars['Int'];
totalUserCount: Scalars['Int'];
};
export type ServerStats = {
__typename?: 'ServerStats';
/** An array of objects currently structured as { created_month: Date, count: int }. */