1// Copyright (C) 2019 Storj Labs, Inc.
2// See LICENSE for copying information.
3
4package consoleql
5
6import (
7	"strconv"
8	"time"
9
10	"github.com/graphql-go/graphql"
11
12	"storj.io/common/memory"
13	"storj.io/storj/satellite/accounting"
14	"storj.io/storj/satellite/console"
15)
16
17const (
18	// ProjectType is a graphql type name for project.
19	ProjectType = "project"
20	// ProjectInputType is a graphql type name for project input.
21	ProjectInputType = "projectInput"
22	// ProjectLimitType is a graphql type name for project limit.
23	ProjectLimitType = "projectLimit"
24	// ProjectUsageType is a graphql type name for project usage.
25	ProjectUsageType = "projectUsage"
26	// ProjectsCursorInputType is a graphql input type name for projects cursor.
27	ProjectsCursorInputType = "projectsCursor"
28	// ProjectsPageType is a graphql type name for projects page.
29	ProjectsPageType = "projectsPage"
30	// BucketUsageCursorInputType is a graphql input
31	// type name for bucket usage cursor.
32	BucketUsageCursorInputType = "bucketUsageCursor"
33	// BucketUsageType is a graphql type name for bucket usage.
34	BucketUsageType = "bucketUsage"
35	// BucketUsagePageType is a field name for bucket usage page.
36	BucketUsagePageType = "bucketUsagePage"
37	// ProjectMembersPageType is a field name for project members page.
38	ProjectMembersPageType = "projectMembersPage"
39	// ProjectMembersCursorInputType is a graphql type name for project members.
40	ProjectMembersCursorInputType = "projectMembersCursor"
41	// APIKeysPageType is a field name for api keys page.
42	APIKeysPageType = "apiKeysPage"
43	// APIKeysCursorInputType is a graphql type name for api keys.
44	APIKeysCursorInputType = "apiKeysCursor"
45	// FieldOwnerID is a field name for "ownerId".
46	FieldOwnerID = "ownerId"
47	// FieldName is a field name for "name".
48	FieldName = "name"
49	// FieldBucketName is a field name for "bucket name".
50	FieldBucketName = "bucketName"
51	// FieldDescription is a field name for description.
52	FieldDescription = "description"
53	// FieldMembers is field name for members.
54	FieldMembers = "members"
55	// FieldAPIKeys is a field name for api keys.
56	FieldAPIKeys = "apiKeys"
57	// FieldUsage is a field name for usage rollup.
58	FieldUsage = "usage"
59	// FieldBucketUsages is a field name for bucket usages.
60	FieldBucketUsages = "bucketUsages"
61	// FieldStorageLimit is a field name for the storage limit.
62	FieldStorageLimit = "storageLimit"
63	// FieldBandwidthLimit is a field name for bandwidth limit.
64	FieldBandwidthLimit = "bandwidthLimit"
65	// FieldStorage is a field name for storage total.
66	FieldStorage = "storage"
67	// FieldEgress is a field name for egress total.
68	FieldEgress = "egress"
69	// FieldSegmentCount is a field name for segments count.
70	FieldSegmentCount = "segmentCount"
71	// FieldObjectCount is a field name for objects count.
72	FieldObjectCount = "objectCount"
73	// FieldPageCount is a field name for total page count.
74	FieldPageCount = "pageCount"
75	// FieldCurrentPage is a field name for current page number.
76	FieldCurrentPage = "currentPage"
77	// FieldTotalCount is a field name for bucket usage count total.
78	FieldTotalCount = "totalCount"
79	// FieldMemberCount is a field name for number of project members.
80	FieldMemberCount = "memberCount"
81	// FieldProjects is a field name for projects.
82	FieldProjects = "projects"
83	// FieldProjectMembers is a field name for project members.
84	FieldProjectMembers = "projectMembers"
85	// CursorArg is an argument name for cursor.
86	CursorArg = "cursor"
87	// PageArg ia an argument name for page number.
88	PageArg = "page"
89	// LimitArg is argument name for limit.
90	LimitArg = "limit"
91	// OffsetArg is argument name for offset.
92	OffsetArg = "offset"
93	// SearchArg is argument name for search.
94	SearchArg = "search"
95	// OrderArg is argument name for order.
96	OrderArg = "order"
97	// OrderDirectionArg is argument name for order direction.
98	OrderDirectionArg = "orderDirection"
99	// SinceArg marks start of the period.
100	SinceArg = "since"
101	// BeforeArg marks end of the period.
102	BeforeArg = "before"
103)
104
105// graphqlProject creates *graphql.Object type representation of satellite.ProjectInfo.
106func graphqlProject(service *console.Service, types *TypeCreator) *graphql.Object {
107	return graphql.NewObject(graphql.ObjectConfig{
108		Name: ProjectType,
109		Fields: graphql.Fields{
110			FieldID: &graphql.Field{
111				Type: graphql.String,
112			},
113			FieldName: &graphql.Field{
114				Type: graphql.String,
115			},
116			FieldOwnerID: &graphql.Field{
117				Type: graphql.String,
118			},
119			FieldDescription: &graphql.Field{
120				Type: graphql.String,
121			},
122			FieldCreatedAt: &graphql.Field{
123				Type: graphql.DateTime,
124			},
125			FieldMemberCount: &graphql.Field{
126				Type: graphql.Int,
127			},
128			FieldMembers: &graphql.Field{
129				Type: types.projectMemberPage,
130				Args: graphql.FieldConfigArgument{
131					CursorArg: &graphql.ArgumentConfig{
132						Type: graphql.NewNonNull(types.projectMembersCursor),
133					},
134				},
135				Resolve: func(p graphql.ResolveParams) (interface{}, error) {
136					project, _ := p.Source.(*console.Project)
137
138					_, err := console.GetAuth(p.Context)
139					if err != nil {
140						return nil, err
141					}
142
143					cursor := cursorArgsToProjectMembersCursor(p.Args[CursorArg].(map[string]interface{}))
144					page, err := service.GetProjectMembers(p.Context, project.ID, cursor)
145					if err != nil {
146						return nil, err
147					}
148
149					var users []projectMember
150					for _, member := range page.ProjectMembers {
151						user, err := service.GetUser(p.Context, member.MemberID)
152						if err != nil {
153							return nil, err
154						}
155
156						users = append(users, projectMember{
157							User:     user,
158							JoinedAt: member.CreatedAt,
159						})
160					}
161
162					projectMembersPage := projectMembersPage{
163						ProjectMembers: users,
164						TotalCount:     page.TotalCount,
165						Offset:         page.Offset,
166						Limit:          page.Limit,
167						Order:          int(page.Order),
168						OrderDirection: int(page.OrderDirection),
169						Search:         page.Search,
170						CurrentPage:    page.CurrentPage,
171						PageCount:      page.PageCount,
172					}
173					return projectMembersPage, nil
174				},
175			},
176			FieldAPIKeys: &graphql.Field{
177				Type: types.apiKeyPage,
178				Args: graphql.FieldConfigArgument{
179					CursorArg: &graphql.ArgumentConfig{
180						Type: graphql.NewNonNull(types.apiKeysCursor),
181					},
182				},
183				Resolve: func(p graphql.ResolveParams) (interface{}, error) {
184					project, _ := p.Source.(*console.Project)
185
186					_, err := console.GetAuth(p.Context)
187					if err != nil {
188						return nil, err
189					}
190
191					cursor := cursorArgsToAPIKeysCursor(p.Args[CursorArg].(map[string]interface{}))
192					page, err := service.GetAPIKeys(p.Context, project.ID, cursor)
193					if err != nil {
194						return nil, err
195					}
196
197					apiKeysPage := apiKeysPage{
198						APIKeys:        page.APIKeys,
199						TotalCount:     page.TotalCount,
200						Offset:         page.Offset,
201						Limit:          page.Limit,
202						Order:          int(page.Order),
203						OrderDirection: int(page.OrderDirection),
204						Search:         page.Search,
205						CurrentPage:    page.CurrentPage,
206						PageCount:      page.PageCount,
207					}
208
209					return apiKeysPage, err
210				},
211			},
212			FieldUsage: &graphql.Field{
213				Type: types.projectUsage,
214				Args: graphql.FieldConfigArgument{
215					SinceArg: &graphql.ArgumentConfig{
216						Type: graphql.NewNonNull(graphql.DateTime),
217					},
218					BeforeArg: &graphql.ArgumentConfig{
219						Type: graphql.NewNonNull(graphql.DateTime),
220					},
221				},
222				Resolve: func(p graphql.ResolveParams) (interface{}, error) {
223					project, _ := p.Source.(*console.Project)
224
225					since := p.Args[SinceArg].(time.Time)
226					before := p.Args[BeforeArg].(time.Time)
227
228					usage, err := service.GetProjectUsage(p.Context, project.ID, since, before)
229					if err != nil {
230						return nil, err
231					}
232
233					return usage, nil
234				},
235			},
236			FieldBucketUsages: &graphql.Field{
237				Type: types.bucketUsagePage,
238				Args: graphql.FieldConfigArgument{
239					BeforeArg: &graphql.ArgumentConfig{
240						Type: graphql.NewNonNull(graphql.DateTime),
241					},
242					CursorArg: &graphql.ArgumentConfig{
243						Type: graphql.NewNonNull(types.bucketUsageCursor),
244					},
245				},
246				Resolve: func(p graphql.ResolveParams) (interface{}, error) {
247					project, _ := p.Source.(*console.Project)
248
249					before := p.Args[BeforeArg].(time.Time)
250					cursor := fromMapBucketUsageCursor(p.Args[CursorArg].(map[string]interface{}))
251
252					page, err := service.GetBucketTotals(p.Context, project.ID, cursor, before)
253					if err != nil {
254						return nil, err
255					}
256
257					return page, nil
258				},
259			},
260		},
261	})
262}
263
264// graphqlProjectInput creates graphql.InputObject type needed to create/update satellite.Project.
265func graphqlProjectInput() *graphql.InputObject {
266	return graphql.NewInputObject(graphql.InputObjectConfig{
267		Name: ProjectInputType,
268		Fields: graphql.InputObjectConfigFieldMap{
269			FieldName: &graphql.InputObjectFieldConfig{
270				Type: graphql.String,
271			},
272			FieldDescription: &graphql.InputObjectFieldConfig{
273				Type: graphql.String,
274			},
275		},
276	})
277}
278
279// graphqlProjectLimit creates graphql.InputObject type needed to create/update satellite.Project.
280func graphqlProjectLimit() *graphql.InputObject {
281	return graphql.NewInputObject(graphql.InputObjectConfig{
282		Name: ProjectLimitType,
283		Fields: graphql.InputObjectConfigFieldMap{
284			FieldStorageLimit: &graphql.InputObjectFieldConfig{
285				Type: graphql.String,
286			},
287			FieldBandwidthLimit: &graphql.InputObjectFieldConfig{
288				Type: graphql.String,
289			},
290		},
291	})
292}
293
294// graphqlBucketUsageCursor creates bucket usage cursor graphql input type.
295func graphqlProjectsCursor() *graphql.InputObject {
296	return graphql.NewInputObject(graphql.InputObjectConfig{
297		Name: ProjectsCursorInputType,
298		Fields: graphql.InputObjectConfigFieldMap{
299			LimitArg: &graphql.InputObjectFieldConfig{
300				Type: graphql.NewNonNull(graphql.Int),
301			},
302			PageArg: &graphql.InputObjectFieldConfig{
303				Type: graphql.NewNonNull(graphql.Int),
304			},
305		},
306	})
307}
308
309// graphqlBucketUsageCursor creates bucket usage cursor graphql input type.
310func graphqlBucketUsageCursor() *graphql.InputObject {
311	return graphql.NewInputObject(graphql.InputObjectConfig{
312		Name: BucketUsageCursorInputType,
313		Fields: graphql.InputObjectConfigFieldMap{
314			SearchArg: &graphql.InputObjectFieldConfig{
315				Type: graphql.NewNonNull(graphql.String),
316			},
317			LimitArg: &graphql.InputObjectFieldConfig{
318				Type: graphql.NewNonNull(graphql.Int),
319			},
320			PageArg: &graphql.InputObjectFieldConfig{
321				Type: graphql.NewNonNull(graphql.Int),
322			},
323		},
324	})
325}
326
327// graphqlBucketUsage creates bucket usage grapqhl type.
328func graphqlBucketUsage() *graphql.Object {
329	return graphql.NewObject(graphql.ObjectConfig{
330		Name: BucketUsageType,
331		Fields: graphql.Fields{
332			FieldBucketName: &graphql.Field{
333				Type: graphql.String,
334			},
335			FieldStorage: &graphql.Field{
336				Type: graphql.Float,
337			},
338			FieldEgress: &graphql.Field{
339				Type: graphql.Float,
340			},
341			FieldObjectCount: &graphql.Field{
342				Type: graphql.Float,
343			},
344			FieldSegmentCount: &graphql.Field{
345				Type: graphql.Float,
346			},
347			SinceArg: &graphql.Field{
348				Type: graphql.DateTime,
349			},
350			BeforeArg: &graphql.Field{
351				Type: graphql.DateTime,
352			},
353		},
354	})
355}
356
357// graphqlProjectsPage creates a projects page graphql object.
358func graphqlProjectsPage(types *TypeCreator) *graphql.Object {
359	return graphql.NewObject(graphql.ObjectConfig{
360		Name: ProjectsPageType,
361		Fields: graphql.Fields{
362			FieldProjects: &graphql.Field{
363				Type: graphql.NewList(types.project),
364			},
365			LimitArg: &graphql.Field{
366				Type: graphql.Int,
367			},
368			OffsetArg: &graphql.Field{
369				Type: graphql.Int,
370			},
371			FieldPageCount: &graphql.Field{
372				Type: graphql.Int,
373			},
374			FieldCurrentPage: &graphql.Field{
375				Type: graphql.Int,
376			},
377			FieldTotalCount: &graphql.Field{
378				Type: graphql.Int,
379			},
380		},
381	})
382}
383
384// graphqlBucketUsagePage creates bucket usage page graphql object.
385func graphqlBucketUsagePage(types *TypeCreator) *graphql.Object {
386	return graphql.NewObject(graphql.ObjectConfig{
387		Name: BucketUsagePageType,
388		Fields: graphql.Fields{
389			FieldBucketUsages: &graphql.Field{
390				Type: graphql.NewList(types.bucketUsage),
391			},
392			SearchArg: &graphql.Field{
393				Type: graphql.String,
394			},
395			LimitArg: &graphql.Field{
396				Type: graphql.Int,
397			},
398			OffsetArg: &graphql.Field{
399				Type: graphql.Int,
400			},
401			FieldPageCount: &graphql.Field{
402				Type: graphql.Int,
403			},
404			FieldCurrentPage: &graphql.Field{
405				Type: graphql.Int,
406			},
407			FieldTotalCount: &graphql.Field{
408				Type: graphql.Int,
409			},
410		},
411	})
412}
413
414// graphqlProjectUsage creates project usage graphql type.
415func graphqlProjectUsage() *graphql.Object {
416	return graphql.NewObject(graphql.ObjectConfig{
417		Name: ProjectUsageType,
418		Fields: graphql.Fields{
419			FieldStorage: &graphql.Field{
420				Type: graphql.Float,
421			},
422			FieldEgress: &graphql.Field{
423				Type: graphql.Float,
424			},
425			FieldObjectCount: &graphql.Field{
426				Type: graphql.Float,
427			},
428			FieldSegmentCount: &graphql.Field{
429				Type: graphql.Float,
430			},
431			SinceArg: &graphql.Field{
432				Type: graphql.DateTime,
433			},
434			BeforeArg: &graphql.Field{
435				Type: graphql.DateTime,
436			},
437		},
438	})
439}
440
441// fromMapProjectInfo creates console.ProjectInfo from input args.
442func fromMapProjectInfo(args map[string]interface{}) (project console.ProjectInfo) {
443	project.Name, _ = args[FieldName].(string)
444	project.Description, _ = args[FieldDescription].(string)
445
446	return
447}
448
449// fromMapProjectInfoProjectLimits creates console.ProjectInfo from input args.
450func fromMapProjectInfoProjectLimits(projectInfo, projectLimits map[string]interface{}) (project console.ProjectInfo, err error) {
451	project.Name, _ = projectInfo[FieldName].(string)
452	project.Description, _ = projectInfo[FieldDescription].(string)
453	storageLimit, err := strconv.Atoi(projectLimits[FieldStorageLimit].(string))
454	if err != nil {
455		return project, err
456	}
457	project.StorageLimit = memory.Size(storageLimit)
458	bandwidthLimit, err := strconv.Atoi(projectLimits[FieldBandwidthLimit].(string))
459	if err != nil {
460		return project, err
461	}
462	project.BandwidthLimit = memory.Size(bandwidthLimit)
463
464	return
465}
466
467// fromMapProjectsCursor creates console.ProjectsCursor from input args.
468func fromMapProjectsCursor(args map[string]interface{}) (cursor console.ProjectsCursor) {
469	cursor.Limit = args[LimitArg].(int)
470	cursor.Page = args[PageArg].(int)
471	return
472}
473
474// fromMapBucketUsageCursor creates accounting.BucketUsageCursor from input args.
475func fromMapBucketUsageCursor(args map[string]interface{}) (cursor accounting.BucketUsageCursor) {
476	limit, _ := args[LimitArg].(int)
477	page, _ := args[PageArg].(int)
478
479	cursor.Limit = uint(limit)
480	cursor.Page = uint(page)
481	cursor.Search, _ = args[SearchArg].(string)
482	return
483}
484
485func cursorArgsToProjectMembersCursor(args map[string]interface{}) console.ProjectMembersCursor {
486	limit, _ := args[LimitArg].(int)
487	page, _ := args[PageArg].(int)
488	order, _ := args[OrderArg].(int)
489	orderDirection, _ := args[OrderDirectionArg].(int)
490
491	var cursor console.ProjectMembersCursor
492
493	cursor.Limit = uint(limit)
494	cursor.Page = uint(page)
495	cursor.Order = console.ProjectMemberOrder(order)
496	cursor.OrderDirection = console.OrderDirection(orderDirection)
497	cursor.Search, _ = args[SearchArg].(string)
498
499	return cursor
500}
501
502func cursorArgsToAPIKeysCursor(args map[string]interface{}) console.APIKeyCursor {
503	limit, _ := args[LimitArg].(int)
504	page, _ := args[PageArg].(int)
505	order, _ := args[OrderArg].(int)
506	orderDirection, _ := args[OrderDirectionArg].(int)
507
508	var cursor console.APIKeyCursor
509
510	cursor.Limit = uint(limit)
511	cursor.Page = uint(page)
512	cursor.Order = console.APIKeyOrder(order)
513	cursor.OrderDirection = console.OrderDirection(orderDirection)
514	cursor.Search, _ = args[SearchArg].(string)
515
516	return cursor
517}
518