Browse Source

Feature: Mobile - Added GraphQL backend architecture.

Martin Gruner 3 years ago
parent
commit
a2e9943405

+ 6 - 0
.gitlab/ci/pre.yml

@@ -61,6 +61,12 @@
     - bundle-audit --ignore CVE-2015-9284
     - bundle-audit --ignore CVE-2015-9284
     - echo "Rails zeitwerk:check autoloader check..."
     - echo "Rails zeitwerk:check autoloader check..."
     - bundle exec rails zeitwerk:check
     - bundle exec rails zeitwerk:check
+    - echo "Checking if auto-generated GraphQL API is up-to-date..."
+    - echo "Use the command 'yarn run generate-graphql-api' to re-generate it, if required."
+    - yarn install
+    - cp app/frontend/apps/mobile/graphql/api.ts app/frontend/apps/mobile/graphql/api.ts.bak
+    - yarn run generate-graphql-api
+    - cmp -s app/frontend/apps/mobile/graphql/api.ts app/frontend/apps/mobile/graphql/api.ts.bak
 
 
 "lint: ruby, js & css":
 "lint: ruby, js & css":
   <<: *template_pre
   <<: *template_pre

+ 14 - 18
.graphql_code_generator.yml

@@ -1,18 +1,14 @@
-# TODO:
-# overwrite: true
-# schema:
-#   - http://localhost:3000/graphql:
-#       headers:
-#         SkipAuthenticityTokenCheck: 'true'
-# documents: 'app/frontend/apps/mobile/graphql/**/*.graphql'
-# generates:
-#   ./app/frontend/apps/mobile/graphql/api.ts:
-#     plugins:
-#       - typescript
-#       - typescript-operations
-#       - typescript-vue-apollo
-#     config:
-#       vueCompositionApiImportFrom: vue
-#   ./app/frontend/apps/mobile/graphql/schema.json:
-#     plugins:
-#       - 'introspection'
+overwrite: true
+schema: tmp/graphql_introspection.json
+documents: 'app/frontend/apps/mobile/graphql/**/*.graphql'
+generates:
+  ./app/frontend/apps/mobile/graphql/api.ts:
+    plugins:
+      - typescript
+      - typescript-operations
+      - typescript-vue-apollo
+    config:
+      vueCompositionApiImportFrom: vue
+  ./app/frontend/apps/mobile/graphql/schema.json:
+    plugins:
+      - 'introspection'

+ 19 - 1
.rubocop/default.yml

@@ -3,10 +3,11 @@
 
 
 require:
 require:
   - rubocop-faker
   - rubocop-faker
+  - rubocop-graphql
+  - rubocop-inflector
   - rubocop-performance
   - rubocop-performance
   - rubocop-rails
   - rubocop-rails
   - rubocop-rspec
   - rubocop-rspec
-  - rubocop-inflector
   - ../config/initializers/inflections.rb
   - ../config/initializers/inflections.rb
   - ./rubocop_zammad.rb
   - ./rubocop_zammad.rb
 
 
@@ -269,6 +270,23 @@ Rails/Output:
   Include:
   Include:
     - "**/*_spec.rb"
     - "**/*_spec.rb"
 
 
+GraphQL/OrderedArguments:
+  Enabled: false
+
+GraphQL/OrderedFields:
+  Enabled: false
+
+# Our models can have many fields, and we should not enforce a different GraphQL structure just for the sake of it.
+GraphQL/ExtractType:
+  Enabled: false
+
+GraphQL/ExtractInputType:
+  MaxArguments: 3
+
+# Enforcing field descriptions for all model fields will be too verbose/redundant.
+GraphQL/FieldDescription:
+  Enabled: false
+
 # RSpec tests
 # RSpec tests
 Style/NumericPredicate:
 Style/NumericPredicate:
   Description: >-
   Description: >-

+ 2 - 1
Gemfile

