Commit 1d77fcca authored by Andrei Mihu's avatar Andrei Mihu
Browse files

Console storage handler functions. (#311)

parent 1733e6ba
Loading
Loading
Loading
Loading
+161 −170

File changed.

Preview size limit exceeded, changes collapsed.

+2 −4
Original line number Diff line number Diff line
@@ -216,7 +216,7 @@ service Console {
  }

  // Write a new storage object or replace an existing one.
  rpc WriteStorageObject (WriteStorageObjectRequest) returns (google.protobuf.Empty) {
  rpc WriteStorageObject (WriteStorageObjectRequest) returns (nakama.api.StorageObjectAck) {
    option (google.api.http).post = "/v2/console/storage/{collection}/{key}/{user_id}";
  }
}
@@ -343,8 +343,6 @@ message StorageList {
  repeated nakama.api.StorageObject objects = 1;
  // Approximate total number of storage objects.
  int32 total_count = 2;
  // An (optional) cursor for paging results.
  bytes cursor = 3;
}

// Unlink a particular device ID from a user's account.
@@ -466,7 +464,7 @@ message WriteStorageObjectRequest {
  // Owner user ID.
  string user_id = 3;
  // Value.
  google.protobuf.StringValue value = 4;
  string value = 4;
  // Version for OCC.
  string version = 5;
  // Read permission value.
+23 −6
Original line number Diff line number Diff line
@@ -699,7 +699,7 @@
          "200": {
            "description": "A successful response.",
            "schema": {
              "properties": {}
              "$ref": "#/definitions/apiStorageObjectAck"
            }
          }
        },
@@ -1278,6 +1278,28 @@
      },
      "description": "An object within the storage engine."
    },
    "apiStorageObjectAck": {
      "type": "object",
      "properties": {
        "collection": {
          "type": "string",
          "description": "The collection which stores the object."
        },
        "key": {
          "type": "string",
          "description": "The key of the object within the collection."
        },
        "version": {
          "type": "string",
          "description": "The version hash of the object."
        },
        "user_id": {
          "type": "string",
          "description": "The owner of the object."
        }
      },
      "description": "A storage acknowledgement."
    },
    "apiUser": {
      "type": "object",
      "properties": {
@@ -1492,11 +1514,6 @@
          "type": "integer",
          "format": "int32",
          "description": "Approximate total number of storage objects."
        },
        "cursor": {
          "type": "string",
          "format": "byte",
          "description": "An (optional) cursor for paging results."
        }
      },
      "description": "List of storage objects."
