package fire import ( "context" "encoding/json" "errors" "net/http" "strings" "cloud.google.com/go/firestore" firebase "firebase.google.com/go" ) type onCall func(ctx context.Context, method string, ep *EndPoint) error type onLookup func(ctx context.Context, method string, ep *EndPoint) (map[string]interface{}, error) type onLoadUser func(client *firestore.Client, tx *firestore.Transaction, id string) (*User, error) // EndPoint defines the end point configuration type EndPoint struct { app *firebase.App // firebase app FirestoreClient *firestore.Client // firestore clinent Tx *firestore.Transaction // transaction // collection paths to create, update or delete CollectionPaths []string // doc paths to create, update or delete DocPaths []string // callback to load user OnLoadUser onLoadUser // callback invoked before updating documents Before onCall // callback invoked after updating documents After onCall // privileges of this endpoint Privileges []string // callback invoked between 'Before' and 'After' callbacks // it expects additional data for payload OnLookup onLookup // Payload to update document Payload *Payload // source is original document in 'PUT' and 'DELETE' source map[string]interface{} // user claims userclaim UserClaim } func NewEndPoint(app *firebase.App, colPaths []string, privileges []string, onLoadUser onLoadUser) *EndPoint { return &EndPoint{app: app, CollectionPaths: colPaths, Privileges: privileges, OnLoadUser: onLoadUser} } // // Fire has firebase variables // type Fire struct { // FirestoreClient *firestore.Client // Tx *firestore.Transaction // } func (r EndPoint) getUpdateFields() []firestore.Update { var fields []firestore.Update for k, v := range r.Payload.data { if k == "id" { continue } fields = append(fields, firestore.Update{Path: k, Value: v}) } return fields } func (r EndPoint) getDeleteFields() []firestore.Update { var fields []firestore.Update fields = append(fields, firestore.Update{Path: "update_time", Value: r.Payload.Value("update_time")}) fields = append(fields, firestore.Update{Path: "updated_by", Value: r.Payload.Value("updated_by")}) fields = append(fields, firestore.Update{Path: "updated_by_id", Value: r.Payload.Value("updated_by_id")}) fields = append(fields, firestore.Update{Path: "updated_date", Value: r.Payload.Value("updated_date")}) fields = append(fields, firestore.Update{Path: "delete_time", Value: r.Payload.Value("delete_time")}) return fields } func (r EndPoint) isDeleted() bool { if v, ok := r.source["delete_time"]; ok { var deleteTime int64 switch value := v.(type) { case int: deleteTime = int64(value) case int64: deleteTime = value default: return false } return deleteTime > 0 } return false } func (r EndPoint) substituteVar(ctx context.Context, p string) string { if strings.Index(p, "$") == 0 { key := p[1:] // looup from context _p, ok := getContextValue(ctx, key) if ok { return _p } // lookup from payload v := r.Payload.String(key) if v != "" { return _p } // return '/' return "/" } return p } func (r EndPoint) getPaths(ctx context.Context) []string { paths := make([]string, 0) for _, colPath := range r.CollectionPaths { path := "" ps := strings.Split(colPath, "/") for _, p := range ps { _p := r.substituteVar(ctx, p) if path == "" { path = _p } else { path = path + "/" + _p } } paths = append(paths, path) } return paths } // addPayloadTimeAndUser adds update time, update user func (rec *EndPoint) addPayloadTimeAndUser(client *firestore.Client, tx *firestore.Transaction, userID string, isDelete bool) error { now, _ := NowMM() sec := TimestampMilli(*now) _user, err := rec.OnLoadUser(client, tx, userID) if err != nil { return err } rec.Payload.data["update_time"] = sec rec.Payload.data["updated_by"] = _user.UserName rec.Payload.data["updated_by_id"] = userID rec.Payload.data["updated_date"] = now deleteTime := int64(0) if isDelete { deleteTime = sec } rec.Payload.data["delete_time"] = deleteTime return nil } func (ep *EndPoint) Post(ctx context.Context) (createdDoc map[string]interface{}, err error) { client, err := ep.app.Firestore(ctx) if err != nil { return nil, err } id, _ := ep.Payload.getID() refs := make([]*firestore.DocumentRef, 0) paths := ep.getPaths(ctx) for _, p := range paths { var ref *firestore.DocumentRef if id != "" { ref = ColRef(client, p).Doc(id) } else { ref = ColRef(client, p).NewDoc() } refs = append(refs, ref) } err = client.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error { err := ep.addPayloadTimeAndUser(client, tx, ep.userclaim.UserID, false) if err != nil { return err } ep.FirestoreClient = client ep.Tx = tx // fire := Fire{FirestoreClient: client, Tx: tx} if ep.Before != nil { err = ep.Before(ctx, "POST", ep) if err != nil { return err } } if ep.OnLookup != nil { data, err := ep.OnLookup(ctx, "POST", ep) if err != nil { return err } ep.Payload.Add(data) } for _, ref := range refs { if err := tx.Create(ref, ep.Payload.data); err != nil { return err } refLog := ref.Collection("logs").NewDoc() if err := tx.Create(refLog, ep.Payload); err != nil { return err } } if ep.After != nil { err = ep.After(ctx, "POST", ep) if err != nil { return err } } return nil }) if err != nil { return nil, err } return getDocWithID(ctx, refs[0]) } func (ep *EndPoint) Put(ctx context.Context) (map[string]interface{}, error) { id, err := ep.Payload.getID() if err != nil { return nil, err } client, err := ep.app.Firestore(ctx) if err != nil { return nil, err } refs := make([]*firestore.DocumentRef, 0) // collectin path paths := ep.getPaths(ctx) for _, p := range paths { ref := ColRef(client, p).Doc(id) refs = append(refs, ref) } // doc paths for _, p := range ep.DocPaths { ref := DocRef(client, p) refs = append(refs, ref) } err = client.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error { err := ep.addPayloadTimeAndUser(client, tx, ep.userclaim.UserID, false) if err != nil { return err } ep.FirestoreClient = client ep.Tx = tx if ep.Before != nil { err = ep.Before(ctx, "PUT", ep) if err != nil { return err } } if ep.OnLookup != nil { data, err := ep.OnLookup(ctx, "PUT", ep) if err != nil { return err } ep.Payload.Add(data) } snaps, err := tx.GetAll(refs) if err != nil { return err } for _, snap := range snaps { ep.source = snap.Data() if ep.isDeleted() { return errors.New("unable to update deleted data") } if err := tx.Update(snap.Ref, ep.getUpdateFields()); err != nil { return err } refLog := snap.Ref.Collection("logs").NewDoc() if err := tx.Create(refLog, ep.Payload); err != nil { return err } } if ep.After != nil { err = ep.After(ctx, "PUT", ep) return err } return nil }) if err != nil { return nil, err } return getDocWithID(ctx, refs[0]) } func (ep *EndPoint) Delete(ctx context.Context) (map[string]interface{}, error) { client, err := ep.app.Firestore(ctx) if err != nil { return nil, err } id, err := ep.Payload.getID() if err != nil { return nil, err } refs := make([]*firestore.DocumentRef, 0) // collection paths paths := ep.getPaths(ctx) for _, p := range paths { ref := ColRef(client, p).Doc(id) refs = append(refs, ref) } // doc paths for _, p := range ep.DocPaths { ref := DocRef(client, p) refs = append(refs, ref) } err = client.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error { err := ep.addPayloadTimeAndUser(client, tx, ep.userclaim.UserID, true) if err != nil { return err } ep.FirestoreClient = client ep.Tx = tx if ep.Before != nil { err = ep.Before(ctx, "DELETE", ep) if err != nil { return err } } if ep.OnLookup != nil { data, err := ep.OnLookup(ctx, "DELETE", ep) if err != nil { return err } ep.Payload.Add(data) } snaps, err := tx.GetAll(refs) if err != nil { return err } for _, snap := range snaps { ep.source = snap.Data() if ep.isDeleted() { return errors.New("already deleted") } if err := tx.Update(snap.Ref, ep.getDeleteFields()); err != nil { return err } refLog := snap.Ref.Collection("logs").NewDoc() if err := tx.Create(refLog, ep.Payload); err != nil { return err } } if ep.After != nil { err = ep.After(ctx, "DELETE", ep) return err } return err }) if err != nil { return nil, err } return getDocWithID(ctx, refs[0]) } // Load loads payload and userClaim into EndPoint // return error if no claim found or no privilege in the claim func (ep *EndPoint) Load(ctx context.Context, r *http.Request) error { userClaim, ok := GetUserClaim(ctx) if !ok { return errors.New("user claim not found") } if userClaim.UserID == "" { return errors.New("unidentified user id") } if len(ep.Privileges) > 0 { if has := userClaim.HasPrivileges(ep.Privileges...); !has { return errors.New("no privilege") } } var data map[string]interface{} decoder := json.NewDecoder(r.Body) err := decoder.Decode(&data) if err != nil { return err } ep.Payload = NewPayload(data) ep.userclaim = userClaim return nil }