@@ -56,7 +56,7 @@ gem 'dalli', require: false
 #   having unneeded runtime dependencies like NodeJS.
 #   having unneeded runtime dependencies like NodeJS.
 group :assets do
 group :assets do
   # asset handling - frontend tool chain
   # asset handling - frontend tool chain
-  gem 'vite_rails'
+  gem 'vite_rails', require: false
 
 
   # asset handling - javascript execution for e.g. linux
   # asset handling - javascript execution for e.g. linux
   gem 'execjs', require: false
   gem 'execjs', require: false
@@ -207,6 +207,7 @@ group :development, :test do
   gem 'overcommit'
   gem 'overcommit'
   gem 'rubocop'
   gem 'rubocop'
   gem 'rubocop-faker'
   gem 'rubocop-faker'
+  gem 'rubocop-graphql'
   gem 'rubocop-inflector'
   gem 'rubocop-inflector'
   gem 'rubocop-performance'
   gem 'rubocop-performance'
   gem 'rubocop-rails'
   gem 'rubocop-rails'

+ 3 - 0
Gemfile.lock

@@ -493,6 +493,8 @@ GEM
     rubocop-faker (1.1.0)
     rubocop-faker (1.1.0)
       faker (>= 2.12.0)
       faker (>= 2.12.0)
       rubocop (>= 0.82.0)
       rubocop (>= 0.82.0)
+    rubocop-graphql (0.10.2)
+      rubocop (>= 0.87, < 2)
     rubocop-inflector (0.2.1)
     rubocop-inflector (0.2.1)
       activesupport
       activesupport
       rubocop
       rubocop
@@ -702,6 +704,7 @@ DEPENDENCIES
   rszr
   rszr
   rubocop
   rubocop
   rubocop-faker
   rubocop-faker
+  rubocop-graphql
   rubocop-inflector
   rubocop-inflector
   rubocop-performance
   rubocop-performance
   rubocop-rails
   rubocop-rails

+ 61 - 0
app/controllers/graphql_controller.rb

@@ -0,0 +1,61 @@
+# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
+
+class GraphqlController < ApplicationController
+  # If accessing from outside this domain, nullify the session
+  # This allows for outside API access while preventing CSRF attacks,
+  # but you'll have to authenticate your user separately
+  # protect_from_forgery with: :null_session
+
+  skip_before_action :verify_csrf_token, if: lambda {
+    # required for (extend list if you use this header):
+    # - GraphQL Code Generator
+    Rails.env.development? && request.headers['SkipAuthenticityTokenCheck'] == 'true'
+  }
+
+  prepend_before_action :authentication_check_only
+
+  def execute
+    variables = prepare_variables(params[:variables])
+    query = params[:query]
+    operation_name = params[:operationName]
+    context = {
+      current_user: current_user,
+      controller:   self,
+    }
+    result = Gql::ZammadSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
+    render json: result
+  rescue => e
+    raise e if !Rails.env.development?
+
+    handle_error_in_development(e)
+  end
+
+  private
+
+  # Handle variables in form data, JSON body, or a blank value
+  def prepare_variables(variables_param)
+    case variables_param
+    when String
+      if variables_param.present?
+        JSON.parse(variables_param) || {}
+      else
+        {}
+      end
+    when Hash
+      variables_param
+    when ActionController::Parameters
+      variables_param.to_unsafe_hash # GraphQL-Ruby will validate name and type of incoming variables.
+    when nil
+      {}
+    else
+      raise ArgumentError, "Unexpected parameter: #{variables_param}"
+    end
+  end
+
+  def handle_error_in_development(e)
+    logger.error e.message
+    logger.error e.backtrace.join("\n")
+
+    render json: { errors: [{ message: e.message, backtrace: e.backtrace }], data: {} }, status: :internal_server_error
+  end
+end

+ 307 - 0
app/frontend/apps/mobile/graphql/api.ts