+17 −7
Original line number Diff line number Diff line
@@ -206,6 +206,17 @@ export interface ApiStorageObject {
  // The version hash of the object.
  version?: string;
}
/** A storage acknowledgement. */
export interface ApiStorageObjectAck {
  // The collection which stores the object.
  collection?: string;
  // The key of the object within the collection.
  key?: string;
  // The owner of the object.
  user_id?: string;
  // The version hash of the object.
  version?: string;
}
/** A user in the server. */
export interface ApiUser {
  // A URL for an avatar image.
@@ -291,15 +302,15 @@ export interface ConsoleStatusList {
}
/** List of storage objects. */
export interface ConsoleStorageList {
  // An (optional) cursor for paging results.
  cursor?: string;
  // List of storage objects matching list/filter operation.
  objects?: Array<ApiStorageObject>;
  // Approximate total number of storage objects.
  total_count?: number;
}
/** A list of users. */
export interface ConsoleUserList {
  // A cursor to fetch more results.
  cursor?: string;
  // Approximate total number of users.
  total_count?: number;
  // A list of users.
  users?: Array<ApiUser>;
}
@@ -759,7 +770,7 @@ export const NakamaApi = (configuration: ConfigurationParameters = {
      return this.doFetch(urlPath, "DELETE", queryParams, _body, options)
    },
    /** Write a new storage object or replace an existing one. */
    writeStorageObject(collection: string, key: string, userId: string, options: any = {}): Promise<any> {
    writeStorageObject(collection: string, key: string, userId: string, options: any = {}): Promise<ApiStorageObjectAck> {
      if (collection === null || collection === undefined) {
        throw new Error("'collection' is a required parameter but is null or undefined.");
      }
@@ -820,14 +831,13 @@ export const NakamaApi = (configuration: ConfigurationParameters = {
      return this.doFetch(urlPath, "DELETE", queryParams, _body, options)
    },
    /** List (and optionally filter) users. */
    listUsers(filter?: string, banned?: boolean, tombstones?: boolean, cursor?: string, options: any = {}): Promise<ConsoleUserList> {
    listUsers(filter?: string, banned?: boolean, tombstones?: boolean, options: any = {}): Promise<ConsoleUserList> {
      const urlPath = "/v2/console/user";

      const queryParams = {
        filter: filter,
        banned: banned,
        tombstones: tombstones,
        cursor: cursor,
      } as any;

      let _body = null;
+160 −3
Original line number Diff line number Diff line
@@ -16,23 +16,180 @@ package server

import (
	"context"
	"database/sql"
	"encoding/json"
	"github.com/gofrs/uuid"
	"github.com/golang/protobuf/ptypes/timestamp"
	"github.com/heroiclabs/nakama/api"
	"github.com/heroiclabs/nakama/console"
	"github.com/lib/pq"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"github.com/golang/protobuf/ptypes/empty"
)

func (s *ConsoleServer) DeleteStorage(ctx context.Context, in *empty.Empty) (*empty.Empty, error) {
	_, err := s.db.Exec("TRUNCATE TABLE storage")
	if err != nil {
		s.logger.Error("Failed to truncate Storage table.", zap.Error(err))
		return nil, status.Error(codes.Internal, "An error occurred while deleting storage objects.")
	}
	return &empty.Empty{}, nil
}

func (s *ConsoleServer) DeleteStorageObject(ctx context.Context, in *console.DeleteStorageObjectRequest) (*empty.Empty, error) {
	if in.Collection == "" {
		return nil, status.Error(codes.InvalidArgument, "Requires a valid collection.")
	}
	if in.Key == "" {
		return nil, status.Error(codes.InvalidArgument, "Requires a valid key.")
	}
	userID, err := uuid.FromString(in.UserId)
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID.")
	}

	code, err := StorageDeleteObjects(ctx, s.logger, s.db, true, map[uuid.UUID][]*api.DeleteStorageObjectId{
		userID: []*api.DeleteStorageObjectId{
			&api.DeleteStorageObjectId{
				Collection: in.Collection,
				Key:        in.Key,
				Version:    in.Version,
			},
		},
	})

	if err != nil {
		if code == codes.Internal {
			s.logger.Error("Failed to delete storage object.", zap.Error(err))
			return nil, status.Error(codes.Internal, "An error occurred while deleting storage object.")
		}

		// OCC error or storage not found, no need to log.
		return nil, err
	}

	return &empty.Empty{}, nil
}

func (s *ConsoleServer) ListStorage(ctx context.Context, in *console.ListStorageRequest) (*console.StorageList, error) {
	return &console.StorageList{}, nil
	var userID *uuid.UUID
	if in.UserId != "" {
		uid, err := uuid.FromString(in.UserId)
		if err != nil {
			return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID when provided.")
		}
		userID = &uid
	}

func (s *ConsoleServer) WriteStorageObject(ctx context.Context, in *console.WriteStorageObjectRequest) (*empty.Empty, error) {
	return &empty.Empty{}, nil
	var query string
	params := make([]interface{}, 0, 1)
	if userID == nil {
		query = "SELECT collection, key, user_id, value, version, read, write, create_time, update_time FROM storage LIMIT 50"
	} else {
		query = "SELECT collection, key, user_id, value, version, read, write, create_time, update_time FROM storage WHERE user_id = $1 LIMIT 50"
		params = append(params, *userID)
	}

	rows, err := s.db.QueryContext(ctx, query, params...)
	if err != nil {
		s.logger.Error("Error querying storage objects.", zap.Any("in", in), zap.Error(err))
		return nil, status.Error(codes.Internal, "An error occurred while trying to list storage objects.")
	}

	objects := make([]*api.StorageObject, 0, 50)

	for rows.Next() {
		o := &api.StorageObject{CreateTime: &timestamp.Timestamp{}, UpdateTime: &timestamp.Timestamp{}}
		var createTime pq.NullTime
		var updateTime pq.NullTime
		var userID sql.NullString
		if err := rows.Scan(&o.Collection, &o.Key, &userID, &o.Value, &o.Version, &o.PermissionRead, &o.PermissionWrite, &createTime, &updateTime); err != nil {
			rows.Close()
			s.logger.Error("Error scanning storage objects.", zap.Any("in", in), zap.Error(err))
			return nil, status.Error(codes.Internal, "An error occurred while trying to list storage objects.")
		}

		o.CreateTime.Seconds = createTime.Time.Unix()
		o.UpdateTime.Seconds = updateTime.Time.Unix()

		o.UserId = userID.String
		objects = append(objects, o)
	}
	rows.Close()

	return &console.StorageList{
		Objects:    objects,
		TotalCount: countStorage(ctx, s.logger, s.db),
	}, nil
}

func (s *ConsoleServer) WriteStorageObject(ctx context.Context, in *console.WriteStorageObjectRequest) (*api.StorageObjectAck, error) {
	if in.Collection == "" {
		return nil, status.Error(codes.InvalidArgument, "Requires a valid collection.")
	}
	if in.Key == "" {
		return nil, status.Error(codes.InvalidArgument, "Requires a valid key.")
	}
	userID, err := uuid.FromString(in.UserId)
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID.")
	}
	if in.PermissionRead != nil {
		permissionRead := in.PermissionRead.GetValue()
		if permissionRead < 0 || permissionRead > 2 {
			return nil, status.Error(codes.InvalidArgument, "Requires a valid read permission read if supplied (0-2).")
		}
	}
	if in.PermissionWrite != nil {
		permissionWrite := in.PermissionWrite.GetValue()
		if permissionWrite < 0 || permissionWrite > 1 {
			return nil, status.Error(codes.InvalidArgument, "Requires a valid write permission if supplied (0-1).")
		}
	}

	var maybeJSON map[string]interface{}
	if json.Unmarshal([]byte(in.Value), &maybeJSON) != nil {
		return nil, status.Error(codes.InvalidArgument, "Requires a valid JSON object value.")
	}

	acks, code, err := StorageWriteObjects(ctx, s.logger, s.db, true, map[uuid.UUID][]*api.WriteStorageObject{
		userID: []*api.WriteStorageObject{
			&api.WriteStorageObject{
				Collection:      in.Collection,
				Key:             in.Key,
				Value:           in.Value,
				Version:         in.Version,
				PermissionRead:  in.PermissionRead,
				PermissionWrite: in.PermissionWrite,
			},
		},
	})

	if err != nil {
		if code == codes.Internal {
			s.logger.Error("Failed to write storage object.", zap.Error(err))
			return nil, status.Error(codes.Internal, "An error occurred while writing storage object.")
		}

		// OCC error, no need to log.
		return nil, err
	}

	if acks == nil || len(acks.Acks) != 1 {
		s.logger.Error("Failed to get storage object acks.")
		return nil, status.Error(codes.Internal, "An error occurred while writing storage object.")
	}

	return acks.Acks[0], nil
}

func countStorage(ctx context.Context, logger *zap.Logger, db *sql.DB) int32 {
	var count int
	if err := db.QueryRowContext(ctx, "SELECT count(collection) FROM storage").Scan(&count); err != nil {
		logger.Error("Error counting storage objects.", zap.Error(err))
	}
	return int32(count)
}