// Create creates a new work item in the repository // returns BadParameterError, ConversionError or InternalError func (r *GormWorkItemRepository) Create(ctx context.Context, typeID string, fields map[string]interface{}, creator string) (*app.WorkItem, error) { wiType, err := r.wir.LoadTypeFromDB(ctx, typeID) if err != nil { return nil, errors.NewBadParameterError("type", typeID) } wi := WorkItem{ Type: typeID, Fields: Fields{}, } fields[SystemCreator] = creator for fieldName, fieldDef := range wiType.Fields { if fieldName == SystemCreatedAt { continue } fieldValue := fields[fieldName] var err error wi.Fields[fieldName], err = fieldDef.ConvertToModel(fieldName, fieldValue) if err != nil { return nil, errors.NewBadParameterError(fieldName, fieldValue) } if fieldName == SystemDescription && wi.Fields[fieldName] != nil { description := rendering.NewMarkupContentFromMap(wi.Fields[fieldName].(map[string]interface{})) if !rendering.IsMarkupSupported(description.Markup) { return nil, errors.NewBadParameterError(fieldName, fieldValue) } } } tx := r.db if err = tx.Create(&wi).Error; err != nil { return nil, errors.NewInternalError(err.Error()) } return convertWorkItemModelToApp(wiType, &wi) }
// Refresh obtain a new access token using the refresh token. func (c *LoginController) Refresh(ctx *app.RefreshLoginContext) error { refreshToken := ctx.Payload.RefreshToken if refreshToken == nil { return jsonapi.JSONErrorResponse(ctx, errors.NewBadParameterError("refresh_token", nil).Expected("not nil")) } client := &http.Client{Timeout: 10 * time.Second} res, err := client.PostForm(configuration.GetKeycloakEndpointToken(), url.Values{ "client_id": {configuration.GetKeycloakClientID()}, "client_secret": {configuration.GetKeycloakSecret()}, "refresh_token": {*refreshToken}, "grant_type": {"refresh_token"}, }) if err != nil { return jsonapi.JSONErrorResponse(ctx, errors.NewInternalError("Error when obtaining token "+err.Error())) } switch res.StatusCode { case 200: // OK case 401: return jsonapi.JSONErrorResponse(ctx, errors.NewUnauthorizedError(res.Status+" "+readBody(res.Body))) case 400: return jsonapi.JSONErrorResponse(ctx, errors.NewBadParameterError(readBody(res.Body), nil)) default: return jsonapi.JSONErrorResponse(ctx, errors.NewInternalError(res.Status+" "+readBody(res.Body))) } token, err := readToken(res, ctx) if err != nil { return err } return ctx.OK(&app.AuthToken{Token: token}) }
// ValidateCorrectSourceAndTargetType returns an error if the Path of // the source WIT as defined by the work item link type is not part of // the actual source's WIT; the same applies for the target. func (r *GormWorkItemLinkRepository) ValidateCorrectSourceAndTargetType(ctx context.Context, sourceID, targetID uint64, linkTypeID satoriuuid.UUID) error { linkType, err := r.workItemLinkTypeRepo.LoadTypeFromDBByID(ctx, linkTypeID) if err != nil { return errs.WithStack(err) } // Fetch the source work item source, err := r.workItemRepo.LoadFromDB(ctx, strconv.FormatUint(sourceID, 10)) if err != nil { return errs.WithStack(err) } // Fetch the target work item target, err := r.workItemRepo.LoadFromDB(ctx, strconv.FormatUint(targetID, 10)) if err != nil { return errs.WithStack(err) } // Fetch the concrete work item types of the target and the source. sourceWorkItemType, err := r.workItemTypeRepo.LoadTypeFromDB(ctx, source.Type) if err != nil { return errs.WithStack(err) } targetWorkItemType, err := r.workItemTypeRepo.LoadTypeFromDB(ctx, target.Type) if err != nil { return errs.WithStack(err) } // Check type paths if !sourceWorkItemType.IsTypeOrSubtypeOf(linkType.SourceTypeName) { return errors.NewBadParameterError("source work item type", source.Type) } if !targetWorkItemType.IsTypeOrSubtypeOf(linkType.TargetTypeName) { return errors.NewBadParameterError("target work item type", target.Type) } return nil }
// Save updates the given space in the db. Version must be the same as the one in the stored version // returns NotFoundError, BadParameterError, VersionConflictError or InternalError func (r *GormRepository) Save(ctx context.Context, p *Space) (*Space, error) { pr := Space{} tx := r.db.Where("id=?", p.ID).First(&pr) oldVersion := p.Version p.Version++ if tx.RecordNotFound() { // treating this as a not found error: the fact that we're using number internal is implementation detail return nil, errors.NewNotFoundError("space", p.ID.String()) } if err := tx.Error; err != nil { return nil, errors.NewInternalError(err.Error()) } tx = tx.Where("Version = ?", oldVersion).Save(p) if err := tx.Error; err != nil { if gormsupport.IsCheckViolation(tx.Error, "spaces_name_check") { return nil, errors.NewBadParameterError("Name", p.Name).Expected("not empty") } if gormsupport.IsUniqueViolation(tx.Error, "spaces_name_idx") { return nil, errors.NewBadParameterError("Name", p.Name).Expected("unique") } return nil, errors.NewInternalError(err.Error()) } if tx.RowsAffected == 0 { return nil, errors.NewVersionConflictError("version conflict") } log.Info(ctx, map[string]interface{}{ "pkg": "space", "spaceID": p.ID, }, "space updated successfully") return p, nil }
// Create creates a new work item in the repository // returns BadParameterError, ConversionError or InternalError func (r *GormWorkItemTypeRepository) Create(ctx context.Context, extendedTypeName *string, name string, fields map[string]app.FieldDefinition) (*app.WorkItemType, error) { existing, _ := r.LoadTypeFromDB(ctx, name) if existing != nil { log.Error(ctx, map[string]interface{}{"witName": name}, "unable to create new work item type") return nil, errors.NewBadParameterError("name", name) } allFields := map[string]FieldDefinition{} path := name if extendedTypeName != nil { extendedType := WorkItemType{} db := r.db.First(&extendedType, "name = ?", extendedTypeName) if db.RecordNotFound() { return nil, errors.NewBadParameterError("extendedTypeName", *extendedTypeName) } if err := db.Error; err != nil { return nil, errors.NewInternalError(err.Error()) } // copy fields from extended type for key, value := range extendedType.Fields { allFields[key] = value } path = extendedType.Path + pathSep + name } // now process new fields, checking whether they are ok to add. for field, definition := range fields { existing, exists := allFields[field] ct, err := convertFieldTypeToModels(*definition.Type) if err != nil { return nil, errs.WithStack(err) } converted := FieldDefinition{ Required: definition.Required, Type: ct, } if exists && !compatibleFields(existing, converted) { return nil, fmt.Errorf("incompatible change for field %s", field) } allFields[field] = converted } created := WorkItemType{ Version: 0, Name: name, Path: path, Fields: allFields, } if err := r.db.Save(&created).Error; err != nil { return nil, errors.NewInternalError(err.Error()) } result := convertTypeFromModels(&created) return &result, nil }
func TestNewBadParameterError(t *testing.T) { t.Parallel() resource.Require(t, resource.UnitTest) param := "assigness" value := 10 expectedValue := 11 err := errors.NewBadParameterError(param, value) assert.Equal(t, fmt.Sprintf("Bad value for parameter '%s': '%v'", param, value), err.Error()) err = errors.NewBadParameterError(param, value).Expected(expectedValue) assert.Equal(t, fmt.Sprintf("Bad value for parameter '%s': '%v' (expected: '%v')", param, value, expectedValue), err.Error()) }
func validateCreateSpace(ctx *app.CreateSpaceContext) error { if ctx.Payload.Data == nil { return errors.NewBadParameterError("data", nil).Expected("not nil") } if ctx.Payload.Data.Attributes == nil { return errors.NewBadParameterError("data.attributes", nil).Expected("not nil") } if ctx.Payload.Data.Attributes.Name == nil { return errors.NewBadParameterError("data.attributes.name", nil).Expected("not nil") } return nil }
// Update does PATCH workitem func (c *WorkitemController) Update(ctx *app.UpdateWorkitemContext) error { return application.Transactional(c.db, func(appl application.Application) error { if ctx.Payload == nil || ctx.Payload.Data == nil || ctx.Payload.Data.ID == nil { return jsonapi.JSONErrorResponse(ctx, errors.NewBadParameterError("missing data.ID element in request", nil)) } wi, err := appl.WorkItems().Load(ctx, *ctx.Payload.Data.ID) if err != nil { return jsonapi.JSONErrorResponse(ctx, errs.Wrap(err, fmt.Sprintf("Failed to load work item with id %v", *ctx.Payload.Data.ID))) } // Type changes of WI are not allowed which is why we overwrite it the // type with the old one after the WI has been converted. oldType := wi.Type err = ConvertJSONAPIToWorkItem(appl, *ctx.Payload.Data, wi) if err != nil { return jsonapi.JSONErrorResponse(ctx, err) } wi.Type = oldType wi, err = appl.WorkItems().Save(ctx, *wi) if err != nil { return jsonapi.JSONErrorResponse(ctx, errs.Wrap(err, "Error updating work item")) } wi2 := ConvertWorkItem(ctx.RequestData, wi) resp := &app.WorkItem2Single{ Data: wi2, Links: &app.WorkItemLinks{ Self: buildAbsoluteURL(ctx.RequestData), }, } return ctx.OK(resp) }) }
// Create creates a new work item link type in the repository. // Returns BadParameterError, ConversionError or InternalError func (r *GormWorkItemLinkTypeRepository) Create(ctx context.Context, name string, description *string, sourceTypeName, targetTypeName, forwardName, reverseName, topology string, linkCategoryID satoriuuid.UUID) (*app.WorkItemLinkTypeSingle, error) { linkType := &WorkItemLinkType{ Name: name, Description: description, SourceTypeName: sourceTypeName, TargetTypeName: targetTypeName, ForwardName: forwardName, ReverseName: reverseName, Topology: topology, LinkCategoryID: linkCategoryID, } if err := linkType.CheckValidForCreation(); err != nil { return nil, errs.WithStack(err) } // Check link category exists linkCategory := WorkItemLinkCategory{} db := r.db.Where("id=?", linkType.LinkCategoryID).Find(&linkCategory) if db.RecordNotFound() { return nil, errors.NewBadParameterError("work item link category", linkType.LinkCategoryID) } if db.Error != nil { return nil, errors.NewInternalError(fmt.Sprintf("Failed to find work item link category: %s", db.Error.Error())) } db = r.db.Create(linkType) if db.Error != nil { return nil, errors.NewInternalError(db.Error.Error()) } // Convert the created link type entry into a JSONAPI response result := ConvertLinkTypeFromModel(*linkType) return &result, nil }
// Create runs the create action. func (c *SpaceIterationsController) Create(ctx *app.CreateSpaceIterationsContext) error { _, err := login.ContextIdentity(ctx) if err != nil { return jsonapi.JSONErrorResponse(ctx, goa.ErrUnauthorized(err.Error())) } spaceID, err := uuid.FromString(ctx.ID) if err != nil { return jsonapi.JSONErrorResponse(ctx, goa.ErrNotFound(err.Error())) } // Validate Request if ctx.Payload.Data == nil { return jsonapi.JSONErrorResponse(ctx, errors.NewBadParameterError("data", nil).Expected("not nil")) } reqIter := ctx.Payload.Data if reqIter.Attributes.Name == nil { return jsonapi.JSONErrorResponse(ctx, errors.NewBadParameterError("data.attributes.name", nil).Expected("not nil")) } return application.Transactional(c.db, func(appl application.Application) error { _, err = appl.Spaces().Load(ctx, spaceID) if err != nil { return jsonapi.JSONErrorResponse(ctx, goa.ErrNotFound(err.Error())) } newItr := iteration.Iteration{ SpaceID: spaceID, Name: *reqIter.Attributes.Name, StartAt: reqIter.Attributes.StartAt, EndAt: reqIter.Attributes.EndAt, } if reqIter.Attributes.Description != nil { newItr.Description = reqIter.Attributes.Description } err = appl.Iterations().Create(ctx, &newItr) if err != nil { return jsonapi.JSONErrorResponse(ctx, err) } res := &app.IterationSingle{ Data: ConvertIteration(ctx.RequestData, &newItr), } ctx.ResponseData.Header().Set("Location", rest.AbsoluteURL(ctx.RequestData, app.IterationHref(res.Data.ID))) return ctx.Created(res) }) }
func getVersion(version interface{}) (int, error) { if version != nil { v, err := strconv.Atoi(fmt.Sprintf("%v", version)) if err != nil { return -1, errors.NewBadParameterError("data.attributes.version", version) } return v, nil } return -1, nil }
// Create runs the create action. func (c *SpaceAreasController) Create(ctx *app.CreateSpaceAreasContext) error { _, err := login.ContextIdentity(ctx) if err != nil { return jsonapi.JSONErrorResponse(ctx, goa.ErrUnauthorized(err.Error())) } spaceID, err := uuid.FromString(ctx.ID) if err != nil { return jsonapi.JSONErrorResponse(ctx, goa.ErrNotFound(err.Error())) } // Validate Request if ctx.Payload.Data == nil { return jsonapi.JSONErrorResponse(ctx, errors.NewBadParameterError("data", nil).Expected("not nil")) } reqIter := ctx.Payload.Data if reqIter.Attributes.Name == nil { return jsonapi.JSONErrorResponse(ctx, errors.NewBadParameterError("data.attributes.name", nil).Expected("not nil")) } return application.Transactional(c.db, func(appl application.Application) error { _, err = appl.Spaces().Load(ctx, spaceID) if err != nil { return jsonapi.JSONErrorResponse(ctx, goa.ErrNotFound(err.Error())) } newArea := area.Area{ SpaceID: spaceID, Name: *reqIter.Attributes.Name, } err = appl.Areas().Create(ctx, &newArea) if err != nil { return jsonapi.JSONErrorResponse(ctx, err) } res := &app.AreaSingle{ Data: ConvertArea(appl, ctx.RequestData, &newArea, addResolvedPath), } ctx.ResponseData.Header().Set("Location", rest.AbsoluteURL(ctx.RequestData, app.AreaHref(res.Data.ID))) return ctx.Created(res) }) }
// Create creates a new Space in the db // returns BadParameterError or InternalError func (r *GormRepository) Create(ctx context.Context, space *Space) (*Space, error) { space.ID = satoriuuid.NewV4() tx := r.db.Create(space) if err := tx.Error; err != nil { if gormsupport.IsCheckViolation(tx.Error, "spaces_name_check") { return nil, errors.NewBadParameterError("Name", space.Name).Expected("not empty") } if gormsupport.IsUniqueViolation(tx.Error, "spaces_name_idx") { return nil, errors.NewBadParameterError("Name", space.Name).Expected("unique") } return nil, errors.NewInternalError(err.Error()) } log.Info(ctx, map[string]interface{}{ "pkg": "space", "spaceID": space.ID, }, "Space created successfully") return space, nil }
// CanStartIteration checks the rule - Only one iteration from a space can have state=start at a time. // More rules can be added as needed in this function func (m *GormIterationRepository) CanStartIteration(ctx context.Context, i *Iteration) (bool, error) { var count int64 m.db.Model(&Iteration{}).Where("space_id=? and state=?", i.SpaceID, IterationStateStart).Count(&count) if count != 0 { log.Error(ctx, map[string]interface{}{ "iterationID": i.ID, "spaceID": i.SpaceID, }, "one iteration from given space is already running!") return false, errors.NewBadParameterError("state", "One iteration from given space is already running") } return true, nil }
// Render runs the render action. func (c *RenderController) Render(ctx *app.RenderRenderContext) error { content := ctx.Payload.Data.Attributes.Content markup := ctx.Payload.Data.Attributes.Markup if !rendering.IsMarkupSupported(markup) { return jsonapi.JSONErrorResponse(ctx, errors.NewBadParameterError("Unsupported markup type", markup)) } htmlResult := rendering.RenderMarkupToHTML(content, markup) res := &app.MarkupRenderingSingle{Data: &app.MarkupRenderingData{ ID: uuid.NewV4().String(), Type: RenderingType, Attributes: &app.MarkupRenderingDataAttributes{ RenderedContent: htmlResult, }}} return ctx.OK(res) }
// Create creates a new work item link category in the repository. // Returns BadParameterError, ConversionError or InternalError func (r *GormWorkItemLinkCategoryRepository) Create(ctx context.Context, name *string, description *string) (*app.WorkItemLinkCategorySingle, error) { if name == nil || *name == "" { return nil, errors.NewBadParameterError("name", name) } created := WorkItemLinkCategory{ // Omit "lifecycle" and "ID" fields as they will be filled by the DB Name: *name, Description: description, } db := r.db.Create(&created) if db.Error != nil { return nil, errors.NewInternalError(db.Error.Error()) } // Convert the created link category entry into a JSONAPI response result := ConvertLinkCategoryFromModel(created) return &result, nil }
// Save updates the given work item link in storage. Version must be the same as the one int the stored version. // returns NotFoundError, VersionConflictError, ConversionError or InternalError func (r *GormWorkItemLinkRepository) Save(ctx context.Context, lt app.WorkItemLinkSingle) (*app.WorkItemLinkSingle, error) { res := WorkItemLink{} if lt.Data.ID == nil { return nil, errors.NewBadParameterError("work item link", nil) } db := r.db.Model(&res).Where("id=?", *lt.Data.ID).First(&res) if db.RecordNotFound() { log.Error(ctx, map[string]interface{}{ "wilID": *lt.Data.ID, }, "work item link not found") return nil, errors.NewNotFoundError("work item link", *lt.Data.ID) } if db.Error != nil { log.Error(ctx, map[string]interface{}{ "wilID": *lt.Data.ID, "err": db.Error, }, "unable to find work item link") return nil, errors.NewInternalError(db.Error.Error()) } if lt.Data.Attributes.Version == nil || res.Version != *lt.Data.Attributes.Version { return nil, errors.NewVersionConflictError("version conflict") } if err := ConvertLinkToModel(lt, &res); err != nil { return nil, errs.WithStack(err) } res.Version = res.Version + 1 if err := r.ValidateCorrectSourceAndTargetType(ctx, res.SourceID, res.TargetID, res.LinkTypeID); err != nil { return nil, errs.WithStack(err) } db = r.db.Save(&res) if db.Error != nil { log.Error(ctx, map[string]interface{}{ "wilID": res.ID, "err": db.Error, }, "unable to save work item link") return nil, errors.NewInternalError(db.Error.Error()) } log.Info(ctx, map[string]interface{}{ "pkg": "link", "wilID": res.ID, }, "Work item link updated") result := ConvertLinkFromModel(res) return &result, nil }
// CreateChild runs the create-child action. func (c *AreaController) CreateChild(ctx *app.CreateChildAreaContext) error { _, err := login.ContextIdentity(ctx) if err != nil { return jsonapi.JSONErrorResponse(ctx, goa.ErrUnauthorized(err.Error())) } parentID, err := uuid.FromString(ctx.ID) if err != nil { return jsonapi.JSONErrorResponse(ctx, goa.ErrNotFound(err.Error())) } return application.Transactional(c.db, func(appl application.Application) error { parent, err := appl.Areas().Load(ctx, parentID) if err != nil { return jsonapi.JSONErrorResponse(ctx, goa.ErrNotFound(err.Error())) } reqArea := ctx.Payload.Data if reqArea.Attributes.Name == nil { return jsonapi.JSONErrorResponse(ctx, errors.NewBadParameterError("data.attributes.name", nil).Expected("not nil")) } childPath := area.ConvertToLtreeFormat(parentID.String()) if parent.Path != "" { childPath = parent.Path + pathSepInDatabase + childPath } newArea := area.Area{ SpaceID: parent.SpaceID, Path: childPath, Name: *reqArea.Attributes.Name, } err = appl.Areas().Create(ctx, &newArea) if err != nil { return jsonapi.JSONErrorResponse(ctx, err) } res := &app.AreaSingle{ Data: ConvertArea(appl, ctx.RequestData, &newArea, addResolvedPath), } ctx.ResponseData.Header().Set("Location", rest.AbsoluteURL(ctx.RequestData, app.AreaHref(res.Data.ID))) return ctx.Created(res) }) }
// List runs the list action. // Prev and Next links will be present only when there actually IS a next or previous page. // Last will always be present. Total Item count needs to be computed from the "Last" link. func (c *WorkitemController) List(ctx *app.ListWorkitemContext) error { var additionalQuery []string exp, err := query.Parse(ctx.Filter) if err != nil { return jsonapi.JSONErrorResponse(ctx, errors.NewBadParameterError("could not parse filter", err)) } if ctx.FilterAssignee != nil { assignee := ctx.FilterAssignee exp = criteria.And(exp, criteria.Equals(criteria.Field("system.assignees"), criteria.Literal([]string{*assignee}))) additionalQuery = append(additionalQuery, "filter[assignee]="+*assignee) } if ctx.FilterIteration != nil { iteration := ctx.FilterIteration exp = criteria.And(exp, criteria.Equals(criteria.Field(workitem.SystemIteration), criteria.Literal(string(*iteration)))) additionalQuery = append(additionalQuery, "filter[iteration]="+*iteration) } if ctx.FilterWorkitemtype != nil { wit := ctx.FilterWorkitemtype exp = criteria.And(exp, criteria.Equals(criteria.Field("Type"), criteria.Literal([]string{*wit}))) additionalQuery = append(additionalQuery, "filter[workitemtype]="+*wit) } if ctx.FilterArea != nil { area := ctx.FilterArea exp = criteria.And(exp, criteria.Equals(criteria.Field(workitem.SystemArea), criteria.Literal(string(*area)))) additionalQuery = append(additionalQuery, "filter[area]="+*area) } offset, limit := computePagingLimts(ctx.PageOffset, ctx.PageLimit) return application.Transactional(c.db, func(tx application.Application) error { result, tc, err := tx.WorkItems().List(ctx.Context, exp, &offset, &limit) count := int(tc) if err != nil { return jsonapi.JSONErrorResponse(ctx, errs.Wrap(err, "Error listing work items")) } response := app.WorkItem2List{ Links: &app.PagingLinks{}, Meta: &app.WorkItemListResponseMeta{TotalCount: count}, Data: ConvertWorkItems(ctx.RequestData, result), } setPagingLinks(response.Links, buildAbsoluteURL(ctx.RequestData), len(result), offset, limit, count, additionalQuery...) return ctx.OK(&response) }) }
// parseSearchString accepts a raw string and generates a searchKeyword object func parseSearchString(rawSearchString string) (searchKeyword, error) { // TODO remove special characters and exclaimations if any rawSearchString = strings.Trim(rawSearchString, "/") // get rid of trailing slashes rawSearchString = strings.Trim(rawSearchString, "\"") parts := strings.Fields(rawSearchString) var res searchKeyword for _, part := range parts { // QueryUnescape is required in case of encoded url strings. // And does not harm regular search strings // but this processing is required because at this moment, we do not know if // search input is a regular string or a URL part, err := url.QueryUnescape(part) if err != nil { log.Warn(nil, map[string]interface{}{ "pkg": "search", "part": part, }, "unable to escape url!") } // IF part is for search with id:1234 // TODO: need to find out the way to use ID fields. if strings.HasPrefix(part, "id:") { res.id = append(res.id, strings.TrimPrefix(part, "id:")+":*A") } else if strings.HasPrefix(part, "type:") { typeName := strings.TrimPrefix(part, "type:") if len(typeName) == 0 { return res, errors.NewBadParameterError("Type name must not be empty", part) } res.workItemTypes = append(res.workItemTypes, typeName) } else if govalidator.IsURL(part) { part := strings.ToLower(part) part = trimProtocolFromURLString(part) searchQueryFromURL := getSearchQueryFromURLString(part) res.words = append(res.words, searchQueryFromURL) } else { part := strings.ToLower(part) part = sanitizeURL(part) res.words = append(res.words, part+":*") } } return res, nil }
// Create does POST workitem func (c *WorkitemController) Create(ctx *app.CreateWorkitemContext) error { currentUser, err := login.ContextIdentity(ctx) if err != nil { jerrors, _ := jsonapi.ErrorToJSONAPIErrors(goa.ErrUnauthorized(err.Error())) return ctx.Unauthorized(jerrors) } var wit *string if ctx.Payload.Data != nil && ctx.Payload.Data.Relationships != nil && ctx.Payload.Data.Relationships.BaseType != nil && ctx.Payload.Data.Relationships.BaseType.Data != nil { wit = &ctx.Payload.Data.Relationships.BaseType.Data.ID } if wit == nil { // TODO Figure out path source etc. Should be a required relation return jsonapi.JSONErrorResponse(ctx, errors.NewBadParameterError("Data.Relationships.BaseType.Data.ID", err)) } wi := app.WorkItem{ Fields: make(map[string]interface{}), } return application.Transactional(c.db, func(appl application.Application) error { err := ConvertJSONAPIToWorkItem(appl, *ctx.Payload.Data, &wi) if err != nil { return jsonapi.JSONErrorResponse(ctx, errs.Wrap(err, fmt.Sprintf("Error creating work item"))) } wi, err := appl.WorkItems().Create(ctx, *wit, wi.Fields, currentUser) if err != nil { return jsonapi.JSONErrorResponse(ctx, errs.Wrap(err, fmt.Sprintf("Error creating work item"))) } wi2 := ConvertWorkItem(ctx.RequestData, wi) resp := &app.WorkItem2Single{ Data: wi2, Links: &app.WorkItemLinks{ Self: buildAbsoluteURL(ctx.RequestData), }, } ctx.ResponseData.Header().Set("Location", app.WorkitemHref(wi2.ID)) return ctx.Created(resp) }) }
// Create creates a new work item link in the repository. // Returns BadParameterError, ConversionError or InternalError func (r *GormWorkItemLinkRepository) Create(ctx context.Context, sourceID, targetID uint64, linkTypeID satoriuuid.UUID) (*app.WorkItemLinkSingle, error) { link := &WorkItemLink{ SourceID: sourceID, TargetID: targetID, LinkTypeID: linkTypeID, } if err := link.CheckValidForCreation(); err != nil { return nil, errs.WithStack(err) } if err := r.ValidateCorrectSourceAndTargetType(ctx, sourceID, targetID, linkTypeID); err != nil { return nil, errs.WithStack(err) } db := r.db.Create(link) if db.Error != nil { if gormsupport.IsUniqueViolation(db.Error, "work_item_links_unique_idx") { // TODO(kwk): Make NewBadParameterError a variadic function to avoid this ugliness ;) return nil, errors.NewBadParameterError("data.relationships.source_id + data.relationships.target_id + data.relationships.link_type_id", sourceID).Expected("unique") } return nil, errors.NewInternalError(db.Error.Error()) } // Convert the created link type entry into a JSONAPI response result := ConvertLinkFromModel(*link) return &result, nil }
// CheckValidForCreation returns an error if the work item link type // cannot be used for the creation of a new work item link type. func (t *WorkItemLinkType) CheckValidForCreation() error { if t.Name == "" { return errors.NewBadParameterError("name", t.Name) } if t.SourceTypeName == "" { return errors.NewBadParameterError("source_type_name", t.SourceTypeName) } if t.TargetTypeName == "" { return errors.NewBadParameterError("target_type_name", t.TargetTypeName) } if t.ForwardName == "" { return errors.NewBadParameterError("forward_name", t.ForwardName) } if t.ReverseName == "" { return errors.NewBadParameterError("reverse_name", t.ReverseName) } if err := CheckValidTopology(t.Topology); err != nil { return errs.WithStack(err) } if t.LinkCategoryID == satoriuuid.Nil { return errors.NewBadParameterError("link_category_id", t.LinkCategoryID) } return nil }
// ConvertJSONAPIToWorkItem is responsible for converting given WorkItem model object into a // response resource object by jsonapi.org specifications func ConvertJSONAPIToWorkItem(appl application.Application, source app.WorkItem2, target *app.WorkItem) error { // construct default values from input WI version, err := getVersion(source.Attributes["version"]) if err != nil { return err } target.Version = version if source.Relationships != nil && source.Relationships.Assignees != nil { if source.Relationships.Assignees.Data == nil { delete(target.Fields, workitem.SystemAssignees) } else { var ids []string for _, d := range source.Relationships.Assignees.Data { assigneeUUID, err := uuid.FromString(*d.ID) if err != nil { return errors.NewBadParameterError("data.relationships.assignees.data.id", *d.ID) } if ok := appl.Identities().IsValid(context.Background(), assigneeUUID); !ok { return errors.NewBadParameterError("data.relationships.assignees.data.id", *d.ID) } ids = append(ids, assigneeUUID.String()) } target.Fields[workitem.SystemAssignees] = ids } } if source.Relationships != nil && source.Relationships.Iteration != nil { if source.Relationships.Iteration.Data == nil { delete(target.Fields, workitem.SystemIteration) } else { d := source.Relationships.Iteration.Data iterationUUID, err := uuid.FromString(*d.ID) if err != nil { return errors.NewBadParameterError("data.relationships.iteration.data.id", *d.ID) } if _, err = appl.Iterations().Load(context.Background(), iterationUUID); err != nil { return errors.NewBadParameterError("data.relationships.iteration.data.id", *d.ID) } target.Fields[workitem.SystemIteration] = iterationUUID.String() } } if source.Relationships != nil && source.Relationships.Area != nil { if source.Relationships.Area.Data == nil { delete(target.Fields, workitem.SystemArea) } else { d := source.Relationships.Area.Data areaUUID, err := uuid.FromString(*d.ID) if err != nil { return errors.NewBadParameterError("data.relationships.area.data.id", *d.ID) } if _, err = appl.Areas().Load(context.Background(), areaUUID); err != nil { return errors.NewBadParameterError("data.relationships.area.data.id", *d.ID) } target.Fields[workitem.SystemArea] = areaUUID.String() } } if source.Relationships != nil && source.Relationships.BaseType != nil { if source.Relationships.BaseType.Data != nil { target.Type = source.Relationships.BaseType.Data.ID } } for key, val := range source.Attributes { // convert legacy description to markup content if key == workitem.SystemDescription { if m := rendering.NewMarkupContentFromValue(val); m != nil { target.Fields[key] = *m } } else { target.Fields[key] = val } } if description, ok := target.Fields[workitem.SystemDescription].(rendering.MarkupContent); ok { // verify the description markup if !rendering.IsMarkupSupported(description.Markup) { return errors.NewBadParameterError("data.relationships.attributes[system.description].markup", description.Markup) } } return nil }
// List all comments related to a single item func (m *GormCommentRepository) List(ctx context.Context, parent string, start *int, limit *int) ([]*Comment, uint64, error) { defer goa.MeasureSince([]string{"goa", "db", "comment", "query"}, time.Now()) db := m.db.Model(&Comment{}).Where("parent_id = ?", parent) orgDB := db if start != nil { if *start < 0 { return nil, 0, errors.NewBadParameterError("start", *start) } db = db.Offset(*start) } if limit != nil { if *limit <= 0 { return nil, 0, errors.NewBadParameterError("limit", *limit) } db = db.Limit(*limit) } db = db.Select("count(*) over () as cnt2 , *").Order("created_at desc") rows, err := db.Rows() if err != nil { return nil, 0, err } defer rows.Close() result := []*Comment{} columns, err := rows.Columns() if err != nil { return nil, 0, errors.NewInternalError(err.Error()) } // need to set up a result for Scan() in order to extract total count. var count uint64 var ignore interface{} columnValues := make([]interface{}, len(columns)) for index := range columnValues { columnValues[index] = &ignore } columnValues[0] = &count first := true for rows.Next() { value := &Comment{} db.ScanRows(rows, value) if first { first = false if err = rows.Scan(columnValues...); err != nil { return nil, 0, errors.NewInternalError(err.Error()) } } result = append(result, value) } if first { // means 0 rows were returned from the first query (maybe becaus of offset outside of total count), // need to do a count(*) to find out total orgDB := orgDB.Select("count(*)") rows2, err := orgDB.Rows() defer rows2.Close() if err != nil { return nil, 0, err } rows2.Next() // count(*) will always return a row rows2.Scan(&count) } return result, count, nil }
// extracted this function from List() in order to close the rows object with "defer" for more readability // workaround for https://github.com/lib/pq/issues/81 func (r *GormRepository) listSpaceFromDB(ctx context.Context, q *string, start *int, limit *int) ([]*Space, uint64, error) { db := r.db.Model(&Space{}) orgDB := db if start != nil { if *start < 0 { return nil, 0, errors.NewBadParameterError("start", *start) } db = db.Offset(*start) } if limit != nil { if *limit <= 0 { return nil, 0, errors.NewBadParameterError("limit", *limit) } db = db.Limit(*limit) } db = db.Select("count(*) over () as cnt2 , *") if q != nil { db = db.Where("LOWER(name) LIKE ?", "%"+strings.ToLower(*q)+"%") db = db.Or("LOWER(description) LIKE ?", "%"+strings.ToLower(*q)+"%") } rows, err := db.Rows() if err != nil { return nil, 0, errs.WithStack(err) } defer rows.Close() result := []*Space{} columns, err := rows.Columns() if err != nil { return nil, 0, errors.NewInternalError(err.Error()) } // need to set up a result for Scan() in order to extract total count. var count uint64 var ignore interface{} columnValues := make([]interface{}, len(columns)) for index := range columnValues { columnValues[index] = &ignore } columnValues[0] = &count first := true for rows.Next() { value := Space{} db.ScanRows(rows, &value) if first { first = false if err = rows.Scan(columnValues...); err != nil { return nil, 0, errors.NewInternalError(err.Error()) } } result = append(result, &value) } if first { if q != nil { // If 0 rows were returned from first query during search, then total is 0 count = 0 } else { // means 0 rows were returned from the first query (maybe becaus of offset outside of total count), // need to do a count(*) to find out total orgDB := orgDB.Select("count(*)") rows2, err := orgDB.Rows() defer rows2.Close() if err != nil { return nil, 0, errs.WithStack(err) } rows2.Next() // count(*) will always return a row rows2.Scan(&count) } } return result, count, nil }
// Save updates the given work item in storage. Version must be the same as the one int the stored version // returns NotFoundError, VersionConflictError, ConversionError or InternalError func (r *GormWorkItemRepository) Save(ctx context.Context, wi app.WorkItem) (*app.WorkItem, error) { res := WorkItem{} id, err := strconv.ParseUint(wi.ID, 10, 64) if err != nil || id == 0 { return nil, errors.NewNotFoundError("work item", wi.ID) } log.Info(ctx, map[string]interface{}{ "pkg": "workitem", "wiID": wi.ID, }, "Looking for id for the work item repository") tx := r.db.First(&res, id) if tx.RecordNotFound() { log.Error(ctx, map[string]interface{}{ "wiID": wi.ID, }, "work item repository not found") return nil, errors.NewNotFoundError("work item", wi.ID) } if tx.Error != nil { return nil, errors.NewInternalError(err.Error()) } if res.Version != wi.Version { return nil, errors.NewVersionConflictError("version conflict") } wiType, err := r.wir.LoadTypeFromDB(ctx, wi.Type) if err != nil { return nil, errors.NewBadParameterError("Type", wi.Type) } res.Version = res.Version + 1 res.Type = wi.Type res.Fields = Fields{} for fieldName, fieldDef := range wiType.Fields { if fieldName == SystemCreatedAt { continue } fieldValue := wi.Fields[fieldName] var err error res.Fields[fieldName], err = fieldDef.ConvertToModel(fieldName, fieldValue) if err != nil { return nil, errors.NewBadParameterError(fieldName, fieldValue) } } tx = tx.Where("Version = ?", wi.Version).Save(&res) if err := tx.Error; err != nil { log.Error(ctx, map[string]interface{}{ "wiID": wi.ID, "err": err, }, "unable to save the work item repository") return nil, errors.NewInternalError(err.Error()) } if tx.RowsAffected == 0 { return nil, errors.NewVersionConflictError("version conflict") } log.Info(ctx, map[string]interface{}{ "pkg": "workitem", "wiID": wi.ID, }, "Updated work item repository") return convertWorkItemModelToApp(wiType, &res) }
// extracted this function from List() in order to close the rows object with "defer" for more readability // workaround for https://github.com/lib/pq/issues/81 func (r *GormWorkItemRepository) listItemsFromDB(ctx context.Context, criteria criteria.Expression, start *int, limit *int) ([]WorkItem, uint64, error) { where, parameters, compileError := Compile(criteria) if compileError != nil { return nil, 0, errors.NewBadParameterError("expression", criteria) } log.Info(ctx, map[string]interface{}{ "pkg": "workitem", "where": where, "parameters": parameters, }, "Executing query : '%s' with params %v", where, parameters) db := r.db.Model(&WorkItem{}).Where(where, parameters...) orgDB := db if start != nil { if *start < 0 { return nil, 0, errors.NewBadParameterError("start", *start) } db = db.Offset(*start) } if limit != nil { if *limit <= 0 { return nil, 0, errors.NewBadParameterError("limit", *limit) } db = db.Limit(*limit) } db = db.Select("count(*) over () as cnt2 , *") rows, err := db.Rows() if err != nil { return nil, 0, errs.WithStack(err) } defer rows.Close() result := []WorkItem{} columns, err := rows.Columns() if err != nil { return nil, 0, errors.NewInternalError(err.Error()) } // need to set up a result for Scan() in order to extract total count. var count uint64 var ignore interface{} columnValues := make([]interface{}, len(columns)) for index := range columnValues { columnValues[index] = &ignore } columnValues[0] = &count first := true for rows.Next() { value := WorkItem{} db.ScanRows(rows, &value) if first { first = false if err = rows.Scan(columnValues...); err != nil { return nil, 0, errors.NewInternalError(err.Error()) } } result = append(result, value) } if first { // means 0 rows were returned from the first query (maybe becaus of offset outside of total count), // need to do a count(*) to find out total orgDB := orgDB.Select("count(*)") rows2, err := orgDB.Rows() defer rows2.Close() if err != nil { return nil, 0, errs.WithStack(err) } rows2.Next() // count(*) will always return a row rows2.Scan(&count) } return result, count, nil }
// ConvertLinkToModel converts the incoming app representation of a work item link to the model layout. // Values are only overwrriten if they are set in "in", otherwise the values in "out" remain. // NOTE: Only the LinkTypeID, SourceID, and TargetID fields will be set. // You need to preload the elements after calling this function. func ConvertLinkToModel(in app.WorkItemLinkSingle, out *WorkItemLink) error { attrs := in.Data.Attributes rel := in.Data.Relationships var err error if in.Data.ID != nil { id, err := satoriuuid.FromString(*in.Data.ID) if err != nil { //log.Printf("Error when converting %s to UUID: %s", *in.Data.ID, err.Error()) // treat as not found: clients don't know it must be a UUID return errors.NewNotFoundError("work item link", id.String()) } out.ID = id } if in.Data.Type != EndpointWorkItemLinks { return errors.NewBadParameterError("data.type", in.Data.Type).Expected(EndpointWorkItemLinks) } if attrs != nil { if attrs.Version != nil { out.Version = *attrs.Version } } if rel != nil && rel.LinkType != nil && rel.LinkType.Data != nil { d := rel.LinkType.Data // If the the link category is not nil, it MUST be "workitemlinktypes" if d.Type != EndpointWorkItemLinkTypes { return errors.NewBadParameterError("data.relationships.link_type.data.type", d.Type).Expected(EndpointWorkItemLinkTypes) } // The the link type id MUST NOT be empty if d.ID == "" { return errors.NewBadParameterError("data.relationships.link_type.data.id", d.ID) } if out.LinkTypeID, err = satoriuuid.FromString(d.ID); err != nil { //log.Printf("Error when converting %s to UUID: %s", in.Data.ID, err.Error()) // treat as not found: clients don't know it must be a UUID return errors.NewNotFoundError("work item link type", d.ID) } } if rel != nil && rel.Source != nil && rel.Source.Data != nil { d := rel.Source.Data // If the the source type is not nil, it MUST be "workitems" if d.Type != EndpointWorkItems { return errors.NewBadParameterError("data.relationships.source.data.type", d.Type).Expected(EndpointWorkItems) } // The the work item id MUST NOT be empty if d.ID == "" { return errors.NewBadParameterError("data.relationships.source.data.id", d.ID) } if out.SourceID, err = strconv.ParseUint(d.ID, 10, 64); err != nil { return errors.NewBadParameterError("data.relationships.source.data.id", d.ID) } } if rel != nil && rel.Target != nil && rel.Target.Data != nil { d := rel.Target.Data // If the the target type is not nil, it MUST be "workitems" if d.Type != EndpointWorkItems { return errors.NewBadParameterError("data.relationships.target.data.type", d.Type).Expected(EndpointWorkItems) } // The the work item id MUST NOT be empty if d.ID == "" { return errors.NewBadParameterError("data.relationships.target.data.id", d.ID) } if out.TargetID, err = strconv.ParseUint(d.ID, 10, 64); err != nil { return errors.NewBadParameterError("data.relationships.target.data.id", d.ID) } } return nil }
// CheckValidForCreation returns an error if the work item link // cannot be used for the creation of a new work item link. func (l *WorkItemLink) CheckValidForCreation() error { if satoriuuid.Equal(l.LinkTypeID, satoriuuid.Nil) { return errors.NewBadParameterError("link_type_id", l.LinkTypeID) } return nil }