Jelajahi Sumber

app crud & test

appflowy 3 tahun lalu
induk
melakukan
f520769283
64 mengubah file dengan 2982 tambahan dan 434 penghapusan
  1. 105 0
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/app_create.pb.dart
  2. 14 0
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/app_create.pbjson.dart
  3. 47 0
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/app_delete.pb.dart
  4. 10 0
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/app_delete.pbjson.dart
  5. 75 0
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/app_query.pb.dart
  6. 12 0
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/app_query.pbjson.dart
  7. 184 0
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/app_update.pb.dart
  8. 22 0
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/app_update.pbjson.dart
  9. 4 0
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/errors.pbenum.dart
  10. 3 1
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/errors.pbjson.dart
  11. 0 13
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/workspace_create.pb.dart
  12. 2 5
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/workspace_create.pbjson.dart
  13. 1 0
      backend/Cargo.toml
  14. 17 1
      backend/doc/database_setup.md
  15. 1 1
      backend/migrations/20210819065837_user.sql
  16. 1 1
      backend/migrations/20210824033032_workspace.sql
  17. 1 1
      backend/migrations/20210824033742_app.sql
  18. 1 1
      backend/migrations/20210824033748_view.sql
  19. 4 4
      backend/src/application.rs
  20. 1 2
      backend/src/routers/utils.rs
  21. 3 0
      backend/src/sqlx_ext/mod.rs
  22. 151 19
      backend/src/sqlx_ext/query.rs
  23. 9 0
      backend/src/sqlx_ext/utils.rs
  24. 1 2
      backend/src/user_service/auth.rs
  25. 1 2
      backend/src/user_service/utils.rs
  26. 214 0
      backend/src/workspace_service/app/app.rs
  27. 26 12
      backend/src/workspace_service/app/router.rs
  28. 1 1
      backend/src/workspace_service/workspace/router.rs
  29. 73 54
      backend/src/workspace_service/workspace/workspace.rs
  30. 35 5
      backend/tests/api/helper.rs
  31. 138 15
      backend/tests/api/workspace.rs
  32. 4 0
      rust-lib/flowy-derive/src/derive_cache/derive_cache.rs
  33. 7 14
      rust-lib/flowy-net/src/errors.rs
  34. 20 7
      rust-lib/flowy-net/src/request/request.rs
  35. 2 6
      rust-lib/flowy-net/src/response/response.rs
  36. 0 1
      rust-lib/flowy-user/src/errors.rs
  37. 4 2
      rust-lib/flowy-user/src/services/user/user_server.rs
  38. 14 1
      rust-lib/flowy-workspace/src/entities/app/app_create.rs
  39. 4 2
      rust-lib/flowy-workspace/src/entities/app/app_delete.rs
  40. 8 2
      rust-lib/flowy-workspace/src/entities/app/app_query.rs
  41. 14 2
      rust-lib/flowy-workspace/src/entities/app/app_update.rs
  42. 17 0
      rust-lib/flowy-workspace/src/entities/app/parser/app_desc.rs
  43. 4 4
      rust-lib/flowy-workspace/src/entities/app/parser/app_id.rs
  44. 4 2
      rust-lib/flowy-workspace/src/entities/app/parser/mod.rs
  45. 2 2
      rust-lib/flowy-workspace/src/entities/view/view_create.rs
  46. 2 0
      rust-lib/flowy-workspace/src/entities/workspace/parser/mod.rs
  47. 18 0
      rust-lib/flowy-workspace/src/entities/workspace/parser/workspace_desc.rs
  48. 11 5
      rust-lib/flowy-workspace/src/entities/workspace/workspace_create.rs
  49. 1 1
      rust-lib/flowy-workspace/src/entities/workspace/workspace_delete.rs
  50. 15 3
      rust-lib/flowy-workspace/src/errors.rs
  51. 408 46
      rust-lib/flowy-workspace/src/protobuf/model/app_create.rs
  52. 169 5
      rust-lib/flowy-workspace/src/protobuf/model/app_delete.rs
  53. 252 11
      rust-lib/flowy-workspace/src/protobuf/model/app_query.rs
  54. 606 26
      rust-lib/flowy-workspace/src/protobuf/model/app_update.rs
  55. 58 46
      rust-lib/flowy-workspace/src/protobuf/model/errors.rs
  56. 57 98
      rust-lib/flowy-workspace/src/protobuf/model/workspace_create.rs
  57. 7 0
      rust-lib/flowy-workspace/src/protobuf/proto/app_create.proto
  58. 3 0
      rust-lib/flowy-workspace/src/protobuf/proto/app_delete.proto
  59. 5 0
      rust-lib/flowy-workspace/src/protobuf/proto/app_query.proto
  60. 8 0
      rust-lib/flowy-workspace/src/protobuf/proto/app_update.proto
  61. 2 0
      rust-lib/flowy-workspace/src/protobuf/proto/errors.proto
  62. 1 1
      rust-lib/flowy-workspace/src/protobuf/proto/workspace_create.proto
  63. 56 1
      rust-lib/flowy-workspace/src/services/app_controller.rs
  64. 42 6
      rust-lib/flowy-workspace/src/services/workspace_controller.rs

+ 105 - 0
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/app_create.pb.dart

@@ -150,6 +150,111 @@ class ColorStyle extends $pb.GeneratedMessage {
   void clearThemeColor() => clearField(1);
 }
 
+class CreateAppParams extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'CreateAppParams', createEmptyInstance: create)
+    ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'workspaceId')
+    ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'name')
+    ..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'desc')
+    ..aOM<ColorStyle>(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'colorStyle', subBuilder: ColorStyle.create)
+    ..aOS(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'userId')
+    ..hasRequiredFields = false
+  ;
+
+  CreateAppParams._() : super();
+  factory CreateAppParams({
+    $core.String? workspaceId,
+    $core.String? name,
+    $core.String? desc,
+    ColorStyle? colorStyle,
+    $core.String? userId,
+  }) {
+    final _result = create();
+    if (workspaceId != null) {
+      _result.workspaceId = workspaceId;
+    }
+    if (name != null) {
+      _result.name = name;
+    }
+    if (desc != null) {
+      _result.desc = desc;
+    }
+    if (colorStyle != null) {
+      _result.colorStyle = colorStyle;
+    }
+    if (userId != null) {
+      _result.userId = userId;
+    }
+    return _result;
+  }
+  factory CreateAppParams.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory CreateAppParams.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  CreateAppParams clone() => CreateAppParams()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  CreateAppParams copyWith(void Function(CreateAppParams) updates) => super.copyWith((message) => updates(message as CreateAppParams)) as CreateAppParams; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static CreateAppParams create() => CreateAppParams._();
+  CreateAppParams createEmptyInstance() => create();
+  static $pb.PbList<CreateAppParams> createRepeated() => $pb.PbList<CreateAppParams>();
+  @$core.pragma('dart2js:noInline')
+  static CreateAppParams getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<CreateAppParams>(create);
+  static CreateAppParams? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.String get workspaceId => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set workspaceId($core.String v) { $_setString(0, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasWorkspaceId() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearWorkspaceId() => clearField(1);
+
+  @$pb.TagNumber(2)
+  $core.String get name => $_getSZ(1);
+  @$pb.TagNumber(2)
+  set name($core.String v) { $_setString(1, v); }
+  @$pb.TagNumber(2)
+  $core.bool hasName() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearName() => clearField(2);
+
+  @$pb.TagNumber(3)
+  $core.String get desc => $_getSZ(2);
+  @$pb.TagNumber(3)
+  set desc($core.String v) { $_setString(2, v); }
+  @$pb.TagNumber(3)
+  $core.bool hasDesc() => $_has(2);
+  @$pb.TagNumber(3)
+  void clearDesc() => clearField(3);
+
+  @$pb.TagNumber(4)
+  ColorStyle get colorStyle => $_getN(3);
+  @$pb.TagNumber(4)
+  set colorStyle(ColorStyle v) { setField(4, v); }
+  @$pb.TagNumber(4)
+  $core.bool hasColorStyle() => $_has(3);
+  @$pb.TagNumber(4)
+  void clearColorStyle() => clearField(4);
+  @$pb.TagNumber(4)
+  ColorStyle ensureColorStyle() => $_ensure(3);
+
+  @$pb.TagNumber(5)
+  $core.String get userId => $_getSZ(4);
+  @$pb.TagNumber(5)
+  set userId($core.String v) { $_setString(4, v); }
+  @$pb.TagNumber(5)
+  $core.bool hasUserId() => $_has(4);
+  @$pb.TagNumber(5)
+  void clearUserId() => clearField(5);
+}
+
 class App extends $pb.GeneratedMessage {
   static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'App', createEmptyInstance: create)
     ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'id')

+ 14 - 0
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/app_create.pbjson.dart