@@ -0,0 +1,307 @@
+import gql from 'graphql-tag';
+import * as VueApolloComposable from '@vue/apollo-composable';
+import * as VueCompositionApi from 'vue';
+export type Maybe<T> = T | null;
+export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
+export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
+export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
+export type ReactiveFunction<TParam> = () => TParam;
+/** All built-in and custom scalars, mapped to their actual values */
+export type Scalars = {
+  ID: string;
+  String: string;
+  Boolean: boolean;
+  Int: number;
+  Float: number;
+  /** An ISO 8601-encoded date */
+  ISO8601Date: any;
+  /** An ISO 8601-encoded datetime */
+  ISO8601DateTime: any;
+  /** Represents untyped JSON */
+  JSON: any;
+};
+
+/** Autogenerated return type of Login */
+export type LoginPayload = {
+  __typename?: 'LoginPayload';
+  /** The logged-in user */
+  currentUser: User;
+  /** The current session */
+  session: Session;
+};
+
+/** Autogenerated return type of Logout */
+export type LogoutPayload = {
+  __typename?: 'LogoutPayload';
+  /** Was the logout successful? */
+  success: Scalars['Boolean'];
+};
+
+/** All available mutations. */
+export type Mutations = {
+  __typename?: 'Mutations';
+  /** Performs a user login to create a session */
+  login?: Maybe<LoginPayload>;
+  /** End the current session */
+  logout?: Maybe<LogoutPayload>;
+};
+
+
+/** All available mutations. */
+export type MutationsLoginArgs = {
+  fingerprint: Scalars['String'];
+  login: Scalars['String'];
+  password: Scalars['String'];
+};
+
+/** An object with an ID. */
+export type Node = {
+  /** ID of the object. */
+  id: Scalars['ID'];
+};
+
+/** Organizations that users can belong to */
+export type Organization = Node & {
+  __typename?: 'Organization';
+  active: Scalars['Boolean'];
+  /** Create date/time of the record */
+  createdAt: Scalars['ISO8601DateTime'];
+  /** User that created this record */
+  createdBy: User;
+  domain?: Maybe<Scalars['String']>;
+  domainAssignment: Scalars['Boolean'];
+  id: Scalars['ID'];
+  members: UserConnection;
+  name: Scalars['String'];
+  note?: Maybe<Scalars['String']>;
+  shared: Scalars['Boolean'];
+  /** Last update date/time of the record */
+  updatedAt: Scalars['ISO8601DateTime'];
+  /** Last user that updated this record */
+  updatedBy: User;
+};
+
+
+/** Organizations that users can belong to */
+export type OrganizationMembersArgs = {
+  after?: Maybe<Scalars['String']>;
+  before?: Maybe<Scalars['String']>;
+  first?: Maybe<Scalars['Int']>;
+  last?: Maybe<Scalars['Int']>;
+};
+
+/** Information about pagination in a connection. */
+export type PageInfo = {
+  __typename?: 'PageInfo';
+  /** When paginating forwards, the cursor to continue. */
+  endCursor?: Maybe<Scalars['String']>;
+  /** When paginating forwards, are there more items? */
+  hasNextPage: Scalars['Boolean'];
+  /** When paginating backwards, are there more items? */
+  hasPreviousPage: Scalars['Boolean'];
+  /** When paginating backwards, the cursor to continue. */
+  startCursor?: Maybe<Scalars['String']>;
+};
+
+/** All available queries */
+export type Queries = {
+  __typename?: 'Queries';
+  /** Fetches an object given its ID. */
+  node?: Maybe<Node>;
+  /** Fetches a list of objects given a list of IDs. */
+  nodes: Array<Maybe<Node>>;
+  /** Information about the current user session */
+  session: Session;
+};
+
+
+/** All available queries */
+export type QueriesNodeArgs = {
+  id: Scalars['ID'];
+};
+
+
+/** All available queries */
+export type QueriesNodesArgs = {
+  ids: Array<Scalars['ID']>;
+};
+
+/** Data of a current session */
+export type Session = {
+  __typename?: 'Session';
+  data?: Maybe<Scalars['JSON']>;
+  sessionId: Scalars['String'];
+};
+
+/** Users (admins, agents and customers) */
+export type User = Node & {
+  __typename?: 'User';
+  active: Scalars['Boolean'];
+  address?: Maybe<Scalars['String']>;
+  city?: Maybe<Scalars['String']>;
+  country?: Maybe<Scalars['String']>;
+  /** Create date/time of the record */
+  createdAt: Scalars['ISO8601DateTime'];
+  /** User that created this record */
+  createdById: Scalars['Int'];
+  department?: Maybe<Scalars['String']>;
+  email?: Maybe<Scalars['String']>;
+  fax?: Maybe<Scalars['String']>;
+  firstname?: Maybe<Scalars['String']>;
+  id: Scalars['ID'];
+  image?: Maybe<Scalars['String']>;
+  imageSource?: Maybe<Scalars['String']>;
+  lastLogin?: Maybe<Scalars['ISO8601DateTime']>;
+  lastname?: Maybe<Scalars['String']>;
+  login: Scalars['String'];
+  loginFailed: Scalars['Int'];
+  mobile?: Maybe<Scalars['String']>;
+  note?: Maybe<Scalars['String']>;
+  organization?: Maybe<Organization>;
+  outOfOffice: Scalars['Boolean'];
+  outOfOfficeEndAt?: Maybe<Scalars['ISO8601Date']>;
+  outOfOfficeReplacementId?: Maybe<Scalars['Int']>;
+  outOfOfficeStartAt?: Maybe<Scalars['ISO8601Date']>;
+  password?: Maybe<Scalars['String']>;
+  phone?: Maybe<Scalars['String']>;
+  preferences?: Maybe<Scalars['JSON']>;
+  source?: Maybe<Scalars['String']>;
+  street?: Maybe<Scalars['String']>;
+  /** Last update date/time of the record */
+  updatedAt: Scalars['ISO8601DateTime'];
+  /** Last user that updated this record */
+  updatedById: Scalars['Int'];
+  verified: Scalars['Boolean'];
+  vip?: Maybe<Scalars['Boolean']>;
+  web?: Maybe<Scalars['String']>;
+  zip?: Maybe<Scalars['String']>;
+};
+
+/** The connection type for User. */
+export type UserConnection = {
+  __typename?: 'UserConnection';
+  /** A list of edges. */
+  edges?: Maybe<Array<Maybe<UserEdge>>>;
+  /** A list of nodes. */
+  nodes?: Maybe<Array<Maybe<User>>>;
+  /** Information to aid in pagination. */
+  pageInfo: PageInfo;
+};
+
+/** An edge in a connection. */
+export type UserEdge = {
+  __typename?: 'UserEdge';
+  /** A cursor for use in pagination. */
+  cursor: Scalars['String'];
+  /** The item at the end of the edge. */
+  node?: Maybe<User>;
+};
+
+export type LoginMutationVariables = Exact<{
+  login: Scalars['String'];
+  password: Scalars['String'];
+  fingerprint: Scalars['String'];
+}>;
+
+
+export type LoginMutation = { __typename?: 'Mutations', login?: { __typename?: 'LoginPayload', currentUser: { __typename?: 'User', firstname?: string | null | undefined, lastname?: string | null | undefined }, session: { __typename?: 'Session', sessionId: string, data?: any | null | undefined } } | null | undefined };
+
+export type LogoutMutationVariables = Exact<{ [key: string]: never; }>;
+
+
+export type LogoutMutation = { __typename?: 'Mutations', logout?: { __typename?: 'LogoutPayload', success: boolean } | null | undefined };
+
+export type SessionQueryVariables = Exact<{ [key: string]: never; }>;
+
+
+export type SessionQuery = { __typename?: 'Queries', session: { __typename?: 'Session', sessionId: string, data?: any | null | undefined } };
+
+
+export const LoginDocument = gql`
+    mutation login($login: String!, $password: String!, $fingerprint: String!) {
+  login(login: $login, password: $password, fingerprint: $fingerprint) {
+    currentUser {
+      firstname
+      lastname
+    }
+    session {
+      sessionId
+      data
+    }
+  }
+}
+    `;
+
+/**
+ * __useLoginMutation__
+ *
+ * To run a mutation, you first call `useLoginMutation` within a Vue component and pass it any options that fit your needs.
+ * When your component renders, `useLoginMutation` returns an object that includes:
+ * - A mutate function that you can call at any time to execute the mutation
+ * - Several other properties: https://v4.apollo.vuejs.org/api/use-mutation.html#return
+ *
+ * @param options that will be passed into the mutation, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/mutation.html#options;
+ *
+ * @example
+ * const { mutate, loading, error, onDone } = useLoginMutation({
+ *   variables: {
+ *     login: // value for 'login'
+ *     password: // value for 'password'
+ *     fingerprint: // value for 'fingerprint'
+ *   },
+ * });
+ */
+export function useLoginMutation(options: VueApolloComposable.UseMutationOptions<LoginMutation, LoginMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<LoginMutation, LoginMutationVariables>>) {
+  return VueApolloComposable.useMutation<LoginMutation, LoginMutationVariables>(LoginDocument, options);
+}
+export type LoginMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<LoginMutation, LoginMutationVariables>;
+export const LogoutDocument = gql`
+    mutation logout {
+  logout {
+    success
+  }
+}
+    `;
+
+/**
+ * __useLogoutMutation__
+ *
+ * To run a mutation, you first call `useLogoutMutation` within a Vue component and pass it any options that fit your needs.
+ * When your component renders, `useLogoutMutation` returns an object that includes:
+ * - A mutate function that you can call at any time to execute the mutation
+ * - Several other properties: https://v4.apollo.vuejs.org/api/use-mutation.html#return
+ *
+ * @param options that will be passed into the mutation, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/mutation.html#options;
+ *
+ * @example
+ * const { mutate, loading, error, onDone } = useLogoutMutation();
+ */
+export function useLogoutMutation(options: VueApolloComposable.UseMutationOptions<LogoutMutation, LogoutMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<LogoutMutation, LogoutMutationVariables>> = {}) {
+  return VueApolloComposable.useMutation<LogoutMutation, LogoutMutationVariables>(LogoutDocument, options);
+}
+export type LogoutMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<LogoutMutation, LogoutMutationVariables>;
+export const SessionDocument = gql`
+    query session {
+  session {
+    sessionId
+    data
+  }
+}
+    `;
+
+/**
+ * __useSessionQuery__
+ *
+ * To run a query within a Vue component, call `useSessionQuery` and pass it any options that fit your needs.
+ * When your component renders, `useSessionQuery` returns an object from Apollo Client that contains result, loading and error properties
+ * you can use to render your UI.
+ *
+ * @param options that will be passed into the query, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/query.html#options;
+ *
+ * @example
+ * const { result, loading, error } = useSessionQuery();
+ */
+export function useSessionQuery(options: VueApolloComposable.UseQueryOptions<SessionQuery, SessionQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<SessionQuery, SessionQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<SessionQuery, SessionQueryVariables>> = {}) {
+  return VueApolloComposable.useQuery<SessionQuery, SessionQueryVariables>(SessionDocument, {}, options);
+}
+export type SessionQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<SessionQuery, SessionQueryVariables>;

+ 12 - 0
app/frontend/apps/mobile/graphql/mutation/login.graphql

@@ -0,0 +1,12 @@
+mutation login($login: String!, $password: String!, $fingerprint: String!) {
+  login(login: $login, password: $password, fingerprint: $fingerprint) {
+    currentUser {
+      firstname
+      lastname
+    }
+    session {
+      sessionId
+      data
+    }
+  }
+}

+ 5 - 0
app/frontend/apps/mobile/graphql/mutation/logout.graphql

@@ -0,0 +1,5 @@
+mutation logout {
+  logout {
+    success
+  }
+}

+ 6 - 0
app/frontend/apps/mobile/graphql/queries/session.graphql

@@ -0,0 +1,6 @@
+query session {
+  session {
+    sessionId
+    data
+  }
+}

Some files were not shown because too many files changed in this diff