package notebooks import ( "context" "database/sql" "errors" "time" "github.com/influxdata/influxdb/v2" "github.com/influxdata/influxdb/v2/kit/platform" "github.com/influxdata/influxdb/v2/snowflake" "github.com/influxdata/influxdb/v2/sqlite" ) var _ influxdb.NotebookService = (*Service)(nil) type Service struct { store *sqlite.SqlStore idGenerator platform.IDGenerator } func NewService(store *sqlite.SqlStore) *Service { return &Service{ store: store, idGenerator: snowflake.NewIDGenerator(), } } func (s *Service) GetNotebook(ctx context.Context, id platform.ID) (*influxdb.Notebook, error) { var n influxdb.Notebook query := ` SELECT id, org_id, name, spec, created_at, updated_at FROM notebooks WHERE id = $1` if err := s.store.DB.GetContext(ctx, &n, query, id); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, influxdb.ErrNotebookNotFound } return nil, err } return &n, nil } // CreateNotebook creates a notebook. Note that this and all "write" operations on the database need to use the Mutex lock, // since sqlite can only handle 1 concurrent write operation at a time. func (s *Service) CreateNotebook(ctx context.Context, create *influxdb.NotebookReqBody) (*influxdb.Notebook, error) { s.store.Mu.Lock() defer s.store.Mu.Unlock() nowTime := time.Now().UTC() n := influxdb.Notebook{ ID: s.idGenerator.ID(), OrgID: create.OrgID, Name: create.Name, Spec: create.Spec, CreatedAt: nowTime, UpdatedAt: nowTime, } query := ` INSERT INTO notebooks (id, org_id, name, spec, created_at, updated_at) VALUES (:id, :org_id, :name, :spec, :created_at, :updated_at)` _, err := s.store.DB.NamedExecContext(ctx, query, &n) if err != nil { return nil, err } // Ideally, the create query would use "RETURNING" in order to avoid making a separate query. // Unfortunately this breaks the scanning of values into the result struct, so we have to make a separate // SELECT request to return the result from the database. return s.GetNotebook(ctx, n.ID) } // UpdateNotebook updates a notebook. func (s *Service) UpdateNotebook(ctx context.Context, id platform.ID, update *influxdb.NotebookReqBody) (*influxdb.Notebook, error) { s.store.Mu.Lock() defer s.store.Mu.Unlock() nowTime := time.Now().UTC() n := influxdb.Notebook{ ID: id, OrgID: update.OrgID, Name: update.Name, Spec: update.Spec, UpdatedAt: nowTime, } query := ` UPDATE notebooks SET org_id = :org_id, name = :name, spec = :spec, updated_at = :updated_at WHERE id = :id` _, err := s.store.DB.NamedExecContext(ctx, query, &n) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, influxdb.ErrNotebookNotFound } return nil, err } return s.GetNotebook(ctx, n.ID) } // DeleteNotebook deletes a notebook. func (s *Service) DeleteNotebook(ctx context.Context, id platform.ID) error { s.store.Mu.Lock() defer s.store.Mu.Unlock() query := ` DELETE FROM notebooks WHERE id = $1` res, err := s.store.DB.ExecContext(ctx, query, id.String()) if err != nil { return err } r, err := res.RowsAffected() if err != nil { return err } if r == 0 { return influxdb.ErrNotebookNotFound } return nil } // ListNotebooks lists notebooks matching the provided filter. Currently, only org_id is used in the filter. // Future uses may support pagination via this filter as well. func (s *Service) ListNotebooks(ctx context.Context, filter influxdb.NotebookListFilter) ([]*influxdb.Notebook, error) { ns := []*influxdb.Notebook{} query := ` SELECT id, org_id, name, spec, created_at, updated_at FROM notebooks WHERE org_id = $1` if err := s.store.DB.SelectContext(ctx, &ns, query, filter.OrgID); err != nil { return nil, err } return ns, nil }