@@ -31,6 +31,20 @@ const ColorStyle$json = const {
 
 /// Descriptor for `ColorStyle`. Decode as a `google.protobuf.DescriptorProto`.
 final $typed_data.Uint8List colorStyleDescriptor = $convert.base64Decode('CgpDb2xvclN0eWxlEh8KC3RoZW1lX2NvbG9yGAEgASgJUgp0aGVtZUNvbG9y');
+@$core.Deprecated('Use createAppParamsDescriptor instead')
+const CreateAppParams$json = const {
+  '1': 'CreateAppParams',
+  '2': const [
+    const {'1': 'workspace_id', '3': 1, '4': 1, '5': 9, '10': 'workspaceId'},
+    const {'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'},
+    const {'1': 'desc', '3': 3, '4': 1, '5': 9, '10': 'desc'},
+    const {'1': 'color_style', '3': 4, '4': 1, '5': 11, '6': '.ColorStyle', '10': 'colorStyle'},
+    const {'1': 'user_id', '3': 5, '4': 1, '5': 9, '10': 'userId'},
+  ],
+};
+
+/// Descriptor for `CreateAppParams`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List createAppParamsDescriptor = $convert.base64Decode('Cg9DcmVhdGVBcHBQYXJhbXMSIQoMd29ya3NwYWNlX2lkGAEgASgJUgt3b3Jrc3BhY2VJZBISCgRuYW1lGAIgASgJUgRuYW1lEhIKBGRlc2MYAyABKAlSBGRlc2MSLAoLY29sb3Jfc3R5bGUYBCABKAsyCy5Db2xvclN0eWxlUgpjb2xvclN0eWxlEhcKB3VzZXJfaWQYBSABKAlSBnVzZXJJZA==');
 @$core.Deprecated('Use appDescriptor instead')
 const App$json = const {
   '1': 'App',

+ 47 - 0
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/app_delete.pb.dart

@@ -56,3 +56,50 @@ class DeleteAppRequest extends $pb.GeneratedMessage {
   void clearAppId() => clearField(1);
 }
 
+class DeleteAppParams extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'DeleteAppParams', createEmptyInstance: create)
+    ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'appId')
+    ..hasRequiredFields = false
+  ;
+
+  DeleteAppParams._() : super();
+  factory DeleteAppParams({
+    $core.String? appId,
+  }) {
+    final _result = create();
+    if (appId != null) {
+      _result.appId = appId;
+    }
+    return _result;
+  }
+  factory DeleteAppParams.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory DeleteAppParams.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  DeleteAppParams clone() => DeleteAppParams()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  DeleteAppParams copyWith(void Function(DeleteAppParams) updates) => super.copyWith((message) => updates(message as DeleteAppParams)) as DeleteAppParams; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static DeleteAppParams create() => DeleteAppParams._();
+  DeleteAppParams createEmptyInstance() => create();
+  static $pb.PbList<DeleteAppParams> createRepeated() => $pb.PbList<DeleteAppParams>();
+  @$core.pragma('dart2js:noInline')
+  static DeleteAppParams getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DeleteAppParams>(create);
+  static DeleteAppParams? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.String get appId => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set appId($core.String v) { $_setString(0, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasAppId() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearAppId() => clearField(1);
+}
+

+ 10 - 0
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/app_delete.pbjson.dart

@@ -18,3 +18,13 @@ const DeleteAppRequest$json = const {
 
 /// Descriptor for `DeleteAppRequest`. Decode as a `google.protobuf.DescriptorProto`.
 final $typed_data.Uint8List deleteAppRequestDescriptor = $convert.base64Decode('ChBEZWxldGVBcHBSZXF1ZXN0EhUKBmFwcF9pZBgBIAEoCVIFYXBwSWQ=');
+@$core.Deprecated('Use deleteAppParamsDescriptor instead')
+const DeleteAppParams$json = const {
+  '1': 'DeleteAppParams',
+  '2': const [
+    const {'1': 'app_id', '3': 1, '4': 1, '5': 9, '10': 'appId'},
+  ],
+};
+
+/// Descriptor for `DeleteAppParams`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List deleteAppParamsDescriptor = $convert.base64Decode('Cg9EZWxldGVBcHBQYXJhbXMSFQoGYXBwX2lkGAEgASgJUgVhcHBJZA==');

+ 75 - 0
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/app_query.pb.dart

@@ -84,3 +84,78 @@ class QueryAppRequest extends $pb.GeneratedMessage {
   void clearIsTrash() => clearField(3);
 }
 
+class QueryAppParams extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'QueryAppParams', createEmptyInstance: create)
+    ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'appId')
+    ..aOB(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'readBelongings')
+    ..aOB(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'isTrash')
+    ..hasRequiredFields = false
+  ;
+
+  QueryAppParams._() : super();
+  factory QueryAppParams({
+    $core.String? appId,
+    $core.bool? readBelongings,
+    $core.bool? isTrash,
+  }) {
+    final _result = create();
+    if (appId != null) {
+      _result.appId = appId;
+    }
+    if (readBelongings != null) {
+      _result.readBelongings = readBelongings;
+    }
+    if (isTrash != null) {
+      _result.isTrash = isTrash;
+    }
+    return _result;
+  }
+  factory QueryAppParams.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory QueryAppParams.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  QueryAppParams clone() => QueryAppParams()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  QueryAppParams copyWith(void Function(QueryAppParams) updates) => super.copyWith((message) => updates(message as QueryAppParams)) as QueryAppParams; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static QueryAppParams create() => QueryAppParams._();
+  QueryAppParams createEmptyInstance() => create();
+  static $pb.PbList<QueryAppParams> createRepeated() => $pb.PbList<QueryAppParams>();
+  @$core.pragma('dart2js:noInline')
+  static QueryAppParams getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<QueryAppParams>(create);
+  static QueryAppParams? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.String get appId => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set appId($core.String v) { $_setString(0, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasAppId() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearAppId() => clearField(1);
+
+  @$pb.TagNumber(2)
+  $core.bool get readBelongings => $_getBF(1);
+  @$pb.TagNumber(2)
+  set readBelongings($core.bool v) { $_setBool(1, v); }
+  @$pb.TagNumber(2)
+  $core.bool hasReadBelongings() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearReadBelongings() => clearField(2);
+
+  @$pb.TagNumber(3)
+  $core.bool get isTrash => $_getBF(2);
+  @$pb.TagNumber(3)
+  set isTrash($core.bool v) { $_setBool(2, v); }
+  @$pb.TagNumber(3)
+  $core.bool hasIsTrash() => $_has(2);
+  @$pb.TagNumber(3)
+  void clearIsTrash() => clearField(3);
+}
+

+ 12 - 0
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/app_query.pbjson.dart

@@ -20,3 +20,15 @@ const QueryAppRequest$json = const {
 
 /// Descriptor for `QueryAppRequest`. Decode as a `google.protobuf.DescriptorProto`.
 final $typed_data.Uint8List queryAppRequestDescriptor = $convert.base64Decode('Cg9RdWVyeUFwcFJlcXVlc3QSFQoGYXBwX2lkGAEgASgJUgVhcHBJZBInCg9yZWFkX2JlbG9uZ2luZ3MYAiABKAhSDnJlYWRCZWxvbmdpbmdzEhkKCGlzX3RyYXNoGAMgASgIUgdpc1RyYXNo');
+@$core.Deprecated('Use queryAppParamsDescriptor instead')
+const QueryAppParams$json = const {
+  '1': 'QueryAppParams',
+  '2': const [
+    const {'1': 'app_id', '3': 1, '4': 1, '5': 9, '10': 'appId'},
+    const {'1': 'read_belongings', '3': 2, '4': 1, '5': 8, '10': 'readBelongings'},
+    const {'1': 'is_trash', '3': 3, '4': 1, '5': 8, '10': 'isTrash'},
+  ],
+};
+
+/// Descriptor for `QueryAppParams`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List queryAppParamsDescriptor = $convert.base64Decode('Cg5RdWVyeUFwcFBhcmFtcxIVCgZhcHBfaWQYASABKAlSBWFwcElkEicKD3JlYWRfYmVsb25naW5ncxgCIAEoCFIOcmVhZEJlbG9uZ2luZ3MSGQoIaXNfdHJhc2gYAyABKAhSB2lzVHJhc2g=');

+ 184 - 0
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/app_update.pb.dart

@@ -195,3 +195,187 @@ class UpdateAppRequest extends $pb.GeneratedMessage {
   void clearIsTrash() => clearField(6);
 }
 
+enum UpdateAppParams_OneOfWorkspaceId {
+  workspaceId, 
+  notSet
+}
+
+enum UpdateAppParams_OneOfName {
+  name, 
+  notSet
+}
+
+enum UpdateAppParams_OneOfDesc {
+  desc, 
+  notSet
+}
+
+enum UpdateAppParams_OneOfColorStyle {
+  colorStyle, 
+  notSet
+}
+
+enum UpdateAppParams_OneOfIsTrash {
+  isTrash, 
+  notSet
+}
+
+class UpdateAppParams extends $pb.GeneratedMessage {
+  static const $core.Map<$core.int, UpdateAppParams_OneOfWorkspaceId> _UpdateAppParams_OneOfWorkspaceIdByTag = {
+    2 : UpdateAppParams_OneOfWorkspaceId.workspaceId,
+    0 : UpdateAppParams_OneOfWorkspaceId.notSet
+  };
+  static const $core.Map<$core.int, UpdateAppParams_OneOfName> _UpdateAppParams_OneOfNameByTag = {
+    3 : UpdateAppParams_OneOfName.name,
+    0 : UpdateAppParams_OneOfName.notSet
+  };
+  static const $core.Map<$core.int, UpdateAppParams_OneOfDesc> _UpdateAppParams_OneOfDescByTag = {
+    4 : UpdateAppParams_OneOfDesc.desc,
+    0 : UpdateAppParams_OneOfDesc.notSet
+  };
+  static const $core.Map<$core.int, UpdateAppParams_OneOfColorStyle> _UpdateAppParams_OneOfColorStyleByTag = {
+    5 : UpdateAppParams_OneOfColorStyle.colorStyle,
+    0 : UpdateAppParams_OneOfColorStyle.notSet
+  };
+  static const $core.Map<$core.int, UpdateAppParams_OneOfIsTrash> _UpdateAppParams_OneOfIsTrashByTag = {
+    6 : UpdateAppParams_OneOfIsTrash.isTrash,
+    0 : UpdateAppParams_OneOfIsTrash.notSet
+  };
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'UpdateAppParams', createEmptyInstance: create)
+    ..oo(0, [2])
+    ..oo(1, [3])
+    ..oo(2, [4])
+    ..oo(3, [5])
+    ..oo(4, [6])
+    ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'appId')
+    ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'workspaceId')
+    ..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'name')
+    ..aOS(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'desc')
+    ..aOM<$0.ColorStyle>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'colorStyle', subBuilder: $0.ColorStyle.create)
+    ..aOB(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'isTrash')
+    ..hasRequiredFields = false
+  ;
+
+  UpdateAppParams._() : super();
+  factory UpdateAppParams({
+    $core.String? appId,
+    $core.String? workspaceId,
+    $core.String? name,
+    $core.String? desc,
+    $0.ColorStyle? colorStyle,
+    $core.bool? isTrash,
+  }) {
+    final _result = create();
+    if (appId != null) {
+      _result.appId = appId;
+    }
+    if (workspaceId != null) {
+      _result.workspaceId = workspaceId;
+    }
+    if (name != null) {
+      _result.name = name;
+    }
+    if (desc != null) {
+      _result.desc = desc;
+    }
+    if (colorStyle != null) {
+      _result.colorStyle = colorStyle;
+    }
+    if (isTrash != null) {
+      _result.isTrash = isTrash;
+    }
+    return _result;
+  }
+  factory UpdateAppParams.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory UpdateAppParams.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  UpdateAppParams clone() => UpdateAppParams()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  UpdateAppParams copyWith(void Function(UpdateAppParams) updates) => super.copyWith((message) => updates(message as UpdateAppParams)) as UpdateAppParams; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static UpdateAppParams create() => UpdateAppParams._();
+  UpdateAppParams createEmptyInstance() => create();
+  static $pb.PbList<UpdateAppParams> createRepeated() => $pb.PbList<UpdateAppParams>();
+  @$core.pragma('dart2js:noInline')
+  static UpdateAppParams getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<UpdateAppParams>(create);
+  static UpdateAppParams? _defaultInstance;
+
+  UpdateAppParams_OneOfWorkspaceId whichOneOfWorkspaceId() => _UpdateAppParams_OneOfWorkspaceIdByTag[$_whichOneof(0)]!;
+  void clearOneOfWorkspaceId() => clearField($_whichOneof(0));
+
+  UpdateAppParams_OneOfName whichOneOfName() => _UpdateAppParams_OneOfNameByTag[$_whichOneof(1)]!;
+  void clearOneOfName() => clearField($_whichOneof(1));
+
+  UpdateAppParams_OneOfDesc whichOneOfDesc() => _UpdateAppParams_OneOfDescByTag[$_whichOneof(2)]!;
+  void clearOneOfDesc() => clearField($_whichOneof(2));
+
+  UpdateAppParams_OneOfColorStyle whichOneOfColorStyle() => _UpdateAppParams_OneOfColorStyleByTag[$_whichOneof(3)]!;
+  void clearOneOfColorStyle() => clearField($_whichOneof(3));
+
+  UpdateAppParams_OneOfIsTrash whichOneOfIsTrash() => _UpdateAppParams_OneOfIsTrashByTag[$_whichOneof(4)]!;
+  void clearOneOfIsTrash() => clearField($_whichOneof(4));
+
+  @$pb.TagNumber(1)
+  $core.String get appId => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set appId($core.String v) { $_setString(0, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasAppId() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearAppId() => clearField(1);
+
+  @$pb.TagNumber(2)
+  $core.String get workspaceId => $_getSZ(1);
+  @$pb.TagNumber(2)
+  set workspaceId($core.String v) { $_setString(1, v); }
+  @$pb.TagNumber(2)
+  $core.bool hasWorkspaceId() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearWorkspaceId() => clearField(2);
+
+  @$pb.TagNumber(3)
+  $core.String get name => $_getSZ(2);
+  @$pb.TagNumber(3)
+  set name($core.String v) { $_setString(2, v); }
+  @$pb.TagNumber(3)
+  $core.bool hasName() => $_has(2);
+  @$pb.TagNumber(3)
+  void clearName() => clearField(3);
+
+  @$pb.TagNumber(4)
+  $core.String get desc => $_getSZ(3);
+  @$pb.TagNumber(4)
+  set desc($core.String v) { $_setString(3, v); }
+  @$pb.TagNumber(4)
+  $core.bool hasDesc() => $_has(3);
+  @$pb.TagNumber(4)
+  void clearDesc() => clearField(4);
+
+  @$pb.TagNumber(5)
+  $0.ColorStyle get colorStyle => $_getN(4);
+  @$pb.TagNumber(5)
+  set colorStyle($0.ColorStyle v) { setField(5, v); }
+  @$pb.TagNumber(5)
+  $core.bool hasColorStyle() => $_has(4);
+  @$pb.TagNumber(5)
+  void clearColorStyle() => clearField(5);
+  @$pb.TagNumber(5)
+  $0.ColorStyle ensureColorStyle() => $_ensure(4);
+
+  @$pb.TagNumber(6)
+  $core.bool get isTrash => $_getBF(5);
+  @$pb.TagNumber(6)
+  set isTrash($core.bool v) { $_setBool(5, v); }
+  @$pb.TagNumber(6)
+  $core.bool hasIsTrash() => $_has(5);
+  @$pb.TagNumber(6)
+  void clearIsTrash() => clearField(6);
+}
+

+ 22 - 0
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/app_update.pbjson.dart

@@ -30,3 +30,25 @@ const UpdateAppRequest$json = const {
 
 /// Descriptor for `UpdateAppRequest`. Decode as a `google.protobuf.DescriptorProto`.
 final $typed_data.Uint8List updateAppRequestDescriptor = $convert.base64Decode('ChBVcGRhdGVBcHBSZXF1ZXN0EhUKBmFwcF9pZBgBIAEoCVIFYXBwSWQSIwoMd29ya3NwYWNlX2lkGAIgASgJSABSC3dvcmtzcGFjZUlkEhQKBG5hbWUYAyABKAlIAVIEbmFtZRIUCgRkZXNjGAQgASgJSAJSBGRlc2MSLgoLY29sb3Jfc3R5bGUYBSABKAsyCy5Db2xvclN0eWxlSANSCmNvbG9yU3R5bGUSGwoIaXNfdHJhc2gYBiABKAhIBFIHaXNUcmFzaEIVChNvbmVfb2Zfd29ya3NwYWNlX2lkQg0KC29uZV9vZl9uYW1lQg0KC29uZV9vZl9kZXNjQhQKEm9uZV9vZl9jb2xvcl9zdHlsZUIRCg9vbmVfb2ZfaXNfdHJhc2g=');
+@$core.Deprecated('Use updateAppParamsDescriptor instead')
+const UpdateAppParams$json = const {
+  '1': 'UpdateAppParams',
+  '2': const [
+    const {'1': 'app_id', '3': 1, '4': 1, '5': 9, '10': 'appId'},
+    const {'1': 'workspace_id', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'workspaceId'},
+    const {'1': 'name', '3': 3, '4': 1, '5': 9, '9': 1, '10': 'name'},
+    const {'1': 'desc', '3': 4, '4': 1, '5': 9, '9': 2, '10': 'desc'},
+    const {'1': 'color_style', '3': 5, '4': 1, '5': 11, '6': '.ColorStyle', '9': 3, '10': 'colorStyle'},
+    const {'1': 'is_trash', '3': 6, '4': 1, '5': 8, '9': 4, '10': 'isTrash'},
+  ],
+  '8': const [
+    const {'1': 'one_of_workspace_id'},
+    const {'1': 'one_of_name'},
+    const {'1': 'one_of_desc'},
+    const {'1': 'one_of_color_style'},
+    const {'1': 'one_of_is_trash'},
+  ],
+};
+
+/// Descriptor for `UpdateAppParams`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List updateAppParamsDescriptor = $convert.base64Decode('Cg9VcGRhdGVBcHBQYXJhbXMSFQoGYXBwX2lkGAEgASgJUgVhcHBJZBIjCgx3b3Jrc3BhY2VfaWQYAiABKAlIAFILd29ya3NwYWNlSWQSFAoEbmFtZRgDIAEoCUgBUgRuYW1lEhQKBGRlc2MYBCABKAlIAlIEZGVzYxIuCgtjb2xvcl9zdHlsZRgFIAEoCzILLkNvbG9yU3R5bGVIA1IKY29sb3JTdHlsZRIbCghpc190cmFzaBgGIAEoCEgEUgdpc1RyYXNoQhUKE29uZV9vZl93b3Jrc3BhY2VfaWRCDQoLb25lX29mX25hbWVCDQoLb25lX29mX2Rlc2NCFAoSb25lX29mX2NvbG9yX3N0eWxlQhEKD29uZV9vZl9pc190cmFzaA==');

+ 4 - 0
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/errors.pbenum.dart

@@ -14,6 +14,7 @@ class WsErrCode extends $pb.ProtobufEnum {
   static const WsErrCode WorkspaceNameInvalid = WsErrCode._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'WorkspaceNameInvalid');
   static const WsErrCode WorkspaceIdInvalid = WsErrCode._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'WorkspaceIdInvalid');
   static const WsErrCode AppColorStyleInvalid = WsErrCode._(3, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'AppColorStyleInvalid');
+  static const WsErrCode WorkspaceDescInvalid = WsErrCode._(4, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'WorkspaceDescInvalid');
   static const WsErrCode AppIdInvalid = WsErrCode._(10, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'AppIdInvalid');
   static const WsErrCode AppNameInvalid = WsErrCode._(11, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'AppNameInvalid');
   static const WsErrCode ViewNameInvalid = WsErrCode._(20, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ViewNameInvalid');
@@ -25,12 +26,14 @@ class WsErrCode extends $pb.ProtobufEnum {
   static const WsErrCode UserInternalError = WsErrCode._(102, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'UserInternalError');
   static const WsErrCode UserNotLoginYet = WsErrCode._(103, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'UserNotLoginYet');
   static const WsErrCode ServerError = WsErrCode._(1000, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ServerError');
+  static const WsErrCode RecordNotFound = WsErrCode._(1001, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'RecordNotFound');
 
   static const $core.List<WsErrCode> values = <WsErrCode> [
     Unknown,
     WorkspaceNameInvalid,
     WorkspaceIdInvalid,
     AppColorStyleInvalid,
+    WorkspaceDescInvalid,
     AppIdInvalid,
     AppNameInvalid,
     ViewNameInvalid,
@@ -42,6 +45,7 @@ class WsErrCode extends $pb.ProtobufEnum {
     UserInternalError,
     UserNotLoginYet,
     ServerError,
+    RecordNotFound,
   ];
 
   static final $core.Map<$core.int, WsErrCode> _byValue = $pb.ProtobufEnum.initByValue(values);

+ 3 - 1
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/errors.pbjson.dart

@@ -16,6 +16,7 @@ const WsErrCode$json = const {
     const {'1': 'WorkspaceNameInvalid', '2': 1},
     const {'1': 'WorkspaceIdInvalid', '2': 2},
     const {'1': 'AppColorStyleInvalid', '2': 3},
+    const {'1': 'WorkspaceDescInvalid', '2': 4},
     const {'1': 'AppIdInvalid', '2': 10},
     const {'1': 'AppNameInvalid', '2': 11},
     const {'1': 'ViewNameInvalid', '2': 20},
@@ -27,11 +28,12 @@ const WsErrCode$json = const {
     const {'1': 'UserInternalError', '2': 102},
     const {'1': 'UserNotLoginYet', '2': 103},
     const {'1': 'ServerError', '2': 1000},
+    const {'1': 'RecordNotFound', '2': 1001},
   ],
 };
 
 /// Descriptor for `WsErrCode`. Decode as a `google.protobuf.EnumDescriptorProto`.
-final $typed_data.Uint8List wsErrCodeDescriptor = $convert.base64Decode('CglXc0VyckNvZGUSCwoHVW5rbm93bhAAEhgKFFdvcmtzcGFjZU5hbWVJbnZhbGlkEAESFgoSV29ya3NwYWNlSWRJbnZhbGlkEAISGAoUQXBwQ29sb3JTdHlsZUludmFsaWQQAxIQCgxBcHBJZEludmFsaWQQChISCg5BcHBOYW1lSW52YWxpZBALEhMKD1ZpZXdOYW1lSW52YWxpZBAUEhgKFFZpZXdUaHVtYm5haWxJbnZhbGlkEBUSEQoNVmlld0lkSW52YWxpZBAWEhMKD1ZpZXdEZXNjSW52YWxpZBAXEhoKFkRhdGFiYXNlQ29ubmVjdGlvbkZhaWwQZBIaChZXb3Jrc3BhY2VEYXRhYmFzZUVycm9yEGUSFQoRVXNlckludGVybmFsRXJyb3IQZhITCg9Vc2VyTm90TG9naW5ZZXQQZxIQCgtTZXJ2ZXJFcnJvchDoBw==');
+final $typed_data.Uint8List wsErrCodeDescriptor = $convert.base64Decode('CglXc0VyckNvZGUSCwoHVW5rbm93bhAAEhgKFFdvcmtzcGFjZU5hbWVJbnZhbGlkEAESFgoSV29ya3NwYWNlSWRJbnZhbGlkEAISGAoUQXBwQ29sb3JTdHlsZUludmFsaWQQAxIYChRXb3Jrc3BhY2VEZXNjSW52YWxpZBAEEhAKDEFwcElkSW52YWxpZBAKEhIKDkFwcE5hbWVJbnZhbGlkEAsSEwoPVmlld05hbWVJbnZhbGlkEBQSGAoUVmlld1RodW1ibmFpbEludmFsaWQQFRIRCg1WaWV3SWRJbnZhbGlkEBYSEwoPVmlld0Rlc2NJbnZhbGlkEBcSGgoWRGF0YWJhc2VDb25uZWN0aW9uRmFpbBBkEhoKFldvcmtzcGFjZURhdGFiYXNlRXJyb3IQZRIVChFVc2VySW50ZXJuYWxFcnJvchBmEhMKD1VzZXJOb3RMb2dpbllldBBnEhAKC1NlcnZlckVycm9yEOgHEhMKDlJlY29yZE5vdEZvdW5kEOkH');
 @$core.Deprecated('Use workspaceErrorDescriptor instead')
 const WorkspaceError$json = const {
   '1': 'WorkspaceError',

+ 0 - 13
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/workspace_create.pb.dart

@@ -72,18 +72,8 @@ class CreateWorkspaceRequest extends $pb.GeneratedMessage {
   void clearDesc() => clearField(2);
 }
 
-enum CreateWorkspaceParams_OneOfUserId {
-  userId, 
-  notSet
-}
-
 class CreateWorkspaceParams extends $pb.GeneratedMessage {
-  static const $core.Map<$core.int, CreateWorkspaceParams_OneOfUserId> _CreateWorkspaceParams_OneOfUserIdByTag = {
-    3 : CreateWorkspaceParams_OneOfUserId.userId,
-    0 : CreateWorkspaceParams_OneOfUserId.notSet
-  };
   static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'CreateWorkspaceParams', createEmptyInstance: create)
-    ..oo(0, [3])
     ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'name')
     ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'desc')
     ..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'userId')
@@ -129,9 +119,6 @@ class CreateWorkspaceParams extends $pb.GeneratedMessage {
   static CreateWorkspaceParams getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<CreateWorkspaceParams>(create);
   static CreateWorkspaceParams? _defaultInstance;
 
-  CreateWorkspaceParams_OneOfUserId whichOneOfUserId() => _CreateWorkspaceParams_OneOfUserIdByTag[$_whichOneof(0)]!;
-  void clearOneOfUserId() => clearField($_whichOneof(0));
-
   @$pb.TagNumber(1)
   $core.String get name => $_getSZ(0);
   @$pb.TagNumber(1)

+ 2 - 5
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/workspace_create.pbjson.dart

@@ -25,15 +25,12 @@ const CreateWorkspaceParams$json = const {
   '2': const [
     const {'1': 'name', '3': 1, '4': 1, '5': 9, '10': 'name'},
     const {'1': 'desc', '3': 2, '4': 1, '5': 9, '10': 'desc'},
-    const {'1': 'user_id', '3': 3, '4': 1, '5': 9, '9': 0, '10': 'userId'},
-  ],
-  '8': const [
-    const {'1': 'one_of_user_id'},
+    const {'1': 'user_id', '3': 3, '4': 1, '5': 9, '10': 'userId'},
   ],
 };
 
 /// Descriptor for `CreateWorkspaceParams`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List createWorkspaceParamsDescriptor = $convert.base64Decode('ChVDcmVhdGVXb3Jrc3BhY2VQYXJhbXMSEgoEbmFtZRgBIAEoCVIEbmFtZRISCgRkZXNjGAIgASgJUgRkZXNjEhkKB3VzZXJfaWQYAyABKAlIAFIGdXNlcklkQhAKDm9uZV9vZl91c2VyX2lk');
+final $typed_data.Uint8List createWorkspaceParamsDescriptor = $convert.base64Decode('ChVDcmVhdGVXb3Jrc3BhY2VQYXJhbXMSEgoEbmFtZRgBIAEoCVIEbmFtZRISCgRkZXNjGAIgASgJUgRkZXNjEhcKB3VzZXJfaWQYAyABKAlSBnVzZXJJZA==');
 @$core.Deprecated('Use workspaceDescriptor instead')
 const Workspace$json = const {
   '1': 'Workspace',

+ 1 - 0
backend/Cargo.toml

@@ -34,6 +34,7 @@ anyhow = "1.0.40"
 thiserror = "1.0.24"
 bcrypt = "0.10"
 jsonwebtoken = "7.2"
+sql-builder = "3.1.1"
 
 flowy-log = { path = "../rust-lib/flowy-log" }
 flowy-user = { path = "../rust-lib/flowy-user" }

+ 17 - 1
backend/doc/database_setup.md

@@ -55,4 +55,20 @@ By default, Docker images do not expose their ports to the underlying host machi
 
 **Type mapping**
 * [postgres type map](https://docs.rs/sqlx/0.5.7/sqlx/postgres/types/index.html)
-* [postgres and diesel type map](https://kotiri.com/2018/01/31/postgresql-diesel-rust-types.html)
+* [postgres and diesel type map](https://kotiri.com/2018/01/31/postgresql-diesel-rust-types.html)
+
+
+## Q&A
+1. Receive` { code: 24, kind: Other, message: "Too many open files" } on arbiter` after running cargo test on backend.
+> This is due to a limit enforced by the operating system on the maximum number of open file descriptors (including sockets) for each process.
+> Raising the file descriptor limit using `ulimit -n 2048` to solve this issue. It won't stay after reboot so check on google how to persist 
+> that value if you want to.
+> 
+> or you can try:
+> `launchctl limit maxfiles 2048 2048`
+> `launchctl limit maxfiles`
+> 
+> Don't forget to relaunch your terminal.
+
+## More
+* [11-database-drivers](https://blog.logrocket.com/11-database-drivers-and-orms-for-rust-that-are-ready-for-production/) 

+ 1 - 1
backend/migrations/20210819065837_user.sql

@@ -1,5 +1,5 @@
 -- Add migration script here
-CREATE TABLE user_table(
+CREATE TABLE IF NOT EXISTS user_table(
     id uuid NOT NULL,
     PRIMARY KEY (id),
     email TEXT NOT NULL UNIQUE,

+ 1 - 1
backend/migrations/20210824033032_workspace.sql

@@ -1,5 +1,5 @@
 -- Add migration script here
-CREATE TABLE workspace_table(
+CREATE TABLE IF NOT EXISTS workspace_table(
    id uuid NOT NULL,
    PRIMARY KEY (id),
    name TEXT NOT NULL,

+ 1 - 1
backend/migrations/20210824033742_app.sql

@@ -1,5 +1,5 @@
 -- Add migration script here
-CREATE TABLE app_table(
+CREATE TABLE IF NOT EXISTS app_table(
     id uuid NOT NULL,
     PRIMARY KEY (id),
     workspace_id TEXT NOT NULL,

+ 1 - 1
backend/migrations/20210824033748_view.sql

@@ -1,5 +1,5 @@
 -- Add migration script here
-CREATE TABLE view_table(
+CREATE TABLE IF NOT EXISTS view_table(
       id uuid NOT NULL,
       PRIMARY KEY (id),
       belong_to_id TEXT NOT NULL,

+ 4 - 4
backend/src/application.rs

@@ -80,10 +80,10 @@ fn user_scope() -> Scope {
             .route(web::patch().to(workspace::update_handler))
         )
         .service(web::resource("/app")
-            .route(web::post().to(app::create_app))
-            .route(web::delete().to(app::delete_app))
-            .route(web::get().to(app::read_app))
-            .route(web::patch().to(app::update_app))
+            .route(web::post().to(app::create_handler))
+            .route(web::delete().to(app::delete_handler))
+            .route(web::get().to(app::read_handler))
+            .route(web::patch().to(app::update_handler))
         )
         .service(web::resource("/view")
             .route(web::post().to(view::create_view))

+ 1 - 2
backend/src/routers/utils.rs

@@ -1,7 +1,7 @@
 use crate::config::MAX_PAYLOAD_SIZE;
 use actix_web::web;
 use flowy_net::{
-    errors::{ErrorCode, Kind, ServerError},
+    errors::{ErrorCode, ServerError},
     response::*,
 };
 use futures::StreamExt;
@@ -29,7 +29,6 @@ pub async fn poll_payload(mut payload: web::Payload) -> Result<web::BytesMut, Se
             return Err(ServerError::new(
                 "Payload overflow".to_string(),
                 ErrorCode::PayloadOverflow,
-                Kind::Other,
             ));
         }
         body.extend_from_slice(&chunk);

+ 3 - 0
backend/src/sqlx_ext/mod.rs

@@ -1,3 +1,6 @@
 mod query;
+mod utils;
+
+pub use utils::*;
 
 pub use query::*;

+ 151 - 19
backend/src/sqlx_ext/query.rs

@@ -1,38 +1,170 @@
-use sqlx::{any::AnyArguments, Arguments, Encode, PgPool, Postgres, Type};
+use flowy_net::errors::ServerError;
+use sql_builder::SqlBuilder as InnerBuilder;
+use sqlx::{any::AnyArguments, postgres::PgArguments, Arguments, Encode, PgPool, Postgres, Type};
 
-use sqlx::postgres::PgArguments;
+enum BuilderType {
+    Create,
+    Select,
+    Update,
+    Delete,
+}
 
-pub struct UpdateBuilder {
-    arguments: PgArguments,
+pub struct SqlBuilder {
     table: String,
-    fields: String,
+    fields: Vec<String>,
+    filters: Vec<String>,
+    fields_args: PgArguments,
+    ty: BuilderType,
 }
 
-impl UpdateBuilder {
-    pub fn new(table: &str) -> Self {
+impl SqlBuilder {
+    fn new(table: &str) -> Self {
         Self {
             table: table.to_owned(),
-            fields: String::new(),
-            arguments: PgArguments::default(),
+            fields: vec![],
+            filters: vec![],
+            fields_args: PgArguments::default(),
+            ty: BuilderType::Select,
         }
     }
 
-    pub fn add_argument<'a, T>(&mut self, column: &str, arg: Option<T>)
+    pub fn create(table: &str) -> Self {
+        let mut builder = Self::new(table);
+        builder.ty = BuilderType::Create;
+        builder
+    }
+
+    pub fn select(table: &str) -> Self {
+        let mut builder = Self::new(table);
+        builder.ty = BuilderType::Select;
+        builder
+    }
+
+    pub fn update(table: &str) -> Self {
+        let mut builder = Self::new(table);
+        builder.ty = BuilderType::Update;
+        builder
+    }
+
+    pub fn delete(table: &str) -> Self {
+        let mut builder = Self::new(table);
+        builder.ty = BuilderType::Delete;
+        builder
+    }
+
+    pub fn add_arg<'a, T>(mut self, field: &str, arg: T) -> Self
+    where
+        T: 'a + Send + Encode<'a, Postgres> + Type<Postgres>,
+    {
+        self.fields.push(field.to_owned());
+        self.fields_args.add(arg);
+        self
+    }
+
+    pub fn add_arg_if<'a, T>(self, add: bool, field: &str, arg: T) -> Self
+    where
+        T: 'a + Send + Encode<'a, Postgres> + Type<Postgres>,
+    {
+        if add {
+            self.add_arg(field, arg)
+        } else {
+            self
+        }
+    }
+
+    pub fn add_some_arg<'a, T>(self, field: &str, arg: Option<T>) -> Self
     where
         T: 'a + Send + Encode<'a, Postgres> + Type<Postgres>,
     {
         if let Some(arg) = arg {
-            if self.fields.is_empty() {
-                self.fields += &format!("{}=?", column);
-            } else {
-                self.fields += &format!(", {}=?", column);
-            }
-            self.arguments.add(arg);
+            self.add_arg(field, arg)
+        } else {
+            self
         }
     }
 
-    pub fn build(self) -> (String, PgArguments) {
-        let sql = format!("UPDATE {} SET {} WHERE id=?", self.table, self.fields);
-        (sql, self.arguments)
+    pub fn add_field(mut self, field: &str) -> Self {
+        self.fields.push(field.to_owned());
+        self
+    }
+
+    pub fn and_where_eq<'a, T>(mut self, field: &str, arg: T) -> Self
+    where
+        T: 'a + Send + Encode<'a, Postgres> + Type<Postgres>,
+    {
+        self.filters.push(field.to_owned());
+        self.fields_args.add(arg);
+        self
+    }
+
+    pub fn build(self) -> Result<(String, PgArguments), ServerError> {
+        match self.ty {
+            BuilderType::Create => {
+                let mut inner = InnerBuilder::insert_into(&self.table);
+                self.fields.iter().for_each(|field| {
+                    inner.field(field);
+                });
+
+                let values = self
+                    .fields
+                    .iter()
+                    .enumerate()
+                    .map(|(index, _)| format!("${}", index + 1))
+                    .collect::<Vec<String>>();
+
+                inner.values(&values);
+
+                let sql = inner.sql()?;
+                Ok((sql, self.fields_args))
+            },
+            BuilderType::Select => {
+                let mut inner = InnerBuilder::select_from(&self.table);
+                self.fields.into_iter().for_each(|field| {
+                    inner.field(field);
+                });
+
+                self.filters
+                    .into_iter()
+                    .enumerate()
+                    .for_each(|(index, filter)| {
+                        inner.and_where_eq(filter, format!("${}", index + 1));
+                    });
+
+                let sql = inner.sql()?;
+                Ok((sql, self.fields_args))
+            },
+            BuilderType::Update => {
+                let mut inner = InnerBuilder::update_table(&self.table);
+                let field_len = self.fields.len();
+                self.fields
+                    .into_iter()
+                    .enumerate()
+                    .for_each(|(index, field)| {
+                        inner.set(&field, format!("${}", index + 1));
+                    });
+
+                self.filters
+                    .into_iter()
+                    .enumerate()
+                    .for_each(|(index, filter)| {
+                        let index = index + field_len;
+                        inner.and_where_eq(filter, format!("${}", index + 1));
+                    });
+
+                let sql = inner.sql()?;
+                Ok((sql, self.fields_args))
+            },
+            BuilderType::Delete => {
+                let mut inner = InnerBuilder::delete_from(&self.table);
+                self.filters
+                    .into_iter()
+                    .enumerate()
+                    .for_each(|(index, filter)| {
+                        inner.and_where_eq(filter, format!("${}", index + 1));
+                    });
+                let sql = inner.sql()?;
+                Ok((sql, self.fields_args))
+            },
+        }
     }
 }

+ 9 - 0
backend/src/sqlx_ext/utils.rs

@@ -0,0 +1,9 @@
+use flowy_net::errors::{ErrorCode, ServerError};
+use sqlx::Error;
+
+pub fn map_sqlx_error(error: sqlx::Error) -> ServerError {
+    match error {
+        Error::RowNotFound => ServerError::new("".to_string(), ErrorCode::RecordNotFound),
+        _ => ServerError::internal().context(error),
+    }
+}

+ 1 - 2
backend/src/user_service/auth.rs

@@ -6,7 +6,7 @@ use actix_identity::Identity;
 use anyhow::Context;
 use chrono::Utc;
 use flowy_net::{
-    errors::{ErrorCode, Kind, ServerError},
+    errors::{ErrorCode, ServerError},
     response::FlowyResponse,
 };
 use flowy_user::{
@@ -102,7 +102,6 @@ async fn is_email_exist(
         Some(_) => Err(ServerError {
             code: ErrorCode::EmailAlreadyExists,
             msg: format!("{} already exists", email),
-            kind: Kind::User,
         }),
         None => Ok(()),
     }

+ 1 - 2
backend/src/user_service/utils.rs

@@ -1,5 +1,5 @@
 use bcrypt::{hash, verify, BcryptError, DEFAULT_COST};
-use flowy_net::errors::{ErrorCode, Kind, ServerError};
+use flowy_net::errors::{ErrorCode, ServerError};
 use jsonwebtoken::Algorithm;
 
 pub fn uuid() -> String { uuid::Uuid::new_v4().to_string() }
@@ -19,7 +19,6 @@ pub fn verify_password(source: &str, hash: &str) -> Result<bool, ServerError> {
         _ => Err(ServerError::new(
             "Username and password don't match".to_string(),
             ErrorCode::PasswordNotMatch,
-            Kind::User,
         )),
     }
 }

+ 214 - 0
backend/src/workspace_service/app/app.rs

@@ -0,0 +1,214 @@
+use flowy_net::{errors::ServerError, response::FlowyResponse};
+
+use crate::{
+    entities::workspace::AppTable,
+    sqlx_ext::{map_sqlx_error, SqlBuilder},
+};
+use anyhow::Context;
+use chrono::Utc;
+use flowy_net::errors::invalid_params;
+use flowy_user::entities::parser::UserId;
+use flowy_workspace::{
+    entities::{
+        app::{
+            parser::{AppColorStyle, AppDesc, AppId, AppName},
+            App,
+        },
+        view::RepeatedView,
+        workspace::parser::WorkspaceId,
+    },
+    protobuf::{CreateAppParams, QueryAppParams, UpdateAppParams},
+};
+use protobuf::Message;
+use sqlx::{postgres::PgArguments, PgPool, Postgres};
+use uuid::Uuid;
+
+pub(crate) async fn create_app(
+    pool: &PgPool,
+    params: CreateAppParams,
+) -> Result<FlowyResponse, ServerError> {
+    let color_bytes = params.get_color_style().write_to_bytes()?;
+    let name = AppName::parse(params.name).map_err(invalid_params)?;
+    let workspace_id = WorkspaceId::parse(params.workspace_id).map_err(invalid_params)?;
+    let user_id = UserId::parse(params.user_id).map_err(invalid_params)?;
+    let desc = AppDesc::parse(params.desc).map_err(invalid_params)?;
+
+    let mut transaction = pool
+        .begin()
+        .await
+        .context("Failed to acquire a Postgres connection to create app")?;
+
+    let uuid = uuid::Uuid::new_v4();
+    let time = Utc::now();
+
+    let (sql, args) = SqlBuilder::create("app_table")
+        .add_arg("id", uuid)
+        .add_arg("workspace_id", workspace_id.as_ref())
+        .add_arg("name", name.as_ref())
+        .add_arg("description", desc.as_ref())
+        .add_arg("color_style", color_bytes)
+        .add_arg("modified_time", &time)
+        .add_arg("create_time", &time)
+        .add_arg("user_id", user_id.as_ref())
+        .build()?;
+
+    let _ = sqlx::query_with(&sql, args)
+        .execute(&mut transaction)
+        .await
+        .map_err(map_sqlx_error)?;
+
+    transaction
+        .commit()
+        .await
+        .context("Failed to commit SQL transaction to create app.")?;
+
+    let app = App {
+        id: uuid.to_string(),
+        workspace_id: workspace_id.as_ref().to_owned(),
+        name: name.as_ref().to_string(),
+        desc: desc.as_ref().to_string(),
+        belongings: RepeatedView::default(),
+        version: 0,
+    };
+
+    FlowyResponse::success().data(app)
+}
+
+pub(crate) async fn read_app(
+    pool: &PgPool,
+    params: QueryAppParams,
+) -> Result<FlowyResponse, ServerError> {
+    let app_id = check_app_id(params.app_id)?;
+
+    let mut transaction = pool
+        .begin()
+        .await
+        .context("Failed to acquire a Postgres connection to read app")?;
+
+    let (sql, args) = SqlBuilder::select("app_table")
+        .add_field("*")
+        .and_where_eq("id", app_id)
+        .build()?;
+
+    let table = sqlx::query_as_with::<Postgres, AppTable, PgArguments>(&sql, args)
+        .fetch_one(&mut transaction)
+        .await
+        .map_err(map_sqlx_error)?;
+
+    transaction
+        .commit()
+        .await
+        .context("Failed to commit SQL transaction to read app.")?;
+
+    let mut app = App {
+        id: table.id.to_string(),
+        workspace_id: table.workspace_id,
+        name: table.name,
+        desc: table.description,
+        belongings: RepeatedView::default(),
+        version: 0,
+    };
+
+    if params.read_belongings {
+        // app.belongings
+    }
+
+    FlowyResponse::success().data(app)
+}
+
+pub(crate) async fn update_app(
+    pool: &PgPool,
+    params: UpdateAppParams,
+) -> Result<FlowyResponse, ServerError> {
+    let app_id = check_app_id(params.get_app_id().to_string())?;
+    let name = match params.has_name() {
+        false => None,
+        true => Some(
+            AppName::parse(params.get_name().to_owned())
+                .map_err(invalid_params)?
+                .0,
+        ),
+    };
+
+    let workspace_id = match params.has_workspace_id() {
+        false => None,
+        true => Some(
+            WorkspaceId::parse(params.get_workspace_id().to_owned())
+                .map_err(invalid_params)?
+                .0,
+        ),
+    };
+
+    let color_style = match params.has_color_style() {
+        false => None,
+        true => {
+            let color_bytes = params.get_color_style().write_to_bytes()?;
+            Some(color_bytes)
+        },
+    };
+
+    let desc = match params.has_desc() {
+        false => None,
+        true => Some(
+            AppDesc::parse(params.get_desc().to_owned())
+                .map_err(invalid_params)?
+                .0,
+        ),
+    };
+
+    let mut transaction = pool
+        .begin()
+        .await
+        .context("Failed to acquire a Postgres connection to update app")?;
+
+    let (sql, args) = SqlBuilder::update("app_table")
+        .add_some_arg("name", name)
+        .add_some_arg("workspace_id", workspace_id)
+        .add_some_arg("color_style", color_style)
+        .add_some_arg("description", desc)
+        .add_arg_if(params.has_is_trash(), "is_trash", params.get_is_trash())
+        .and_where_eq("id", app_id)
+        .build()?;
+
+    sqlx::query_with(&sql, args)
+        .execute(&mut transaction)
+        .await
+        .map_err(map_sqlx_error)?;
+
+    transaction
+        .commit()
+        .await
+        .context("Failed to commit SQL transaction to update app.")?;
+
+    Ok(FlowyResponse::success())
+}
+
+pub(crate) async fn delete_app(pool: &PgPool, app_id: &str) -> Result<FlowyResponse, ServerError> {
+    let app_id = check_app_id(app_id.to_owned())?;
+    let mut transaction = pool
+        .begin()
+        .await
+        .context("Failed to acquire a Postgres connection to delete app")?;
+
+    let (sql, args) = SqlBuilder::delete("app_table")
+        .and_where_eq("id", app_id)
+        .build()?;
+
+    let _ = sqlx::query_with(&sql, args)
+        .execute(&mut transaction)
+        .await
+        .map_err(map_sqlx_error)?;
+
+    transaction
+        .commit()
+        .await
+        .context("Failed to commit SQL transaction to delete app.")?;
+
+    Ok(FlowyResponse::success())
+}
+
+fn check_app_id(id: String) -> Result<Uuid, ServerError> {
+    let app_id = AppId::parse(id).map_err(invalid_params)?;
+    let app_id = Uuid::parse_str(app_id.as_ref())?;
+    Ok(app_id)
+}

+ 26 - 12
backend/src/workspace_service/app/router.rs

@@ -1,3 +1,7 @@
+use crate::{
+    routers::utils::parse_from_payload,
+    workspace_service::app::app::{create_app, delete_app, read_app, update_app},
+};
 use actix_identity::Identity;
 use actix_web::{
     web::{Data, Payload},
@@ -6,36 +10,46 @@ use actix_web::{
     HttpResponse,
 };
 use flowy_net::errors::ServerError;
+use flowy_workspace::protobuf::{
+    CreateAppParams,
+    DeleteAppParams,
+    QueryAppParams,
+    UpdateAppParams,
+};
 use sqlx::PgPool;
 
-pub async fn create_app(
+pub async fn create_handler(
     payload: Payload,
-    id: Identity,
     pool: Data<PgPool>,
 ) -> Result<HttpResponse, ServerError> {
-    unimplemented!()
+    let params: CreateAppParams = parse_from_payload(payload).await?;
+    let resp = create_app(pool.get_ref(), params).await?;
+    Ok(resp.into())
 }
 
-pub async fn read_app(
+pub async fn read_handler(
     payload: Payload,
-    id: Identity,
     pool: Data<PgPool>,
 ) -> Result<HttpResponse, ServerError> {
-    unimplemented!()
+    let params: QueryAppParams = parse_from_payload(payload).await?;
+    let resp = read_app(pool.get_ref(), params).await?;
+    Ok(resp.into())
 }
 
-pub async fn update_app(
+pub async fn update_handler(
     payload: Payload,
-    id: Identity,
     pool: Data<PgPool>,
 ) -> Result<HttpResponse, ServerError> {
-    unimplemented!()
+    let params: UpdateAppParams = parse_from_payload(payload).await?;
+    let resp = update_app(pool.get_ref(), params).await?;
+    Ok(resp.into())
 }
 
-pub async fn delete_app(
+pub async fn delete_handler(
     payload: Payload,
-    id: Identity,
     pool: Data<PgPool>,
 ) -> Result<HttpResponse, ServerError> {
-    unimplemented!()
+    let params: DeleteAppParams = parse_from_payload(payload).await?;
+    let resp = delete_app(pool.get_ref(), &params.app_id).await?;
+    Ok(resp.into())
 }

+ 1 - 1
backend/src/workspace_service/workspace/router.rs

@@ -46,7 +46,7 @@ pub async fn delete_handler(
     pool: Data<PgPool>,
 ) -> Result<HttpResponse, ServerError> {
     let params: DeleteWorkspaceParams = parse_from_payload(payload).await?;
-    let resp = delete_workspace(pool.get_ref(), params).await?;
+    let resp = delete_workspace(pool.get_ref(), params.get_workspace_id()).await?;
     Ok(resp.into())
 }
 

+ 73 - 54
backend/src/workspace_service/workspace/workspace.rs

@@ -1,13 +1,16 @@
-use crate::{entities::workspace::WorkspaceTable, sqlx_ext::UpdateBuilder};
+use crate::{entities::workspace::WorkspaceTable, sqlx_ext::*};
 use anyhow::Context;
 use chrono::Utc;
-use flowy_net::{errors::ServerError, response::FlowyResponse};
+use flowy_net::{
+    errors::{invalid_params, ServerError},
+    response::FlowyResponse,
+};
 use flowy_user::entities::parser::UserId;
 use flowy_workspace::{
     entities::{
         app::RepeatedApp,
         workspace::{
-            parser::{WorkspaceId, WorkspaceName},
+            parser::{WorkspaceDesc, WorkspaceId, WorkspaceName},
             Workspace,
         },
     },
@@ -18,17 +21,16 @@ use flowy_workspace::{
         UpdateWorkspaceParams,
     },
 };
-use sqlx::{PgPool, Postgres};
+use sqlx::{postgres::PgArguments, Arguments, PgPool, Postgres};
 use uuid::Uuid;
 
 pub(crate) async fn create_workspace(
     pool: &PgPool,
     params: CreateWorkspaceParams,
 ) -> Result<FlowyResponse, ServerError> {
-    let name = WorkspaceName::parse(params.get_name().to_owned())
-        .map_err(|e| ServerError::params_invalid().context(e))?;
-    let user_id = UserId::parse(params.get_user_id().to_owned())
-        .map_err(|e| ServerError::params_invalid().context(e))?;
+    let name = WorkspaceName::parse(params.get_name().to_owned()).map_err(invalid_params)?;
+    let desc = WorkspaceDesc::parse(params.get_desc().to_owned()).map_err(invalid_params)?;
+    let user_id = UserId::parse(params.user_id).map_err(invalid_params)?;
 
     let mut transaction = pool
         .begin()
@@ -37,21 +39,21 @@ pub(crate) async fn create_workspace(
 
     let uuid = uuid::Uuid::new_v4();
     let time = Utc::now();
-    let _ = sqlx::query!(
-        r#"
-            INSERT INTO workspace_table (id, name, description, modified_time, create_time, user_id)
-            VALUES ($1, $2, $3, $4, $5, $6)
-        "#,
-        uuid,
-        name.as_ref(),
-        params.desc,
-        time,
-        time,
-        user_id.as_ref(),
-    )
-    .execute(&mut transaction)
-    .await
-    .map_err(|e| ServerError::internal().context(e))?;
+
+    // TODO: use macro to fetch each field from struct
+    let (sql, args) = SqlBuilder::create("workspace_table")
+        .add_arg("id", uuid)
+        .add_arg("name", name.as_ref())
+        .add_arg("description", desc.as_ref())
+        .add_arg("modified_time", &time)
+        .add_arg("create_time", &time)
+        .add_arg("user_id", user_id.as_ref())
+        .build()?;
+
+    let _ = sqlx::query_with(&sql, args)
+        .execute(&mut transaction)
+        .await
+        .map_err(map_sqlx_error)?;
 
     transaction
         .commit()
@@ -61,7 +63,7 @@ pub(crate) async fn create_workspace(
     let workspace = Workspace {
         id: uuid.to_string(),
         name: name.as_ref().to_owned(),
-        desc: params.desc,
+        desc: desc.as_ref().to_owned(),
         apps: RepeatedApp::default(),
     };
 
@@ -78,16 +80,15 @@ pub(crate) async fn read_workspace(
         .await
         .context("Failed to acquire a Postgres connection to read workspace")?;
 
-    let uuid = Uuid::parse_str(workspace_id.as_ref())?;
-    let table =
-        sqlx::query_as::<Postgres, WorkspaceTable>("SELECT * FROM workspace_table WHERE id = $1")
-            .bind(uuid)
-            .fetch_one(&mut transaction)
-            .await
-            .map_err(|err| {
-                //
-                ServerError::internal().context(err)
-            })?;
+    let (sql, args) = SqlBuilder::select("workspace_table")
+        .add_field("*")
+        .and_where_eq("id", workspace_id)
+        .build()?;
+
+    let table = sqlx::query_as_with::<Postgres, WorkspaceTable, PgArguments>(&sql, args)
+        .fetch_one(&mut transaction)
+        .await
+        .map_err(map_sqlx_error)?;
 
     transaction
         .commit()
@@ -95,7 +96,6 @@ pub(crate) async fn read_workspace(
         .context("Failed to commit SQL transaction to read workspace.")?;
 
     let mut workspace = Workspace::new(table.id.to_string(), table.name, table.description);
-
     if params.get_read_apps() {
         workspace.apps = RepeatedApp { items: vec![] }
     }
@@ -108,49 +108,68 @@ pub(crate) async fn update_workspace(
     params: UpdateWorkspaceParams,
 ) -> Result<FlowyResponse, ServerError> {
     let workspace_id = check_workspace_id(params.get_id().to_owned())?;
+    let name = match params.has_name() {
+        false => None,
+        true => {
+            let name = WorkspaceName::parse(params.get_name().to_owned())
+                .map_err(invalid_params)?
+                .0;
+            Some(name)
+        },
+    };
+
+    let desc = match params.has_desc() {
+        false => None,
+        true => {
+            let desc = WorkspaceDesc::parse(params.get_desc().to_owned())
+                .map_err(invalid_params)?
+                .0;
+            Some(desc)
+        },
+    };
+
     let mut transaction = pool
         .begin()
         .await
         .context("Failed to acquire a Postgres connection to update workspace")?;
 
-    let mut builder = UpdateBuilder::new("workspace_table");
-    if params.has_name() {
-        builder.add_argument("name", Some(params.get_name()));
-    }
-    if params.has_desc() {
-        builder.add_argument("description", Some(params.get_desc()));
-    }
-    builder.add_argument("id", Some(workspace_id.as_ref()));
-    let (sql, args) = builder.build();
+    let (sql, args) = SqlBuilder::update("workspace_table")
+        .add_some_arg("name", name)
+        .add_some_arg("description", desc)
+        .and_where_eq("id", workspace_id)
+        .build()?;
 
     sqlx::query_with(&sql, args)
         .execute(&mut transaction)
         .await
-        .map_err(|err| ServerError::internal().context(err))?;
+        .map_err(map_sqlx_error)?;
 
     transaction
         .commit()
         .await
         .context("Failed to commit SQL transaction to update workspace.")?;
 
-    unimplemented!()
+    Ok(FlowyResponse::success())
 }
 
 pub(crate) async fn delete_workspace(
     pool: &PgPool,
-    params: DeleteWorkspaceParams,
+    workspace_id: &str,
 ) -> Result<FlowyResponse, ServerError> {
-    let workspace_id = check_workspace_id(params.get_workspace_id().to_owned())?;
+    let workspace_id = check_workspace_id(workspace_id.to_owned())?;
     let mut transaction = pool
         .begin()
         .await
         .context("Failed to acquire a Postgres connection to delete workspace")?;
 
-    let _ = sqlx::query(r#"DELETE FROM workspace_table where workspace_id = $1"#)
-        .bind(workspace_id.as_ref())
+    let (sql, args) = SqlBuilder::delete("workspace_table")
+        .and_where_eq("id", workspace_id)
+        .build()?;
+
+    let _ = sqlx::query_with(&sql, args)
         .execute(&mut transaction)
         .await
-        .map_err(|e| ServerError::internal().context(e))?;
+        .map_err(map_sqlx_error)?;
 
     transaction
         .commit()
@@ -160,8 +179,8 @@ pub(crate) async fn delete_workspace(
     Ok(FlowyResponse::success())
 }
 
-fn check_workspace_id(id: String) -> Result<WorkspaceId, ServerError> {
-    let workspace_id =
-        WorkspaceId::parse(id).map_err(|e| ServerError::params_invalid().context(e))?;
+fn check_workspace_id(id: String) -> Result<Uuid, ServerError> {
+    let workspace_id = WorkspaceId::parse(id).map_err(invalid_params)?;
+    let workspace_id = Uuid::parse_str(workspace_id.as_ref())?;
     Ok(workspace_id)
 }

+ 35 - 5
backend/tests/api/helper.rs

@@ -27,21 +27,51 @@ impl TestApp {
         resp
     }
 
-    pub async fn create_workspace(&self, mut params: CreateWorkspaceParams) -> Workspace {
+    pub async fn create_workspace(&self, params: CreateWorkspaceParams) -> Workspace {
         let url = format!("{}/api/workspace", self.address);
-        let response = self.register_test_user().await;
-        params.user_id = Some(response.uid);
         let workspace = create_workspace_request(params, &url).await.unwrap();
         workspace
     }
 
-    pub async fn read_workspace(&self, params: QueryWorkspaceParams) -> Workspace {
+    pub async fn read_workspace(&self, params: QueryWorkspaceParams) -> Option<Workspace> {
         let url = format!("{}/api/workspace", self.address);
         let workspace = read_workspace_request(params, &url).await.unwrap();
         workspace
     }
 
-    async fn register_test_user(&self) -> SignUpResponse {
+    pub async fn update_workspace(&self, params: UpdateWorkspaceParams) {
+        let url = format!("{}/api/workspace", self.address);
+        update_workspace_request(params, &url).await.unwrap();
+    }
+
+    pub async fn delete_workspace(&self, params: DeleteWorkspaceParams) {
+        let url = format!("{}/api/workspace", self.address);
+        delete_workspace_request(params, &url).await.unwrap();
+    }
+
+    pub async fn create_app(&self, params: CreateAppParams) -> App {
+        let url = format!("{}/api/app", self.address);
+        let app = create_app_request(params, &url).await.unwrap();
+        app
+    }
+
+    pub async fn read_app(&self, params: QueryAppParams) -> Option<App> {
+        let url = format!("{}/api/app", self.address);
+        let app = read_app_request(params, &url).await.unwrap();
+        app
+    }
+
+    pub async fn update_app(&self, params: UpdateAppParams) {
+        let url = format!("{}/api/app", self.address);
+        update_app_request(params, &url).await.unwrap();
+    }
+
+    pub async fn delete_app(&self, params: DeleteAppParams) {
+        let url = format!("{}/api/app", self.address);
+        delete_app_request(params, &url).await.unwrap();
+    }
+
+    pub(crate) async fn register_test_user(&self) -> SignUpResponse {
         let params = SignUpParams {
             email: "[email protected]".to_string(),
             name: "annie".to_string(),

+ 138 - 15
backend/tests/api/workspace.rs

@@ -1,35 +1,158 @@
 use crate::helper::{spawn_app, TestApp};
-use flowy_workspace::entities::workspace::{CreateWorkspaceParams, QueryWorkspaceParams};
+use flowy_workspace::entities::{
+    app::{App, ColorStyle, CreateAppParams, DeleteAppParams, QueryAppParams, UpdateAppParams},
+    workspace::{
+        CreateWorkspaceParams,
+        DeleteWorkspaceParams,
+        QueryWorkspaceParams,
+        UpdateWorkspaceParams,
+        Workspace,
+    },
+};
 
 #[actix_rt::test]
 async fn workspace_create() {
     let app = spawn_app().await;
+    let (workspace, _) = create_test_workspace(&app).await;
+    log::info!("{:?}", workspace);
+}
 
-    let params = CreateWorkspaceParams {
-        name: "My first workspace".to_string(),
-        desc: "This is my first workspace".to_string(),
-        user_id: None,
+#[actix_rt::test]
+async fn workspace_read() {
+    let app = spawn_app().await;
+    let (workspace_1, _) = create_test_workspace(&app).await;
+
+    let read_params = QueryWorkspaceParams {
+        workspace_id: workspace_1.id.clone(),
+        read_apps: false,
     };
 
-    let workspace = app.create_workspace(params).await;
-    log::info!("{:?}", workspace);
+    log::info!("{:?}", app.read_workspace(read_params).await.unwrap());
 }
 
 #[actix_rt::test]
-async fn workspace_read() {
+async fn workspace_update() {
     let app = spawn_app().await;
-    let params = CreateWorkspaceParams {
-        name: "My first workspace".to_string(),
-        desc: "This is my first workspace".to_string(),
-        user_id: None,
+    let (workspace_1, _) = create_test_workspace(&app).await;
+    let update_params = UpdateWorkspaceParams {
+        id: workspace_1.id.clone(),
+        name: Some("workspace 2".to_string()),
+        desc: Some("rename workspace description".to_string()),
     };
-    let workspace_1 = app.create_workspace(params).await;
+    app.update_workspace(update_params).await;
 
     let read_params = QueryWorkspaceParams {
         workspace_id: workspace_1.id.clone(),
         read_apps: false,
     };
-    let workspace_2 = app.read_workspace(read_params).await;
-
+    let workspace_2 = app.read_workspace(read_params).await.unwrap();
     log::info!("{:?}", workspace_2);
 }
+
+#[actix_rt::test]
+async fn workspace_delete() {
+    let app = spawn_app().await;
+    let (workspace, _) = create_test_workspace(&app).await;
+    let delete_params = DeleteWorkspaceParams {
+        workspace_id: workspace.id.clone(),
+    };
+
+    let _ = app.delete_workspace(delete_params).await;
+    let read_params = QueryWorkspaceParams {
+        workspace_id: workspace.id.clone(),
+        read_apps: false,
+    };
+    assert_eq!(app.read_workspace(read_params).await.is_none(), true);
+}
+
+async fn create_test_workspace(app: &TestApp) -> (Workspace, String) {
+    let response = app.register_test_user().await;
+
+    let params = CreateWorkspaceParams {
+        name: "My first workspace".to_string(),
+        desc: "This is my first workspace".to_string(),
+        user_id: response.uid.clone(),
+    };
+    let workspace = app.create_workspace(params).await;
+    (workspace, response.uid)
+}
+
+#[actix_rt::test]
+async fn app_create() {
+    let application = spawn_app().await;
+    let app = create_test_app(&application).await;
+    log::info!("{:?}", app);
+}
+
+#[actix_rt::test]
+async fn app_read() {
+    let application = spawn_app().await;
+    let app = create_test_app(&application).await;
+
+    let read_params = QueryAppParams {
+        app_id: app.id,
+        read_belongings: false,
+        is_trash: false,
+    };
+
+    log::info!("{:?}", application.read_app(read_params).await.unwrap());
+}
+
+#[actix_rt::test]
+async fn app_update() {
+    let application = spawn_app().await;
+    let app = create_test_app(&application).await;
+
+    let update_params = UpdateAppParams {
+        app_id: app.id.clone(),
+        workspace_id: None,
+        name: Some("flowy".to_owned()),
+        desc: None,
+        color_style: None,
+        is_trash: None,
+    };
+    application.update_app(update_params).await;
+
+    let read_params = QueryAppParams {
+        app_id: app.id,
+        read_belongings: false,
+        is_trash: false,
+    };
+
+    let app = application.read_app(read_params).await.unwrap();
+    log::info!("{:?}", app);
+}
+
+#[actix_rt::test]
+async fn app_delete() {
+    let application = spawn_app().await;
+    let app = create_test_app(&application).await;
+
+    let delete_params = DeleteAppParams {
+        app_id: app.id.clone(),
+    };
+    application.delete_app(delete_params).await;
+
+    let read_params = QueryAppParams {
+        app_id: app.id,
+        read_belongings: false,
+        is_trash: false,
+    };
+
+    assert_eq!(application.read_app(read_params).await.is_none(), true);
+}
+
+async fn create_test_app(app: &TestApp) -> App {
+    let (workspace, user_id) = create_test_workspace(&app).await;
+
+    let params = CreateAppParams {
+        workspace_id: workspace.id,
+        name: "My first app".to_string(),
+        desc: "This is my first app".to_string(),
+        color_style: ColorStyle::default(),
+        user_id,
+    };
+
+    let app = app.create_app(params).await;
+    app
+}

+ 4 - 0
rust-lib/flowy-derive/src/derive_cache/derive_cache.rs

@@ -18,12 +18,16 @@ pub fn category_from_str(type_str: &str) -> TypeCategory {
         "ObservableSubject"
         | "KeyValue"
         | "QueryAppRequest"
+        | "QueryAppParams"
         | "CreateAppRequest"
         | "ColorStyle"
+        | "CreateAppParams"
         | "App"
         | "RepeatedApp"
         | "UpdateAppRequest"
+        | "UpdateAppParams"
         | "DeleteAppRequest"
+        | "DeleteAppParams"
         | "UpdateWorkspaceRequest"
         | "UpdateWorkspaceParams"
         | "DeleteWorkspaceRequest"

+ 7 - 14
rust-lib/flowy-net/src/errors.rs

@@ -10,7 +10,6 @@ use uuid::Error;
 pub struct ServerError {
     pub code: ErrorCode,
     pub msg: String,
-    pub kind: Kind,
 }
 
 macro_rules! static_error {
@@ -20,7 +19,6 @@ macro_rules! static_error {
             ServerError {
                 code: $status,
                 msg: format!("{}", $status),
-                kind: Kind::Other,
             }
         }
     };
@@ -38,19 +36,18 @@ impl ServerError {
     static_error!(connect_cancel, ErrorCode::ConnectCancel);
     static_error!(connect_refused, ErrorCode::ConnectRefused);
 
-    pub fn new(msg: String, code: ErrorCode, kind: Kind) -> Self { Self { code, msg, kind } }
-
-    pub fn kind(mut self, kind: Kind) -> Self {
-        self.kind = kind;
-        self
-    }
+    pub fn new(msg: String, code: ErrorCode) -> Self { Self { code, msg } }
 
     pub fn context<T: Debug>(mut self, error: T) -> Self {
         self.msg = format!("{:?}", error);
         self
     }
+
+    pub fn is_not_found(&self) -> bool { self.code == ErrorCode::RecordNotFound }
 }
 
+pub fn invalid_params<T: Debug>(e: T) -> ServerError { ServerError::params_invalid().context(e) }
+
 impl std::fmt::Display for ServerError {
     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
         let msg = format!("{:?}:{}", self.code, self.msg);
@@ -67,12 +64,6 @@ impl std::convert::From<&ServerError> for FlowyResponse {
     }
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub enum Kind {
-    User,
-    Other,
-}
-
 #[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug, Clone, derive_more::Display)]
 #[repr(u16)]
 pub enum ErrorCode {
@@ -112,6 +103,8 @@ pub enum ErrorCode {
 
     #[display(fmt = "Sql error")]
     SqlError           = 200,
+    #[display(fmt = "Record not found")]
+    RecordNotFound     = 201,
 
     #[display(fmt = "Http request error")]
     HttpError          = 300,

+ 20 - 7
rust-lib/flowy-net/src/request/request.rs

@@ -15,7 +15,7 @@ use tokio::sync::oneshot;
 pub struct HttpRequestBuilder {
     url: String,
     body: Option<Bytes>,
-    response: Option<Bytes>,
+    response: Option<Response>,
     method: Method,
 }
 
@@ -41,6 +41,18 @@ impl HttpRequestBuilder {
         builder
     }
 
+    pub fn patch(url: &str) -> Self {
+        let mut builder = Self::new(url);
+        builder.method = Method::PATCH;
+        builder
+    }
+
+    pub fn delete(url: &str) -> Self {
+        let mut builder = Self::new(url);
+        builder.method = Method::DELETE;
+        builder
+    }
+
     pub fn protobuf<T1>(mut self, body: T1) -> Result<Self, ServerError>
     where
         T1: TryInto<Bytes, Error = ProtobufError>,
@@ -70,22 +82,23 @@ impl HttpRequestBuilder {
         });
 
         let response = rx.await??;
-        let data = get_response_data(response).await?;
-        self.response = Some(data);
+        self.response = Some(response);
         Ok(self)
     }
 
-    pub fn response<T2>(mut self) -> Result<T2, ServerError>
+    pub async fn response<T2>(self) -> Result<T2, ServerError>
     where
         T2: TryFrom<Bytes, Error = ProtobufError>,
     {
-        let data = self.response.take();
-        match data {
+        match self.response {
             None => {
                 let msg = format!("Request: {} receives unexpected empty body", self.url);
                 Err(ServerError::payload_none().context(msg))
             },
-            Some(data) => Ok(T2::try_from(data)?),
+            Some(response) => {
+                let data = get_response_data(response).await?;
+                Ok(T2::try_from(data)?)
+            },
         }
     }
 }

+ 2 - 6
rust-lib/flowy-net/src/response/response.rs

@@ -1,4 +1,4 @@
-use crate::errors::{ErrorCode, Kind, ServerError};
+use crate::errors::{ErrorCode, ServerError};
 use bytes::Bytes;
 use serde::{Deserialize, Serialize};
 use std::{convert::TryInto, error::Error, fmt::Debug};
@@ -69,11 +69,7 @@ impl std::convert::From<reqwest::Error> for ServerError {
 
                     if hyper_error.is_timeout() {}
 
-                    ServerError {
-                        code,
-                        msg,
-                        kind: Kind::Other,
-                    }
+                    ServerError { code, msg }
                 },
             };
         }

+ 0 - 1
rust-lib/flowy-user/src/errors.rs

@@ -2,7 +2,6 @@ use bytes::Bytes;
 use derive_more::Display;
 use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
 use flowy_dispatch::prelude::{EventResponse, ResponseBuilder};
-use flowy_net::errors::Kind;
 use std::{
     convert::TryInto,
     fmt::{Debug, Formatter},

+ 4 - 2
rust-lib/flowy-user/src/services/user/user_server.rs

@@ -53,7 +53,8 @@ pub async fn user_sign_up(params: SignUpParams, url: &str) -> Result<SignUpRespo
         .protobuf(params)?
         .send()
         .await?
-        .response()?;
+        .response()
+        .await?;
     Ok(response)
 }
 
@@ -62,7 +63,8 @@ pub async fn user_sign_in(params: SignInParams, url: &str) -> Result<SignInRespo
         .protobuf(params)?
         .send()
         .await?
-        .response()?;
+        .response()
+        .await?;
     Ok(response)
 }
 

+ 14 - 1
rust-lib/flowy-workspace/src/entities/app/app_create.rs

@@ -7,6 +7,7 @@ use crate::{
     errors::*,
     impl_def_and_def_mut,
 };
+use bytes::Bytes;
 use flowy_derive::ProtoBuf;
 use std::convert::TryInto;
 
@@ -25,17 +26,28 @@ pub struct CreateAppRequest {
     pub color_style: ColorStyle,
 }
 
-#[derive(ProtoBuf, Default, Debug)]
+#[derive(ProtoBuf, Default, Debug, Clone)]
 pub struct ColorStyle {
     #[pb(index = 1)]
     pub theme_color: String,
 }
 
+#[derive(ProtoBuf, Default)]
 pub struct CreateAppParams {
+    #[pb(index = 1)]
     pub workspace_id: String,
+
+    #[pb(index = 2)]
     pub name: String,
+
+    #[pb(index = 3)]
     pub desc: String,
+
+    #[pb(index = 4)]
     pub color_style: ColorStyle,
+
+    #[pb(index = 5)]
+    pub user_id: String,
 }
 
 impl TryInto<CreateAppParams> for CreateAppRequest {
@@ -62,6 +74,7 @@ impl TryInto<CreateAppParams> for CreateAppRequest {
             name: name.0,
             desc: self.desc,
             color_style: color_style.0,
+            user_id: "".to_string(),
         })
     }
 }

+ 4 - 2
rust-lib/flowy-workspace/src/entities/app/app_delete.rs

@@ -1,5 +1,5 @@
 use crate::{
-    entities::app::parser::BelongToId,
+    entities::app::parser::AppId,
     errors::{ErrorBuilder, WorkspaceError, WsErrCode},
 };
 use flowy_derive::ProtoBuf;
@@ -11,7 +11,9 @@ pub struct DeleteAppRequest {
     pub app_id: String,
 }
 
+#[derive(Default, ProtoBuf)]
 pub struct DeleteAppParams {
+    #[pb(index = 1)]
     pub app_id: String,
 }
 
@@ -19,7 +21,7 @@ impl TryInto<DeleteAppParams> for DeleteAppRequest {
     type Error = WorkspaceError;
 
     fn try_into(self) -> Result<DeleteAppParams, Self::Error> {
-        let app_id = BelongToId::parse(self.app_id)
+        let app_id = AppId::parse(self.app_id)
             .map_err(|e| ErrorBuilder::new(WsErrCode::AppIdInvalid).msg(e).build())?
             .0;
 

+ 8 - 2
rust-lib/flowy-workspace/src/entities/app/app_query.rs

@@ -1,4 +1,4 @@
-use crate::{entities::app::parser::BelongToId, errors::*};
+use crate::{entities::app::parser::AppId, errors::*};
 use flowy_derive::ProtoBuf;
 use std::convert::TryInto;
 
@@ -34,9 +34,15 @@ impl QueryAppRequest {
     }
 }
 
+#[derive(ProtoBuf, Default)]
 pub struct QueryAppParams {
+    #[pb(index = 1)]
     pub app_id: String,
+
+    #[pb(index = 2)]
     pub read_belongings: bool,
+
+    #[pb(index = 3)]
     pub is_trash: bool,
 }
 
@@ -44,7 +50,7 @@ impl TryInto<QueryAppParams> for QueryAppRequest {
     type Error = WorkspaceError;
 
     fn try_into(self) -> Result<QueryAppParams, Self::Error> {
-        let app_id = BelongToId::parse(self.app_id)
+        let app_id = AppId::parse(self.app_id)
             .map_err(|e| ErrorBuilder::new(WsErrCode::AppIdInvalid).msg(e).build())?
             .0;
 

+ 14 - 2
rust-lib/flowy-workspace/src/entities/app/app_update.rs

@@ -1,7 +1,7 @@
 use crate::{
     entities::{
         app::{
-            parser::{AppColorStyle, AppName, BelongToId},
+            parser::{AppColorStyle, AppId, AppName},
             ColorStyle,
         },
         workspace::parser::WorkspaceId,
@@ -32,12 +32,24 @@ pub struct UpdateAppRequest {
     pub is_trash: Option<bool>,
 }
 
+#[derive(ProtoBuf, Default)]
 pub struct UpdateAppParams {
+    #[pb(index = 1)]
     pub app_id: String,
+
+    #[pb(index = 2, one_of)]
     pub workspace_id: Option<String>,
+
+    #[pb(index = 3, one_of)]
     pub name: Option<String>,
+
+    #[pb(index = 4, one_of)]
     pub desc: Option<String>,
+
+    #[pb(index = 5, one_of)]
     pub color_style: Option<ColorStyle>,
+
+    #[pb(index = 6, one_of)]
     pub is_trash: Option<bool>,
 }
 
@@ -45,7 +57,7 @@ impl TryInto<UpdateAppParams> for UpdateAppRequest {
     type Error = WorkspaceError;
 
     fn try_into(self) -> Result<UpdateAppParams, Self::Error> {
-        let app_id = BelongToId::parse(self.app_id)
+        let app_id = AppId::parse(self.app_id)
             .map_err(|e| ErrorBuilder::new(WsErrCode::AppIdInvalid).msg(e).build())?
             .0;
 

+ 17 - 0
rust-lib/flowy-workspace/src/entities/app/parser/app_desc.rs

@@ -0,0 +1,17 @@
+use unicode_segmentation::UnicodeSegmentation;
+#[derive(Debug)]
+pub struct AppDesc(pub String);
+
+impl AppDesc {
+    pub fn parse(s: String) -> Result<AppDesc, String> {
+        if s.graphemes(true).count() > 1024 {
+            return Err(format!("Workspace description too long"));
+        }
+
+        Ok(Self(s))
+    }
+}
+
+impl AsRef<str> for AppDesc {
+    fn as_ref(&self) -> &str { &self.0 }
+}

+ 4 - 4
rust-lib/flowy-workspace/src/entities/app/parser/belong_to_id.rs → rust-lib/flowy-workspace/src/entities/app/parser/app_id.rs

@@ -1,8 +1,8 @@
 #[derive(Debug)]
-pub struct BelongToId(pub String);
+pub struct AppId(pub String);
 
-impl BelongToId {
-    pub fn parse(s: String) -> Result<BelongToId, String> {
+impl AppId {
+    pub fn parse(s: String) -> Result<AppId, String> {
         if s.trim().is_empty() {
             return Err(format!("App id can not be empty or whitespace"));
         }
@@ -11,6 +11,6 @@ impl BelongToId {
     }
 }
 
-impl AsRef<str> for BelongToId {
+impl AsRef<str> for AppId {
     fn as_ref(&self) -> &str { &self.0 }
 }

+ 4 - 2
rust-lib/flowy-workspace/src/entities/app/parser/mod.rs

@@ -1,7 +1,9 @@
 mod app_color_style;
+mod app_desc;
+mod app_id;
 mod app_name;
-mod belong_to_id;
 
 pub use app_color_style::*;
+pub use app_desc::*;
+pub use app_id::*;
 pub use app_name::*;
-pub use belong_to_id::*;

+ 2 - 2
rust-lib/flowy-workspace/src/entities/view/view_create.rs

@@ -1,5 +1,5 @@
 use crate::{
-    entities::{app::parser::BelongToId, view::parser::*},
+    entities::{app::parser::AppId, view::parser::*},
     errors::{ErrorBuilder, WorkspaceError, WsErrCode},
     impl_def_and_def_mut,
     sql_tables::view::ViewTableType,
@@ -51,7 +51,7 @@ impl TryInto<CreateViewParams> for CreateViewRequest {
             .map_err(|e| ErrorBuilder::new(WsErrCode::ViewNameInvalid).msg(e).build())?
             .0;
 
-        let belong_to_id = BelongToId::parse(self.belong_to_id)
+        let belong_to_id = AppId::parse(self.belong_to_id)
             .map_err(|e| ErrorBuilder::new(WsErrCode::AppIdInvalid).msg(e).build())?
             .0;
 

+ 2 - 0
rust-lib/flowy-workspace/src/entities/workspace/parser/mod.rs

@@ -1,5 +1,7 @@
+mod workspace_desc;
 mod workspace_id;
 mod workspace_name;
 
+pub use workspace_desc::*;
 pub use workspace_id::*;
 pub use workspace_name::*;

+ 18 - 0
rust-lib/flowy-workspace/src/entities/workspace/parser/workspace_desc.rs

@@ -0,0 +1,18 @@
+use unicode_segmentation::UnicodeSegmentation;
+
+#[derive(Debug)]
+pub struct WorkspaceDesc(pub String);
+
+impl WorkspaceDesc {
+    pub fn parse(s: String) -> Result<WorkspaceDesc, String> {
+        if s.graphemes(true).count() > 1024 {
+            return Err(format!("Workspace description too long"));
+        }
+
+        Ok(Self(s))
+    }
+}
+
+impl AsRef<str> for WorkspaceDesc {
+    fn as_ref(&self) -> &str { &self.0 }
+}

+ 11 - 5
rust-lib/flowy-workspace/src/entities/workspace/workspace_create.rs

@@ -23,8 +23,8 @@ pub struct CreateWorkspaceParams {
     #[pb(index = 2)]
     pub desc: String,
 
-    #[pb(index = 3, one_of)]
-    pub user_id: Option<String>,
+    #[pb(index = 3)]
+    pub user_id: String,
 }
 
 impl TryInto<CreateWorkspaceParams> for CreateWorkspaceRequest {
@@ -32,15 +32,21 @@ impl TryInto<CreateWorkspaceParams> for CreateWorkspaceRequest {
 
     fn try_into(self) -> Result<CreateWorkspaceParams, Self::Error> {
         let name = WorkspaceName::parse(self.name).map_err(|e| {
-            ErrorBuilder::new(WsErrCode::WorkspaceNameInvalid)
+            ErrorBuilder::new(WsErrCode::WorkspaceDescInvalid)
+                .msg(e)
+                .build()
+        })?;
+
+        let desc = WorkspaceDesc::parse(self.desc).map_err(|e| {
+            ErrorBuilder::new(WsErrCode::WorkspaceDescInvalid)
                 .msg(e)
                 .build()
         })?;
 
         Ok(CreateWorkspaceParams {
             name: name.0,
-            desc: self.desc,
-            user_id: None,
+            desc: desc.0,
+            user_id: "".to_string(),
         })
     }
 }

+ 1 - 1
rust-lib/flowy-workspace/src/entities/workspace/workspace_delete.rs

@@ -14,7 +14,7 @@ pub struct DeleteWorkspaceRequest {
 #[derive(ProtoBuf, Default)]
 pub struct DeleteWorkspaceParams {
     #[pb(index = 1)]
-    workspace_id: String,
+    pub workspace_id: String,
 }
 
 impl TryInto<DeleteWorkspaceParams> for DeleteWorkspaceRequest {

+ 15 - 3
rust-lib/flowy-workspace/src/errors.rs

@@ -2,6 +2,7 @@ use bytes::Bytes;
 use derive_more::Display;
 use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
 use flowy_dispatch::prelude::{EventResponse, ResponseBuilder};
+use flowy_net::errors::ErrorCode;
 use std::convert::TryInto;
 
 #[derive(Debug, Default, Clone, ProtoBuf)]
@@ -36,6 +37,9 @@ pub enum WsErrCode {
     #[display(fmt = "Color style of the App is invalid")]
     AppColorStyleInvalid = 3,
 
+    #[display(fmt = "Workspace desc is invalid")]
+    WorkspaceDescInvalid = 4,
+
     #[display(fmt = "Id of the App  is invalid")]
     AppIdInvalid         = 10,
 
@@ -68,6 +72,8 @@ pub enum WsErrCode {
 
     #[display(fmt = "Server error")]
     ServerError          = 1000,
+    #[display(fmt = "Record not found")]
+    RecordNotFound       = 1001,
 }
 
 impl std::default::Default for WsErrCode {
@@ -76,9 +82,15 @@ impl std::default::Default for WsErrCode {
 
 impl std::convert::From<flowy_net::errors::ServerError> for WorkspaceError {
     fn from(error: flowy_net::errors::ServerError) -> Self {
-        ErrorBuilder::new(WsErrCode::ServerError)
-            .error(error.msg)
-            .build()
+        match error.code {
+            ErrorCode::RecordNotFound => ErrorBuilder::new(WsErrCode::RecordNotFound)
+                .error(error.msg)
+                .build(),
+
+            _ => ErrorBuilder::new(WsErrCode::ServerError)
+                .error(error.msg)
+                .build(),
+        }
     }
 }
 

+ 408 - 46
rust-lib/flowy-workspace/src/protobuf/model/app_create.rs

@@ -482,6 +482,348 @@ impl ::protobuf::reflect::ProtobufValue for ColorStyle {
     }
 }
 
+#[derive(PartialEq,Clone,Default)]
+pub struct CreateAppParams {
+    // message fields
+    pub workspace_id: ::std::string::String,
+    pub name: ::std::string::String,
+    pub desc: ::std::string::String,
+    pub color_style: ::protobuf::SingularPtrField<ColorStyle>,
+    pub user_id: ::std::string::String,
+    // special fields
+    pub unknown_fields: ::protobuf::UnknownFields,
+    pub cached_size: ::protobuf::CachedSize,
+}
+
+impl<'a> ::std::default::Default for &'a CreateAppParams {
+    fn default() -> &'a CreateAppParams {
+        <CreateAppParams as ::protobuf::Message>::default_instance()
+    }
+}
+
+impl CreateAppParams {
+    pub fn new() -> CreateAppParams {
+        ::std::default::Default::default()
+    }
+
+    // string workspace_id = 1;
+
+
+    pub fn get_workspace_id(&self) -> &str {
+        &self.workspace_id
+    }
+    pub fn clear_workspace_id(&mut self) {
+        self.workspace_id.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_workspace_id(&mut self, v: ::std::string::String) {
+        self.workspace_id = v;
+    }
+
+    // Mutable pointer to the field.
+    // If field is not initialized, it is initialized with default value first.
+    pub fn mut_workspace_id(&mut self) -> &mut ::std::string::String {
+        &mut self.workspace_id
+    }
+
+    // Take field
+    pub fn take_workspace_id(&mut self) -> ::std::string::String {
+        ::std::mem::replace(&mut self.workspace_id, ::std::string::String::new())
+    }
+
+    // string name = 2;
+
+
+    pub fn get_name(&self) -> &str {
+        &self.name
+    }
+    pub fn clear_name(&mut self) {
+        self.name.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_name(&mut self, v: ::std::string::String) {
+        self.name = v;
+    }
+
+    // Mutable pointer to the field.
+    // If field is not initialized, it is initialized with default value first.
+    pub fn mut_name(&mut self) -> &mut ::std::string::String {
+        &mut self.name
+    }
+
+    // Take field
+    pub fn take_name(&mut self) -> ::std::string::String {
+        ::std::mem::replace(&mut self.name, ::std::string::String::new())
+    }
+
+    // string desc = 3;
+
+
+    pub fn get_desc(&self) -> &str {
+        &self.desc
+    }
+    pub fn clear_desc(&mut self) {
+        self.desc.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_desc(&mut self, v: ::std::string::String) {
+        self.desc = v;
+    }
+
+    // Mutable pointer to the field.
+    // If field is not initialized, it is initialized with default value first.
+    pub fn mut_desc(&mut self) -> &mut ::std::string::String {
+        &mut self.desc
+    }
+
+    // Take field
+    pub fn take_desc(&mut self) -> ::std::string::String {
+        ::std::mem::replace(&mut self.desc, ::std::string::String::new())
+    }
+
+    // .ColorStyle color_style = 4;
+
+
+    pub fn get_color_style(&self) -> &ColorStyle {
+        self.color_style.as_ref().unwrap_or_else(|| <ColorStyle as ::protobuf::Message>::default_instance())
+    }
+    pub fn clear_color_style(&mut self) {
+        self.color_style.clear();
+    }
+
+    pub fn has_color_style(&self) -> bool {
+        self.color_style.is_some()
+    }
+
+    // Param is passed by value, moved
+    pub fn set_color_style(&mut self, v: ColorStyle) {
+        self.color_style = ::protobuf::SingularPtrField::some(v);
+    }
+
+    // Mutable pointer to the field.
+    // If field is not initialized, it is initialized with default value first.
+    pub fn mut_color_style(&mut self) -> &mut ColorStyle {
+        if self.color_style.is_none() {
+            self.color_style.set_default();
+        }
+        self.color_style.as_mut().unwrap()
+    }
+
+    // Take field
+    pub fn take_color_style(&mut self) -> ColorStyle {
+        self.color_style.take().unwrap_or_else(|| ColorStyle::new())
+    }
+
+    // string user_id = 5;
+
+
+    pub fn get_user_id(&self) -> &str {
+        &self.user_id
+    }
+    pub fn clear_user_id(&mut self) {
+        self.user_id.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_user_id(&mut self, v: ::std::string::String) {
+        self.user_id = v;
+    }
+
+    // Mutable pointer to the field.
+    // If field is not initialized, it is initialized with default value first.
+    pub fn mut_user_id(&mut self) -> &mut ::std::string::String {
+        &mut self.user_id
+    }
+
+    // Take field
+    pub fn take_user_id(&mut self) -> ::std::string::String {
+        ::std::mem::replace(&mut self.user_id, ::std::string::String::new())
+    }
+}
+
+impl ::protobuf::Message for CreateAppParams {
+    fn is_initialized(&self) -> bool {
+        for v in &self.color_style {
+            if !v.is_initialized() {
+                return false;
+            }
+        };
+        true
+    }
+
+    fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+        while !is.eof()? {
+            let (field_number, wire_type) = is.read_tag_unpack()?;
+            match field_number {
+                1 => {
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.workspace_id)?;
+                },
+                2 => {
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.name)?;
+                },
+                3 => {
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.desc)?;
+                },
+                4 => {
+                    ::protobuf::rt::read_singular_message_into(wire_type, is, &mut self.color_style)?;
+                },
+                5 => {
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.user_id)?;
+                },
+                _ => {
+                    ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
+                },
+            };
+        }
+        ::std::result::Result::Ok(())
+    }
+
+    // Compute sizes of nested messages
+    #[allow(unused_variables)]
+    fn compute_size(&self) -> u32 {
+        let mut my_size = 0;
+        if !self.workspace_id.is_empty() {
+            my_size += ::protobuf::rt::string_size(1, &self.workspace_id);
+        }
+        if !self.name.is_empty() {
+            my_size += ::protobuf::rt::string_size(2, &self.name);
+        }
+        if !self.desc.is_empty() {
+            my_size += ::protobuf::rt::string_size(3, &self.desc);
+        }
+        if let Some(ref v) = self.color_style.as_ref() {
+            let len = v.compute_size();
+            my_size += 1 + ::protobuf::rt::compute_raw_varint32_size(len) + len;
+        }
+        if !self.user_id.is_empty() {
+            my_size += ::protobuf::rt::string_size(5, &self.user_id);
+        }
+        my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
+        self.cached_size.set(my_size);
+        my_size
+    }
+
+    fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+        if !self.workspace_id.is_empty() {
+            os.write_string(1, &self.workspace_id)?;
+        }
+        if !self.name.is_empty() {
+            os.write_string(2, &self.name)?;
+        }
+        if !self.desc.is_empty() {
+            os.write_string(3, &self.desc)?;
+        }
+        if let Some(ref v) = self.color_style.as_ref() {
+            os.write_tag(4, ::protobuf::wire_format::WireTypeLengthDelimited)?;
+            os.write_raw_varint32(v.get_cached_size())?;
+            v.write_to_with_cached_sizes(os)?;
+        }
+        if !self.user_id.is_empty() {
+            os.write_string(5, &self.user_id)?;
+        }
+        os.write_unknown_fields(self.get_unknown_fields())?;
+        ::std::result::Result::Ok(())
+    }
+
+    fn get_cached_size(&self) -> u32 {
+        self.cached_size.get()
+    }
+
+    fn get_unknown_fields(&self) -> &::protobuf::UnknownFields {
+        &self.unknown_fields
+    }
+
+    fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields {
+        &mut self.unknown_fields
+    }
+
+    fn as_any(&self) -> &dyn (::std::any::Any) {
+        self as &dyn (::std::any::Any)
+    }
+    fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) {
+        self as &mut dyn (::std::any::Any)
+    }
+    fn into_any(self: ::std::boxed::Box<Self>) -> ::std::boxed::Box<dyn (::std::any::Any)> {
+        self
+    }
+
+    fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor {
+        Self::descriptor_static()
+    }
+
+    fn new() -> CreateAppParams {
+        CreateAppParams::new()
+    }
+
+    fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
+        static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT;
+        descriptor.get(|| {
+            let mut fields = ::std::vec::Vec::new();
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
+                "workspace_id",
+                |m: &CreateAppParams| { &m.workspace_id },
+                |m: &mut CreateAppParams| { &mut m.workspace_id },
+            ));
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
+                "name",
+                |m: &CreateAppParams| { &m.name },
+                |m: &mut CreateAppParams| { &mut m.name },
+            ));
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
+                "desc",
+                |m: &CreateAppParams| { &m.desc },
+                |m: &mut CreateAppParams| { &mut m.desc },
+            ));
+            fields.push(::protobuf::reflect::accessor::make_singular_ptr_field_accessor::<_, ::protobuf::types::ProtobufTypeMessage<ColorStyle>>(
+                "color_style",
+                |m: &CreateAppParams| { &m.color_style },
+                |m: &mut CreateAppParams| { &mut m.color_style },
+            ));
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
+                "user_id",
+                |m: &CreateAppParams| { &m.user_id },
+                |m: &mut CreateAppParams| { &mut m.user_id },
+            ));
+            ::protobuf::reflect::MessageDescriptor::new_pb_name::<CreateAppParams>(
+                "CreateAppParams",
+                fields,
+                file_descriptor_proto()
+            )
+        })
+    }
+
+    fn default_instance() -> &'static CreateAppParams {
+        static instance: ::protobuf::rt::LazyV2<CreateAppParams> = ::protobuf::rt::LazyV2::INIT;
+        instance.get(CreateAppParams::new)
+    }
+}
+
+impl ::protobuf::Clear for CreateAppParams {
+    fn clear(&mut self) {
+        self.workspace_id.clear();
+        self.name.clear();
+        self.desc.clear();
+        self.color_style.clear();
+        self.user_id.clear();
+        self.unknown_fields.clear();
+    }
+}
+
+impl ::std::fmt::Debug for CreateAppParams {
+    fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+        ::protobuf::text_format::fmt(self, f)
+    }
+}
+
+impl ::protobuf::reflect::ProtobufValue for CreateAppParams {
+    fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
+        ::protobuf::reflect::ReflectValueRef::Message(self)
+    }
+}
+
 #[derive(PartialEq,Clone,Default)]
 pub struct App {
     // message fields
@@ -1031,52 +1373,72 @@ static file_descriptor_proto_data: &'static [u8] = b"\
     \x12\n\x04name\x18\x02\x20\x01(\tR\x04name\x12\x12\n\x04desc\x18\x03\x20\
     \x01(\tR\x04desc\x12,\n\x0bcolor_style\x18\x04\x20\x01(\x0b2\x0b.ColorSt\
     yleR\ncolorStyle\"-\n\nColorStyle\x12\x1f\n\x0btheme_color\x18\x01\x20\
-    \x01(\tR\nthemeColor\"\xa9\x01\n\x03App\x12\x0e\n\x02id\x18\x01\x20\x01(\
-    \tR\x02id\x12!\n\x0cworkspace_id\x18\x02\x20\x01(\tR\x0bworkspaceId\x12\
-    \x12\n\x04name\x18\x03\x20\x01(\tR\x04name\x12\x12\n\x04desc\x18\x04\x20\
-    \x01(\tR\x04desc\x12-\n\nbelongings\x18\x05\x20\x01(\x0b2\r.RepeatedView\
-    R\nbelongings\x12\x18\n\x07version\x18\x06\x20\x01(\x03R\x07version\")\n\
-    \x0bRepeatedApp\x12\x1a\n\x05items\x18\x01\x20\x03(\x0b2\x04.AppR\x05ite\
-    msJ\x9f\x06\n\x06\x12\x04\0\0\x16\x01\n\x08\n\x01\x0c\x12\x03\0\0\x12\n\
-    \t\n\x02\x03\0\x12\x03\x01\0\x1b\n\n\n\x02\x04\0\x12\x04\x03\0\x08\x01\n\
-    \n\n\x03\x04\0\x01\x12\x03\x03\x08\x18\n\x0b\n\x04\x04\0\x02\0\x12\x03\
-    \x04\x04\x1c\n\x0c\n\x05\x04\0\x02\0\x05\x12\x03\x04\x04\n\n\x0c\n\x05\
-    \x04\0\x02\0\x01\x12\x03\x04\x0b\x17\n\x0c\n\x05\x04\0\x02\0\x03\x12\x03\
-    \x04\x1a\x1b\n\x0b\n\x04\x04\0\x02\x01\x12\x03\x05\x04\x14\n\x0c\n\x05\
-    \x04\0\x02\x01\x05\x12\x03\x05\x04\n\n\x0c\n\x05\x04\0\x02\x01\x01\x12\
-    \x03\x05\x0b\x0f\n\x0c\n\x05\x04\0\x02\x01\x03\x12\x03\x05\x12\x13\n\x0b\
-    \n\x04\x04\0\x02\x02\x12\x03\x06\x04\x14\n\x0c\n\x05\x04\0\x02\x02\x05\
-    \x12\x03\x06\x04\n\n\x0c\n\x05\x04\0\x02\x02\x01\x12\x03\x06\x0b\x0f\n\
-    \x0c\n\x05\x04\0\x02\x02\x03\x12\x03\x06\x12\x13\n\x0b\n\x04\x04\0\x02\
-    \x03\x12\x03\x07\x04\x1f\n\x0c\n\x05\x04\0\x02\x03\x06\x12\x03\x07\x04\
-    \x0e\n\x0c\n\x05\x04\0\x02\x03\x01\x12\x03\x07\x0f\x1a\n\x0c\n\x05\x04\0\
-    \x02\x03\x03\x12\x03\x07\x1d\x1e\n\n\n\x02\x04\x01\x12\x04\t\0\x0b\x01\n\
-    \n\n\x03\x04\x01\x01\x12\x03\t\x08\x12\n\x0b\n\x04\x04\x01\x02\0\x12\x03\
-    \n\x04\x1b\n\x0c\n\x05\x04\x01\x02\0\x05\x12\x03\n\x04\n\n\x0c\n\x05\x04\
-    \x01\x02\0\x01\x12\x03\n\x0b\x16\n\x0c\n\x05\x04\x01\x02\0\x03\x12\x03\n\
-    \x19\x1a\n\n\n\x02\x04\x02\x12\x04\x0c\0\x13\x01\n\n\n\x03\x04\x02\x01\
-    \x12\x03\x0c\x08\x0b\n\x0b\n\x04\x04\x02\x02\0\x12\x03\r\x04\x12\n\x0c\n\
-    \x05\x04\x02\x02\0\x05\x12\x03\r\x04\n\n\x0c\n\x05\x04\x02\x02\0\x01\x12\
-    \x03\r\x0b\r\n\x0c\n\x05\x04\x02\x02\0\x03\x12\x03\r\x10\x11\n\x0b\n\x04\
-    \x04\x02\x02\x01\x12\x03\x0e\x04\x1c\n\x0c\n\x05\x04\x02\x02\x01\x05\x12\
-    \x03\x0e\x04\n\n\x0c\n\x05\x04\x02\x02\x01\x01\x12\x03\x0e\x0b\x17\n\x0c\
-    \n\x05\x04\x02\x02\x01\x03\x12\x03\x0e\x1a\x1b\n\x0b\n\x04\x04\x02\x02\
-    \x02\x12\x03\x0f\x04\x14\n\x0c\n\x05\x04\x02\x02\x02\x05\x12\x03\x0f\x04\
-    \n\n\x0c\n\x05\x04\x02\x02\x02\x01\x12\x03\x0f\x0b\x0f\n\x0c\n\x05\x04\
-    \x02\x02\x02\x03\x12\x03\x0f\x12\x13\n\x0b\n\x04\x04\x02\x02\x03\x12\x03\
-    \x10\x04\x14\n\x0c\n\x05\x04\x02\x02\x03\x05\x12\x03\x10\x04\n\n\x0c\n\
-    \x05\x04\x02\x02\x03\x01\x12\x03\x10\x0b\x0f\n\x0c\n\x05\x04\x02\x02\x03\
-    \x03\x12\x03\x10\x12\x13\n\x0b\n\x04\x04\x02\x02\x04\x12\x03\x11\x04\x20\
-    \n\x0c\n\x05\x04\x02\x02\x04\x06\x12\x03\x11\x04\x10\n\x0c\n\x05\x04\x02\
-    \x02\x04\x01\x12\x03\x11\x11\x1b\n\x0c\n\x05\x04\x02\x02\x04\x03\x12\x03\
-    \x11\x1e\x1f\n\x0b\n\x04\x04\x02\x02\x05\x12\x03\x12\x04\x16\n\x0c\n\x05\
-    \x04\x02\x02\x05\x05\x12\x03\x12\x04\t\n\x0c\n\x05\x04\x02\x02\x05\x01\
-    \x12\x03\x12\n\x11\n\x0c\n\x05\x04\x02\x02\x05\x03\x12\x03\x12\x14\x15\n\
-    \n\n\x02\x04\x03\x12\x04\x14\0\x16\x01\n\n\n\x03\x04\x03\x01\x12\x03\x14\
-    \x08\x13\n\x0b\n\x04\x04\x03\x02\0\x12\x03\x15\x04\x1b\n\x0c\n\x05\x04\
-    \x03\x02\0\x04\x12\x03\x15\x04\x0c\n\x0c\n\x05\x04\x03\x02\0\x06\x12\x03\
-    \x15\r\x10\n\x0c\n\x05\x04\x03\x02\0\x01\x12\x03\x15\x11\x16\n\x0c\n\x05\
-    \x04\x03\x02\0\x03\x12\x03\x15\x19\x1ab\x06proto3\
+    \x01(\tR\nthemeColor\"\xa3\x01\n\x0fCreateAppParams\x12!\n\x0cworkspace_\
+    id\x18\x01\x20\x01(\tR\x0bworkspaceId\x12\x12\n\x04name\x18\x02\x20\x01(\
+    \tR\x04name\x12\x12\n\x04desc\x18\x03\x20\x01(\tR\x04desc\x12,\n\x0bcolo\
+    r_style\x18\x04\x20\x01(\x0b2\x0b.ColorStyleR\ncolorStyle\x12\x17\n\x07u\
+    ser_id\x18\x05\x20\x01(\tR\x06userId\"\xa9\x01\n\x03App\x12\x0e\n\x02id\
+    \x18\x01\x20\x01(\tR\x02id\x12!\n\x0cworkspace_id\x18\x02\x20\x01(\tR\
+    \x0bworkspaceId\x12\x12\n\x04name\x18\x03\x20\x01(\tR\x04name\x12\x12\n\
+    \x04desc\x18\x04\x20\x01(\tR\x04desc\x12-\n\nbelongings\x18\x05\x20\x01(\
+    \x0b2\r.RepeatedViewR\nbelongings\x12\x18\n\x07version\x18\x06\x20\x01(\
+    \x03R\x07version\")\n\x0bRepeatedApp\x12\x1a\n\x05items\x18\x01\x20\x03(\
+    \x0b2\x04.AppR\x05itemsJ\xca\x08\n\x06\x12\x04\0\0\x1d\x01\n\x08\n\x01\
+    \x0c\x12\x03\0\0\x12\n\t\n\x02\x03\0\x12\x03\x01\0\x1b\n\n\n\x02\x04\0\
+    \x12\x04\x03\0\x08\x01\n\n\n\x03\x04\0\x01\x12\x03\x03\x08\x18\n\x0b\n\
+    \x04\x04\0\x02\0\x12\x03\x04\x04\x1c\n\x0c\n\x05\x04\0\x02\0\x05\x12\x03\
+    \x04\x04\n\n\x0c\n\x05\x04\0\x02\0\x01\x12\x03\x04\x0b\x17\n\x0c\n\x05\
+    \x04\0\x02\0\x03\x12\x03\x04\x1a\x1b\n\x0b\n\x04\x04\0\x02\x01\x12\x03\
+    \x05\x04\x14\n\x0c\n\x05\x04\0\x02\x01\x05\x12\x03\x05\x04\n\n\x0c\n\x05\
+    \x04\0\x02\x01\x01\x12\x03\x05\x0b\x0f\n\x0c\n\x05\x04\0\x02\x01\x03\x12\
+    \x03\x05\x12\x13\n\x0b\n\x04\x04\0\x02\x02\x12\x03\x06\x04\x14\n\x0c\n\
+    \x05\x04\0\x02\x02\x05\x12\x03\x06\x04\n\n\x0c\n\x05\x04\0\x02\x02\x01\
+    \x12\x03\x06\x0b\x0f\n\x0c\n\x05\x04\0\x02\x02\x03\x12\x03\x06\x12\x13\n\
+    \x0b\n\x04\x04\0\x02\x03\x12\x03\x07\x04\x1f\n\x0c\n\x05\x04\0\x02\x03\
+    \x06\x12\x03\x07\x04\x0e\n\x0c\n\x05\x04\0\x02\x03\x01\x12\x03\x07\x0f\
+    \x1a\n\x0c\n\x05\x04\0\x02\x03\x03\x12\x03\x07\x1d\x1e\n\n\n\x02\x04\x01\
+    \x12\x04\t\0\x0b\x01\n\n\n\x03\x04\x01\x01\x12\x03\t\x08\x12\n\x0b\n\x04\
+    \x04\x01\x02\0\x12\x03\n\x04\x1b\n\x0c\n\x05\x04\x01\x02\0\x05\x12\x03\n\
+    \x04\n\n\x0c\n\x05\x04\x01\x02\0\x01\x12\x03\n\x0b\x16\n\x0c\n\x05\x04\
+    \x01\x02\0\x03\x12\x03\n\x19\x1a\n\n\n\x02\x04\x02\x12\x04\x0c\0\x12\x01\
+    \n\n\n\x03\x04\x02\x01\x12\x03\x0c\x08\x17\n\x0b\n\x04\x04\x02\x02\0\x12\
+    \x03\r\x04\x1c\n\x0c\n\x05\x04\x02\x02\0\x05\x12\x03\r\x04\n\n\x0c\n\x05\
+    \x04\x02\x02\0\x01\x12\x03\r\x0b\x17\n\x0c\n\x05\x04\x02\x02\0\x03\x12\
+    \x03\r\x1a\x1b\n\x0b\n\x04\x04\x02\x02\x01\x12\x03\x0e\x04\x14\n\x0c\n\
+    \x05\x04\x02\x02\x01\x05\x12\x03\x0e\x04\n\n\x0c\n\x05\x04\x02\x02\x01\
+    \x01\x12\x03\x0e\x0b\x0f\n\x0c\n\x05\x04\x02\x02\x01\x03\x12\x03\x0e\x12\
+    \x13\n\x0b\n\x04\x04\x02\x02\x02\x12\x03\x0f\x04\x14\n\x0c\n\x05\x04\x02\
+    \x02\x02\x05\x12\x03\x0f\x04\n\n\x0c\n\x05\x04\x02\x02\x02\x01\x12\x03\
+    \x0f\x0b\x0f\n\x0c\n\x05\x04\x02\x02\x02\x03\x12\x03\x0f\x12\x13\n\x0b\n\
+    \x04\x04\x02\x02\x03\x12\x03\x10\x04\x1f\n\x0c\n\x05\x04\x02\x02\x03\x06\
+    \x12\x03\x10\x04\x0e\n\x0c\n\x05\x04\x02\x02\x03\x01\x12\x03\x10\x0f\x1a\
+    \n\x0c\n\x05\x04\x02\x02\x03\x03\x12\x03\x10\x1d\x1e\n\x0b\n\x04\x04\x02\
+    \x02\x04\x12\x03\x11\x04\x17\n\x0c\n\x05\x04\x02\x02\x04\x05\x12\x03\x11\
+    \x04\n\n\x0c\n\x05\x04\x02\x02\x04\x01\x12\x03\x11\x0b\x12\n\x0c\n\x05\
+    \x04\x02\x02\x04\x03\x12\x03\x11\x15\x16\n\n\n\x02\x04\x03\x12\x04\x13\0\
+    \x1a\x01\n\n\n\x03\x04\x03\x01\x12\x03\x13\x08\x0b\n\x0b\n\x04\x04\x03\
+    \x02\0\x12\x03\x14\x04\x12\n\x0c\n\x05\x04\x03\x02\0\x05\x12\x03\x14\x04\
+    \n\n\x0c\n\x05\x04\x03\x02\0\x01\x12\x03\x14\x0b\r\n\x0c\n\x05\x04\x03\
+    \x02\0\x03\x12\x03\x14\x10\x11\n\x0b\n\x04\x04\x03\x02\x01\x12\x03\x15\
+    \x04\x1c\n\x0c\n\x05\x04\x03\x02\x01\x05\x12\x03\x15\x04\n\n\x0c\n\x05\
+    \x04\x03\x02\x01\x01\x12\x03\x15\x0b\x17\n\x0c\n\x05\x04\x03\x02\x01\x03\
+    \x12\x03\x15\x1a\x1b\n\x0b\n\x04\x04\x03\x02\x02\x12\x03\x16\x04\x14\n\
+    \x0c\n\x05\x04\x03\x02\x02\x05\x12\x03\x16\x04\n\n\x0c\n\x05\x04\x03\x02\
+    \x02\x01\x12\x03\x16\x0b\x0f\n\x0c\n\x05\x04\x03\x02\x02\x03\x12\x03\x16\
+    \x12\x13\n\x0b\n\x04\x04\x03\x02\x03\x12\x03\x17\x04\x14\n\x0c\n\x05\x04\
+    \x03\x02\x03\x05\x12\x03\x17\x04\n\n\x0c\n\x05\x04\x03\x02\x03\x01\x12\
+    \x03\x17\x0b\x0f\n\x0c\n\x05\x04\x03\x02\x03\x03\x12\x03\x17\x12\x13\n\
+    \x0b\n\x04\x04\x03\x02\x04\x12\x03\x18\x04\x20\n\x0c\n\x05\x04\x03\x02\
+    \x04\x06\x12\x03\x18\x04\x10\n\x0c\n\x05\x04\x03\x02\x04\x01\x12\x03\x18\
+    \x11\x1b\n\x0c\n\x05\x04\x03\x02\x04\x03\x12\x03\x18\x1e\x1f\n\x0b\n\x04\
+    \x04\x03\x02\x05\x12\x03\x19\x04\x16\n\x0c\n\x05\x04\x03\x02\x05\x05\x12\
+    \x03\x19\x04\t\n\x0c\n\x05\x04\x03\x02\x05\x01\x12\x03\x19\n\x11\n\x0c\n\
+    \x05\x04\x03\x02\x05\x03\x12\x03\x19\x14\x15\n\n\n\x02\x04\x04\x12\x04\
+    \x1b\0\x1d\x01\n\n\n\x03\x04\x04\x01\x12\x03\x1b\x08\x13\n\x0b\n\x04\x04\
+    \x04\x02\0\x12\x03\x1c\x04\x1b\n\x0c\n\x05\x04\x04\x02\0\x04\x12\x03\x1c\
+    \x04\x0c\n\x0c\n\x05\x04\x04\x02\0\x06\x12\x03\x1c\r\x10\n\x0c\n\x05\x04\
+    \x04\x02\0\x01\x12\x03\x1c\x11\x16\n\x0c\n\x05\x04\x04\x02\0\x03\x12\x03\
+    \x1c\x19\x1ab\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 169 - 5
rust-lib/flowy-workspace/src/protobuf/model/app_delete.rs

@@ -182,13 +182,177 @@ impl ::protobuf::reflect::ProtobufValue for DeleteAppRequest {
     }
 }
 
+#[derive(PartialEq,Clone,Default)]
+pub struct DeleteAppParams {
+    // message fields
+    pub app_id: ::std::string::String,
+    // special fields
+    pub unknown_fields: ::protobuf::UnknownFields,
+    pub cached_size: ::protobuf::CachedSize,
+}
+
+impl<'a> ::std::default::Default for &'a DeleteAppParams {
+    fn default() -> &'a DeleteAppParams {
+        <DeleteAppParams as ::protobuf::Message>::default_instance()
+    }
+}
+
+impl DeleteAppParams {
+    pub fn new() -> DeleteAppParams {
+        ::std::default::Default::default()
+    }
+
+    // string app_id = 1;
+
+
+    pub fn get_app_id(&self) -> &str {
+        &self.app_id
+    }
+    pub fn clear_app_id(&mut self) {
+        self.app_id.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_app_id(&mut self, v: ::std::string::String) {
+        self.app_id = v;
+    }
+
+    // Mutable pointer to the field.
+    // If field is not initialized, it is initialized with default value first.
+    pub fn mut_app_id(&mut self) -> &mut ::std::string::String {
+        &mut self.app_id
+    }
+
+    // Take field
+    pub fn take_app_id(&mut self) -> ::std::string::String {
+        ::std::mem::replace(&mut self.app_id, ::std::string::String::new())
+    }
+}
+
+impl ::protobuf::Message for DeleteAppParams {
+    fn is_initialized(&self) -> bool {
+        true
+    }
+
+    fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+        while !is.eof()? {
+            let (field_number, wire_type) = is.read_tag_unpack()?;
+            match field_number {
+                1 => {
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.app_id)?;
+                },
+                _ => {
+                    ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
+                },
+            };
+        }
+        ::std::result::Result::Ok(())
+    }
+
+    // Compute sizes of nested messages
+    #[allow(unused_variables)]
+    fn compute_size(&self) -> u32 {
+        let mut my_size = 0;
+        if !self.app_id.is_empty() {
+            my_size += ::protobuf::rt::string_size(1, &self.app_id);
+        }
+        my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
+        self.cached_size.set(my_size);
+        my_size
+    }
+
+    fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+        if !self.app_id.is_empty() {
+            os.write_string(1, &self.app_id)?;
+        }
+        os.write_unknown_fields(self.get_unknown_fields())?;
+        ::std::result::Result::Ok(())
+    }
+
+    fn get_cached_size(&self) -> u32 {
+        self.cached_size.get()
+    }
+
+    fn get_unknown_fields(&self) -> &::protobuf::UnknownFields {
+        &self.unknown_fields
+    }
+
+    fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields {
+        &mut self.unknown_fields
+    }
+
+    fn as_any(&self) -> &dyn (::std::any::Any) {
+        self as &dyn (::std::any::Any)
+    }
+    fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) {
+        self as &mut dyn (::std::any::Any)
+    }
+    fn into_any(self: ::std::boxed::Box<Self>) -> ::std::boxed::Box<dyn (::std::any::Any)> {
+        self
+    }
+
+    fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor {
+        Self::descriptor_static()
+    }
+
+    fn new() -> DeleteAppParams {
+        DeleteAppParams::new()
+    }
+
+    fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
+        static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT;
+        descriptor.get(|| {
+            let mut fields = ::std::vec::Vec::new();
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
+                "app_id",
+                |m: &DeleteAppParams| { &m.app_id },
+                |m: &mut DeleteAppParams| { &mut m.app_id },
+            ));
+            ::protobuf::reflect::MessageDescriptor::new_pb_name::<DeleteAppParams>(
+                "DeleteAppParams",
+                fields,
+                file_descriptor_proto()
+            )
+        })
+    }
+
+    fn default_instance() -> &'static DeleteAppParams {
+        static instance: ::protobuf::rt::LazyV2<DeleteAppParams> = ::protobuf::rt::LazyV2::INIT;
+        instance.get(DeleteAppParams::new)
+    }
+}
+
+impl ::protobuf::Clear for DeleteAppParams {
+    fn clear(&mut self) {
+        self.app_id.clear();
+        self.unknown_fields.clear();
+    }
+}
+
+impl ::std::fmt::Debug for DeleteAppParams {
+    fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+        ::protobuf::text_format::fmt(self, f)
+    }
+}
+
+impl ::protobuf::reflect::ProtobufValue for DeleteAppParams {
+    fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
+        ::protobuf::reflect::ReflectValueRef::Message(self)
+    }
+}
+
 static file_descriptor_proto_data: &'static [u8] = b"\
     \n\x10app_delete.proto\")\n\x10DeleteAppRequest\x12\x15\n\x06app_id\x18\
-    \x01\x20\x01(\tR\x05appIdJa\n\x06\x12\x04\0\0\x04\x01\n\x08\n\x01\x0c\
-    \x12\x03\0\0\x12\n\n\n\x02\x04\0\x12\x04\x02\0\x04\x01\n\n\n\x03\x04\0\
-    \x01\x12\x03\x02\x08\x18\n\x0b\n\x04\x04\0\x02\0\x12\x03\x03\x04\x16\n\
-    \x0c\n\x05\x04\0\x02\0\x05\x12\x03\x03\x04\n\n\x0c\n\x05\x04\0\x02\0\x01\
-    \x12\x03\x03\x0b\x11\n\x0c\n\x05\x04\0\x02\0\x03\x12\x03\x03\x14\x15b\
+    \x01\x20\x01(\tR\x05appId\"(\n\x0fDeleteAppParams\x12\x15\n\x06app_id\
+    \x18\x01\x20\x01(\tR\x05appIdJ\xb0\x01\n\x06\x12\x04\0\0\x07\x01\n\x08\n\
+    \x01\x0c\x12\x03\0\0\x12\n\n\n\x02\x04\0\x12\x04\x02\0\x04\x01\n\n\n\x03\
+    \x04\0\x01\x12\x03\x02\x08\x18\n\x0b\n\x04\x04\0\x02\0\x12\x03\x03\x04\
+    \x16\n\x0c\n\x05\x04\0\x02\0\x05\x12\x03\x03\x04\n\n\x0c\n\x05\x04\0\x02\
+    \0\x01\x12\x03\x03\x0b\x11\n\x0c\n\x05\x04\0\x02\0\x03\x12\x03\x03\x14\
+    \x15\n\n\n\x02\x04\x01\x12\x04\x05\0\x07\x01\n\n\n\x03\x04\x01\x01\x12\
+    \x03\x05\x08\x17\n\x0b\n\x04\x04\x01\x02\0\x12\x03\x06\x04\x16\n\x0c\n\
+    \x05\x04\x01\x02\0\x05\x12\x03\x06\x04\n\n\x0c\n\x05\x04\x01\x02\0\x01\
+    \x12\x03\x06\x0b\x11\n\x0c\n\x05\x04\x01\x02\0\x03\x12\x03\x06\x14\x15b\
     \x06proto3\
 ";
 

+ 252 - 11
rust-lib/flowy-workspace/src/protobuf/model/app_query.rs

@@ -252,21 +252,262 @@ impl ::protobuf::reflect::ProtobufValue for QueryAppRequest {
     }
 }
 
+#[derive(PartialEq,Clone,Default)]
+pub struct QueryAppParams {
+    // message fields
+    pub app_id: ::std::string::String,
+    pub read_belongings: bool,
+    pub is_trash: bool,
+    // special fields
+    pub unknown_fields: ::protobuf::UnknownFields,
+    pub cached_size: ::protobuf::CachedSize,
+}
+
+impl<'a> ::std::default::Default for &'a QueryAppParams {
+    fn default() -> &'a QueryAppParams {
+        <QueryAppParams as ::protobuf::Message>::default_instance()
+    }
+}
+
+impl QueryAppParams {
+    pub fn new() -> QueryAppParams {
+        ::std::default::Default::default()
+    }
+
+    // string app_id = 1;
+
+
+    pub fn get_app_id(&self) -> &str {
+        &self.app_id
+    }
+    pub fn clear_app_id(&mut self) {
+        self.app_id.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_app_id(&mut self, v: ::std::string::String) {
+        self.app_id = v;
+    }
+
+    // Mutable pointer to the field.
+    // If field is not initialized, it is initialized with default value first.
+    pub fn mut_app_id(&mut self) -> &mut ::std::string::String {
+        &mut self.app_id
+    }
+
+    // Take field
+    pub fn take_app_id(&mut self) -> ::std::string::String {
+        ::std::mem::replace(&mut self.app_id, ::std::string::String::new())
+    }
+
+    // bool read_belongings = 2;
+
+
+    pub fn get_read_belongings(&self) -> bool {
+        self.read_belongings
+    }
+    pub fn clear_read_belongings(&mut self) {
+        self.read_belongings = false;
+    }
+
+    // Param is passed by value, moved
+    pub fn set_read_belongings(&mut self, v: bool) {
+        self.read_belongings = v;
+    }
+
+    // bool is_trash = 3;
+
+
+    pub fn get_is_trash(&self) -> bool {
+        self.is_trash
+    }
+    pub fn clear_is_trash(&mut self) {
+        self.is_trash = false;
+    }
+
+    // Param is passed by value, moved
+    pub fn set_is_trash(&mut self, v: bool) {
+        self.is_trash = v;
+    }
+}
+
+impl ::protobuf::Message for QueryAppParams {
+    fn is_initialized(&self) -> bool {
+        true
+    }
+
+    fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+        while !is.eof()? {
+            let (field_number, wire_type) = is.read_tag_unpack()?;
+            match field_number {
+                1 => {
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.app_id)?;
+                },
+                2 => {
+                    if wire_type != ::protobuf::wire_format::WireTypeVarint {
+                        return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
+                    }
+                    let tmp = is.read_bool()?;
+                    self.read_belongings = tmp;
+                },
+                3 => {
+                    if wire_type != ::protobuf::wire_format::WireTypeVarint {
+                        return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
+                    }
+                    let tmp = is.read_bool()?;
+                    self.is_trash = tmp;
+                },
+                _ => {
+                    ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
+                },
+            };
+        }
+        ::std::result::Result::Ok(())
+    }
+
+    // Compute sizes of nested messages
+    #[allow(unused_variables)]
+    fn compute_size(&self) -> u32 {
+        let mut my_size = 0;
+        if !self.app_id.is_empty() {
+            my_size += ::protobuf::rt::string_size(1, &self.app_id);
+        }
+        if self.read_belongings != false {
+            my_size += 2;
+        }
+        if self.is_trash != false {
+            my_size += 2;
+        }
+        my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
+        self.cached_size.set(my_size);
+        my_size
+    }
+
+    fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+        if !self.app_id.is_empty() {
+            os.write_string(1, &self.app_id)?;
+        }
+        if self.read_belongings != false {
+            os.write_bool(2, self.read_belongings)?;
+        }
+        if self.is_trash != false {
+            os.write_bool(3, self.is_trash)?;
+        }
+        os.write_unknown_fields(self.get_unknown_fields())?;
+        ::std::result::Result::Ok(())
+    }
+
+    fn get_cached_size(&self) -> u32 {
+        self.cached_size.get()
+    }
+
+    fn get_unknown_fields(&self) -> &::protobuf::UnknownFields {
+        &self.unknown_fields
+    }
+
+    fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields {
+        &mut self.unknown_fields
+    }
+
+    fn as_any(&self) -> &dyn (::std::any::Any) {
+        self as &dyn (::std::any::Any)
+    }
+    fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) {
+        self as &mut dyn (::std::any::Any)
+    }
+    fn into_any(self: ::std::boxed::Box<Self>) -> ::std::boxed::Box<dyn (::std::any::Any)> {
+        self
+    }
+
+    fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor {
+        Self::descriptor_static()
+    }
+
+    fn new() -> QueryAppParams {
+        QueryAppParams::new()
+    }
+
+    fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
+        static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT;
+        descriptor.get(|| {
+            let mut fields = ::std::vec::Vec::new();
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
+                "app_id",
+                |m: &QueryAppParams| { &m.app_id },
+                |m: &mut QueryAppParams| { &mut m.app_id },
+            ));
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeBool>(
+                "read_belongings",
+                |m: &QueryAppParams| { &m.read_belongings },
+                |m: &mut QueryAppParams| { &mut m.read_belongings },
+            ));
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeBool>(
+                "is_trash",
+                |m: &QueryAppParams| { &m.is_trash },
+                |m: &mut QueryAppParams| { &mut m.is_trash },
+            ));
+            ::protobuf::reflect::MessageDescriptor::new_pb_name::<QueryAppParams>(
+                "QueryAppParams",
+                fields,
+                file_descriptor_proto()
+            )
+        })
+    }
+
+    fn default_instance() -> &'static QueryAppParams {
+        static instance: ::protobuf::rt::LazyV2<QueryAppParams> = ::protobuf::rt::LazyV2::INIT;
+        instance.get(QueryAppParams::new)
+    }
+}
+
+impl ::protobuf::Clear for QueryAppParams {
+    fn clear(&mut self) {
+        self.app_id.clear();
+        self.read_belongings = false;
+        self.is_trash = false;
+        self.unknown_fields.clear();
+    }
+}
+
+impl ::std::fmt::Debug for QueryAppParams {
+    fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+        ::protobuf::text_format::fmt(self, f)
+    }
+}
+
+impl ::protobuf::reflect::ProtobufValue for QueryAppParams {
+    fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
+        ::protobuf::reflect::ReflectValueRef::Message(self)
+    }
+}
+
 static file_descriptor_proto_data: &'static [u8] = b"\
     \n\x0fapp_query.proto\"l\n\x0fQueryAppRequest\x12\x15\n\x06app_id\x18\
     \x01\x20\x01(\tR\x05appId\x12'\n\x0fread_belongings\x18\x02\x20\x01(\x08\
     R\x0ereadBelongings\x12\x19\n\x08is_trash\x18\x03\x20\x01(\x08R\x07isTra\
-    shJ\xcf\x01\n\x06\x12\x04\0\0\x06\x01\n\x08\n\x01\x0c\x12\x03\0\0\x12\n\
-    \n\n\x02\x04\0\x12\x04\x02\0\x06\x01\n\n\n\x03\x04\0\x01\x12\x03\x02\x08\
-    \x17\n\x0b\n\x04\x04\0\x02\0\x12\x03\x03\x04\x16\n\x0c\n\x05\x04\0\x02\0\
-    \x05\x12\x03\x03\x04\n\n\x0c\n\x05\x04\0\x02\0\x01\x12\x03\x03\x0b\x11\n\
-    \x0c\n\x05\x04\0\x02\0\x03\x12\x03\x03\x14\x15\n\x0b\n\x04\x04\0\x02\x01\
-    \x12\x03\x04\x04\x1d\n\x0c\n\x05\x04\0\x02\x01\x05\x12\x03\x04\x04\x08\n\
-    \x0c\n\x05\x04\0\x02\x01\x01\x12\x03\x04\t\x18\n\x0c\n\x05\x04\0\x02\x01\
-    \x03\x12\x03\x04\x1b\x1c\n\x0b\n\x04\x04\0\x02\x02\x12\x03\x05\x04\x16\n\
-    \x0c\n\x05\x04\0\x02\x02\x05\x12\x03\x05\x04\x08\n\x0c\n\x05\x04\0\x02\
-    \x02\x01\x12\x03\x05\t\x11\n\x0c\n\x05\x04\0\x02\x02\x03\x12\x03\x05\x14\
-    \x15b\x06proto3\
+    sh\"k\n\x0eQueryAppParams\x12\x15\n\x06app_id\x18\x01\x20\x01(\tR\x05app\
+    Id\x12'\n\x0fread_belongings\x18\x02\x20\x01(\x08R\x0ereadBelongings\x12\
+    \x19\n\x08is_trash\x18\x03\x20\x01(\x08R\x07isTrashJ\x8c\x03\n\x06\x12\
+    \x04\0\0\x0b\x01\n\x08\n\x01\x0c\x12\x03\0\0\x12\n\n\n\x02\x04\0\x12\x04\
+    \x02\0\x06\x01\n\n\n\x03\x04\0\x01\x12\x03\x02\x08\x17\n\x0b\n\x04\x04\0\
+    \x02\0\x12\x03\x03\x04\x16\n\x0c\n\x05\x04\0\x02\0\x05\x12\x03\x03\x04\n\
+    \n\x0c\n\x05\x04\0\x02\0\x01\x12\x03\x03\x0b\x11\n\x0c\n\x05\x04\0\x02\0\
+    \x03\x12\x03\x03\x14\x15\n\x0b\n\x04\x04\0\x02\x01\x12\x03\x04\x04\x1d\n\
+    \x0c\n\x05\x04\0\x02\x01\x05\x12\x03\x04\x04\x08\n\x0c\n\x05\x04\0\x02\
+    \x01\x01\x12\x03\x04\t\x18\n\x0c\n\x05\x04\0\x02\x01\x03\x12\x03\x04\x1b\
+    \x1c\n\x0b\n\x04\x04\0\x02\x02\x12\x03\x05\x04\x16\n\x0c\n\x05\x04\0\x02\
+    \x02\x05\x12\x03\x05\x04\x08\n\x0c\n\x05\x04\0\x02\x02\x01\x12\x03\x05\t\
+    \x11\n\x0c\n\x05\x04\0\x02\x02\x03\x12\x03\x05\x14\x15\n\n\n\x02\x04\x01\
+    \x12\x04\x07\0\x0b\x01\n\n\n\x03\x04\x01\x01\x12\x03\x07\x08\x16\n\x0b\n\
+    \x04\x04\x01\x02\0\x12\x03\x08\x04\x16\n\x0c\n\x05\x04\x01\x02\0\x05\x12\
+    \x03\x08\x04\n\n\x0c\n\x05\x04\x01\x02\0\x01\x12\x03\x08\x0b\x11\n\x0c\n\
+    \x05\x04\x01\x02\0\x03\x12\x03\x08\x14\x15\n\x0b\n\x04\x04\x01\x02\x01\
+    \x12\x03\t\x04\x1d\n\x0c\n\x05\x04\x01\x02\x01\x05\x12\x03\t\x04\x08\n\
+    \x0c\n\x05\x04\x01\x02\x01\x01\x12\x03\t\t\x18\n\x0c\n\x05\x04\x01\x02\
+    \x01\x03\x12\x03\t\x1b\x1c\n\x0b\n\x04\x04\x01\x02\x02\x12\x03\n\x04\x16\
+    \n\x0c\n\x05\x04\x01\x02\x02\x05\x12\x03\n\x04\x08\n\x0c\n\x05\x04\x01\
+    \x02\x02\x01\x12\x03\n\t\x11\n\x0c\n\x05\x04\x01\x02\x02\x03\x12\x03\n\
+    \x14\x15b\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 606 - 26
rust-lib/flowy-workspace/src/protobuf/model/app_update.rs

@@ -572,6 +572,555 @@ impl ::protobuf::reflect::ProtobufValue for UpdateAppRequest {
     }
 }
 
+#[derive(PartialEq,Clone,Default)]
+pub struct UpdateAppParams {
+    // message fields
+    pub app_id: ::std::string::String,
+    // message oneof groups
+    pub one_of_workspace_id: ::std::option::Option<UpdateAppParams_oneof_one_of_workspace_id>,
+    pub one_of_name: ::std::option::Option<UpdateAppParams_oneof_one_of_name>,
+    pub one_of_desc: ::std::option::Option<UpdateAppParams_oneof_one_of_desc>,
+    pub one_of_color_style: ::std::option::Option<UpdateAppParams_oneof_one_of_color_style>,
+    pub one_of_is_trash: ::std::option::Option<UpdateAppParams_oneof_one_of_is_trash>,
+    // special fields
+    pub unknown_fields: ::protobuf::UnknownFields,
+    pub cached_size: ::protobuf::CachedSize,
+}
+
+impl<'a> ::std::default::Default for &'a UpdateAppParams {
+    fn default() -> &'a UpdateAppParams {
+        <UpdateAppParams as ::protobuf::Message>::default_instance()
+    }
+}
+
+#[derive(Clone,PartialEq,Debug)]
+pub enum UpdateAppParams_oneof_one_of_workspace_id {
+    workspace_id(::std::string::String),
+}
+
+#[derive(Clone,PartialEq,Debug)]
+pub enum UpdateAppParams_oneof_one_of_name {
+    name(::std::string::String),
+}
+
+#[derive(Clone,PartialEq,Debug)]
+pub enum UpdateAppParams_oneof_one_of_desc {
+    desc(::std::string::String),
+}
+
+#[derive(Clone,PartialEq,Debug)]
+pub enum UpdateAppParams_oneof_one_of_color_style {
+    color_style(super::app_create::ColorStyle),
+}
+
+#[derive(Clone,PartialEq,Debug)]
+pub enum UpdateAppParams_oneof_one_of_is_trash {
+    is_trash(bool),
+}
+
+impl UpdateAppParams {
+    pub fn new() -> UpdateAppParams {
+        ::std::default::Default::default()
+    }
+
+    // string app_id = 1;
+
+
+    pub fn get_app_id(&self) -> &str {
+        &self.app_id
+    }
+    pub fn clear_app_id(&mut self) {
+        self.app_id.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_app_id(&mut self, v: ::std::string::String) {
+        self.app_id = v;
+    }
+
+    // Mutable pointer to the field.
+    // If field is not initialized, it is initialized with default value first.
+    pub fn mut_app_id(&mut self) -> &mut ::std::string::String {
+        &mut self.app_id
+    }
+
+    // Take field
+    pub fn take_app_id(&mut self) -> ::std::string::String {
+        ::std::mem::replace(&mut self.app_id, ::std::string::String::new())
+    }
+
+    // string workspace_id = 2;
+
+
+    pub fn get_workspace_id(&self) -> &str {
+        match self.one_of_workspace_id {
+            ::std::option::Option::Some(UpdateAppParams_oneof_one_of_workspace_id::workspace_id(ref v)) => v,
+            _ => "",
+        }
+    }
+    pub fn clear_workspace_id(&mut self) {
+        self.one_of_workspace_id = ::std::option::Option::None;
+    }
+
+    pub fn has_workspace_id(&self) -> bool {
+        match self.one_of_workspace_id {
+            ::std::option::Option::Some(UpdateAppParams_oneof_one_of_workspace_id::workspace_id(..)) => true,
+            _ => false,
+        }
+    }
+
+    // Param is passed by value, moved
+    pub fn set_workspace_id(&mut self, v: ::std::string::String) {
+        self.one_of_workspace_id = ::std::option::Option::Some(UpdateAppParams_oneof_one_of_workspace_id::workspace_id(v))
+    }
+
+    // Mutable pointer to the field.
+    pub fn mut_workspace_id(&mut self) -> &mut ::std::string::String {
+        if let ::std::option::Option::Some(UpdateAppParams_oneof_one_of_workspace_id::workspace_id(_)) = self.one_of_workspace_id {
+        } else {
+            self.one_of_workspace_id = ::std::option::Option::Some(UpdateAppParams_oneof_one_of_workspace_id::workspace_id(::std::string::String::new()));
+        }
+        match self.one_of_workspace_id {
+            ::std::option::Option::Some(UpdateAppParams_oneof_one_of_workspace_id::workspace_id(ref mut v)) => v,
+            _ => panic!(),
+        }
+    }
+
+    // Take field
+    pub fn take_workspace_id(&mut self) -> ::std::string::String {
+        if self.has_workspace_id() {
+            match self.one_of_workspace_id.take() {
+                ::std::option::Option::Some(UpdateAppParams_oneof_one_of_workspace_id::workspace_id(v)) => v,
+                _ => panic!(),
+            }
+        } else {
+            ::std::string::String::new()
+        }
+    }
+
+    // string name = 3;
+
+
+    pub fn get_name(&self) -> &str {
+        match self.one_of_name {
+            ::std::option::Option::Some(UpdateAppParams_oneof_one_of_name::name(ref v)) => v,
+            _ => "",
+        }
+    }
+    pub fn clear_name(&mut self) {
+        self.one_of_name = ::std::option::Option::None;
+    }
+
+    pub fn has_name(&self) -> bool {
+        match self.one_of_name {
+            ::std::option::Option::Some(UpdateAppParams_oneof_one_of_name::name(..)) => true,
+            _ => false,
+        }
+    }
+
+    // Param is passed by value, moved
+    pub fn set_name(&mut self, v: ::std::string::String) {
+        self.one_of_name = ::std::option::Option::Some(UpdateAppParams_oneof_one_of_name::name(v))
+    }
+
+    // Mutable pointer to the field.
+    pub fn mut_name(&mut self) -> &mut ::std::string::String {
+        if let ::std::option::Option::Some(UpdateAppParams_oneof_one_of_name::name(_)) = self.one_of_name {
+        } else {
+            self.one_of_name = ::std::option::Option::Some(UpdateAppParams_oneof_one_of_name::name(::std::string::String::new()));
+        }
+        match self.one_of_name {
+            ::std::option::Option::Some(UpdateAppParams_oneof_one_of_name::name(ref mut v)) => v,
+            _ => panic!(),
+        }
+    }
+
+    // Take field
+    pub fn take_name(&mut self) -> ::std::string::String {
+        if self.has_name() {
+            match self.one_of_name.take() {
+                ::std::option::Option::Some(UpdateAppParams_oneof_one_of_name::name(v)) => v,
+                _ => panic!(),
+            }
+        } else {
+            ::std::string::String::new()
+        }
+    }
+
+    // string desc = 4;
+
+
+    pub fn get_desc(&self) -> &str {
+        match self.one_of_desc {
+            ::std::option::Option::Some(UpdateAppParams_oneof_one_of_desc::desc(ref v)) => v,
+            _ => "",
+        }
+    }
+    pub fn clear_desc(&mut self) {
+        self.one_of_desc = ::std::option::Option::None;
+    }
+
+    pub fn has_desc(&self) -> bool {
+        match self.one_of_desc {
+            ::std::option::Option::Some(UpdateAppParams_oneof_one_of_desc::desc(..)) => true,
+            _ => false,
+        }
+    }
+
+    // Param is passed by value, moved
+    pub fn set_desc(&mut self, v: ::std::string::String) {
+        self.one_of_desc = ::std::option::Option::Some(UpdateAppParams_oneof_one_of_desc::desc(v))
+    }
+
+    // Mutable pointer to the field.
+    pub fn mut_desc(&mut self) -> &mut ::std::string::String {
+        if let ::std::option::Option::Some(UpdateAppParams_oneof_one_of_desc::desc(_)) = self.one_of_desc {
+        } else {
+            self.one_of_desc = ::std::option::Option::Some(UpdateAppParams_oneof_one_of_desc::desc(::std::string::String::new()));
+        }
+        match self.one_of_desc {
+            ::std::option::Option::Some(UpdateAppParams_oneof_one_of_desc::desc(ref mut v)) => v,
+            _ => panic!(),
+        }
+    }
+
+    // Take field
+    pub fn take_desc(&mut self) -> ::std::string::String {
+        if self.has_desc() {
+            match self.one_of_desc.take() {
+                ::std::option::Option::Some(UpdateAppParams_oneof_one_of_desc::desc(v)) => v,
+                _ => panic!(),
+            }
+        } else {
+            ::std::string::String::new()
+        }
+    }
+
+    // .ColorStyle color_style = 5;
+
+
+    pub fn get_color_style(&self) -> &super::app_create::ColorStyle {
+        match self.one_of_color_style {
+            ::std::option::Option::Some(UpdateAppParams_oneof_one_of_color_style::color_style(ref v)) => v,
+            _ => <super::app_create::ColorStyle as ::protobuf::Message>::default_instance(),
+        }
+    }
+    pub fn clear_color_style(&mut self) {
+        self.one_of_color_style = ::std::option::Option::None;
+    }
+
+    pub fn has_color_style(&self) -> bool {
+        match self.one_of_color_style {
+            ::std::option::Option::Some(UpdateAppParams_oneof_one_of_color_style::color_style(..)) => true,
+            _ => false,
+        }
+    }
+
+    // Param is passed by value, moved
+    pub fn set_color_style(&mut self, v: super::app_create::ColorStyle) {
+        self.one_of_color_style = ::std::option::Option::Some(UpdateAppParams_oneof_one_of_color_style::color_style(v))
+    }
+
+    // Mutable pointer to the field.
+    pub fn mut_color_style(&mut self) -> &mut super::app_create::ColorStyle {
+        if let ::std::option::Option::Some(UpdateAppParams_oneof_one_of_color_style::color_style(_)) = self.one_of_color_style {
+        } else {
+            self.one_of_color_style = ::std::option::Option::Some(UpdateAppParams_oneof_one_of_color_style::color_style(super::app_create::ColorStyle::new()));
+        }
+        match self.one_of_color_style {
+            ::std::option::Option::Some(UpdateAppParams_oneof_one_of_color_style::color_style(ref mut v)) => v,
+            _ => panic!(),
+        }
+    }
+
+    // Take field
+    pub fn take_color_style(&mut self) -> super::app_create::ColorStyle {
+        if self.has_color_style() {
+            match self.one_of_color_style.take() {
+                ::std::option::Option::Some(UpdateAppParams_oneof_one_of_color_style::color_style(v)) => v,
+                _ => panic!(),
+            }
+        } else {
+            super::app_create::ColorStyle::new()
+        }
+    }
+
+    // bool is_trash = 6;
+
+
+    pub fn get_is_trash(&self) -> bool {
+        match self.one_of_is_trash {
+            ::std::option::Option::Some(UpdateAppParams_oneof_one_of_is_trash::is_trash(v)) => v,
+            _ => false,
+        }
+    }
+    pub fn clear_is_trash(&mut self) {
+        self.one_of_is_trash = ::std::option::Option::None;
+    }
+
+    pub fn has_is_trash(&self) -> bool {
+        match self.one_of_is_trash {
+            ::std::option::Option::Some(UpdateAppParams_oneof_one_of_is_trash::is_trash(..)) => true,
+            _ => false,
+        }
+    }
+
+    // Param is passed by value, moved
+    pub fn set_is_trash(&mut self, v: bool) {
+        self.one_of_is_trash = ::std::option::Option::Some(UpdateAppParams_oneof_one_of_is_trash::is_trash(v))
+    }
+}
+
+impl ::protobuf::Message for UpdateAppParams {
+    fn is_initialized(&self) -> bool {
+        if let Some(UpdateAppParams_oneof_one_of_color_style::color_style(ref v)) = self.one_of_color_style {
+            if !v.is_initialized() {
+                return false;
+            }
+        }
+        true
+    }
+
+    fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+        while !is.eof()? {
+            let (field_number, wire_type) = is.read_tag_unpack()?;
+            match field_number {
+                1 => {
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.app_id)?;
+                },
+                2 => {
+                    if wire_type != ::protobuf::wire_format::WireTypeLengthDelimited {
+                        return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
+                    }
+                    self.one_of_workspace_id = ::std::option::Option::Some(UpdateAppParams_oneof_one_of_workspace_id::workspace_id(is.read_string()?));
+                },
+                3 => {
+                    if wire_type != ::protobuf::wire_format::WireTypeLengthDelimited {
+                        return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
+                    }
+                    self.one_of_name = ::std::option::Option::Some(UpdateAppParams_oneof_one_of_name::name(is.read_string()?));
+                },
+                4 => {
+                    if wire_type != ::protobuf::wire_format::WireTypeLengthDelimited {
+                        return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
+                    }
+                    self.one_of_desc = ::std::option::Option::Some(UpdateAppParams_oneof_one_of_desc::desc(is.read_string()?));
+                },
+                5 => {
+                    if wire_type != ::protobuf::wire_format::WireTypeLengthDelimited {
+                        return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
+                    }
+                    self.one_of_color_style = ::std::option::Option::Some(UpdateAppParams_oneof_one_of_color_style::color_style(is.read_message()?));
+                },
+                6 => {
+                    if wire_type != ::protobuf::wire_format::WireTypeVarint {
+                        return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
+                    }
+                    self.one_of_is_trash = ::std::option::Option::Some(UpdateAppParams_oneof_one_of_is_trash::is_trash(is.read_bool()?));
+                },
+                _ => {
+                    ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
+                },
+            };
+        }
+        ::std::result::Result::Ok(())
+    }
+
+    // Compute sizes of nested messages
+    #[allow(unused_variables)]
+    fn compute_size(&self) -> u32 {
+        let mut my_size = 0;
+        if !self.app_id.is_empty() {
+            my_size += ::protobuf::rt::string_size(1, &self.app_id);
+        }
+        if let ::std::option::Option::Some(ref v) = self.one_of_workspace_id {
+            match v {
+                &UpdateAppParams_oneof_one_of_workspace_id::workspace_id(ref v) => {
+                    my_size += ::protobuf::rt::string_size(2, &v);
+                },
+            };
+        }
+        if let ::std::option::Option::Some(ref v) = self.one_of_name {
+            match v {
+                &UpdateAppParams_oneof_one_of_name::name(ref v) => {
+                    my_size += ::protobuf::rt::string_size(3, &v);
+                },
+            };
+        }
+        if let ::std::option::Option::Some(ref v) = self.one_of_desc {
+            match v {
+                &UpdateAppParams_oneof_one_of_desc::desc(ref v) => {
+                    my_size += ::protobuf::rt::string_size(4, &v);
+                },
+            };
+        }
+        if let ::std::option::Option::Some(ref v) = self.one_of_color_style {
+            match v {
+                &UpdateAppParams_oneof_one_of_color_style::color_style(ref v) => {
+                    let len = v.compute_size();
+                    my_size += 1 + ::protobuf::rt::compute_raw_varint32_size(len) + len;
+                },
+            };
+        }
+        if let ::std::option::Option::Some(ref v) = self.one_of_is_trash {
+            match v {
+                &UpdateAppParams_oneof_one_of_is_trash::is_trash(v) => {
+                    my_size += 2;
+                },
+            };
+        }
+        my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
+        self.cached_size.set(my_size);
+        my_size
+    }
+
+    fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+        if !self.app_id.is_empty() {
+            os.write_string(1, &self.app_id)?;
+        }
+        if let ::std::option::Option::Some(ref v) = self.one_of_workspace_id {
+            match v {
+                &UpdateAppParams_oneof_one_of_workspace_id::workspace_id(ref v) => {
+                    os.write_string(2, v)?;
+                },
+            };
+        }
+        if let ::std::option::Option::Some(ref v) = self.one_of_name {
+            match v {
+                &UpdateAppParams_oneof_one_of_name::name(ref v) => {
+                    os.write_string(3, v)?;
+                },
+            };
+        }
+        if let ::std::option::Option::Some(ref v) = self.one_of_desc {
+            match v {
+                &UpdateAppParams_oneof_one_of_desc::desc(ref v) => {
+                    os.write_string(4, v)?;
+                },
+            };
+        }
+        if let ::std::option::Option::Some(ref v) = self.one_of_color_style {
+            match v {
+                &UpdateAppParams_oneof_one_of_color_style::color_style(ref v) => {
+                    os.write_tag(5, ::protobuf::wire_format::WireTypeLengthDelimited)?;
+                    os.write_raw_varint32(v.get_cached_size())?;
+                    v.write_to_with_cached_sizes(os)?;
+                },
+            };
+        }
+        if let ::std::option::Option::Some(ref v) = self.one_of_is_trash {
+            match v {
+                &UpdateAppParams_oneof_one_of_is_trash::is_trash(v) => {
+                    os.write_bool(6, v)?;
+                },
+            };
+        }
+        os.write_unknown_fields(self.get_unknown_fields())?;
+        ::std::result::Result::Ok(())
+    }
+
+    fn get_cached_size(&self) -> u32 {
+        self.cached_size.get()
+    }
+
+    fn get_unknown_fields(&self) -> &::protobuf::UnknownFields {
+        &self.unknown_fields
+    }
+
+    fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields {
+        &mut self.unknown_fields
+    }
+
+    fn as_any(&self) -> &dyn (::std::any::Any) {
+        self as &dyn (::std::any::Any)
+    }
+    fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) {
+        self as &mut dyn (::std::any::Any)
+    }
+    fn into_any(self: ::std::boxed::Box<Self>) -> ::std::boxed::Box<dyn (::std::any::Any)> {
+        self
+    }
+
+    fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor {
+        Self::descriptor_static()
+    }
+
+    fn new() -> UpdateAppParams {
+        UpdateAppParams::new()
+    }
+
+    fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
+        static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT;
+        descriptor.get(|| {
+            let mut fields = ::std::vec::Vec::new();
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
+                "app_id",
+                |m: &UpdateAppParams| { &m.app_id },
+                |m: &mut UpdateAppParams| { &mut m.app_id },
+            ));
+            fields.push(::protobuf::reflect::accessor::make_singular_string_accessor::<_>(
+                "workspace_id",
+                UpdateAppParams::has_workspace_id,
+                UpdateAppParams::get_workspace_id,
+            ));
+            fields.push(::protobuf::reflect::accessor::make_singular_string_accessor::<_>(
+                "name",
+                UpdateAppParams::has_name,
+                UpdateAppParams::get_name,
+            ));
+            fields.push(::protobuf::reflect::accessor::make_singular_string_accessor::<_>(
+                "desc",
+                UpdateAppParams::has_desc,
+                UpdateAppParams::get_desc,
+            ));
+            fields.push(::protobuf::reflect::accessor::make_singular_message_accessor::<_, super::app_create::ColorStyle>(
+                "color_style",
+                UpdateAppParams::has_color_style,
+                UpdateAppParams::get_color_style,
+            ));
+            fields.push(::protobuf::reflect::accessor::make_singular_bool_accessor::<_>(
+                "is_trash",
+                UpdateAppParams::has_is_trash,
+                UpdateAppParams::get_is_trash,
+            ));
+            ::protobuf::reflect::MessageDescriptor::new_pb_name::<UpdateAppParams>(
+                "UpdateAppParams",
+                fields,
+                file_descriptor_proto()
+            )
+        })
+    }
+
+    fn default_instance() -> &'static UpdateAppParams {
+        static instance: ::protobuf::rt::LazyV2<UpdateAppParams> = ::protobuf::rt::LazyV2::INIT;
+        instance.get(UpdateAppParams::new)
+    }
+}
+
+impl ::protobuf::Clear for UpdateAppParams {
+    fn clear(&mut self) {
+        self.app_id.clear();
+        self.one_of_workspace_id = ::std::option::Option::None;
+        self.one_of_name = ::std::option::Option::None;
+        self.one_of_desc = ::std::option::Option::None;
+        self.one_of_color_style = ::std::option::Option::None;
+        self.one_of_is_trash = ::std::option::Option::None;
+        self.unknown_fields.clear();
+    }
+}
+
+impl ::std::fmt::Debug for UpdateAppParams {
+    fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+        ::protobuf::text_format::fmt(self, f)
+    }
+}
+
+impl ::protobuf::reflect::ProtobufValue for UpdateAppParams {
+    fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
+        ::protobuf::reflect::ReflectValueRef::Message(self)
+    }
+}
+
 static file_descriptor_proto_data: &'static [u8] = b"\
     \n\x10app_update.proto\x1a\x10app_create.proto\"\xa5\x02\n\x10UpdateAppR\
     equest\x12\x15\n\x06app_id\x18\x01\x20\x01(\tR\x05appId\x12#\n\x0cworksp\
@@ -580,32 +1129,63 @@ static file_descriptor_proto_data: &'static [u8] = b"\
     desc\x12.\n\x0bcolor_style\x18\x05\x20\x01(\x0b2\x0b.ColorStyleH\x03R\nc\
     olorStyle\x12\x1b\n\x08is_trash\x18\x06\x20\x01(\x08H\x04R\x07isTrashB\
     \x15\n\x13one_of_workspace_idB\r\n\x0bone_of_nameB\r\n\x0bone_of_descB\
-    \x14\n\x12one_of_color_styleB\x11\n\x0fone_of_is_trashJ\x86\x04\n\x06\
-    \x12\x04\0\0\n\x01\n\x08\n\x01\x0c\x12\x03\0\0\x12\n\t\n\x02\x03\0\x12\
-    \x03\x01\0\x1a\n\n\n\x02\x04\0\x12\x04\x03\0\n\x01\n\n\n\x03\x04\0\x01\
-    \x12\x03\x03\x08\x18\n\x0b\n\x04\x04\0\x02\0\x12\x03\x04\x04\x16\n\x0c\n\
-    \x05\x04\0\x02\0\x05\x12\x03\x04\x04\n\n\x0c\n\x05\x04\0\x02\0\x01\x12\
-    \x03\x04\x0b\x11\n\x0c\n\x05\x04\0\x02\0\x03\x12\x03\x04\x14\x15\n\x0b\n\
-    \x04\x04\0\x08\0\x12\x03\x05\x04:\n\x0c\n\x05\x04\0\x08\0\x01\x12\x03\
-    \x05\n\x1d\n\x0b\n\x04\x04\0\x02\x01\x12\x03\x05\x208\n\x0c\n\x05\x04\0\
-    \x02\x01\x05\x12\x03\x05\x20&\n\x0c\n\x05\x04\0\x02\x01\x01\x12\x03\x05'\
-    3\n\x0c\n\x05\x04\0\x02\x01\x03\x12\x03\x0567\n\x0b\n\x04\x04\0\x08\x01\
-    \x12\x03\x06\x04*\n\x0c\n\x05\x04\0\x08\x01\x01\x12\x03\x06\n\x15\n\x0b\
-    \n\x04\x04\0\x02\x02\x12\x03\x06\x18(\n\x0c\n\x05\x04\0\x02\x02\x05\x12\
-    \x03\x06\x18\x1e\n\x0c\n\x05\x04\0\x02\x02\x01\x12\x03\x06\x1f#\n\x0c\n\
-    \x05\x04\0\x02\x02\x03\x12\x03\x06&'\n\x0b\n\x04\x04\0\x08\x02\x12\x03\
-    \x07\x04*\n\x0c\n\x05\x04\0\x08\x02\x01\x12\x03\x07\n\x15\n\x0b\n\x04\
-    \x04\0\x02\x03\x12\x03\x07\x18(\n\x0c\n\x05\x04\0\x02\x03\x05\x12\x03\
-    \x07\x18\x1e\n\x0c\n\x05\x04\0\x02\x03\x01\x12\x03\x07\x1f#\n\x0c\n\x05\
-    \x04\0\x02\x03\x03\x12\x03\x07&'\n\x0b\n\x04\x04\0\x08\x03\x12\x03\x08\
-    \x04<\n\x0c\n\x05\x04\0\x08\x03\x01\x12\x03\x08\n\x1c\n\x0b\n\x04\x04\0\
-    \x02\x04\x12\x03\x08\x1f:\n\x0c\n\x05\x04\0\x02\x04\x06\x12\x03\x08\x1f)\
-    \n\x0c\n\x05\x04\0\x02\x04\x01\x12\x03\x08*5\n\x0c\n\x05\x04\0\x02\x04\
-    \x03\x12\x03\x0889\n\x0b\n\x04\x04\0\x08\x04\x12\x03\t\x040\n\x0c\n\x05\
-    \x04\0\x08\x04\x01\x12\x03\t\n\x19\n\x0b\n\x04\x04\0\x02\x05\x12\x03\t\
-    \x1c.\n\x0c\n\x05\x04\0\x02\x05\x05\x12\x03\t\x1c\x20\n\x0c\n\x05\x04\0\
-    \x02\x05\x01\x12\x03\t!)\n\x0c\n\x05\x04\0\x02\x05\x03\x12\x03\t,-b\x06p\
-    roto3\
+    \x14\n\x12one_of_color_styleB\x11\n\x0fone_of_is_trash\"\xa4\x02\n\x0fUp\
+    dateAppParams\x12\x15\n\x06app_id\x18\x01\x20\x01(\tR\x05appId\x12#\n\
+    \x0cworkspace_id\x18\x02\x20\x01(\tH\0R\x0bworkspaceId\x12\x14\n\x04name\
+    \x18\x03\x20\x01(\tH\x01R\x04name\x12\x14\n\x04desc\x18\x04\x20\x01(\tH\
+    \x02R\x04desc\x12.\n\x0bcolor_style\x18\x05\x20\x01(\x0b2\x0b.ColorStyle\
+    H\x03R\ncolorStyle\x12\x1b\n\x08is_trash\x18\x06\x20\x01(\x08H\x04R\x07i\
+    sTrashB\x15\n\x13one_of_workspace_idB\r\n\x0bone_of_nameB\r\n\x0bone_of_\
+    descB\x14\n\x12one_of_color_styleB\x11\n\x0fone_of_is_trashJ\xef\x07\n\
+    \x06\x12\x04\0\0\x12\x01\n\x08\n\x01\x0c\x12\x03\0\0\x12\n\t\n\x02\x03\0\
+    \x12\x03\x01\0\x1a\n\n\n\x02\x04\0\x12\x04\x03\0\n\x01\n\n\n\x03\x04\0\
+    \x01\x12\x03\x03\x08\x18\n\x0b\n\x04\x04\0\x02\0\x12\x03\x04\x04\x16\n\
+    \x0c\n\x05\x04\0\x02\0\x05\x12\x03\x04\x04\n\n\x0c\n\x05\x04\0\x02\0\x01\
+    \x12\x03\x04\x0b\x11\n\x0c\n\x05\x04\0\x02\0\x03\x12\x03\x04\x14\x15\n\
+    \x0b\n\x04\x04\0\x08\0\x12\x03\x05\x04:\n\x0c\n\x05\x04\0\x08\0\x01\x12\
+    \x03\x05\n\x1d\n\x0b\n\x04\x04\0\x02\x01\x12\x03\x05\x208\n\x0c\n\x05\
+    \x04\0\x02\x01\x05\x12\x03\x05\x20&\n\x0c\n\x05\x04\0\x02\x01\x01\x12\
+    \x03\x05'3\n\x0c\n\x05\x04\0\x02\x01\x03\x12\x03\x0567\n\x0b\n\x04\x04\0\
+    \x08\x01\x12\x03\x06\x04*\n\x0c\n\x05\x04\0\x08\x01\x01\x12\x03\x06\n\
+    \x15\n\x0b\n\x04\x04\0\x02\x02\x12\x03\x06\x18(\n\x0c\n\x05\x04\0\x02\
+    \x02\x05\x12\x03\x06\x18\x1e\n\x0c\n\x05\x04\0\x02\x02\x01\x12\x03\x06\
+    \x1f#\n\x0c\n\x05\x04\0\x02\x02\x03\x12\x03\x06&'\n\x0b\n\x04\x04\0\x08\
+    \x02\x12\x03\x07\x04*\n\x0c\n\x05\x04\0\x08\x02\x01\x12\x03\x07\n\x15\n\
+    \x0b\n\x04\x04\0\x02\x03\x12\x03\x07\x18(\n\x0c\n\x05\x04\0\x02\x03\x05\
+    \x12\x03\x07\x18\x1e\n\x0c\n\x05\x04\0\x02\x03\x01\x12\x03\x07\x1f#\n\
+    \x0c\n\x05\x04\0\x02\x03\x03\x12\x03\x07&'\n\x0b\n\x04\x04\0\x08\x03\x12\
+    \x03\x08\x04<\n\x0c\n\x05\x04\0\x08\x03\x01\x12\x03\x08\n\x1c\n\x0b\n\
+    \x04\x04\0\x02\x04\x12\x03\x08\x1f:\n\x0c\n\x05\x04\0\x02\x04\x06\x12\
+    \x03\x08\x1f)\n\x0c\n\x05\x04\0\x02\x04\x01\x12\x03\x08*5\n\x0c\n\x05\
+    \x04\0\x02\x04\x03\x12\x03\x0889\n\x0b\n\x04\x04\0\x08\x04\x12\x03\t\x04\
+    0\n\x0c\n\x05\x04\0\x08\x04\x01\x12\x03\t\n\x19\n\x0b\n\x04\x04\0\x02\
+    \x05\x12\x03\t\x1c.\n\x0c\n\x05\x04\0\x02\x05\x05\x12\x03\t\x1c\x20\n\
+    \x0c\n\x05\x04\0\x02\x05\x01\x12\x03\t!)\n\x0c\n\x05\x04\0\x02\x05\x03\
+    \x12\x03\t,-\n\n\n\x02\x04\x01\x12\x04\x0b\0\x12\x01\n\n\n\x03\x04\x01\
+    \x01\x12\x03\x0b\x08\x17\n\x0b\n\x04\x04\x01\x02\0\x12\x03\x0c\x04\x16\n\
+    \x0c\n\x05\x04\x01\x02\0\x05\x12\x03\x0c\x04\n\n\x0c\n\x05\x04\x01\x02\0\
+    \x01\x12\x03\x0c\x0b\x11\n\x0c\n\x05\x04\x01\x02\0\x03\x12\x03\x0c\x14\
+    \x15\n\x0b\n\x04\x04\x01\x08\0\x12\x03\r\x04:\n\x0c\n\x05\x04\x01\x08\0\
+    \x01\x12\x03\r\n\x1d\n\x0b\n\x04\x04\x01\x02\x01\x12\x03\r\x208\n\x0c\n\
+    \x05\x04\x01\x02\x01\x05\x12\x03\r\x20&\n\x0c\n\x05\x04\x01\x02\x01\x01\
+    \x12\x03\r'3\n\x0c\n\x05\x04\x01\x02\x01\x03\x12\x03\r67\n\x0b\n\x04\x04\
+    \x01\x08\x01\x12\x03\x0e\x04*\n\x0c\n\x05\x04\x01\x08\x01\x01\x12\x03\
+    \x0e\n\x15\n\x0b\n\x04\x04\x01\x02\x02\x12\x03\x0e\x18(\n\x0c\n\x05\x04\
+    \x01\x02\x02\x05\x12\x03\x0e\x18\x1e\n\x0c\n\x05\x04\x01\x02\x02\x01\x12\
+    \x03\x0e\x1f#\n\x0c\n\x05\x04\x01\x02\x02\x03\x12\x03\x0e&'\n\x0b\n\x04\
+    \x04\x01\x08\x02\x12\x03\x0f\x04*\n\x0c\n\x05\x04\x01\x08\x02\x01\x12\
+    \x03\x0f\n\x15\n\x0b\n\x04\x04\x01\x02\x03\x12\x03\x0f\x18(\n\x0c\n\x05\
+    \x04\x01\x02\x03\x05\x12\x03\x0f\x18\x1e\n\x0c\n\x05\x04\x01\x02\x03\x01\
+    \x12\x03\x0f\x1f#\n\x0c\n\x05\x04\x01\x02\x03\x03\x12\x03\x0f&'\n\x0b\n\
+    \x04\x04\x01\x08\x03\x12\x03\x10\x04<\n\x0c\n\x05\x04\x01\x08\x03\x01\
+    \x12\x03\x10\n\x1c\n\x0b\n\x04\x04\x01\x02\x04\x12\x03\x10\x1f:\n\x0c\n\
+    \x05\x04\x01\x02\x04\x06\x12\x03\x10\x1f)\n\x0c\n\x05\x04\x01\x02\x04\
+    \x01\x12\x03\x10*5\n\x0c\n\x05\x04\x01\x02\x04\x03\x12\x03\x1089\n\x0b\n\
+    \x04\x04\x01\x08\x04\x12\x03\x11\x040\n\x0c\n\x05\x04\x01\x08\x04\x01\
+    \x12\x03\x11\n\x19\n\x0b\n\x04\x04\x01\x02\x05\x12\x03\x11\x1c.\n\x0c\n\
+    \x05\x04\x01\x02\x05\x05\x12\x03\x11\x1c\x20\n\x0c\n\x05\x04\x01\x02\x05\
+    \x01\x12\x03\x11!)\n\x0c\n\x05\x04\x01\x02\x05\x03\x12\x03\x11,-b\x06pro\
+    to3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 58 - 46
rust-lib/flowy-workspace/src/protobuf/model/errors.rs

@@ -219,6 +219,7 @@ pub enum WsErrCode {
     WorkspaceNameInvalid = 1,
     WorkspaceIdInvalid = 2,
     AppColorStyleInvalid = 3,
+    WorkspaceDescInvalid = 4,
     AppIdInvalid = 10,
     AppNameInvalid = 11,
     ViewNameInvalid = 20,
@@ -230,6 +231,7 @@ pub enum WsErrCode {
     UserInternalError = 102,
     UserNotLoginYet = 103,
     ServerError = 1000,
+    RecordNotFound = 1001,
 }
 
 impl ::protobuf::ProtobufEnum for WsErrCode {
@@ -243,6 +245,7 @@ impl ::protobuf::ProtobufEnum for WsErrCode {
             1 => ::std::option::Option::Some(WsErrCode::WorkspaceNameInvalid),
             2 => ::std::option::Option::Some(WsErrCode::WorkspaceIdInvalid),
             3 => ::std::option::Option::Some(WsErrCode::AppColorStyleInvalid),
+            4 => ::std::option::Option::Some(WsErrCode::WorkspaceDescInvalid),
             10 => ::std::option::Option::Some(WsErrCode::AppIdInvalid),
             11 => ::std::option::Option::Some(WsErrCode::AppNameInvalid),
             20 => ::std::option::Option::Some(WsErrCode::ViewNameInvalid),
@@ -254,6 +257,7 @@ impl ::protobuf::ProtobufEnum for WsErrCode {
             102 => ::std::option::Option::Some(WsErrCode::UserInternalError),
             103 => ::std::option::Option::Some(WsErrCode::UserNotLoginYet),
             1000 => ::std::option::Option::Some(WsErrCode::ServerError),
+            1001 => ::std::option::Option::Some(WsErrCode::RecordNotFound),
             _ => ::std::option::Option::None
         }
     }
@@ -264,6 +268,7 @@ impl ::protobuf::ProtobufEnum for WsErrCode {
             WsErrCode::WorkspaceNameInvalid,
             WsErrCode::WorkspaceIdInvalid,
             WsErrCode::AppColorStyleInvalid,
+            WsErrCode::WorkspaceDescInvalid,
             WsErrCode::AppIdInvalid,
             WsErrCode::AppNameInvalid,
             WsErrCode::ViewNameInvalid,
@@ -275,6 +280,7 @@ impl ::protobuf::ProtobufEnum for WsErrCode {
             WsErrCode::UserInternalError,
             WsErrCode::UserNotLoginYet,
             WsErrCode::ServerError,
+            WsErrCode::RecordNotFound,
         ];
         values
     }
@@ -305,53 +311,59 @@ impl ::protobuf::reflect::ProtobufValue for WsErrCode {
 static file_descriptor_proto_data: &'static [u8] = b"\
     \n\x0cerrors.proto\"B\n\x0eWorkspaceError\x12\x1e\n\x04code\x18\x01\x20\
     \x01(\x0e2\n.WsErrCodeR\x04code\x12\x10\n\x03msg\x18\x02\x20\x01(\tR\x03\
-    msg*\xd7\x02\n\tWsErrCode\x12\x0b\n\x07Unknown\x10\0\x12\x18\n\x14Worksp\
+    msg*\x86\x03\n\tWsErrCode\x12\x0b\n\x07Unknown\x10\0\x12\x18\n\x14Worksp\
     aceNameInvalid\x10\x01\x12\x16\n\x12WorkspaceIdInvalid\x10\x02\x12\x18\n\
-    \x14AppColorStyleInvalid\x10\x03\x12\x10\n\x0cAppIdInvalid\x10\n\x12\x12\
-    \n\x0eAppNameInvalid\x10\x0b\x12\x13\n\x0fViewNameInvalid\x10\x14\x12\
-    \x18\n\x14ViewThumbnailInvalid\x10\x15\x12\x11\n\rViewIdInvalid\x10\x16\
-    \x12\x13\n\x0fViewDescInvalid\x10\x17\x12\x1a\n\x16DatabaseConnectionFai\
-    l\x10d\x12\x1a\n\x16WorkspaceDatabaseError\x10e\x12\x15\n\x11UserInterna\
-    lError\x10f\x12\x13\n\x0fUserNotLoginYet\x10g\x12\x10\n\x0bServerError\
-    \x10\xe8\x07J\x97\x06\n\x06\x12\x04\0\0\x16\x01\n\x08\n\x01\x0c\x12\x03\
-    \0\0\x12\n\n\n\x02\x04\0\x12\x04\x02\0\x05\x01\n\n\n\x03\x04\0\x01\x12\
-    \x03\x02\x08\x16\n\x0b\n\x04\x04\0\x02\0\x12\x03\x03\x04\x17\n\x0c\n\x05\
-    \x04\0\x02\0\x06\x12\x03\x03\x04\r\n\x0c\n\x05\x04\0\x02\0\x01\x12\x03\
-    \x03\x0e\x12\n\x0c\n\x05\x04\0\x02\0\x03\x12\x03\x03\x15\x16\n\x0b\n\x04\
-    \x04\0\x02\x01\x12\x03\x04\x04\x13\n\x0c\n\x05\x04\0\x02\x01\x05\x12\x03\
-    \x04\x04\n\n\x0c\n\x05\x04\0\x02\x01\x01\x12\x03\x04\x0b\x0e\n\x0c\n\x05\
-    \x04\0\x02\x01\x03\x12\x03\x04\x11\x12\n\n\n\x02\x05\0\x12\x04\x06\0\x16\
-    \x01\n\n\n\x03\x05\0\x01\x12\x03\x06\x05\x0e\n\x0b\n\x04\x05\0\x02\0\x12\
-    \x03\x07\x04\x10\n\x0c\n\x05\x05\0\x02\0\x01\x12\x03\x07\x04\x0b\n\x0c\n\
-    \x05\x05\0\x02\0\x02\x12\x03\x07\x0e\x0f\n\x0b\n\x04\x05\0\x02\x01\x12\
-    \x03\x08\x04\x1d\n\x0c\n\x05\x05\0\x02\x01\x01\x12\x03\x08\x04\x18\n\x0c\
-    \n\x05\x05\0\x02\x01\x02\x12\x03\x08\x1b\x1c\n\x0b\n\x04\x05\0\x02\x02\
-    \x12\x03\t\x04\x1b\n\x0c\n\x05\x05\0\x02\x02\x01\x12\x03\t\x04\x16\n\x0c\
-    \n\x05\x05\0\x02\x02\x02\x12\x03\t\x19\x1a\n\x0b\n\x04\x05\0\x02\x03\x12\
-    \x03\n\x04\x1d\n\x0c\n\x05\x05\0\x02\x03\x01\x12\x03\n\x04\x18\n\x0c\n\
-    \x05\x05\0\x02\x03\x02\x12\x03\n\x1b\x1c\n\x0b\n\x04\x05\0\x02\x04\x12\
-    \x03\x0b\x04\x16\n\x0c\n\x05\x05\0\x02\x04\x01\x12\x03\x0b\x04\x10\n\x0c\
-    \n\x05\x05\0\x02\x04\x02\x12\x03\x0b\x13\x15\n\x0b\n\x04\x05\0\x02\x05\
-    \x12\x03\x0c\x04\x18\n\x0c\n\x05\x05\0\x02\x05\x01\x12\x03\x0c\x04\x12\n\
-    \x0c\n\x05\x05\0\x02\x05\x02\x12\x03\x0c\x15\x17\n\x0b\n\x04\x05\0\x02\
-    \x06\x12\x03\r\x04\x19\n\x0c\n\x05\x05\0\x02\x06\x01\x12\x03\r\x04\x13\n\
-    \x0c\n\x05\x05\0\x02\x06\x02\x12\x03\r\x16\x18\n\x0b\n\x04\x05\0\x02\x07\
-    \x12\x03\x0e\x04\x1e\n\x0c\n\x05\x05\0\x02\x07\x01\x12\x03\x0e\x04\x18\n\
-    \x0c\n\x05\x05\0\x02\x07\x02\x12\x03\x0e\x1b\x1d\n\x0b\n\x04\x05\0\x02\
-    \x08\x12\x03\x0f\x04\x17\n\x0c\n\x05\x05\0\x02\x08\x01\x12\x03\x0f\x04\
-    \x11\n\x0c\n\x05\x05\0\x02\x08\x02\x12\x03\x0f\x14\x16\n\x0b\n\x04\x05\0\
-    \x02\t\x12\x03\x10\x04\x19\n\x0c\n\x05\x05\0\x02\t\x01\x12\x03\x10\x04\
-    \x13\n\x0c\n\x05\x05\0\x02\t\x02\x12\x03\x10\x16\x18\n\x0b\n\x04\x05\0\
-    \x02\n\x12\x03\x11\x04!\n\x0c\n\x05\x05\0\x02\n\x01\x12\x03\x11\x04\x1a\
-    \n\x0c\n\x05\x05\0\x02\n\x02\x12\x03\x11\x1d\x20\n\x0b\n\x04\x05\0\x02\
-    \x0b\x12\x03\x12\x04!\n\x0c\n\x05\x05\0\x02\x0b\x01\x12\x03\x12\x04\x1a\
-    \n\x0c\n\x05\x05\0\x02\x0b\x02\x12\x03\x12\x1d\x20\n\x0b\n\x04\x05\0\x02\
-    \x0c\x12\x03\x13\x04\x1c\n\x0c\n\x05\x05\0\x02\x0c\x01\x12\x03\x13\x04\
-    \x15\n\x0c\n\x05\x05\0\x02\x0c\x02\x12\x03\x13\x18\x1b\n\x0b\n\x04\x05\0\
-    \x02\r\x12\x03\x14\x04\x1a\n\x0c\n\x05\x05\0\x02\r\x01\x12\x03\x14\x04\
-    \x13\n\x0c\n\x05\x05\0\x02\r\x02\x12\x03\x14\x16\x19\n\x0b\n\x04\x05\0\
-    \x02\x0e\x12\x03\x15\x04\x17\n\x0c\n\x05\x05\0\x02\x0e\x01\x12\x03\x15\
-    \x04\x0f\n\x0c\n\x05\x05\0\x02\x0e\x02\x12\x03\x15\x12\x16b\x06proto3\
+    \x14AppColorStyleInvalid\x10\x03\x12\x18\n\x14WorkspaceDescInvalid\x10\
+    \x04\x12\x10\n\x0cAppIdInvalid\x10\n\x12\x12\n\x0eAppNameInvalid\x10\x0b\
+    \x12\x13\n\x0fViewNameInvalid\x10\x14\x12\x18\n\x14ViewThumbnailInvalid\
+    \x10\x15\x12\x11\n\rViewIdInvalid\x10\x16\x12\x13\n\x0fViewDescInvalid\
+    \x10\x17\x12\x1a\n\x16DatabaseConnectionFail\x10d\x12\x1a\n\x16Workspace\
+    DatabaseError\x10e\x12\x15\n\x11UserInternalError\x10f\x12\x13\n\x0fUser\
+    NotLoginYet\x10g\x12\x10\n\x0bServerError\x10\xe8\x07\x12\x13\n\x0eRecor\
+    dNotFound\x10\xe9\x07J\xe9\x06\n\x06\x12\x04\0\0\x18\x01\n\x08\n\x01\x0c\
+    \x12\x03\0\0\x12\n\n\n\x02\x04\0\x12\x04\x02\0\x05\x01\n\n\n\x03\x04\0\
+    \x01\x12\x03\x02\x08\x16\n\x0b\n\x04\x04\0\x02\0\x12\x03\x03\x04\x17\n\
+    \x0c\n\x05\x04\0\x02\0\x06\x12\x03\x03\x04\r\n\x0c\n\x05\x04\0\x02\0\x01\
+    \x12\x03\x03\x0e\x12\n\x0c\n\x05\x04\0\x02\0\x03\x12\x03\x03\x15\x16\n\
+    \x0b\n\x04\x04\0\x02\x01\x12\x03\x04\x04\x13\n\x0c\n\x05\x04\0\x02\x01\
+    \x05\x12\x03\x04\x04\n\n\x0c\n\x05\x04\0\x02\x01\x01\x12\x03\x04\x0b\x0e\
+    \n\x0c\n\x05\x04\0\x02\x01\x03\x12\x03\x04\x11\x12\n\n\n\x02\x05\0\x12\
+    \x04\x06\0\x18\x01\n\n\n\x03\x05\0\x01\x12\x03\x06\x05\x0e\n\x0b\n\x04\
+    \x05\0\x02\0\x12\x03\x07\x04\x10\n\x0c\n\x05\x05\0\x02\0\x01\x12\x03\x07\
+    \x04\x0b\n\x0c\n\x05\x05\0\x02\0\x02\x12\x03\x07\x0e\x0f\n\x0b\n\x04\x05\
+    \0\x02\x01\x12\x03\x08\x04\x1d\n\x0c\n\x05\x05\0\x02\x01\x01\x12\x03\x08\
+    \x04\x18\n\x0c\n\x05\x05\0\x02\x01\x02\x12\x03\x08\x1b\x1c\n\x0b\n\x04\
+    \x05\0\x02\x02\x12\x03\t\x04\x1b\n\x0c\n\x05\x05\0\x02\x02\x01\x12\x03\t\
+    \x04\x16\n\x0c\n\x05\x05\0\x02\x02\x02\x12\x03\t\x19\x1a\n\x0b\n\x04\x05\
+    \0\x02\x03\x12\x03\n\x04\x1d\n\x0c\n\x05\x05\0\x02\x03\x01\x12\x03\n\x04\
+    \x18\n\x0c\n\x05\x05\0\x02\x03\x02\x12\x03\n\x1b\x1c\n\x0b\n\x04\x05\0\
+    \x02\x04\x12\x03\x0b\x04\x1d\n\x0c\n\x05\x05\0\x02\x04\x01\x12\x03\x0b\
+    \x04\x18\n\x0c\n\x05\x05\0\x02\x04\x02\x12\x03\x0b\x1b\x1c\n\x0b\n\x04\
+    \x05\0\x02\x05\x12\x03\x0c\x04\x16\n\x0c\n\x05\x05\0\x02\x05\x01\x12\x03\
+    \x0c\x04\x10\n\x0c\n\x05\x05\0\x02\x05\x02\x12\x03\x0c\x13\x15\n\x0b\n\
+    \x04\x05\0\x02\x06\x12\x03\r\x04\x18\n\x0c\n\x05\x05\0\x02\x06\x01\x12\
+    \x03\r\x04\x12\n\x0c\n\x05\x05\0\x02\x06\x02\x12\x03\r\x15\x17\n\x0b\n\
+    \x04\x05\0\x02\x07\x12\x03\x0e\x04\x19\n\x0c\n\x05\x05\0\x02\x07\x01\x12\
+    \x03\x0e\x04\x13\n\x0c\n\x05\x05\0\x02\x07\x02\x12\x03\x0e\x16\x18\n\x0b\
+    \n\x04\x05\0\x02\x08\x12\x03\x0f\x04\x1e\n\x0c\n\x05\x05\0\x02\x08\x01\
+    \x12\x03\x0f\x04\x18\n\x0c\n\x05\x05\0\x02\x08\x02\x12\x03\x0f\x1b\x1d\n\
+    \x0b\n\x04\x05\0\x02\t\x12\x03\x10\x04\x17\n\x0c\n\x05\x05\0\x02\t\x01\
+    \x12\x03\x10\x04\x11\n\x0c\n\x05\x05\0\x02\t\x02\x12\x03\x10\x14\x16\n\
+    \x0b\n\x04\x05\0\x02\n\x12\x03\x11\x04\x19\n\x0c\n\x05\x05\0\x02\n\x01\
+    \x12\x03\x11\x04\x13\n\x0c\n\x05\x05\0\x02\n\x02\x12\x03\x11\x16\x18\n\
+    \x0b\n\x04\x05\0\x02\x0b\x12\x03\x12\x04!\n\x0c\n\x05\x05\0\x02\x0b\x01\
+    \x12\x03\x12\x04\x1a\n\x0c\n\x05\x05\0\x02\x0b\x02\x12\x03\x12\x1d\x20\n\
+    \x0b\n\x04\x05\0\x02\x0c\x12\x03\x13\x04!\n\x0c\n\x05\x05\0\x02\x0c\x01\
+    \x12\x03\x13\x04\x1a\n\x0c\n\x05\x05\0\x02\x0c\x02\x12\x03\x13\x1d\x20\n\
+    \x0b\n\x04\x05\0\x02\r\x12\x03\x14\x04\x1c\n\x0c\n\x05\x05\0\x02\r\x01\
+    \x12\x03\x14\x04\x15\n\x0c\n\x05\x05\0\x02\r\x02\x12\x03\x14\x18\x1b\n\
+    \x0b\n\x04\x05\0\x02\x0e\x12\x03\x15\x04\x1a\n\x0c\n\x05\x05\0\x02\x0e\
+    \x01\x12\x03\x15\x04\x13\n\x0c\n\x05\x05\0\x02\x0e\x02\x12\x03\x15\x16\
+    \x19\n\x0b\n\x04\x05\0\x02\x0f\x12\x03\x16\x04\x17\n\x0c\n\x05\x05\0\x02\
+    \x0f\x01\x12\x03\x16\x04\x0f\n\x0c\n\x05\x05\0\x02\x0f\x02\x12\x03\x16\
+    \x12\x16\n\x0b\n\x04\x05\0\x02\x10\x12\x03\x17\x04\x1a\n\x0c\n\x05\x05\0\
+    \x02\x10\x01\x12\x03\x17\x04\x12\n\x0c\n\x05\x05\0\x02\x10\x02\x12\x03\
+    \x17\x15\x19b\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 57 - 98
rust-lib/flowy-workspace/src/protobuf/model/workspace_create.rs

@@ -229,8 +229,7 @@ pub struct CreateWorkspaceParams {
     // message fields
     pub name: ::std::string::String,
     pub desc: ::std::string::String,
-    // message oneof groups
-    pub one_of_user_id: ::std::option::Option<CreateWorkspaceParams_oneof_one_of_user_id>,
+    pub user_id: ::std::string::String,
     // special fields
     pub unknown_fields: ::protobuf::UnknownFields,
     pub cached_size: ::protobuf::CachedSize,
@@ -242,11 +241,6 @@ impl<'a> ::std::default::Default for &'a CreateWorkspaceParams {
     }
 }
 
-#[derive(Clone,PartialEq,Debug)]
-pub enum CreateWorkspaceParams_oneof_one_of_user_id {
-    user_id(::std::string::String),
-}
-
 impl CreateWorkspaceParams {
     pub fn new() -> CreateWorkspaceParams {
         ::std::default::Default::default()
@@ -308,49 +302,26 @@ impl CreateWorkspaceParams {
 
 
     pub fn get_user_id(&self) -> &str {
-        match self.one_of_user_id {
-            ::std::option::Option::Some(CreateWorkspaceParams_oneof_one_of_user_id::user_id(ref v)) => v,
-            _ => "",
-        }
+        &self.user_id
     }
     pub fn clear_user_id(&mut self) {
-        self.one_of_user_id = ::std::option::Option::None;
-    }
-
-    pub fn has_user_id(&self) -> bool {
-        match self.one_of_user_id {
-            ::std::option::Option::Some(CreateWorkspaceParams_oneof_one_of_user_id::user_id(..)) => true,
-            _ => false,
-        }
+        self.user_id.clear();
     }
 
     // Param is passed by value, moved
     pub fn set_user_id(&mut self, v: ::std::string::String) {
-        self.one_of_user_id = ::std::option::Option::Some(CreateWorkspaceParams_oneof_one_of_user_id::user_id(v))
+        self.user_id = v;
     }
 
     // Mutable pointer to the field.
+    // If field is not initialized, it is initialized with default value first.
     pub fn mut_user_id(&mut self) -> &mut ::std::string::String {
-        if let ::std::option::Option::Some(CreateWorkspaceParams_oneof_one_of_user_id::user_id(_)) = self.one_of_user_id {
-        } else {
-            self.one_of_user_id = ::std::option::Option::Some(CreateWorkspaceParams_oneof_one_of_user_id::user_id(::std::string::String::new()));
-        }
-        match self.one_of_user_id {
-            ::std::option::Option::Some(CreateWorkspaceParams_oneof_one_of_user_id::user_id(ref mut v)) => v,
-            _ => panic!(),
-        }
+        &mut self.user_id
     }
 
     // Take field
     pub fn take_user_id(&mut self) -> ::std::string::String {
-        if self.has_user_id() {
-            match self.one_of_user_id.take() {
-                ::std::option::Option::Some(CreateWorkspaceParams_oneof_one_of_user_id::user_id(v)) => v,
-                _ => panic!(),
-            }
-        } else {
-            ::std::string::String::new()
-        }
+        ::std::mem::replace(&mut self.user_id, ::std::string::String::new())
     }
 }
 
@@ -370,10 +341,7 @@ impl ::protobuf::Message for CreateWorkspaceParams {
                     ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.desc)?;
                 },
                 3 => {
-                    if wire_type != ::protobuf::wire_format::WireTypeLengthDelimited {
-                        return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
-                    }
-                    self.one_of_user_id = ::std::option::Option::Some(CreateWorkspaceParams_oneof_one_of_user_id::user_id(is.read_string()?));
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.user_id)?;
                 },
                 _ => {
                     ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
@@ -393,12 +361,8 @@ impl ::protobuf::Message for CreateWorkspaceParams {
         if !self.desc.is_empty() {
             my_size += ::protobuf::rt::string_size(2, &self.desc);
         }
-        if let ::std::option::Option::Some(ref v) = self.one_of_user_id {
-            match v {
-                &CreateWorkspaceParams_oneof_one_of_user_id::user_id(ref v) => {
-                    my_size += ::protobuf::rt::string_size(3, &v);
-                },
-            };
+        if !self.user_id.is_empty() {
+            my_size += ::protobuf::rt::string_size(3, &self.user_id);
         }
         my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
         self.cached_size.set(my_size);
@@ -412,12 +376,8 @@ impl ::protobuf::Message for CreateWorkspaceParams {
         if !self.desc.is_empty() {
             os.write_string(2, &self.desc)?;
         }
-        if let ::std::option::Option::Some(ref v) = self.one_of_user_id {
-            match v {
-                &CreateWorkspaceParams_oneof_one_of_user_id::user_id(ref v) => {
-                    os.write_string(3, v)?;
-                },
-            };
+        if !self.user_id.is_empty() {
+            os.write_string(3, &self.user_id)?;
         }
         os.write_unknown_fields(self.get_unknown_fields())?;
         ::std::result::Result::Ok(())
@@ -467,10 +427,10 @@ impl ::protobuf::Message for CreateWorkspaceParams {
                 |m: &CreateWorkspaceParams| { &m.desc },
                 |m: &mut CreateWorkspaceParams| { &mut m.desc },
             ));
-            fields.push(::protobuf::reflect::accessor::make_singular_string_accessor::<_>(
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
                 "user_id",
-                CreateWorkspaceParams::has_user_id,
-                CreateWorkspaceParams::get_user_id,
+                |m: &CreateWorkspaceParams| { &m.user_id },
+                |m: &mut CreateWorkspaceParams| { &mut m.user_id },
             ));
             ::protobuf::reflect::MessageDescriptor::new_pb_name::<CreateWorkspaceParams>(
                 "CreateWorkspaceParams",
@@ -490,7 +450,7 @@ impl ::protobuf::Clear for CreateWorkspaceParams {
     fn clear(&mut self) {
         self.name.clear();
         self.desc.clear();
-        self.one_of_user_id = ::std::option::Option::None;
+        self.user_id.clear();
         self.unknown_fields.clear();
     }
 }
@@ -976,49 +936,48 @@ impl ::protobuf::reflect::ProtobufValue for Workspaces {
 static file_descriptor_proto_data: &'static [u8] = b"\
     \n\x16workspace_create.proto\x1a\x10app_create.proto\"@\n\x16CreateWorks\
     paceRequest\x12\x12\n\x04name\x18\x01\x20\x01(\tR\x04name\x12\x12\n\x04d\
-    esc\x18\x02\x20\x01(\tR\x04desc\"l\n\x15CreateWorkspaceParams\x12\x12\n\
+    esc\x18\x02\x20\x01(\tR\x04desc\"X\n\x15CreateWorkspaceParams\x12\x12\n\
     \x04name\x18\x01\x20\x01(\tR\x04name\x12\x12\n\x04desc\x18\x02\x20\x01(\
-    \tR\x04desc\x12\x19\n\x07user_id\x18\x03\x20\x01(\tH\0R\x06userIdB\x10\n\
-    \x0eone_of_user_id\"e\n\tWorkspace\x12\x0e\n\x02id\x18\x01\x20\x01(\tR\
-    \x02id\x12\x12\n\x04name\x18\x02\x20\x01(\tR\x04name\x12\x12\n\x04desc\
-    \x18\x03\x20\x01(\tR\x04desc\x12\x20\n\x04apps\x18\x04\x20\x01(\x0b2\x0c\
-    .RepeatedAppR\x04apps\".\n\nWorkspaces\x12\x20\n\x05items\x18\x01\x20\
-    \x03(\x0b2\n.WorkspaceR\x05itemsJ\xcc\x05\n\x06\x12\x04\0\0\x14\x01\n\
-    \x08\n\x01\x0c\x12\x03\0\0\x12\n\t\n\x02\x03\0\x12\x03\x01\0\x1a\n\n\n\
-    \x02\x04\0\x12\x04\x03\0\x06\x01\n\n\n\x03\x04\0\x01\x12\x03\x03\x08\x1e\
-    \n\x0b\n\x04\x04\0\x02\0\x12\x03\x04\x04\x14\n\x0c\n\x05\x04\0\x02\0\x05\
-    \x12\x03\x04\x04\n\n\x0c\n\x05\x04\0\x02\0\x01\x12\x03\x04\x0b\x0f\n\x0c\
-    \n\x05\x04\0\x02\0\x03\x12\x03\x04\x12\x13\n\x0b\n\x04\x04\0\x02\x01\x12\
-    \x03\x05\x04\x14\n\x0c\n\x05\x04\0\x02\x01\x05\x12\x03\x05\x04\n\n\x0c\n\
-    \x05\x04\0\x02\x01\x01\x12\x03\x05\x0b\x0f\n\x0c\n\x05\x04\0\x02\x01\x03\
-    \x12\x03\x05\x12\x13\n\n\n\x02\x04\x01\x12\x04\x07\0\x0b\x01\n\n\n\x03\
-    \x04\x01\x01\x12\x03\x07\x08\x1d\n\x0b\n\x04\x04\x01\x02\0\x12\x03\x08\
-    \x04\x14\n\x0c\n\x05\x04\x01\x02\0\x05\x12\x03\x08\x04\n\n\x0c\n\x05\x04\
-    \x01\x02\0\x01\x12\x03\x08\x0b\x0f\n\x0c\n\x05\x04\x01\x02\0\x03\x12\x03\
-    \x08\x12\x13\n\x0b\n\x04\x04\x01\x02\x01\x12\x03\t\x04\x14\n\x0c\n\x05\
-    \x04\x01\x02\x01\x05\x12\x03\t\x04\n\n\x0c\n\x05\x04\x01\x02\x01\x01\x12\
-    \x03\t\x0b\x0f\n\x0c\n\x05\x04\x01\x02\x01\x03\x12\x03\t\x12\x13\n\x0b\n\
-    \x04\x04\x01\x08\0\x12\x03\n\x040\n\x0c\n\x05\x04\x01\x08\0\x01\x12\x03\
-    \n\n\x18\n\x0b\n\x04\x04\x01\x02\x02\x12\x03\n\x1b.\n\x0c\n\x05\x04\x01\
-    \x02\x02\x05\x12\x03\n\x1b!\n\x0c\n\x05\x04\x01\x02\x02\x01\x12\x03\n\")\
-    \n\x0c\n\x05\x04\x01\x02\x02\x03\x12\x03\n,-\n\n\n\x02\x04\x02\x12\x04\
-    \x0c\0\x11\x01\n\n\n\x03\x04\x02\x01\x12\x03\x0c\x08\x11\n\x0b\n\x04\x04\
-    \x02\x02\0\x12\x03\r\x04\x12\n\x0c\n\x05\x04\x02\x02\0\x05\x12\x03\r\x04\
-    \n\n\x0c\n\x05\x04\x02\x02\0\x01\x12\x03\r\x0b\r\n\x0c\n\x05\x04\x02\x02\
-    \0\x03\x12\x03\r\x10\x11\n\x0b\n\x04\x04\x02\x02\x01\x12\x03\x0e\x04\x14\
-    \n\x0c\n\x05\x04\x02\x02\x01\x05\x12\x03\x0e\x04\n\n\x0c\n\x05\x04\x02\
-    \x02\x01\x01\x12\x03\x0e\x0b\x0f\n\x0c\n\x05\x04\x02\x02\x01\x03\x12\x03\
-    \x0e\x12\x13\n\x0b\n\x04\x04\x02\x02\x02\x12\x03\x0f\x04\x14\n\x0c\n\x05\
-    \x04\x02\x02\x02\x05\x12\x03\x0f\x04\n\n\x0c\n\x05\x04\x02\x02\x02\x01\
-    \x12\x03\x0f\x0b\x0f\n\x0c\n\x05\x04\x02\x02\x02\x03\x12\x03\x0f\x12\x13\
-    \n\x0b\n\x04\x04\x02\x02\x03\x12\x03\x10\x04\x19\n\x0c\n\x05\x04\x02\x02\
-    \x03\x06\x12\x03\x10\x04\x0f\n\x0c\n\x05\x04\x02\x02\x03\x01\x12\x03\x10\
-    \x10\x14\n\x0c\n\x05\x04\x02\x02\x03\x03\x12\x03\x10\x17\x18\n\n\n\x02\
-    \x04\x03\x12\x04\x12\0\x14\x01\n\n\n\x03\x04\x03\x01\x12\x03\x12\x08\x12\
-    \n\x0b\n\x04\x04\x03\x02\0\x12\x03\x13\x04!\n\x0c\n\x05\x04\x03\x02\0\
-    \x04\x12\x03\x13\x04\x0c\n\x0c\n\x05\x04\x03\x02\0\x06\x12\x03\x13\r\x16\
-    \n\x0c\n\x05\x04\x03\x02\0\x01\x12\x03\x13\x17\x1c\n\x0c\n\x05\x04\x03\
-    \x02\0\x03\x12\x03\x13\x1f\x20b\x06proto3\
+    \tR\x04desc\x12\x17\n\x07user_id\x18\x03\x20\x01(\tR\x06userId\"e\n\tWor\
+    kspace\x12\x0e\n\x02id\x18\x01\x20\x01(\tR\x02id\x12\x12\n\x04name\x18\
+    \x02\x20\x01(\tR\x04name\x12\x12\n\x04desc\x18\x03\x20\x01(\tR\x04desc\
+    \x12\x20\n\x04apps\x18\x04\x20\x01(\x0b2\x0c.RepeatedAppR\x04apps\".\n\n\
+    Workspaces\x12\x20\n\x05items\x18\x01\x20\x03(\x0b2\n.WorkspaceR\x05item\
+    sJ\xb1\x05\n\x06\x12\x04\0\0\x14\x01\n\x08\n\x01\x0c\x12\x03\0\0\x12\n\t\
+    \n\x02\x03\0\x12\x03\x01\0\x1a\n\n\n\x02\x04\0\x12\x04\x03\0\x06\x01\n\n\
+    \n\x03\x04\0\x01\x12\x03\x03\x08\x1e\n\x0b\n\x04\x04\0\x02\0\x12\x03\x04\
+    \x04\x14\n\x0c\n\x05\x04\0\x02\0\x05\x12\x03\x04\x04\n\n\x0c\n\x05\x04\0\
+    \x02\0\x01\x12\x03\x04\x0b\x0f\n\x0c\n\x05\x04\0\x02\0\x03\x12\x03\x04\
+    \x12\x13\n\x0b\n\x04\x04\0\x02\x01\x12\x03\x05\x04\x14\n\x0c\n\x05\x04\0\
+    \x02\x01\x05\x12\x03\x05\x04\n\n\x0c\n\x05\x04\0\x02\x01\x01\x12\x03\x05\
+    \x0b\x0f\n\x0c\n\x05\x04\0\x02\x01\x03\x12\x03\x05\x12\x13\n\n\n\x02\x04\
+    \x01\x12\x04\x07\0\x0b\x01\n\n\n\x03\x04\x01\x01\x12\x03\x07\x08\x1d\n\
+    \x0b\n\x04\x04\x01\x02\0\x12\x03\x08\x04\x14\n\x0c\n\x05\x04\x01\x02\0\
+    \x05\x12\x03\x08\x04\n\n\x0c\n\x05\x04\x01\x02\0\x01\x12\x03\x08\x0b\x0f\
+    \n\x0c\n\x05\x04\x01\x02\0\x03\x12\x03\x08\x12\x13\n\x0b\n\x04\x04\x01\
+    \x02\x01\x12\x03\t\x04\x14\n\x0c\n\x05\x04\x01\x02\x01\x05\x12\x03\t\x04\
+    \n\n\x0c\n\x05\x04\x01\x02\x01\x01\x12\x03\t\x0b\x0f\n\x0c\n\x05\x04\x01\
+    \x02\x01\x03\x12\x03\t\x12\x13\n\x0b\n\x04\x04\x01\x02\x02\x12\x03\n\x04\
+    \x17\n\x0c\n\x05\x04\x01\x02\x02\x05\x12\x03\n\x04\n\n\x0c\n\x05\x04\x01\
+    \x02\x02\x01\x12\x03\n\x0b\x12\n\x0c\n\x05\x04\x01\x02\x02\x03\x12\x03\n\
+    \x15\x16\n\n\n\x02\x04\x02\x12\x04\x0c\0\x11\x01\n\n\n\x03\x04\x02\x01\
+    \x12\x03\x0c\x08\x11\n\x0b\n\x04\x04\x02\x02\0\x12\x03\r\x04\x12\n\x0c\n\
+    \x05\x04\x02\x02\0\x05\x12\x03\r\x04\n\n\x0c\n\x05\x04\x02\x02\0\x01\x12\
+    \x03\r\x0b\r\n\x0c\n\x05\x04\x02\x02\0\x03\x12\x03\r\x10\x11\n\x0b\n\x04\
+    \x04\x02\x02\x01\x12\x03\x0e\x04\x14\n\x0c\n\x05\x04\x02\x02\x01\x05\x12\
+    \x03\x0e\x04\n\n\x0c\n\x05\x04\x02\x02\x01\x01\x12\x03\x0e\x0b\x0f\n\x0c\
+    \n\x05\x04\x02\x02\x01\x03\x12\x03\x0e\x12\x13\n\x0b\n\x04\x04\x02\x02\
+    \x02\x12\x03\x0f\x04\x14\n\x0c\n\x05\x04\x02\x02\x02\x05\x12\x03\x0f\x04\
+    \n\n\x0c\n\x05\x04\x02\x02\x02\x01\x12\x03\x0f\x0b\x0f\n\x0c\n\x05\x04\
+    \x02\x02\x02\x03\x12\x03\x0f\x12\x13\n\x0b\n\x04\x04\x02\x02\x03\x12\x03\
+    \x10\x04\x19\n\x0c\n\x05\x04\x02\x02\x03\x06\x12\x03\x10\x04\x0f\n\x0c\n\
+    \x05\x04\x02\x02\x03\x01\x12\x03\x10\x10\x14\n\x0c\n\x05\x04\x02\x02\x03\
+    \x03\x12\x03\x10\x17\x18\n\n\n\x02\x04\x03\x12\x04\x12\0\x14\x01\n\n\n\
+    \x03\x04\x03\x01\x12\x03\x12\x08\x12\n\x0b\n\x04\x04\x03\x02\0\x12\x03\
+    \x13\x04!\n\x0c\n\x05\x04\x03\x02\0\x04\x12\x03\x13\x04\x0c\n\x0c\n\x05\
+    \x04\x03\x02\0\x06\x12\x03\x13\r\x16\n\x0c\n\x05\x04\x03\x02\0\x01\x12\
+    \x03\x13\x17\x1c\n\x0c\n\x05\x04\x03\x02\0\x03\x12\x03\x13\x1f\x20b\x06p\
+    roto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 7 - 0
rust-lib/flowy-workspace/src/protobuf/proto/app_create.proto

@@ -10,6 +10,13 @@ message CreateAppRequest {
 message ColorStyle {
     string theme_color = 1;
 }
+message CreateAppParams {
+    string workspace_id = 1;
+    string name = 2;
+    string desc = 3;
+    ColorStyle color_style = 4;
+    string user_id = 5;
+}
 message App {
     string id = 1;
     string workspace_id = 2;

+ 3 - 0
rust-lib/flowy-workspace/src/protobuf/proto/app_delete.proto

@@ -3,3 +3,6 @@ syntax = "proto3";
 message DeleteAppRequest {
     string app_id = 1;
 }
+message DeleteAppParams {
+    string app_id = 1;
+}

+ 5 - 0
rust-lib/flowy-workspace/src/protobuf/proto/app_query.proto

@@ -5,3 +5,8 @@ message QueryAppRequest {
     bool read_belongings = 2;
     bool is_trash = 3;
 }
+message QueryAppParams {
+    string app_id = 1;
+    bool read_belongings = 2;
+    bool is_trash = 3;
+}

+ 8 - 0
rust-lib/flowy-workspace/src/protobuf/proto/app_update.proto

@@ -9,3 +9,11 @@ message UpdateAppRequest {
     oneof one_of_color_style { ColorStyle color_style = 5; };
     oneof one_of_is_trash { bool is_trash = 6; };
 }
+message UpdateAppParams {
+    string app_id = 1;
+    oneof one_of_workspace_id { string workspace_id = 2; };
+    oneof one_of_name { string name = 3; };
+    oneof one_of_desc { string desc = 4; };
+    oneof one_of_color_style { ColorStyle color_style = 5; };
+    oneof one_of_is_trash { bool is_trash = 6; };
+}

+ 2 - 0
rust-lib/flowy-workspace/src/protobuf/proto/errors.proto

@@ -9,6 +9,7 @@ enum WsErrCode {
     WorkspaceNameInvalid = 1;
     WorkspaceIdInvalid = 2;
     AppColorStyleInvalid = 3;
+    WorkspaceDescInvalid = 4;
     AppIdInvalid = 10;
     AppNameInvalid = 11;
     ViewNameInvalid = 20;
@@ -20,4 +21,5 @@ enum WsErrCode {
     UserInternalError = 102;
     UserNotLoginYet = 103;
     ServerError = 1000;
+    RecordNotFound = 1001;
 }

+ 1 - 1
rust-lib/flowy-workspace/src/protobuf/proto/workspace_create.proto

@@ -8,7 +8,7 @@ message CreateWorkspaceRequest {
 message CreateWorkspaceParams {
     string name = 1;
     string desc = 2;
-    oneof one_of_user_id { string user_id = 3; };
+    string user_id = 3;
 }
 message Workspace {
     string id = 1;

+ 56 - 1
rust-lib/flowy-workspace/src/services/app_controller.rs

@@ -7,6 +7,7 @@ use crate::{
     sql_tables::app::{AppTable, AppTableChangeset, AppTableSql},
 };
 use flowy_dispatch::prelude::DispatchFuture;
+use flowy_net::request::HttpRequestBuilder;
 use std::sync::Arc;
 
 pub struct AppController {
@@ -29,7 +30,12 @@ impl AppController {
         }
     }
 
-    pub fn create_app(&self, params: CreateAppParams) -> Result<App, WorkspaceError> {
+    pub fn create_app(&self, mut params: CreateAppParams) -> Result<App, WorkspaceError> {
+        let user_id = self.user.user_id()?;
+        params.user_id = user_id;
+
+        // TODO: server
+
         let app_table = AppTable::new(params);
         let app: App = app_table.clone().into();
         let _ = self.sql.create_app(app_table)?;
@@ -73,3 +79,52 @@ impl AppController {
         }
     }
 }
+
+pub async fn create_app_request(params: CreateAppParams, url: &str) -> Result<App, WorkspaceError> {
+    let app = HttpRequestBuilder::post(&url.to_owned())
+        .protobuf(params)?
+        .send()
+        .await?
+        .response()
+        .await?;
+    Ok(app)
+}
+
+pub async fn read_app_request(
+    params: QueryAppParams,
+    url: &str,
+) -> Result<Option<App>, WorkspaceError> {
+    let result = HttpRequestBuilder::get(&url.to_owned())
+        .protobuf(params)?
+        .send()
+        .await?
+        .response::<App>()
+        .await;
+
+    match result {
+        Ok(app) => Ok(Some(app)),
+        Err(e) => {
+            if e.is_not_found() {
+                Ok(None)
+            } else {
+                Err(e.into())
+            }
+        },
+    }
+}
+
+pub async fn update_app_request(params: UpdateAppParams, url: &str) -> Result<(), WorkspaceError> {
+    let _ = HttpRequestBuilder::patch(&url.to_owned())
+        .protobuf(params)?
+        .send()
+        .await?;
+    Ok(())
+}
+
+pub async fn delete_app_request(params: DeleteAppParams, url: &str) -> Result<(), WorkspaceError> {
+    let _ = HttpRequestBuilder::delete(&url.to_owned())
+        .protobuf(params)?
+        .send()
+        .await?;
+    Ok(())
+}

+ 42 - 6
rust-lib/flowy-workspace/src/services/workspace_controller.rs

@@ -35,7 +35,9 @@ impl WorkspaceController {
         mut params: CreateWorkspaceParams,
     ) -> Result<Workspace, WorkspaceError> {
         let user_id = self.user.user_id()?;
-        params.user_id = Some(user_id.clone());
+        params.user_id = user_id.clone();
+
+        // TODO: server
 
         let workspace_table = WorkspaceTable::new(params, &user_id);
         let workspace: Workspace = workspace_table.clone().into();
@@ -124,18 +126,52 @@ pub async fn create_workspace_request(
         .protobuf(params)?
         .send()
         .await?
-        .response()?;
+        .response()
+        .await?;
     Ok(workspace)
 }
 
 pub async fn read_workspace_request(
     params: QueryWorkspaceParams,
     url: &str,
-) -> Result<Workspace, WorkspaceError> {
-    let workspace = HttpRequestBuilder::get(&url.to_owned())
+) -> Result<Option<Workspace>, WorkspaceError> {
+    let result = HttpRequestBuilder::get(&url.to_owned())
         .protobuf(params)?
         .send()
         .await?
-        .response()?;
-    Ok(workspace)
+        .response::<Workspace>()
+        .await;
+
+    match result {
+        Ok(workspace) => Ok(Some(workspace)),
+        Err(e) => {
+            if e.is_not_found() {
+                Ok(None)
+            } else {
+                Err(e.into())
+            }
+        },
+    }
+}
+
+pub async fn update_workspace_request(
+    params: UpdateWorkspaceParams,
+    url: &str,
+) -> Result<(), WorkspaceError> {
+    let _ = HttpRequestBuilder::patch(&url.to_owned())
+        .protobuf(params)?
+        .send()
+        .await?;
+    Ok(())
+}
+
+pub async fn delete_workspace_request(
+    params: DeleteWorkspaceParams,
+    url: &str,
+) -> Result<(), WorkspaceError> {
+    let _ = HttpRequestBuilder::delete(&url.to_owned())
+        .protobuf(params)?
+        .send()
+        .await?;
+    Ok(())
 }