123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426 |
- 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
- }
|