func (s *SearchService) ByView(ctx context.Context, v *View) ([]*Search, error) { span := trace.FromContext(ctx).NewChild("trythings.search.ByView") defer span.Finish() var ss []*Search _, err := datastore.NewQuery("Search"). Ancestor(datastore.NewKey(ctx, "Root", "root", 0, nil)). Filter("ViewID =", v.ID). Order("ViewRank"). GetAll(ctx, &ss) if err != nil { return nil, err } var ac []*Search for _, se := range ss { ok, err := s.IsVisible(ctx, se) if err != nil { // TODO use multierror return nil, err } if ok { ac = append(ac, se) } } return ac, nil }
func (s *SpaceService) ByUser(ctx context.Context, u *User) ([]*Space, error) { span := trace.FromContext(ctx).NewChild("trythings.space.ByUser") defer span.Finish() var sps []*Space _, err := datastore.NewQuery("Space"). Ancestor(datastore.NewKey(ctx, "Root", "root", 0, nil)). Filter("UserIDs =", u.ID). GetAll(ctx, &sps) if err != nil { return nil, err } var ac []*Space for _, sp := range sps { ok, err := s.IsVisible(ctx, sp) if err != nil { // TODO use multierror return nil, err } if ok { ac = append(ac, sp) } } return ac, nil }
func (s *ViewService) ByID(ctx context.Context, id string) (*View, error) { rootKey := datastore.NewKey(ctx, "Root", "root", 0, nil) k := datastore.NewKey(ctx, "View", id, 0, rootKey) cv, ok := CacheFromContext(ctx).Get(k).(*View) if ok { return cv, nil } span := trace.FromContext(ctx).NewChild("trythings.view.ByID") defer span.Finish() var v View err := datastore.Get(ctx, k, &v) if err != nil { return nil, err } ok, err = s.IsVisible(ctx, &v) if err != nil { return nil, err } if !ok { return nil, errors.New("cannot access view") } CacheFromContext(ctx).Set(k, &v) return &v, nil }
func (s *ViewService) BySpace(ctx context.Context, sp *Space) ([]*View, error) { span := trace.FromContext(ctx).NewChild("trythings.view.BySpace") defer span.Finish() var vs []*View _, err := datastore.NewQuery("View"). Ancestor(datastore.NewKey(ctx, "Root", "root", 0, nil)). Filter("SpaceID =", sp.ID). GetAll(ctx, &vs) if err != nil { return nil, err } var ac []*View for _, v := range vs { ok, err := s.IsVisible(ctx, v) if err != nil { // TODO use multierror return nil, err } if ok { ac = append(ac, v) } } return ac, nil }
// ByIDs filters out Tasks that are not visible to the current User. func (s *TaskService) ByIDs(ctx context.Context, ids []string) ([]*Task, error) { span := trace.FromContext(ctx).NewChild("trythings.task.ByIDs") defer span.Finish() rootKey := datastore.NewKey(ctx, "Root", "root", 0, nil) ks := []*datastore.Key{} for _, id := range ids { ks = append(ks, datastore.NewKey(ctx, "Task", id, 0, rootKey)) } var allTasks = make([]*Task, len(ks)) err := datastore.GetMulti(ctx, ks, allTasks) if err != nil { return nil, err } ts := []*Task{} for _, t := range allTasks { // TODO#Perf: Batch the isVisible check. ok, err := s.IsVisible(ctx, t) if err != nil { return nil, err } if !ok { continue } ts = append(ts, t) } return ts, nil }
func (s *SearchService) Update(ctx context.Context, se *Search) error { span := trace.FromContext(ctx).NewChild("trythings.search.Update") defer span.Finish() if se.ID == "" { return errors.New("cannot update search with no ID") } // Make sure we have access to the search before it was modified. _, err := s.ByID(ctx, se.ID) if err != nil { return err } // Make sure we continue to have access to the task after our update. ok, err := s.IsVisible(ctx, se) if err != nil { return err } if !ok { return errors.New("cannot update search to lose access") } rootKey := datastore.NewKey(ctx, "Root", "root", 0, nil) k := datastore.NewKey(ctx, "Search", se.ID, 0, rootKey) _, err = datastore.Put(ctx, k, se) if err != nil { return err } CacheFromContext(ctx).Set(k, se) return nil }
func (s *TaskService) ByID(ctx context.Context, id string) (*Task, error) { rootKey := datastore.NewKey(ctx, "Root", "root", 0, nil) k := datastore.NewKey(ctx, "Task", id, 0, rootKey) ct, ok := CacheFromContext(ctx).Get(k).(*Task) if ok { return ct, nil } span := trace.FromContext(ctx).NewChild("trythings.task.ByID") defer span.Finish() var t Task err := datastore.Get(ctx, k, &t) if err != nil { return nil, err } ok, err = s.IsVisible(ctx, &t) if err != nil { return nil, err } if !ok { return nil, errors.New("cannot access task") } CacheFromContext(ctx).Set(k, &t) return &t, nil }
func (s *SearchService) ByID(ctx context.Context, id string) (*Search, error) { rootKey := datastore.NewKey(ctx, "Root", "root", 0, nil) k := datastore.NewKey(ctx, "Search", id, 0, rootKey) cse, ok := CacheFromContext(ctx).Get(k).(*Search) if ok { return cse, nil } span := trace.FromContext(ctx).NewChild("trythings.search.ByID") defer span.Finish() var se Search err := datastore.Get(ctx, k, &se) if err != nil { return nil, err } ok, err = s.IsVisible(ctx, &se) if err != nil { return nil, err } if !ok { return nil, errors.New("cannot access search") } CacheFromContext(ctx).Set(k, &se) return &se, nil }
func (s *UserService) Create(ctx context.Context, u *User) error { span := trace.FromContext(ctx).NewChild("trythings.user.Create") defer span.Finish() // TODO Make sure u.GoogleID == user.Current(ctx).ID if u.ID != "" { return fmt.Errorf("u already has id %q", u.ID) } if u.CreatedAt.IsZero() { u.CreatedAt = time.Now() } id, _, err := datastore.AllocateIDs(ctx, "User", nil, 1) if err != nil { return err } u.ID = fmt.Sprintf("%x", id) rootKey := datastore.NewKey(ctx, "Root", "root", 0, nil) k := datastore.NewKey(ctx, "User", u.ID, 0, rootKey) k, err = datastore.Put(ctx, k, u) if err != nil { return err } return nil }
func (s *MigrationService) RunAll(ctx context.Context) error { span := trace.FromContext(ctx).NewChild("trythings.migration.RunAll") defer span.Finish() su, err := IsSuperuser(ctx) if err != nil { return err } if !su { return errors.New("must run migrations as superuser") } latest, err := s.latestVersion(ctx) if err != nil { return err } log.Infof(ctx, "running all migrations. latest is %s", latest) for _, m := range migrations { if m.Version.After(latest) { log.Infof(ctx, "running migration version %s", m.Version) err = s.run(ctx, m) if err != nil { return err } } } return nil }
func GetGoogleUser(ctx context.Context, idToken string) (*GoogleUser, error) { span := trace.FromContext(ctx).NewChild("trythings.google_user.GetGoogleUser") defer span.Finish() tok, err := jwt.ParseSigned(idToken) if err != nil { return nil, err } if len(tok.Headers) != 1 { // We must have a header to specify a kid. // We don't know how to handle multiple headers, // since it's unclear which kid to use. return nil, errors.New("expected exactly one token header") } keys := googleKeys.Key(tok.Headers[0].KeyID) if len(keys) == 0 { err := updateGoogleKeys(ctx) if err != nil { return nil, err } keys = googleKeys.Key(tok.Headers[0].KeyID) } if len(keys) != 1 { // We must have a key to check the signature. // We don't know how to deal with multiple keys matching the same kid. return nil, errors.New("expected exactly one key matching kid") } key := keys[0] var payload struct { jwt.Claims GoogleUser } err = tok.Claims(&payload, key.Key) if err != nil { return nil, err } expectedIssuer := "accounts.google.com" if strings.HasPrefix(payload.Issuer, "https://") { expectedIssuer = "https://accounts.google.com" } err = payload.Validate(jwt.Expected{ Issuer: expectedIssuer, Audience: []string{"695504958192-8k3tf807271m7jcllcvlauddeqhbr0hg.apps.googleusercontent.com"}, Time: time.Now(), }) if err != nil { return nil, err } return &payload.GoogleUser, nil }
// FromContext should not be subject to access control, // because it would create a circular dependency. func (s *UserService) FromContext(ctx context.Context) (*User, error) { span := trace.FromContext(ctx).NewChild("trythings.user.FromContext") defer span.Finish() gu, ok := GoogleUserFromContext(ctx) if !ok { return nil, errors.New("expected google user, probably missing Authorization header") } return s.byGoogleID(ctx, gu.ID) }
func (s *TaskService) Index(ctx context.Context, t *Task) error { span := trace.FromContext(ctx).NewChild("trythings.task.Index") defer span.Finish() index, err := search.Open("Task") if err != nil { return err } _, err = index.Put(ctx, t.ID, t) if err != nil { return err } return nil }
func (s *TaskService) Create(ctx context.Context, t *Task) error { span := trace.FromContext(ctx).NewChild("trythings.task.Create") defer span.Finish() if t.ID != "" { return fmt.Errorf("t already has id %q", t.ID) } if t.CreatedAt.IsZero() { t.CreatedAt = time.Now() } if t.SpaceID == "" { return errors.New("SpaceID is required") } ok, err := s.IsVisible(ctx, t) if err != nil { return err } if !ok { return errors.New("cannot access space to create task") } id, _, err := datastore.AllocateIDs(ctx, "Task", nil, 1) if err != nil { return err } t.ID = fmt.Sprintf("%x", id) rootKey := datastore.NewKey(ctx, "Root", "root", 0, nil) k := datastore.NewKey(ctx, "Task", t.ID, 0, rootKey) k, err = datastore.Put(ctx, k, t) if err != nil { return err } err = s.Index(ctx, t) if err != nil { return err } return nil }
func (s *ViewService) Create(ctx context.Context, v *View) error { span := trace.FromContext(ctx).NewChild("trythings.view.Create") defer span.Finish() if v.ID != "" { return fmt.Errorf("v already has id %q", v.ID) } if v.CreatedAt.IsZero() { v.CreatedAt = time.Now() } if v.Name == "" { return errors.New("Name is required") } if v.SpaceID == "" { return errors.New("SpaceID is required") } ok, err := s.IsVisible(ctx, v) if err != nil { return err } if !ok { return errors.New("cannot access space to create view") } id, _, err := datastore.AllocateIDs(ctx, "View", nil, 1) if err != nil { return err } v.ID = fmt.Sprintf("%x", id) rootKey := datastore.NewKey(ctx, "Root", "root", 0, nil) k := datastore.NewKey(ctx, "View", v.ID, 0, rootKey) k, err = datastore.Put(ctx, k, v) if err != nil { return err } return nil }
func (s *SpaceService) Create(ctx context.Context, sp *Space) error { span := trace.FromContext(ctx).NewChild("trythings.space.Create") defer span.Finish() if sp.ID != "" { return fmt.Errorf("sp already has id %q", sp.ID) } if sp.CreatedAt.IsZero() { sp.CreatedAt = time.Now() } if len(sp.UserIDs) > 0 { return errors.New("UserIDs must be empty") } su, err := IsSuperuser(ctx) if err != nil { return err } if !su { u, err := s.UserService.FromContext(ctx) if err != nil { return err } sp.UserIDs = []string{u.ID} } id, _, err := datastore.AllocateIDs(ctx, "Space", nil, 1) if err != nil { return err } sp.ID = fmt.Sprintf("%x", id) rootKey := datastore.NewKey(ctx, "Root", "root", 0, nil) k := datastore.NewKey(ctx, "Space", sp.ID, 0, rootKey) k, err = datastore.Put(ctx, k, sp) if err != nil { return err } return nil }
func updateGoogleKeys(ctx context.Context) error { span := trace.FromContext(ctx).NewChild("trythings.google_user.updateGoogleKeys") defer span.Finish() // Try to fetch new public keys from Google. client := urlfetch.Client(ctx) client.Timeout = 1 * time.Second resp, err := client.Get("https://www.googleapis.com/oauth2/v3/certs") if err != nil { return err } defer resp.Body.Close() err = json.NewDecoder(resp.Body).Decode(&googleKeys) if err != nil { return err } return nil }
func (s *UserService) byGoogleID(ctx context.Context, googleID string) (*User, error) { span := trace.FromContext(ctx).NewChild("trythings.user.byGoogleID") defer span.Finish() var us []*User _, err := datastore.NewQuery("User"). Ancestor(datastore.NewKey(ctx, "Root", "root", 0, nil)). Filter("GoogleID =", googleID). Limit(1). GetAll(ctx, &us) if err != nil { return nil, err } if len(us) == 0 { return nil, ErrUserNotFound } return us[0], nil }
func (api *ViewAPI) Start() error { api.Type = graphql.NewObject(graphql.ObjectConfig{ Name: "View", Fields: graphql.Fields{ "id": relay.GlobalIDField("View", nil), "createdAt": &graphql.Field{ Description: "When the view was first created.", Type: graphql.String, }, "name": &graphql.Field{ Description: "The name to display for the view.", Type: graphql.String, }, "searches": &graphql.Field{ Type: graphql.NewList(api.SearchAPI.Type), Resolve: func(p graphql.ResolveParams) (interface{}, error) { span := trace.FromContext(p.Context).NewChild("trythings.viewAPI.searches") defer span.Finish() v, ok := p.Source.(*View) if !ok { return nil, errors.New("expected view source") } ss, err := api.SearchService.ByView(p.Context, v) if err != nil { return nil, err } return ss, nil }, }, }, Interfaces: []*graphql.Interface{ api.NodeInterface, }, }) return nil }
func (s *TaskService) Update(ctx context.Context, t *Task) error { span := trace.FromContext(ctx).NewChild("trythings.task.Update") defer span.Finish() if t.ID == "" { return errors.New("cannot update task with no ID") } // Make sure we have access to the task to start. _, err := s.ByID(ctx, t.ID) if err != nil { return err } // Make sure we continue to have access to the task after our update. ok, err := s.IsVisible(ctx, t) if err != nil { return err } if !ok { return errors.New("cannot update task to lose access") } rootKey := datastore.NewKey(ctx, "Root", "root", 0, nil) k := datastore.NewKey(ctx, "Task", t.ID, 0, rootKey) _, err = datastore.Put(ctx, k, t) if err != nil { return err } err = s.Index(ctx, t) if err != nil { return err } CacheFromContext(ctx).Set(k, t) return nil }
func (s *SpaceService) IsVisible(ctx context.Context, sp *Space) (isVisible bool, err error) { isVisible, ok := CacheFromContext(ctx).IsVisible(sp) if ok { return isVisible, nil } defer func() { if err == nil { CacheFromContext(ctx).SetIsVisible(sp, isVisible) } }() span := trace.FromContext(ctx).NewChild("trythings.space.IsVisible") defer span.Finish() su, err := IsSuperuser(ctx) if err != nil { return false, err } if su { return true, nil } u, err := s.UserService.FromContext(ctx) if err != nil { return false, err } for _, id := range sp.UserIDs { if u.ID == id { return true, nil } } return false, nil }
func (api *UserAPI) Start() error { api.Type = graphql.NewObject(graphql.ObjectConfig{ Name: "User", Description: "User represents a person who can interact with the app.", Fields: graphql.Fields{ "id": relay.GlobalIDField("User", nil), "isAdmin": &graphql.Field{ Description: "Whether or not the user is an Ellie's Pad admin.", Type: graphql.Boolean, }, "email": &graphql.Field{ Description: "The user's email primary address.", Type: graphql.String, }, "name": &graphql.Field{ Description: "The user's full name.", Type: graphql.String, }, "givenName": &graphql.Field{ Description: "The user's given name.", Type: graphql.String, }, "familyName": &graphql.Field{ Description: "The user's family name.", Type: graphql.String, }, "imageUrl": &graphql.Field{ Description: "The user's profile picture URL.", Type: graphql.String, }, "space": &graphql.Field{ Args: graphql.FieldConfigArgument{ "id": &graphql.ArgumentConfig{ Type: graphql.String, DefaultValue: "", Description: "id can be omitted, which will have space resolve to the user's default space.", }, }, Description: "space is a disjoint universe of views, searches and tasks.", Type: api.SpaceAPI.Type, Resolve: func(p graphql.ResolveParams) (interface{}, error) { span := trace.FromContext(p.Context).NewChild("trythings.userAPI.space") defer span.Finish() id, ok := p.Args["id"].(string) if ok { resolvedID := relay.FromGlobalID(id) if resolvedID == nil { return nil, fmt.Errorf("invalid id %q", id) } sp, err := api.SpaceService.ByID(p.Context, resolvedID.ID) if err != nil { return nil, err } return sp, nil } u, ok := p.Source.(*User) if !ok { return nil, errors.New("expected user source") } sps, err := api.SpaceService.ByUser(p.Context, u) if err != nil { return nil, err } if len(sps) == 0 { return nil, errors.New("could not find default space for user") } return sps[0], nil }, }, "spaces": &graphql.Field{ Type: graphql.NewList(api.SpaceAPI.Type), Resolve: func(p graphql.ResolveParams) (interface{}, error) { span := trace.FromContext(p.Context).NewChild("trythings.userAPI.spaces") defer span.Finish() u, ok := p.Source.(*User) if !ok { return nil, errors.New("expected user source") } sps, err := api.SpaceService.ByUser(p.Context, u) if err != nil { return nil, err } return sps, nil }, }, }, Interfaces: []*graphql.Interface{ api.NodeInterface, }, }) return nil }
func version(timeStr string) time.Time { loc, err := time.LoadLocation("America/Toronto") if err != nil { panic(err) } t, err := time.ParseInLocation("2006-01-02T15:04:05", timeStr, loc) if err != nil { panic(err) } return t } // reindexTasks adds all tasks from the datastore into the search index. var reindexTasks = func(ctx context.Context, s *MigrationService) error { span := trace.FromContext(ctx).NewChild("trythings.migration.reindexTasks") defer span.Finish() var tasks []*Task _, err := datastore.NewQuery("Task"). Ancestor(datastore.NewKey(ctx, "Root", "root", 0, nil)). GetAll(ctx, &tasks) if err != nil { return err } for _, t := range tasks { err = s.TaskService.Index(ctx, t) if err != nil { return err }
func (s *TaskService) Search(ctx context.Context, sp *Space, query string) (ts []*Task, err error) { span := trace.FromContext(ctx).NewChild("trythings.task.Search") defer span.Finish() ts, ok := CacheFromContext(ctx).SearchResults(sp, query) if ok { return ts, nil } originalQuery := query defer func() { if err == nil { CacheFromContext(ctx).SetSearchResults(sp, originalQuery, ts) } }() ok, err = s.SpaceService.IsVisible(ctx, sp) if err != nil { return nil, err } if !ok { return nil, errors.New("cannot access space to search") } // Replace the fake today() expression with the actual date. // TODO: Have this reflect the user's time zone. today := time.Now().Format(" 2006-01-02 ") query = strings.Replace(query, " today() ", today, -1) if query != "" { // Restrict the query to the space. query = fmt.Sprintf("%s AND SpaceID: %q", query, sp.ID) } else { query = fmt.Sprintf("SpaceID: %q", sp.ID) } index, err := search.Open("Task") if err != nil { return nil, err } it := index.Search(ctx, query, &search.SearchOptions{ IDsOnly: true, Sort: &search.SortOptions{ Expressions: []search.SortExpression{ {Expr: "CreatedAt", Reverse: true}, }, }, }) ids := []string{} for { id, err := it.Next(nil) if err == search.Done { break } if err != nil { // TODO: Use multierror return nil, err } ids = append(ids, id) } // FIXME Deleted tasks may still show up in the search index, // so we should just not return them. ts, err = s.ByIDs(ctx, ids) if err != nil { return nil, err } return ts, nil }
func (api *SearchAPI) Start() error { api.Type = graphql.NewObject(graphql.ObjectConfig{ Name: "Search", Fields: graphql.Fields{ "id": relay.GlobalIDField("Search", func(obj interface{}, info graphql.ResolveInfo, ctx context.Context) (string, error) { se, ok := obj.(*Search) if !ok { return "", fmt.Errorf("Search's GlobalIDField() was called with a non-Search") } cid, err := se.ClientID() if err != nil { return "", fmt.Errorf("Failed to create a ClientID for %v", se) } return cid, nil }), "createdAt": &graphql.Field{ Description: "When the search was first saved.", Type: graphql.String, }, "name": &graphql.Field{ Description: "The name to display for the search.", Type: graphql.String, }, // TODO#Perf: Consider storing the search results on the context or ResolveInfo to avoid computing them twice (numResults and results). "numResults": &graphql.Field{ Description: "The total number of results that match the query", Type: graphql.Int, Resolve: func(p graphql.ResolveParams) (interface{}, error) { span := trace.FromContext(p.Context).NewChild("trythings.searchAPI.numResults") defer span.Finish() se, ok := p.Source.(*Search) if !ok { return nil, errors.New("expected search source") } sp, err := api.SearchService.Space(p.Context, se) if err != nil { return nil, err } // TODO#Perf: Run a count query instead of fetching all of the matches. ts, err := api.TaskService.Search(p.Context, sp, se.Query) if err != nil { return nil, err } return len(ts), nil }, }, "query": &graphql.Field{ Description: "The query used to search for tasks.", Type: graphql.String, }, "results": &graphql.Field{ Description: "The tasks that match the query", Type: api.TaskAPI.ConnectionType, Args: relay.ConnectionArgs, Resolve: func(p graphql.ResolveParams) (interface{}, error) { span := trace.FromContext(p.Context).NewChild("trythings.searchAPI.results") defer span.Finish() se, ok := p.Source.(*Search) if !ok { return nil, errors.New("expected search source") } sp, err := api.SearchService.Space(p.Context, se) if err != nil { return nil, err } ts, err := api.TaskService.Search(p.Context, sp, se.Query) if err != nil { return nil, err } objs := []interface{}{} for _, t := range ts { objs = append(objs, *t) } // TODO#Performance: Run a limited query instead of filtering after the query. args := relay.NewConnectionArguments(p.Args) return relay.ConnectionFromArray(objs, args), nil }, }, }, Interfaces: []*graphql.Interface{ api.NodeInterface, }, }) return nil }
func (s *SearchService) Create(ctx context.Context, se *Search) error { span := trace.FromContext(ctx).NewChild("trythings.search.Create") defer span.Finish() if se.ID != "" { return fmt.Errorf("se already has id %q", se.ID) } if se.CreatedAt.IsZero() { se.CreatedAt = time.Now() } if se.Name == "" { return errors.New("Name is required") } if se.ViewID == "" { return errors.New("ViewID is required") } v, err := s.ViewService.ByID(ctx, se.ViewID) if err != nil { return err } if se.SpaceID == "" { se.SpaceID = v.SpaceID } if se.SpaceID != v.SpaceID { return errors.New("Search's SpaceID must match View's") } if len(se.ViewRank) != 0 { return fmt.Errorf("se already has a view rank %x", se.ViewRank) } // TODO#Performance: Add a shared or per-request cache to support these small, repeated queries. if se.Query == "" { return errors.New("Query is required") } rootKey := datastore.NewKey(ctx, "Root", "root", 0, nil) // Create a ViewRank for the search. // It should come after every other search in the view. var ranks []*struct { ViewRank datastore.ByteString } _, err = datastore.NewQuery("Search"). Ancestor(rootKey). Filter("ViewID =", se.ViewID). Project("ViewRank"). Order("-ViewRank"). Limit(1). GetAll(ctx, &ranks) if err != nil { return err } maxViewRank := MinRank if len(ranks) != 0 { maxViewRank = Rank(ranks[0].ViewRank) } rank, err := NewRank(maxViewRank, MaxRank) if err != nil { return err } se.ViewRank = datastore.ByteString(rank) ok, err := s.IsVisible(ctx, se) if err != nil { return err } if !ok { return errors.New("cannot access view to create search") } id, _, err := datastore.AllocateIDs(ctx, "Search", nil, 1) if err != nil { return err } se.ID = fmt.Sprintf("%x", id) k := datastore.NewKey(ctx, "Search", se.ID, 0, rootKey) k, err = datastore.Put(ctx, k, se) if err != nil { return err } return nil }
func (api *SpaceAPI) Start() error { api.Type = graphql.NewObject(graphql.ObjectConfig{ Name: "Space", Description: "Space represents an access-controlled universe of tasks.", Fields: graphql.Fields{ "id": relay.GlobalIDField("Space", nil), "createdAt": &graphql.Field{ Description: "When the space was first created.", Type: graphql.String, }, "name": &graphql.Field{ Description: "The name to display for the space.", Type: graphql.String, }, "savedSearch": &graphql.Field{ Args: graphql.FieldConfigArgument{ "id": &graphql.ArgumentConfig{ Type: graphql.String, }, }, Type: api.SearchAPI.Type, Resolve: func(p graphql.ResolveParams) (interface{}, error) { span := trace.FromContext(p.Context).NewChild("trythings.spaceAPI.savedSearch") defer span.Finish() id, ok := p.Args["id"].(string) if !ok { return nil, errors.New("id is required") } resolvedID := relay.FromGlobalID(id) if resolvedID == nil { return nil, fmt.Errorf("invalid id %q", id) } se, err := api.SearchService.ByClientID(p.Context, resolvedID.ID) if err != nil { return nil, err } return se, nil }, }, "querySearch": &graphql.Field{ Args: graphql.FieldConfigArgument{ "query": &graphql.ArgumentConfig{ Type: graphql.String, DefaultValue: "", Description: "query filters the result to only tasks that contain particular terms in their title or description", }, }, Type: api.SearchAPI.Type, Resolve: func(p graphql.ResolveParams) (interface{}, error) { span := trace.FromContext(p.Context).NewChild("trythings.spaceAPI.querySearch") defer span.Finish() sp, ok := p.Source.(*Space) if !ok { return nil, errors.New("expected a space source") } q, ok := p.Args["query"].(string) if !ok { q = "" // Return all tasks. } return &Search{ Query: q, SpaceID: sp.ID, }, nil }, }, "view": &graphql.Field{ Args: graphql.FieldConfigArgument{ "id": &graphql.ArgumentConfig{ Type: graphql.String, DefaultValue: "", Description: "id can be omitted, which will have view resolve to the space's default view.", }, }, Type: api.ViewAPI.Type, Resolve: func(p graphql.ResolveParams) (interface{}, error) { span := trace.FromContext(p.Context).NewChild("trythings.spaceAPI.view") defer span.Finish() id, ok := p.Args["id"].(string) if ok { resolvedID := relay.FromGlobalID(id) if resolvedID == nil { return nil, fmt.Errorf("invalid id %q", id) } v, err := api.ViewService.ByID(p.Context, resolvedID.ID) if err != nil { return nil, err } return v, nil } sp, ok := p.Source.(*Space) if !ok { return nil, errors.New("expected space source") } vs, err := api.ViewService.BySpace(p.Context, sp) if err != nil { return nil, err } if len(vs) == 0 { return nil, errors.New("could not find default view for space") } return vs[0], nil }, }, "views": &graphql.Field{ Type: graphql.NewList(api.ViewAPI.Type), Resolve: func(p graphql.ResolveParams) (interface{}, error) { span := trace.FromContext(p.Context).NewChild("trythings.spaceAPI.views") defer span.Finish() sp, ok := p.Source.(*Space) if !ok { return nil, errors.New("expected space source") } vs, err := api.ViewService.BySpace(p.Context, sp) if err != nil { return nil, err } return vs, nil }, }, }, Interfaces: []*graphql.Interface{ api.NodeInterface, }, }) return nil }