func (r *JobRepo) Add(job *ct.Job) error { meta, err := json.Marshal(job.Meta) if err != nil { return err } // TODO: actually validate err = r.db.QueryRow("INSERT INTO job_cache (job_id, app_id, release_id, process_type, state, meta) VALUES ($1, $2, $3, $4, $5, $6) RETURNING created_at, updated_at", job.ID, job.AppID, job.ReleaseID, job.Type, job.State, meta).Scan(&job.CreatedAt, &job.UpdatedAt) if postgres.IsUniquenessError(err, "") { err = r.db.QueryRow("UPDATE job_cache SET state = $2, updated_at = now() WHERE job_id = $1 RETURNING created_at, updated_at", job.ID, job.State).Scan(&job.CreatedAt, &job.UpdatedAt) if e, ok := err.(*pq.Error); ok && e.Code.Name() == "check_violation" { return ct.ValidationError{Field: "state", Message: e.Error()} } } if err != nil { return err } // create a job event, ignoring possible duplications uniqueID := strings.Join([]string{job.ID, job.State}, "|") data, err := json.Marshal(job) if err != nil { return err } err = r.db.Exec("INSERT INTO events (app_id, object_id, unique_id, object_type, data) VALUES ($1, $2, $3, $4, $5)", job.AppID, job.ID, uniqueID, string(ct.EventTypeJob), data) if postgres.IsUniquenessError(err, "") { return nil } return err }
func (r *JobRepo) Add(job *ct.Job) error { hostID, jobID, err := cluster.ParseJobID(job.ID) if err != nil { log.Printf("Unable to parse hostID from %q", job.ID) return ErrNotFound } meta := metaToHstore(job.Meta) // TODO: actually validate err = r.db.QueryRow("INSERT INTO job_cache (job_id, host_id, app_id, release_id, process_type, state, meta) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING created_at, updated_at", jobID, hostID, job.AppID, job.ReleaseID, job.Type, job.State, meta).Scan(&job.CreatedAt, &job.UpdatedAt) if postgres.IsUniquenessError(err, "") { err = r.db.QueryRow("UPDATE job_cache SET state = $3, updated_at = now() WHERE job_id = $1 AND host_id = $2 RETURNING created_at, updated_at", jobID, hostID, job.State).Scan(&job.CreatedAt, &job.UpdatedAt) if e, ok := err.(*pq.Error); ok && e.Code.Name() == "check_violation" { return ct.ValidationError{Field: "state", Message: e.Error()} } } if err != nil { return err } // create a job event, ignoring possible duplications err = r.db.Exec("INSERT INTO job_events (job_id, host_id, app_id, state) VALUES ($1, $2, $3, $4)", jobID, hostID, job.AppID, job.State) if postgres.IsUniquenessError(err, "") { return nil } return err }
func (r *FormationRepo) Add(f *ct.Formation) error { if err := r.validateFormProcs(f); err != nil { return err } tx, err := r.db.Begin() if err != nil { return err } procs := procsHstore(f.Processes) err = tx.QueryRow("INSERT INTO formations (app_id, release_id, processes) VALUES ($1, $2, $3) RETURNING created_at, updated_at", f.AppID, f.ReleaseID, procs).Scan(&f.CreatedAt, &f.UpdatedAt) if postgres.IsUniquenessError(err, "") { tx.Rollback() tx, err = r.db.Begin() if err != nil { return err } err = tx.QueryRow("UPDATE formations SET processes = $3, updated_at = now(), deleted_at = NULL WHERE app_id = $1 AND release_id = $2 RETURNING created_at, updated_at", f.AppID, f.ReleaseID, procs).Scan(&f.CreatedAt, &f.UpdatedAt) } if err != nil { tx.Rollback() return err } if err := createEvent(tx.Exec, &ct.Event{ AppID: f.AppID, ObjectID: f.AppID + ":" + f.ReleaseID, ObjectType: ct.EventTypeScale, }, f.Processes); err != nil { tx.Rollback() return err } return tx.Commit() }
func (r *ArtifactRepo) Add(data interface{}) error { a := data.(*ct.Artifact) // TODO: actually validate if a.ID == "" { a.ID = random.UUID() } if a.Type == "" { return ct.ValidationError{Field: "type", Message: "must not be empty"} } if a.URI == "" { return ct.ValidationError{Field: "uri", Message: "must not be empty"} } if a.Type == ct.ArtifactTypeFlynn && a.RawManifest == nil { if a.Size <= 0 { return ct.ValidationError{Field: "size", Message: "must be greater than zero"} } if err := downloadManifest(a); err != nil { return ct.ValidationError{Field: "manifest", Message: fmt.Sprintf("failed to download from %s: %s", a.URI, err)} } } tx, err := r.db.Begin() if err != nil { return err } err = tx.QueryRow("artifact_insert", a.ID, string(a.Type), a.URI, a.Meta, []byte(a.RawManifest), a.Hashes, a.Size, a.LayerURLTemplate).Scan(&a.CreatedAt) if postgres.IsUniquenessError(err, "") { tx.Rollback() tx, err = r.db.Begin() if err != nil { return err } var size *int64 var layerURLTemplate *string err = tx.QueryRow("artifact_select_by_type_and_uri", string(a.Type), a.URI).Scan(&a.ID, &a.Meta, &a.RawManifest, &a.Hashes, &size, &layerURLTemplate, &a.CreatedAt) if err != nil { tx.Rollback() return err } if size != nil { a.Size = *size } if layerURLTemplate != nil { a.LayerURLTemplate = *layerURLTemplate } } if err != nil { tx.Rollback() return err } if err := createEvent(tx.Exec, &ct.Event{ ObjectID: a.ID, ObjectType: ct.EventTypeArtifact, }, a); err != nil { tx.Rollback() return err } return tx.Commit() }
func (d *pgDataStore) Add(r *router.Route) (err error) { switch d.tableName { case tableNameHTTP: err = d.pgx.QueryRow( sqlAddRouteHTTP, r.ParentRef, r.Service, r.Leader, r.Domain, r.TLSCert, r.TLSKey, r.Sticky, r.Path, ).Scan(&r.ID, &r.CreatedAt, &r.UpdatedAt) case tableNameTCP: err = d.pgx.QueryRow( sqlAddRouteTCP, r.ParentRef, r.Service, r.Leader, r.Port, ).Scan(&r.ID, &r.CreatedAt, &r.UpdatedAt) } r.Type = d.routeType if postgres.IsUniquenessError(err, "") { err = ErrConflict } else if postgres.IsPostgresCode(err, postgres.RaiseException) { err = ErrInvalid } return err }
func (r *AppRepo) Add(data interface{}) error { app := data.(*ct.App) tx, err := r.db.Begin() if err != nil { return err } if app.Name == "" { var nameID uint32 if err := tx.QueryRow("SELECT nextval('name_ids')").Scan(&nameID); err != nil { tx.Rollback() return err } app.Name = name.Get(nameID) } if len(app.Name) > 100 || !utils.AppNamePattern.MatchString(app.Name) { return ct.ValidationError{Field: "name", Message: "is invalid"} } if app.ID == "" { app.ID = random.UUID() } if app.Strategy == "" { app.Strategy = "all-at-once" } meta, err := json.Marshal(app.Meta) if err != nil { return err } if err := tx.QueryRow("INSERT INTO apps (app_id, name, meta, strategy) VALUES ($1, $2, $3, $4) RETURNING created_at, updated_at", app.ID, app.Name, meta, app.Strategy).Scan(&app.CreatedAt, &app.UpdatedAt); err != nil { tx.Rollback() if postgres.IsUniquenessError(err, "apps_name_idx") { return httphelper.ObjectExistsErr(fmt.Sprintf("application %q already exists", app.Name)) } return err } if err := createEvent(tx.Exec, &ct.Event{ AppID: app.ID, ObjectID: app.ID, ObjectType: ct.EventTypeApp, }, app); err != nil { tx.Rollback() return err } if err := tx.Commit(); err != nil { return err } if !app.System() && r.defaultDomain != "" { route := (&router.HTTPRoute{ Domain: fmt.Sprintf("%s.%s", app.Name, r.defaultDomain), Service: app.Name + "-web", }).ToRoute() if err := createRoute(r.db, r.router, app.ID, route); err != nil { log.Printf("Error creating default route for %s: %s", app.Name, err) } } return nil }
func (r *AppRepo) Add(data interface{}) error { app := data.(*ct.App) tx, err := r.db.Begin() if err != nil { return err } if app.Name == "" { var nameID int64 if err := tx.QueryRow("app_next_name_id").Scan(&nameID); err != nil { tx.Rollback() return err } // Safe cast because name_ids is limited to 32 bit size in schema app.Name = name.Get(uint32(nameID)) } if len(app.Name) > 100 || !utils.AppNamePattern.MatchString(app.Name) { return ct.ValidationError{Field: "name", Message: "is invalid"} } if app.ID == "" { app.ID = random.UUID() } if app.Strategy == "" { app.Strategy = "all-at-once" } if app.DeployTimeout == 0 { app.DeployTimeout = ct.DefaultDeployTimeout } if err := tx.QueryRow("app_insert", app.ID, app.Name, app.Meta, app.Strategy, app.DeployTimeout).Scan(&app.CreatedAt, &app.UpdatedAt); err != nil { tx.Rollback() if postgres.IsUniquenessError(err, "apps_name_idx") { return httphelper.ObjectExistsErr(fmt.Sprintf("application %q already exists", app.Name)) } return err } if err := createEvent(tx.Exec, &ct.Event{ AppID: app.ID, ObjectID: app.ID, ObjectType: ct.EventTypeApp, }, app); err != nil { tx.Rollback() return err } if err := tx.Commit(); err != nil { return err } if !app.System() && r.defaultDomain != "" { route := (&router.HTTPRoute{ Domain: fmt.Sprintf("%s.%s", app.Name, r.defaultDomain), Service: app.Name + "-web", }).ToRoute() if err := createRoute(r.db, r.router, app.ID, route); err != nil { log.Printf("Error creating default route for %s: %s", app.Name, err) } } return nil }
func (r *JobRepo) Add(job *ct.Job) error { // TODO: actually validate err := r.db.QueryRow( "job_insert", job.ID, job.UUID, job.HostID, job.AppID, job.ReleaseID, job.Type, string(job.State), job.Meta, job.ExitStatus, job.HostError, job.RunAt, job.Restarts, ).Scan(&job.CreatedAt, &job.UpdatedAt) if postgres.IsUniquenessError(err, "") { err = r.db.QueryRow( "job_update", job.UUID, job.ID, job.HostID, string(job.State), job.ExitStatus, job.HostError, job.RunAt, job.Restarts, ).Scan(&job.CreatedAt, &job.UpdatedAt) if postgres.IsPostgresCode(err, postgres.CheckViolation) { return ct.ValidationError{Field: "state", Message: err.Error()} } } if err != nil { return err } // create a job event, ignoring possible duplications uniqueID := strings.Join([]string{job.UUID, string(job.State)}, "|") err = r.db.Exec("event_insert_unique", job.AppID, job.UUID, uniqueID, string(ct.EventTypeJob), job) if postgres.IsUniquenessError(err, "") { return nil } return err }
func (p *PostgresFilesystem) Put(name string, r io.Reader, typ string) error { tx, err := p.db.Begin() if err != nil { return err } var id oid.Oid create: err = tx.QueryRow("INSERT INTO files (name, type) VALUES ($1, $2) RETURNING file_id", name, typ).Scan(&id) if postgres.IsUniquenessError(err, "") { tx.Rollback() tx, err = p.db.Begin() if err != nil { return err } // file exists, delete it first _, err = tx.Exec("DELETE FROM files WHERE name = $1", name) if err != nil { tx.Rollback() return err } goto create } if err != nil { tx.Rollback() return err } lo, err := pq.NewLargeObjects(tx) if err != nil { tx.Rollback() return err } obj, err := lo.Open(id, pq.LargeObjectModeWrite) if err != nil { tx.Rollback() return err } h := sha512.New() size, err := io.Copy(obj, io.TeeReader(r, h)) if err != nil { tx.Rollback() return err } digest := hex.EncodeToString(h.Sum(nil)) _, err = tx.Exec("UPDATE files SET size = $2, digest = $3 WHERE file_id = $1", id, size, digest) if err != nil { tx.Rollback() return err } return tx.Commit() }
func (r *FormationRepo) Add(f *ct.Formation) error { if err := r.validateFormProcs(f); err != nil { return err } procs := procsHstore(f.Processes) err := r.db.QueryRow("INSERT INTO formations (app_id, release_id, processes) VALUES ($1, $2, $3) RETURNING created_at, updated_at", f.AppID, f.ReleaseID, procs).Scan(&f.CreatedAt, &f.UpdatedAt) if postgres.IsUniquenessError(err, "") { err = r.db.QueryRow("UPDATE formations SET processes = $3, updated_at = now(), deleted_at = NULL WHERE app_id = $1 AND release_id = $2 RETURNING created_at, updated_at", f.AppID, f.ReleaseID, procs).Scan(&f.CreatedAt, &f.UpdatedAt) } if err != nil { return err } return nil }
func (r *ArtifactRepo) Add(data interface{}) error { a := data.(*ct.Artifact) // TODO: actually validate if a.ID == "" { a.ID = random.UUID() } if a.Type == "" { return ct.ValidationError{"type", "must not be empty"} } if a.URI == "" { return ct.ValidationError{"uri", "must not be empty"} } tx, err := r.db.Begin() if err != nil { return err } err = tx.QueryRow("INSERT INTO artifacts (artifact_id, type, uri) VALUES ($1, $2, $3) RETURNING created_at", a.ID, a.Type, a.URI).Scan(&a.CreatedAt) if postgres.IsUniquenessError(err, "") { tx.Rollback() tx, err = r.db.Begin() if err != nil { return err } err = tx.QueryRow("SELECT artifact_id, created_at FROM artifacts WHERE type = $1 AND uri = $2", a.Type, a.URI).Scan(&a.ID, &a.CreatedAt) if err != nil { tx.Rollback() return err } } else if err == nil { a.ID = postgres.CleanUUID(a.ID) if err := createEvent(tx.Exec, &ct.Event{ ObjectID: a.ID, ObjectType: ct.EventTypeArtifact, }, a); err != nil { tx.Rollback() return err } } if err != nil { tx.Rollback() return err } return tx.Commit() }
func (r *ArtifactRepo) Add(data interface{}) error { a := data.(*ct.Artifact) // TODO: actually validate if a.ID == "" { a.ID = random.UUID() } if a.Type == "" { return ct.ValidationError{"type", "must not be empty"} } if a.URI == "" { return ct.ValidationError{"uri", "must not be empty"} } tx, err := r.db.Begin() if err != nil { return err } err = tx.QueryRow("artifact_insert", a.ID, a.Type, a.URI).Scan(&a.CreatedAt) if postgres.IsUniquenessError(err, "") { tx.Rollback() tx, err = r.db.Begin() if err != nil { return err } err = tx.QueryRow("artifact_select_by_type_and_uri", a.Type, a.URI).Scan(&a.ID, &a.CreatedAt) if err != nil { tx.Rollback() return err } } else if err == nil { if err := createEvent(tx.Exec, &ct.Event{ ObjectID: a.ID, ObjectType: ct.EventTypeArtifact, }, a); err != nil { tx.Rollback() return err } } if err != nil { tx.Rollback() return err } return tx.Commit() }
func (d *pgDataStore) Add(r *router.Route) (err error) { switch d.tableName { case tableNameHTTP: err = d.addHTTP(r) case tableNameTCP: err = d.addTCP(r) } r.Type = d.routeType if err != nil { if postgres.IsUniquenessError(err, "") { err = ErrConflict } else if postgres.IsPostgresCode(err, postgres.RaiseException) { err = ErrInvalid } return err } return nil }
func (r *FormationRepo) Add(f *ct.Formation) error { if err := r.validateFormProcs(f); err != nil { return err } scale := &ct.Scale{ Processes: f.Processes, ReleaseID: f.ReleaseID, } prevFormation, _ := r.Get(f.AppID, f.ReleaseID) if prevFormation != nil { scale.PrevProcesses = prevFormation.Processes } tx, err := r.db.Begin() if err != nil { return err } err = tx.QueryRow("formation_insert", f.AppID, f.ReleaseID, f.Processes, f.Tags).Scan(&f.CreatedAt, &f.UpdatedAt) if postgres.IsUniquenessError(err, "") { tx.Rollback() tx, err = r.db.Begin() if err != nil { return err } err = tx.QueryRow("formation_update", f.AppID, f.ReleaseID, f.Processes, f.Tags).Scan(&f.CreatedAt, &f.UpdatedAt) } if err != nil { tx.Rollback() return err } if err := createEvent(tx.Exec, &ct.Event{ AppID: f.AppID, ObjectID: f.AppID + ":" + f.ReleaseID, ObjectType: ct.EventTypeScale, }, scale); err != nil { tx.Rollback() return err } return tx.Commit() }
func (r *KeyRepo) Add(data interface{}) error { key := data.(*ct.Key) if key.Key == "" { return errors.New("controller: key must not be blank") } pubKey, comment, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Key)) if err != nil { return err } key.ID = fingerprintKey(pubKey.Marshal()) key.Key = string(bytes.TrimSpace(ssh.MarshalAuthorizedKey(pubKey))) key.Comment = comment tx, err := r.db.Begin() if err != nil { return err } err = tx.QueryRow("INSERT INTO keys (fingerprint, key, comment) VALUES ($1, $2, $3) RETURNING created_at", key.ID, key.Key, key.Comment).Scan(&key.CreatedAt) if postgres.IsUniquenessError(err, "") { tx.Rollback() return nil } if err != nil { tx.Rollback() return err } if err := createEvent(tx.Exec, &ct.Event{ ObjectID: key.ID, ObjectType: ct.EventTypeKey, }, key); err != nil { tx.Rollback() return err } return tx.Commit() }
func (r *AppRepo) Add(data interface{}) error { app := data.(*ct.App) if app.Name == "" { var nameID uint32 if err := r.db.QueryRow("SELECT nextval('name_ids')").Scan(&nameID); err != nil { return err } app.Name = name.Get(nameID) } if len(app.Name) > 100 || !appNamePattern.MatchString(app.Name) { return ct.ValidationError{Field: "name", Message: "is invalid"} } if app.ID == "" { app.ID = random.UUID() } if app.Strategy == "" { app.Strategy = "all-at-once" } meta := metaToHstore(app.Meta) if err := r.db.QueryRow("INSERT INTO apps (app_id, name, meta, strategy) VALUES ($1, $2, $3, $4) RETURNING created_at, updated_at", app.ID, app.Name, meta, app.Strategy).Scan(&app.CreatedAt, &app.UpdatedAt); err != nil { if postgres.IsUniquenessError(err, "apps_name_idx") { return httphelper.ObjectExistsErr(fmt.Sprintf("application %q already exists", app.Name)) } return err } app.ID = postgres.CleanUUID(app.ID) if !app.System() && r.defaultDomain != "" { route := (&router.HTTPRoute{ Domain: fmt.Sprintf("%s.%s", app.Name, r.defaultDomain), Service: app.Name + "-web", }).ToRoute() route.ParentRef = routeParentRef(app.ID) if err := r.router.CreateRoute(route); err != nil { log.Printf("Error creating default route for %s: %s", app.Name, err) } } return nil }
func (r *ArtifactRepo) Add(data interface{}) error { a := data.(*ct.Artifact) // TODO: actually validate if a.ID == "" { a.ID = random.UUID() } if a.Type == "" { return ct.ValidationError{"type", "must not be empty"} } if a.URI == "" { return ct.ValidationError{"uri", "must not be empty"} } err := r.db.QueryRow("INSERT INTO artifacts (artifact_id, type, uri) VALUES ($1, $2, $3) RETURNING created_at", a.ID, a.Type, a.URI).Scan(&a.CreatedAt) if postgres.IsUniquenessError(err, "") { err = r.db.QueryRow("SELECT artifact_id, created_at FROM artifacts WHERE type = $1 AND uri = $2", a.Type, a.URI).Scan(&a.ID, &a.CreatedAt) if err != nil { return err } } a.ID = postgres.CleanUUID(a.ID) return err }
func (r *FileRepo) Put(name string, data io.Reader, offset int64, typ string) error { tx, err := r.db.Begin() if err != nil { return err } info := backend.FileInfo{ Name: name, Type: typ, } h := sha512.New().(resumable.Hash) b := r.defaultBackend create: err = tx.QueryRow("INSERT INTO files (name, backend, type) VALUES ($1, $2, $3) RETURNING file_id", name, b.Name(), typ).Scan(&info.ID) if postgres.IsUniquenessError(err, "") { tx.Rollback() tx, err = r.db.Begin() if err != nil { return err } if offset > 0 { var backendName string var sha512State []byte var externalID *string // file exists, get details if err := tx.QueryRow( "SELECT file_id, file_oid, external_id, backend, size, sha512_state FROM files WHERE name = $1 AND deleted_at IS NULL", name, ).Scan(&info.ID, &info.Oid, &externalID, &backendName, &info.Size, &sha512State); err != nil { tx.Rollback() return err } if externalID != nil { info.ExternalID = *externalID } b, err = r.getBackend(backendName) if err != nil { tx.Rollback() return err } if offset != info.Size { tx.Rollback() // TODO: pass error via HTTP response return fmt.Errorf("blobstore: offset (%d) does not match blob size (%d), unable to append", offset, info.Size) } if len(sha512State) > 0 { err = h.Restore(sha512State) } if (len(sha512State) == 0 || err != nil || h.Len() != info.Size) && info.Size > 0 { // hash state is not resumable, read current data into hash f, err := b.Open(tx, info, false) if err != nil { tx.Rollback() return err } h.Reset() if _, err := io.Copy(h, io.LimitReader(f, info.Size)); err != nil { f.Close() tx.Rollback() return err } f.Close() } } else { // file exists, not appending, overwrite by deleting var di backend.FileInfo var externalID *string var backendName string if err := tx.QueryRow( "UPDATE files SET deleted_at = now() WHERE name = $1 AND deleted_at IS NULL RETURNING file_id, external_id, backend", name, ).Scan(&di.ID, &externalID, &backendName); err != nil { tx.Rollback() return err } if externalID != nil { di.ExternalID = *externalID } // delete old file from backend if err := func() error { if backendName == "postgres" { // no need to call delete, it is done automatically by a trigger return nil } b, err := r.getBackend(backendName) if err != nil { return err } return b.Delete(nil, di) }(); err != nil { log.Printf("Error deleting %s (%s) from backend %s: %s", di.ExternalID, name, backendName, err) } goto create } } else if err != nil { tx.Rollback() return err } sr := newSizeReader(data) sr.size = info.Size if err := b.Put(tx, info, io.TeeReader(sr, h), offset > 0); err != nil { tx.Rollback() return err } sha512State, _ := h.State() if err := tx.Exec( "UPDATE files SET size = $2, sha512 = $3, sha512_state = $4, updated_at = now() WHERE file_id = $1", info.ID, sr.Size(), h.Sum(nil), sha512State, ); err != nil { tx.Rollback() return err } return tx.Commit() }
func (c *controllerAPI) CreateDeployment(ctx context.Context, w http.ResponseWriter, req *http.Request) { var rid releaseID if err := httphelper.DecodeJSON(req, &rid); err != nil { respondWithError(w, err) return } rel, err := c.releaseRepo.Get(rid.ID) if err != nil { if err == ErrNotFound { err = ct.ValidationError{ Message: fmt.Sprintf("could not find release with ID %s", rid.ID), } } respondWithError(w, err) return } release := rel.(*ct.Release) app := c.getApp(ctx) // TODO: wrap all of this in a transaction oldRelease, err := c.appRepo.GetRelease(app.ID) if err == ErrNotFound { oldRelease = &ct.Release{} } else if err != nil { respondWithError(w, err) return } oldFormation, err := c.formationRepo.Get(app.ID, oldRelease.ID) if err == ErrNotFound { oldFormation = &ct.Formation{} } else if err != nil { respondWithError(w, err) return } procCount := 0 for _, i := range oldFormation.Processes { procCount += i } deployment := &ct.Deployment{ AppID: app.ID, NewReleaseID: release.ID, Strategy: app.Strategy, OldReleaseID: oldRelease.ID, Processes: oldFormation.Processes, DeployTimeout: app.DeployTimeout, } if err := schema.Validate(deployment); err != nil { respondWithError(w, err) return } if procCount == 0 { // immediately set app release if err := c.appRepo.SetRelease(app, release.ID); err != nil { respondWithError(w, err) return } now := time.Now() deployment.FinishedAt = &now } d, err := c.deploymentRepo.Add(deployment) if err != nil { if postgres.IsUniquenessError(err, "isolate_deploys") { httphelper.ValidationError(w, "", "Cannot create deploy, there is already one in progress for this app.") return } respondWithError(w, err) return } httphelper.JSON(w, 200, d) }