Browse Source

Feature: Mobile - Add subscription for user updates.

Martin Gruner 2 years ago
parent
commit
07e51071fa

+ 34 - 0
app/frontend/shared/graphql/subscriptions/userUpdates.api.ts

@@ -0,0 +1,34 @@
+import * as Types from '../types';
+
+import gql from 'graphql-tag';
+import { ObjectAttributeValuesFragmentDoc } from '../fragments/objectAttributeValues.api';
+import * as VueApolloComposable from '@vue/apollo-composable';
+import * as VueCompositionApi from 'vue';
+export type ReactiveFunction<TParam> = () => TParam;
+
+export const UserUpdatesDocument = gql`
+    subscription userUpdates($userId: ID!) {
+  userUpdates(userId: $userId) {
+    user {
+      id
+      firstname
+      lastname
+      fullname
+      preferences
+      objectAttributeValues {
+        ...objectAttributeValues
+      }
+      organization {
+        name
+        objectAttributeValues {
+          ...objectAttributeValues
+        }
+      }
+    }
+  }
+}
+    ${ObjectAttributeValuesFragmentDoc}`;
+export function useUserUpdatesSubscription(variables: Types.UserUpdatesSubscriptionVariables | VueCompositionApi.Ref<Types.UserUpdatesSubscriptionVariables> | ReactiveFunction<Types.UserUpdatesSubscriptionVariables>, options: VueApolloComposable.UseSubscriptionOptions<Types.UserUpdatesSubscription, Types.UserUpdatesSubscriptionVariables> | VueCompositionApi.Ref<VueApolloComposable.UseSubscriptionOptions<Types.UserUpdatesSubscription, Types.UserUpdatesSubscriptionVariables>> | ReactiveFunction<VueApolloComposable.UseSubscriptionOptions<Types.UserUpdatesSubscription, Types.UserUpdatesSubscriptionVariables>> = {}) {
+  return VueApolloComposable.useSubscription<Types.UserUpdatesSubscription, Types.UserUpdatesSubscriptionVariables>(UserUpdatesDocument, variables, options);
+}
+export type UserUpdatesSubscriptionCompositionFunctionResult = VueApolloComposable.UseSubscriptionReturn<Types.UserUpdatesSubscription, Types.UserUpdatesSubscriptionVariables>;

+ 20 - 0
app/frontend/shared/graphql/subscriptions/userUpdates.graphql

@@ -0,0 +1,20 @@
+subscription userUpdates($userId: ID!) {
+  userUpdates(userId: $userId) {
+    user {
+      id
+      firstname
+      lastname
+      fullname
+      preferences
+      objectAttributeValues {
+        ...objectAttributeValues
+      }
+      organization {
+        name
+        objectAttributeValues {
+          ...objectAttributeValues
+        }
+      }
+    }
+  }
+}

+ 22 - 0
app/frontend/shared/graphql/types.ts

@@ -460,6 +460,14 @@ export type Subscriptions = {
   configUpdates: ConfigUpdatesPayload;
   /** Broadcast messages to all users */
   pushMessages: PushMessagesPayload;
+  /** Updates to user records */
+  userUpdates: UserUpdatesPayload;
+};
+
+
+/** All available subscriptions */
+export type SubscriptionsUserUpdatesArgs = {
+  userId: Scalars['ID'];
 };
 
 /** Option to choose SQL sorting direction */
@@ -767,6 +775,13 @@ export type UserError = {
   message: Scalars['String'];
 };
 
+/** Autogenerated return type of UserUpdates */
+export type UserUpdatesPayload = {
+  __typename?: 'UserUpdatesPayload';
+  /** Updated user */
+  user?: Maybe<User>;
+};
+
 export type AccountLocaleMutationVariables = Exact<{
   locale: Scalars['String'];
 }>;
@@ -896,3 +911,10 @@ export type PushMessagesSubscriptionVariables = Exact<{ [key: string]: never; }>
 
 
 export type PushMessagesSubscription = { __typename?: 'Subscriptions', pushMessages: { __typename?: 'PushMessagesPayload', title?: string | null, text?: string | null } };
+
+export type UserUpdatesSubscriptionVariables = Exact<{
+  userId: Scalars['ID'];
+}>;
+
+
+export type UserUpdatesSubscription = { __typename?: 'Subscriptions', userUpdates: { __typename?: 'UserUpdatesPayload', user?: { __typename?: 'User', id: string, firstname?: string | null, lastname?: string | null, fullname?: string | null, preferences?: any | null, objectAttributeValues: Array<{ __typename?: 'ObjectAttributeValue', value?: string | null, attribute: { __typename?: 'ObjectManagerAttribute', name: string, display: string, dataType: string, dataOption?: any | null, screens?: any | null, editable: boolean, active: boolean } }>, organization?: { __typename?: 'Organization', name: string, objectAttributeValues: Array<{ __typename?: 'ObjectAttributeValue', value?: string | null, attribute: { __typename?: 'ObjectManagerAttribute', name: string, display: string, dataType: string, dataOption?: any | null, screens?: any | null, editable: boolean, active: boolean } }> } | null } | null } };

