(ns auto-ap.graphql (:require [com.walmartlabs.lacinia.util :refer [attach-resolvers]] [com.walmartlabs.lacinia.schema :as schema] [com.walmartlabs.lacinia :refer [execute]] [com.walmartlabs.lacinia.executor :as executor] [com.walmartlabs.lacinia.resolve :as resolve] [buddy.auth :refer [throw-unauthorized]] [auto-ap.db.invoices :as invoices] [auto-ap.utils :refer [by]] [auto-ap.graphql.utils :refer [assert-admin can-see-company? assert-can-see-company]] [auto-ap.db.vendors :as vendors] [auto-ap.db.companies :as companies] [auto-ap.datomic.clients :as d-clients] [auto-ap.datomic.checks :as d-checks] [auto-ap.datomic.invoices :as d-invoices] [auto-ap.datomic.vendors :as d-vendors] [auto-ap.db.users :as users] [auto-ap.db.checks :as checks] [auto-ap.routes.checks :as rchecks] [auto-ap.graphql.users :as gq-users] [auto-ap.graphql.checks :as gq-checks] [auto-ap.graphql.expense-accounts :as expense-accounts] [auto-ap.graphql.invoices :as gq-invoices] [auto-ap.graphql.transactions :as gq-transactions] [auto-ap.db.reminders :as reminders] [auto-ap.db.invoices-checks :as invoices-checks] [auto-ap.db.utils :as utils] [clojure.walk :as walk] [clojure.string :as str]) (:import (clojure.lang IPersistentMap))) (def integreat-schema { :objects { :company {:fields {:id {:type 'String} :name {:type 'String} :email {:type 'String} :address {:type :address} :locations {:type '(list String)} :bank_accounts {:type '(list :bank_account)}}} :client {:fields {:id {:type 'String} :name {:type 'String} :email {:type 'String} :address {:type :address} :locations {:type '(list String)} :bank_accounts {:type '(list :bank_account)}}} :contact {:fields {:id {:type 'String} :name {:type 'String} :email {:type 'String} :phone {:type 'String}}} :bank_account {:fields {:id {:type 'String} :type {:type 'String} :number {:type 'String} :check_number {:type 'Int} :name {:type 'String} :bank_code {:type 'String} :bank_name {:type 'String}}} :address {:fields {:street1 {:type 'String} :street2 {:type 'String} :city {:type 'String} :state {:type 'String} :zip {:type 'String}}} :vendor {:fields {:id {:type 'String} :name {:type 'String} :code {:type 'String} :print_as {:type 'String} :primary_contact {:type :contact} :secondary_contact {:type :contact} :address {:type :address} :default_expense_account {:type 'Int} :invoice_reminder_schedule {:type 'String}}} :reminder {:fields {:id {:type 'Int} :email {:type 'String} :subject {:type 'String} :body {:type 'String} :scheduled {:type 'String} :sent {:type 'String} :vendor {:type :vendor :resolve :get-vendor-for-invoice} } } :check {:fields {:id {:type 'Int} :type {:type 'String} :amount {:type 'String} :vendor {:type :vendor} :company {:type :company} :date {:type 'String} :bank_account {:type :bank_account} :memo {:type 'String} :s3_url {:type 'String} :check_number {:type 'Int} :status {:type 'String} :invoices {:type '(list :invoice_payment)} }} :payment {:fields {:id {:type 'String} :type {:type 'String} :amount {:type 'String} :vendor {:type :vendor} :client {:type :client} :date {:type 'String} :bank_account {:type :bank_account} :memo {:type 'String} :s3_url {:type 'String} :check_number {:type 'Int} :status {:type 'String} :invoices {:type '(list :invoice_payment)} }} :transaction {:fields {:id {:type 'String} :amount {:type 'String} :description_original {:type 'String} :description_simple {:type 'String} :status {:type 'String} :client {:type :client} :payment {:type :payment} :date {:type 'String} :post_date {:type 'String}}} :invoice_payment {:fields {:id {:type 'Int} :amount {:type 'String} :invoice_id {:type 'String} :payment_id {:type 'String} :payment {:type :payment} :invoice {:type :invoice}}} :user {:fields {:id {:type 'Int} :name {:type 'String} :role {:type 'String} :companies {:type '(list :company) :resolve :get-user-companies}}} :expense_account {:fields {:id {:type 'Int} :location {:type 'String} :name {:type 'String} :parent {:type :expense_account :resolve :get-expense-account-parent}}} :invoices_expense_accounts {:fields {:id {:type 'String} :invoice_id {:type 'String} :expense_account_id {:type 'Int} :location {:type 'String} :expense_account {:type :expense_account :resolve :get-expense-account } :amount {:type 'String}}} :invoice {:fields {:id {:type 'String} :total {:type 'String} :outstanding_balance {:type 'String} :invoice_number {:type 'String} :expense_accounts {:type '(list :invoices_expense_accounts)} :date {:type 'String} :company_id {:type 'Int} :payments {:type '(list :invoice_payment)} :vendor {:type :vendor} :client {:type :client}}} :invoice_page {:fields {:invoices {:type '(list :invoice)} :count {:type 'Int} :total {:type 'Int} :start {:type 'Int} :end {:type 'Int}}} :payment_page {:fields {:payments {:type '(list :payment)} :count {:type 'Int} :total {:type 'Int} :start {:type 'Int} :end {:type 'Int}}} :transaction_page {:fields {:transactions {:type '(list :transaction)} :count {:type 'Int} :total {:type 'Int} :start {:type 'Int} :end {:type 'Int}}} :reminder_page {:fields {:reminders {:type '(list :reminder)} :count {:type 'Int} :total {:type 'Int} :start {:type 'Int} :end {:type 'Int}}} :check_result {:fields {:invoices {:type '(list :invoice)} :pdf_url {:type 'String}}}} :queries {:invoice_page {:type '(list :invoice_page) :args {:imported {:type 'Boolean} :status {:type 'String} :client_id {:type 'String} :start {:type 'Int} :sort_by {:type 'String} :asc {:type 'Boolean}} :resolve :get-invoice-page} :all_invoices {:type '(list :invoice) :args {:client_id {:type 'String} :original_id {:type 'Int}} :resolve :get-all-invoices} :all_payments {:type '(list :payment) :args {:client_id {:type 'String} :original_id {:type 'Int}} :resolve :get-all-payments} :transaction_page {:type '(list :transaction_page) :args {:company_id {:type 'String} :start {:type 'Int} :sort_by {:type 'String} :asc {:type 'Boolean}} :resolve :get-transaction-page} :payment_page {:type '(list :payment_page) :args {:client_id {:type 'String} :start {:type 'Int} :sort_by {:type 'String} :asc {:type 'Boolean}} :resolve :get-payment-page} :reminder_page {:type '(list :reminder_page) :args {:start {:type 'Int} :sort_by {:type 'String} :asc {:type 'Boolean}} :resolve :get-reminder-page} :company {:type '(list :company) :resolve :get-company} :client {:type '(list :company) :resolve :get-company} :vendor {:type '(list :vendor) :resolve :get-vendor} :user {:type '(list :user) :resolve :get-user}} :input-objects { :invoice_payment_amount {:fields {:invoice_id {:type 'String} :amount {:type 'Float}}} :edit_user {:fields {:id {:type 'Int} :name {:type 'String} :role {:type 'String} :companies {:type '(list Int)}}} :edit_expense_account {:fields {:id {:type 'Int} :expense_account_id {:type 'Int} :location {:type 'String} :amount {:type 'String}}} :add_invoice {:fields {:id {:type 'Int} :invoice_number {:type 'String} :location {:type 'String} :date {:type 'String} :company_id {:type 'Int} :vendor_id {:type 'Int} :vendor_name {:type 'String} :total {:type 'Float}}} :edit_invoice {:fields {:id {:type 'Int} :invoice_number {:type 'String} :date {:type 'String} :total {:type 'Float}}}} :mutations {:print_checks {:type :check_result :args {:invoice_payments {:type '(list :invoice_payment_amount)} :bank_account_id {:type 'Int} :type {:type 'String} :company_id {:type 'Int}} :resolve :mutation/print-checks} :add_handwritten_check {:type :check_result :args {:invoice_id {:type 'Int} :amount {:type 'Float} :date {:type 'String} :check_number {:type 'Int} :bank_account_id {:type 'Int}} :resolve :mutation/add-handwritten-check} :edit_user {:type :user :args {:edit_user {:type :edit_user}} :resolve :mutation/edit-user} :add_invoice {:type :invoice :args {:invoice {:type :add_invoice}} :resolve :mutation/add-invoice} :edit_invoice {:type :invoice :args {:invoice {:type :edit_invoice}} :resolve :mutation/edit-invoice} :void_invoice {:type :invoice :args {:invoice_id {:type 'Int}} :resolve :mutation/void-invoice} :void_check {:type :check :args {:check_id {:type 'Int}} :resolve :mutation/void-check} :edit_expense_accounts {:type :invoice :args {:invoice_id {:type 'Int} :expense_accounts {:type '(list :edit_expense_account)}} :resolve :mutation/edit-expense-accounts}}}) (defn snake->kebab [s] (str/replace s #"_" "-")) (defn kebab [x] (keyword (snake->kebab (name x)))) (defn kebab->snake [s] (str/replace s #"-" "_")) (defn snake [x] (keyword (kebab->snake (name x)))) (defn ->graphql [m] (walk/postwalk (fn [node] (cond (keyword? node) (snake node) :else node)) m)) (defn <-graphql [m] (walk/postwalk (fn [node] (cond (keyword? node) (kebab node) :else node)) m)) (defn get-invoice-page [context args value] (let [args (assoc args :id (:id context)) invoices (map ->graphql (d-invoices/get-graphql (<-graphql (assoc args :id (:id context))))) invoice-count (d-invoices/count-graphql (<-graphql args))] [{:invoices invoices :total invoice-count :count (count invoices) :start (:start args 0) :end (+ (:start args 0) (count invoices))}])) (defn get-all-invoices [context args value] (assert-admin (:id context)) (map ->graphql (d-invoices/get-graphql (assoc (<-graphql args) :limit Integer/MAX_VALUE)))) (defn get-all-payments [context args value] (assert-admin (:id context)) (map ->graphql (d-checks/get-graphql (assoc (<-graphql args) :limit Integer/MAX_VALUE)))) (defn get-reminder-page [context args value] (assert-admin (:id context)) (let [extra-context (cond-> {} (executor/selects-field? context :reminder/vendor) (assoc :vendor-cache (by :id (vendors/get-all)))) reminders (map ->graphql (reminders/get-graphql (<-graphql args))) reminder-count (reminders/count-graphql (<-graphql args))] (resolve/with-context [{:reminders reminders :total reminder-count :count (count reminders) :start (:start args 0) :end (+ (:start args 0) (count reminders))}] extra-context))) (defn get-vendor-for-invoice [context args value] (->graphql (if-let [vendor-cache (:vendor-cache context)] (vendor-cache (:vendor_id value)) (vendors/get-by-id (:vendor_id value))))) (defn get-check-by-id [context args value] (->graphql (checks/get-by-id (:check_id value)))) (defn get-invoices-checks [context args value] (->graphql (invoices-checks/get-for-invoice-id (:id value)))) (defn get-checks-invoices [context args value] (->graphql (invoices-checks/get-for-check-id (:id value)))) (defn get-company-for-invoice [context args value] (->graphql (if-let [company-cache (:company-cache context)] (company-cache (:company_id value)) (companies/get-by-id (:company_id value))))) (defn bank-account-for-check [context args value] (->graphql (let [company (if-let [company-cache (:company-cache context)] (company-cache (:company_id value)) (companies/get-by-id (:company_id value)))] (first (filter #(= (:id %) (:bank_account_id value)) (:bank-accounts company))) ))) (defn get-user-companies [context args value] (->graphql (if-let [company-cache (:company-cache context)] (map company-cache (:companies value)) (map companies/get-by-id (:companies value))))) (defn get-company [context args value] (->graphql (filter #(can-see-company? (:id context) %) (d-clients/get-all)))) (defn join-companies [users] (let [companies (by :id (companies/get-all))] (map (fn [u] (update u :companies #(map companies %))) users))) (defn get-user [context args value] (assert-admin (:id context)) (let [ users (users/get-all) extra-context (cond-> context (executor/selects-field? context :user/companies) (assoc :company-cache (by :id (companies/get-all))))] (resolve/with-context (->graphql users) extra-context))) (defn get-vendor [context args value] (->graphql (d-vendors/get-graphql args))) (defn print-checks [context args value] (assert-can-see-company (:id context) (:company_id args)) (->graphql (rchecks/print-checks (map (fn [i] {:invoice-id (:invoice_id i) :amount (:amount i)}) (:invoice_payments args)) (:company_id args) (:bank_account_id args) (:type args)))) (def schema (-> integreat-schema (attach-resolvers {:get-invoice-page get-invoice-page :get-all-invoices get-all-invoices :get-all-payments get-all-payments :bank-account-for-check bank-account-for-check :get-payment-page gq-checks/get-payment-page :get-transaction-page gq-transactions/get-transaction-page :get-reminder-page get-reminder-page :get-vendor-for-invoice get-vendor-for-invoice :get-check-for-transaction gq-transactions/get-check-for-transaction :get-company-for-invoice get-company-for-invoice :get-invoices-checks get-invoices-checks :get-checks-invoices get-checks-invoices :get-check-by-id get-check-by-id :get-invoices-expense-accounts gq-invoices/get-invoices-expense-accounts :get-company get-company :get-user get-user :get-user-companies get-user-companies :mutation/add-handwritten-check gq-checks/add-handwritten-check :mutation/print-checks print-checks :mutation/edit-user gq-users/edit-user :mutation/add-invoice gq-invoices/add-invoice :mutation/edit-invoice gq-invoices/edit-invoice :mutation/void-invoice gq-invoices/void-invoice :mutation/void-check gq-checks/void-check :mutation/edit-expense-accounts gq-invoices/edit-expense-accounts :get-vendor get-vendor :get-expense-account expense-accounts/get-expense-account :get-expense-account-parent expense-accounts/get-parent}) schema/compile)) (defn simplify "Converts all ordered maps nested within the map into standard hash maps, and sequences into vectors, which makes for easier constants in the tests, and eliminates ordering problems." [m] (walk/postwalk (fn [node] (cond (instance? IPersistentMap node) (into {} node) (seq? node) (vec node) (keyword? node) (kebab node) :else node)) m)) (defn query ([id q] (query id q nil )) ([id q v] (println "executing graphql query" id q v) (time (simplify (execute schema q v {:id id})))))