(->auth-result auth ctx)
(apply-fn-with-ctx-at-first auth ctx)
auth: auth 혹은 [fn & args] auth가 fn인 경우, (fn ctx)를 반환합니다 auth가 [fn & args] 형태의 시퀀스인 경우, (apply fn ctx args)를 반환합니다. (thread-first와 유사)
(config-filter-opts filters ctx)
(build-resolvers schema)
GraphQL 스키마 맵을 순회하며 :resolve 함수를 찾습니다. :resolve의 함수 Symbol을 활용해서 새로운 map을 :resolvers에 반환합니다. 변경된 스키마 맵(:schema)과 새롭게 생성한 리졸버 맵(:resolvers)은 lacinia의 attach-resolvers 함수에서 사용합니다.
입력 : schema 출력 : {:schema {...} :resolvers {...}}
예)
{:resolve farmmorning.user/get-address
...
:resolve farmmorning.crop/get-name}
->
{:schema {:resolve :farmmorning.user/get-address
:resolve :farmmorning.crop/get-name
:resolvers {:farmmorning.user/get-address farmmorning.user/get-address
:farmmorning.crop/get-name farmmorning.crop/get-name}}
source
relay.dev spec 의 Connection, Node 를 구현하는 lacinia schema, resolver-fn 을 생성합니다.
gosura resolver-config edn 파일을 정의하면 생성합니다. gosura 에서 생성하는 resolver-fn 이 부적합 할 때는 따로 작성하는 것이 더 적절할 수 있습니다.
주의) resolver-config edn 에 사용하는 ns 는 (ns 선언만 있는 빈 파일을 만들고) 그 네임스페이스를 사용하세요.
(find-resolver-fn resolver-key)
resolver-key 에 따라 적절한 resolver-fn 함수를 리턴한다.
보통 ns gosura.helpers.resolver
의 resolver-fn 를 리턴한다.
source
(generate-all resolver-configs)
GraphQL relay spec에 맞는 기본적인 resolver들을 여러개 동시에 생성한다.
hash-map의 vector를 인자를 받으며 execute-one이 vector의 사이즈만큼 실행된다
source
(generate-one resolver-config)
gosura resolver-config edn 을 받아 :target-ns 에 resolver-fn 을 생성(intern)합니다.
gosura resolver-config
edn 예)
{:target-ns ns
:resolvers {:resolve-connection {:node-type node-type
:db-key db-key
:table-fetcher superfetcher
:pre-process-arguments pre-process-arguments
:post-process-row post-process-row}
:resolve-by-fk {:settings {:auth my-auth-fn}
:node-type node-type
:superfetcher superfetcher/->Fetch
:post-process-row post-process-row
:fk-in-parent :fk-in-parent}
:resolve-connection-by-example-id {:settings {:auth [my-auth-fn-2 #{my-role}]}
:node-type node-type
:superfetcher superfetcher/->FetchByExampleId
:post-process-row post-process-row}}}
(read-config path)
(add-page-options sql {:keys [order-direction order-by limit offset]} table-name-alias)
(add-page-options sql page-options)
(batch-args-filter-pred batch-args)
(batch-args-filter-pred batch-args table-name-alias)
WHERE (key1, key2) IN ((value1-1, value1-2), (value2-1, value2-2) ...) 꼴의 HoneySQL 식을 반환합니다.
* 입력: key가 모두 같고 value만 다른 map의 모음을 받습니다.
예) [{:country-code "JP", :id "1204"}
{:country-code "JP", :id "1205"}
{:country-code "KR", :id "1206"}]
* 출력: [:in (:composite :country-code :id)
((:composite "JP" "1204") (:composite "JP" "1205") (:composite "KR" "1206"))]
=> ["WHERE (country_code, id) IN ((?, ?), (?, ?), (?, ?))" "JP" "1204" "JP" "1205" "KR" "1206"]
(col-with-table-name table-name col-name)
column에 table 이름을 명시한다
input: table-name :g, col-name :col1
output: :g.col1
source
(col-with-table-name? col-name)
(cursor-filter-pred
{:as _page-options, :keys [order-by order-direction cursor-id cursor-ordered-value]}
table-name-alias)
(cursor-filter-pred page-options)
(delete! db table-name where-params)
(execute! ds qs)
(execute! ds qs opts)
(execute-one! ds qs)
(execute-one! ds qs opts)
(fetch! ds qs)
(fetch! ds qs opts)
(fetch-one! ds qs)
(fetch-one! ds qs opts)
(insert db table-name cols data)
(insert db table-name cols data opts)
db
table-name: a table name; keyword; ex. :table-name
data: [{:col1 1 :col2 2 :col3 3}, {:col1 4 :col2 5 :col3 6}, ...]
cols: a vector of keywords. ex) [:col1 :col2]
source
(insert-one db table-name cols data)
(insert-one db table-name cols data opts)
db
table-name: a table name; keyword; ex. :table-name
data: {:col1 1 :col2 2 :col3 3}
cols: a vector of keywords. ex) [:col1 :col2]
source
(join-for-filter-options join-rules filter-options)
지정한 규칙과 필터링 옵션에 따라 조인 조건들을 선택한다.
인자
- join-rules - 조인 규칙들의 시퀀스. 각 규칙은 다음 값을 갖는 해시맵이다.
- :join - 허니 SQL 조인 식
- :for - 조인 조건을 filter-options에서 검사할 키들의 집합
- filter-options - WHERE 식으로 변환하기 전의 DB 조회 조건들의 해시맵
반환 - 선택된 허니 SQL 조인 식들을 모은 벡터
예 (join-for-filter-options [{:join [:users [:= :transactions.buyer-id :users.id]] :for #{:buyer-name :buyer-address :buyer-phone-number}} {:join [:products [:= :transactions.product-id :products.id]] :for #{:product-name}}] {:buyer-name "박연오"}) => [:users [:= :transactions.buyer-id :users.id]]
(order-by-page-options {:keys [order-by order-direction]})
정렬 기준 값(order-by)과 ID를 정렬 방향(order-direction)에 맞춰 정렬하도록 하는 DSL을 만든다.
-
인자
- page-options
- :order-by 정렬 기준 값 (기본값 :id)
- :order-direction 정렬 방향 (기본값 :asc)
- page-options
-
반환 - HoneySQL DSL의 ORDER BY 절
-
함수 작성시 주의점 order-by 와 :id 의 정렬 방향이 일치해야 한다. where 절에서 (cursor.order_by, cursor.id) < (row.order_by, row.id) 으로 두 값을 묶어 자르기 때문이다.
<잘못된 예> 두 정렬값을 서로 다른 방향으로 설정하는 경우 ORDER BY value ASC, ID DESC [1] 1000, 1 [2] 1001, 4 ; 페이지 2 커서 [3] 1001, 3 [4] 1001, 2 [5] 1002, 5
페이지 1 WHERE (-Inf, -Inf) < (row.value, row.id) ; => [1], [2], [3], [4], [5] ORDER BY value ASC, id DESC LIMIT 2 ; => [1], [2]
페이지 2 WHERE (1001, 4) < (row.value, row.id) ; => [5] ORDER BY value ASC, id DESC LIMIT 2 ; => [5] ([3], [4] 누락)
<올바른 예> ORDER BY value ASC, ID DESC [1] 1000, 1 [2] 1001, 2 ; 페이지 2 커서 [3] 1001, 3 [4] 1001, 4 [5] 1002, 5
페이지 1 WHERE (-Inf, -Inf) < (row.value, row.id) ; => [1], [2], [3], [4], [5] ORDER BY value ASC, id ASC LIMIT 2 ; => [1], [2]
페이지 2 WHERE (1001, 2) < (row.value, row.id) ; => [3], [4], [5] ORDER BY value ASC, id DESC LIMIT 2 ; => [3], [4]
waiting to complete query state, unit time: seconds
source
(remove-empty-options options)
인자
- options - WHERE 식으로 변환하기 전의 DB 조회 조건들의 해시맵
예)
(remove-empty-options {:a 1, :b [1 2 3], :c [], :d {}, :e nil, :f #{1 2}})
=> {:a 1, :b [1 2 3], :f #{1 2}}
source
(unqualified-kebab-fetch! ds qs)
(unqualified-kebab-fetch-one! ds qs)
(update! db table-name data where-params)
(error resolver-errors)
(error resolved-value resolver-errors)
resolve-as를 사용하여 error를 반환하고 기본적으로 값으로는 nil을 반환하도록 한다
source
(tag-with-subtype {:keys [subtype], :as row} subtype->node-type)
(build-connection order-by page-direction page-size cursor-id nodes)
order-by: 정렬 기준
page-direction: forward or backward 페이지네이션 방향
page-size: edges의 데이터 개수
cursor-id: 현재 위치한 cursor의 id (db id)
nodes: 노드의 시퀀스. 각 노드들 속에는, :db-id에 db의 row id가 들어 있고, :id에 릴레이 노드 id가 들어 있어야 함
(즉, build-node에 의해 만들어진 노드 맵이어야 함)
source
(build-filter-options arguments additional-filter-opts)
relay connection 조회에 필요한 filter options를 빌드합니다
source
(build-node row node-type)
(build-node row node-type post-process-row)
db fetcher가 반환한 행 하나를 graphql node 형식에 맞도록 가공합니다
source
(build-page-options
{:keys [first last after before order-by order-direction], :or {order-by :id, order-direction :ASC}, :as args})
relay connection 조회에 필요한 page options를 빌드합니다.
(default) 10개의 데이터를 id기준으로 정방향으로 오름차순으로 가지고 옵니다
source
(decode-arguments encoded-arguments)
(decode-cursor cursor)
(decode-global-id->db-id global-id)
(decode-global-ids-by-keys arguments ks)
arguments 맵에서 ks의 키값 값을 재귀적으로 찾아 DB ID로 디코드합니다.
source
(decode-id id)
Relay 노드 ID를 node-type과 db-id의 맵으로 디코드합니다.
"bm90aWNlLTI=" -> {:node-type "notice", :db-id "2"}
주의: decode된 db id는 string임.
source
(encode-arguments arguments)
arguments 인코드, 왜 쓰는가? 통계 쿼리에는 객체의 고유성이라는 게 없다.
하지만 릴레이 클라이언트에서 이전에 요청한 질의에 따라 글로벌 ID를 이용한 갱신이 필요하다.
그래서 객체 ID가 아닌 필터링 인자를 기준으로 글로벌 ID를 도출하여 사용하고 있다.
source
(encode-cursor {:keys [id ordered-values]})
(encode-id node-type db-id)
Node 레코드를 입력받아 Base64 문자열의 Relay 노드 Id로 인코딩합니다. (:notice, 2) -> "bm90aWNlLTI="
(encode-node-id {:keys [node-type db-id], :as node})
DB ID를 노드 ID로 변경한 맵을 반환합니다.
source
(extend-relay-types schema)
lacinia schema에서 정의한 objects들에게
Graphql relay spec에 맞는 edges, connections를 추가해줍니다.
source
(node->cursor order-by node)
order-by: 정렬할 기준 컬럼. 타입: 키워드
node: 커서로 변환할 노드
source
(node->edge order-by node)
Relay가 사용하는 node(id: ID!) 쿼리를 다형적으로 처리하기 위한 defmulti 입니다.
Node 인터페이스를 구현하는 각 타입에서 defmethod를 구현하면 됩니다.
source
resolver-fn 의 모음.
resolve-connection resolve-connection-by-fk resolve-connection-by-pk-list resolve-by-fk resolve-by-parent-pk ...
를 resolver-fn 이라 부르자, 약속해봅니다.
(common-pre-process-arguments arguments)
인자 맵에 일반적인 전처리를 한다.
source
(decode-global-id-in-arguments arguments)
인자 맵의 ID들(열이 id, 그리고 -id로 끝나는 것들)의 값을 글로벌 ID에서 ID로 디코드한다.
source
(decode-global-ids-in-arguments arguments)
인자 맵의 ID들(열이 ids, 그리고 -ids로 끝나는 것들)의 값을 글로벌 ID에서 ID로 디코드한다.
source
(decode-value-in-args args suffix f)
suffix를 가진 key값들을 찾아서 value가 nil이 아닌 경우 value를 디코딩한다
source
(defnode node-type & fdecl)
(defresolver name & fdecl)
Macro.
lacinia 용 resolver 함수를 만듭니다.
입력 name - 함수 이름 doc-string? - 문서 option? - 설정 args - 매개변수 [ctx arg parent] body - 함수 바디
TODO: defn과 같이 attr-map을 받는 기능 추가
가능한 설정
:auth - 인증함수를 넣습니다. gosura.auth의 설명을 참고해주세요.
:kebab-case? - arg 의 key를 kebab-case로 변환할지 설정합니다. (기본값 true)
:node-type - relay resolver 일때 설정하면, edge/node와 :pageInfo의 start/endCursor 처리를 같이 해줍니다.
:required-keys-in-parent - 부모(hash-map)로부터 필요한 required keys를 설정합니다.
source
(keys-not-found parent required-keys-in-parent)
(pack-mutation-result db db-fetcher filter-options {:keys [node-type post-process-row]})
Lacinia 변환 리졸버 응답용 변환 내역을 꾸며 반환한다.
source
(parse-fdecl fdecl)
함수의 이름 뒤에 오는 선언부를 파싱합니다. doc-string와 option이 있을 수도 있고, 없을 수도 있기 때문에 그를 적절히 파싱해 줍니다. (fdecl이라는 이름은 core의 defn 구현에서 쓰이는 이름을 따왔습니다)
(resolve-by-fk
context
_arguments
parent
{:keys [db-key node-type superfetcher post-process-row fk-in-parent additional-filter-opts]})
Lacinia 리졸버로서 config 설정에 따라 단건 조회 쿼리를 처리한다.
- context 리졸버 실행 문맥
- arguments 쿼리 입력
- parent 부모 노드
- config 리졸버 동작 설정
- :db-key 사용할 DB 이름
- :superfetcher 슈퍼페처
- :post-process-row 결과 객체 목록 후처리 함수 (예: identity)
- :fk-in-parent 부모 엔티티로부터 이 엔티티의 ID를 가리키는 FK 칼럼 이름 (예: :gl-crop-id)
- 객체 하나
(resolve-by-parent-pk
context
_arguments
parent
{:keys [db-key node-type superfetcher post-process-row additional-filter-opts]})
parent 객체의 primary key (보통 id)와 child의 foreign key 기반으로 child 하나를 resolve한다. parent:child가 1:0..1일 것을 가정함. DB 제약으로 child가 n개 붙는 것을 막을 수는 없지만, GraphQL에서 Parent->Optional 관계를 지원하기 위한 용도임
- context 리졸버 실행 문맥
- arguments 쿼리 입력 (지원 안함)
- parent 부모 노드
- config 리졸버 동작 설정
- :db-key 사용할 DB 이름
- :superfetcher 슈퍼페처
- :post-process-row 결과 객체 목록 후처리 함수 (예: identity)
- 객체 목록
(resolve-connection
context
arguments
_parent
{:keys [node-type db-key table-fetcher pre-process-arguments post-process-row additional-filter-opts]})
Lacinia 리졸버로서 config 설정에 따라 목록 조회 쿼리를 처리한다.
- context 리졸버 실행 문맥
- arguments 쿼리 입력
- parent 부모 노드
- config 리졸버 동작 설정
- :node-type
- :db-key 조회할 데이터베이스 이름 (예: :db, :farmmorning-db)
- :table-fetcher 테이블 조회 함수 (예: bulk-sale/fetch-crops)
- :pre-process-arguments
- :post-process-row 결과 객체 목록 후처리 함수 (예: identity)
- :additional-filter-options 추가로 필요한 필터 옵션을 제공 받는다 예) jwt 토큰이 담고 있는 id가 가리키는 실제적 이름 등 (예: user-id(대부분), buyer-id)
- Connection
(resolve-connection-by-fk
context
arguments
parent
{:keys [db-key node-type superfetcher post-process-row additional-filter-opts]})
Lacinia 리졸버로서 config 설정에 따라 목록 조회 쿼리를 처리한다.
- context 리졸버 실행 문맥
- arguments 쿼리 입력
- parent 부모 노드
- config 리졸버 동작 설정
- :superfetcher 슈퍼페처
- :post-process-row 결과 객체 목록 후처리 함수 (예: identity)
- 객체 목록
(resolve-connection-by-pk-list
context
arguments
parent
{:keys [db-key node-type table-fetcher pk-list-name post-process-row pk-list-name-in-parent additional-filter-opts]})
Primary Key를 기준으로 데이터를 조회하며 Connection Spec에 맡게 반환한다 Lacinia 리졸버로서 config 설정에 따라 목록 조회 쿼리를 처리한다.
- context 리졸버 실행 문맥
- arguments 쿼리 입력
- parent 부모 노드
- config 리졸버 동작 설정
- :pk-list-name pk-list로 데이터를 조회할 때 이름
- :pk-list-name-in-parent pk-list로 데이터를 조회할 때 부모로부터 전달 받는 이름
- :post-process-row 결과 객체 목록 후처리 함수 (예: identity)
- 객체 목록
(resolve-create-one
ctx
args
_parent
{:keys
[node-type
db-key
table-fetcher
mutation-fn
mutation-tag
additional-filter-opts
pre-process-arguments
post-process-row]})
(resolve-delete-one ctx args _parent {:keys [db-key mutation-fn additional-filter-opts]})
(resolve-one
context
arguments
_parent
{:keys [node-type db-key fetch-one pre-process-arguments post-process-row additional-filter-opts]})
(resolve-update-multi
ctx
args
_parent
{:keys
[node-type
db-key
table-fetcher
mutation-fn
mutation-tag
additional-filter-opts
pre-process-arguments
post-process-row]})
(resolve-update-one
ctx
args
_parent
{:keys
[node-type
db-key
table-fetcher
mutation-fn
mutation-tag
additional-filter-opts
pre-process-arguments
post-process-row]})
(wrap-resolver-body {:keys [this ctx arg parent]} option args body)
Macro.
GraphQL 리졸버가 공통으로 해야 할 auth 처리, case 변환 처리를 resolver body의 앞뒤에서 해 주도록 wrapping합니다.
this, ctx, arg, parent: 상위 리졸버 생성 매크로에서 만든 심벌
option: 리졸버 선언에 지정된 옵션 맵
args: 리졸버 선언의 argument vector 부분
body: 리졸버의 body expression 부분
source
gosura.helpers.resolver의 v2입니다.
(defresolver name & fdecl)
Macro.
lacinia 용 resolver 함수를 만듭니다.
입력 name - 함수 이름 doc-string? - 문서 option? - 설정 args - 매개변수 [ctx arg parent] body - 함수 바디
TODO: defn과 같이 attr-map을 받는 기능 추가
가능한 설정
:auth - 인증함수를 넣습니다. gosura.auth의 설명을 참고해주세요.
:kebab-case? - arg 의 key를 kebab-case로 변환할지 설정합니다. (기본값 true)
:required-keys-in-parent - 부모(hash-map)로부터 필요한 required keys를 설정합니다.
:filters - 특정 필터 로직을 넣습니다
source
(wrap-resolver-body {:keys [this ctx arg parent]} option args body)
Macro.
GraphQL 리졸버가 공통으로 해야 할 auth 처리, case 변환 처리를 resolver body의 앞뒤에서 해 주도록 wrapping합니다.
this, ctx, arg, parent: 상위 리졸버 생성 매크로에서 만든 심벌 option: 리졸버 선언에 지정된 옵션 맵
- :auth - 인증함수를 넣습니다. gosura.auth의 설명을 참고해주세요.
- :kebab-case? - arg 의 key를 kebab-case로 변환할지 설정합니다. (기본값 true)
- :required-keys-in-parent - 부모(hash-map)로부터 필요한 required keys를 설정합니다.
- :decode-ids-by-keys - 키 목록을 받아서 resolver args의 global id들을 db id로 변환 해줍니다.
- :filters - args에 추가할 key-value 값을 필터로 넣습니다.
(->delete-response id)
mutation: delete 시의 response
lacinia schema의 :DeleteSuccess object 를 따른다
source
(->mutation-response node tag)
mutation: create/update (생성/수정) 시의 response
lacinia schema의 :MutationPayload interface를 따른다
mutation response인 {:result {:type :Node}}로 변환한다
source
(error-response error-code message)
error-code
- :INVALID_REQUEST
- :NOT_EXIST
- :NOT_ALLOWED
- :NOT_AVAILABLE
- :NOT_AUTHENTICATED
- :UNKNOWN_ERROR
(server-error profile error)
(server-error profile error message)
profile: 환경
가능한 profile: dev/staging/prod
sentry 메시지도 함께 보낸다
source
(->lacinia-promise sl-result)
(superfetch many env {:keys [db-key table-fetcher id-column filter-key]})
- id-column superfetch 결과의 매핑 단위가 되는 (parent 하나에 대응되는) 컬럼 이름
- filter-key table-fetcher에서 in 조건을 걸어줄 컬럼 이름
source
(superfetch-v2 many env {:keys [db-key table-fetcher id-in-parent]})
superfetcher로부터 N개의 쿼리 전달받아 벌크로 fetch 합니다. fetch 할 때는 인자로 받은 table-fetcher 함수를 이용합니다.
table-fetcher에는 filter-options
가 전달되는데,
해당 맵 안에 batch-args
에는 가공되지 않은 전체 argument가 들어있습니다.
* many - id와 argument를 가진 superfetcher.Fetch 목록
예: (#farmmorning.api_global.country_region.superfetcher.Fetch
{:id 1501533529, :arguments {:country-code "JP", :id "1204", :page-options nil}} ...)
* env - {:db #object[com.zaxxer.hikari.HikariDataSource] ...}
(superfetcher name params)
Macro.
superfetcher 기본 form을 쉽게 제공한다 defrecord를 생성하므로 name은 PascalCase로 작성하는 걸 원칙으로 한다 호출할 때에는 ->name 으로 사용한다 생성예시) (superfetcher FetchByExampleId {...}) 사용예시) ->FetchByExampleId
(superfetcher-v2 name params)
Macro.
superfetcher 기본 form을 쉽게 제공한다
defrecord를 생성하므로 name은 PascalCase로 작성하는 걸 원칙으로 한다
호출할 때에는 ->name 으로 사용한다
생성예시) (superfetcher FetchByExampleId {...})
사용예시) ->FetchByExampleId
source
(with-superlifter ctx body)
resolvers 설정에 필요한 스키마 정의
source
(keyword-vals->string-vals hash-map)
hash-map value 값에 keyword가 있으면 String으로 변환해준다
사용예시) enum 값 때문에 keyword가 들어올 일이 있음 한계: 1 depth 까지만 적용
TODO) 조금 더 발전 시켜서 defresolver나 resolve-xxx 에서 무조건 이 로직을 타도록 하는 것도 좋을 듯
source
(requiring-var! sym|var)
qualified-symbol을 var로 변환합니다
source
(send-sentry-server-event event)
(stringify-ids row)
행의 ID들(열이 id, 그리고 -id로 끝나는 것들)을 문자열로 변경한다.
source
(transform-keys->camelCaseKeyword form)
재귀적으로 form 안에 포함된 모든 key를 camelCase keyword로 변환한다
source
(transform-keys->kebab-case-keyword form)
재귀적으로 form 안에 포함된 모든 key를 camelCase keyword로 변환한다
source
(update-resolver-result resolver-result update-fn)
lacinia resolver의 리턴 값에서, 에러를 제외한 데이터 부분에 대해서 함수를 적용시켜 업데이트해 줍니다.
resolver-result
의 타입은 순수한 map일수도, WrappedValue일수도, ResolverResultImpl일 수도 있습니다.
각 타입별로 resolver result 안에 데이터가 들어가 있는 위치가 다르므로, 그에 맞게 적절한 위치를 찾아서 update-fn 함수를 적용해 줍니다.
(update-vals-having-keys m ks f)
m: map(데이터)
ks: 특정 키값
f: val 업데이트 함수
source