Exemplo n.º 1
0
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
}
Exemplo n.º 2
0
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
}
Exemplo n.º 3
0
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
}
Exemplo n.º 4
0
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
}
Exemplo n.º 5
0
// 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
}
Exemplo n.º 6
0
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
}
Exemplo n.º 7
0
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
}
Exemplo n.º 8
0
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
}
Exemplo n.º 9
0
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
}
Exemplo n.º 10
0
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
}
Exemplo n.º 11
0
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
}
Exemplo n.º 12
0
// 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)
}
Exemplo n.º 13
0
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
}
Exemplo n.º 14
0
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
}
Exemplo n.º 15
0
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
}
Exemplo n.º 16
0
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
}
Exemplo n.º 17
0
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
}
Exemplo n.º 18
0
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
}
Exemplo n.º 19
0
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
}
Exemplo n.º 20
0
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
}
Exemplo n.º 21
0
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
}
Exemplo n.º 22
0
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
}
Exemplo n.º 23
0
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
		}
Exemplo n.º 24
0
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
}
Exemplo n.º 25
0
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
}
Exemplo n.º 26
0
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
}
Exemplo n.º 27
0
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
}