+ 1 - 1
app/graphql/gql/subscriptions/base_subscription.rb

@@ -16,7 +16,7 @@ module Gql::Subscriptions
     # Default subscribe implementation that returns nothing. For this to work, all fields must have null: true.
     # Otherwise, you can provide a subscribe method in the inheriting class.
     #
-    def subscribe
+    def subscribe(...)
       {}
     end
 

+ 31 - 0
app/graphql/gql/subscriptions/user_updates.rb

@@ -0,0 +1,31 @@
+# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+module Gql::Subscriptions
+  class UserUpdates < BaseSubscription
+
+    argument :user_id, GraphQL::Types::ID, 'ID of the user to receive updates for', loads: Gql::Types::UserType
+
+    description 'Updates to user records'
+
+    field :user, Gql::Types::UserType, null: true, description: 'Updated user'
+
+    # Generally requires logged-in user
+    def self.authorize(_obj, ctx)
+      ctx.current_user
+    end
+
+    # Allow subscriptions only for users where the current user has read permission for.
+    def authorized?(user:)
+      Pundit.authorize context.current_user, user, :show?
+    end
+
+    def update(user:)
+      # Safeguard, this should not happen.
+      if user.id != object.id
+        return no_update
+      end
+
+      { user: object }
+    end
+  end
+end

+ 1 - 0
app/models/user.rb

@@ -18,6 +18,7 @@ class User < ApplicationModel
   include User::Search
   include User::SearchIndex
   include User::TouchesOrganization
+  include User::TriggersSubscriptions
   include User::PerformsGeoLookup
   include User::UpdatesTicketOrganization
 

+ 23 - 0
app/models/user/triggers_subscriptions.rb

@@ -0,0 +1,23 @@
+# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+# Trigger GraphQL subscriptions on user changes.
+module User::TriggersSubscriptions
+  extend ActiveSupport::Concern
+
+  included do
+    after_update :trigger_subscriptions
+  end
+
+  private
+
+  def trigger_subscriptions
+    # return if we run import mode
+    return true if Setting.get('import_mode')
+
+    Gql::ZammadSchema.subscriptions.trigger(
+      Gql::Subscriptions::UserUpdates.field_name,
+      { user_id: Gql::ZammadSchema.id_from_object(self) },
+      self
+    )
+  end
+end

+ 66 - 0
spec/graphql/gql/subscriptions/user_updates_spec.rb

@@ -0,0 +1,66 @@
+# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+require 'rails_helper'
+
+RSpec.describe Gql::Subscriptions::UserUpdates, type: :graphql do
+
+  let(:subscription) do
+    read_graphql_file('shared/graphql/subscriptions/userUpdates.graphql') +
+      read_graphql_file('shared/graphql/fragments/objectAttributeValues.graphql')
+  end
+  let(:mock_channel) { build_mock_channel }
+  let(:target)       { create(:user) }
+  let(:variables)    { { userId: Gql::ZammadSchema.id_from_object(target) } }
+
+  before do
+    graphql_execute(subscription, variables: variables, context: { channel: mock_channel })
+  end
+
+  context 'with authenticated user', authenticated_as: :agent do
+    let(:agent) { create(:agent) }
+
+    it 'subscribes' do
+      expect(graphql_response['data']['userUpdates']).to eq({ 'user' => nil })
+    end
+
+    it 'receives user updates for target user' do
+      target.save!
+      expect(mock_channel.mock_broadcasted_messages.first[:result]['data']['userUpdates']['user']['firstname']).to eq(target.firstname)
+    end
+
+    it 'does not receive user updates for other users' do
+      create(:agent).save!
+      expect(mock_channel.mock_broadcasted_messages).to be_empty
+    end
+  end
+
+  context 'with authenticated customer', authenticated_as: :customer do
+    let(:customer) { create(:customer) }
+
+    context 'when subscribing for other users' do
+      it 'does not subscribe' do
+        expect(graphql_response['errors'][0]).to be_present
+      end
+    end
+
+    context 'when subscribing for itself' do
+      let(:target) { customer }
+
+      it 'subscribes' do
+        expect(graphql_response['data']['userUpdates']).to eq({ 'user' => nil })
+      end
+
+      it 'receives user updates for target user' do
+        target.save!
+        expect(mock_channel.mock_broadcasted_messages.first[:result]['data']['userUpdates']['user']['firstname']).to eq(target.firstname)
+      end
+
+      it 'does not receive user updates for other users' do
+        create(:agent).save!
+        expect(mock_channel.mock_broadcasted_messages).to be_empty
+      end
+    end
+  end
+
+  it_behaves_like 'graphql responds with error if unauthenticated'
+end