diff --git a/src/fluree/db/db/json_ld.cljc b/src/fluree/db/db/json_ld.cljc index 32968a79e..bf1040fa0 100644 --- a/src/fluree/db/db/json_ld.cljc +++ b/src/fluree/db/db/json_ld.cljc @@ -247,6 +247,7 @@ (-iri [this subject-id compact-fn] (iri this subject-id compact-fn)) (-search [this fparts] (query-range/search this fparts)) (-query [this query-map] (fql/query this query-map)) + (-query [this query-map opts] (fql/query this query-map opts)) (-stage [db json-ld] (jld-transact/stage db json-ld nil)) (-stage [db json-ld opts] (jld-transact/stage db json-ld opts)) (-index-update [db commit-index] (index-update db commit-index)) diff --git a/src/fluree/db/json_ld/api.cljc b/src/fluree/db/json_ld/api.cljc index 012aca643..fe1ee626c 100644 --- a/src/fluree/db/json_ld/api.cljc +++ b/src/fluree/db/json_ld/api.cljc @@ -51,7 +51,7 @@ - defaults: - did - (optional) DiD information to use, if storing blocks as verifiable credentials, or issuing queries against a permissioned database. - - context - (optional) Default @context map to use for ledgers formed with this connection." + - @context - (optional) Default @context map to use for ledgers formed with this connection." [{:strs [method parallelism] :as opts}] ;; TODO - do some validation (promise-wrap diff --git a/src/fluree/db/json_ld/transact.cljc b/src/fluree/db/json_ld/transact.cljc index 50e5f4b21..66ece16a7 100644 --- a/src/fluree/db/json_ld/transact.cljc +++ b/src/fluree/db/json_ld/transact.cljc @@ -32,32 +32,30 @@ (def registry (merge - (m/base-schemas) - (m/type-schemas) - v/registry - {::iri ::v/iri - ::val ::v/val - ::context ::v/context - ::txn-val [:orn - [:multiple [:sequential - [:or [:ref ::txn-map] [:ref ::txn-val]]]] - [:node [:ref ::txn-map]] - [:iri ::iri] - [:val ::val]] - ::txn-leaf-map [:map - ["@context" {:optional true} ::context] - [::m/default [:map-of ::iri ::txn-val]]] - ::retract-key [:and ::iri [:re retract-key-re]] - ::txn-map [:orn - [:assert ::txn-leaf-map] - [:retract - [:map - ["@context" {:optional true} ::context] - [::m/default [:map-of ::retract-key ::txn-leaf-map]]]]] - ::txn [:orn - [:single-amp ::txn-map] - [:sequence-of-maps [:sequential ::txn-map]]] - ::opts [map?]})) + (m/base-schemas) + (m/type-schemas) + v/registry + {::iri ::v/iri + ::val ::v/val + ::context ::v/context + ::txn-val [:orn + [:multiple [:sequential + [:or [:ref ::txn-map] [:ref ::txn-val]]]] + [:node [:ref ::txn-map]] + [:iri ::iri] + [:val ::val]] + ::txn-leaf-map [:map-of ::iri ::txn-val] + ::retract-key [:and ::iri [:re retract-key-re]] + ::txn-map [:orn + [:assert ::txn-leaf-map] + [:retract [:map-of ::retract-key ::txn-leaf-map]]] + ::txn [:orn + [:single-map ::txn-map] + [:sequence-of-maps [:sequential ::txn-map]]] + ::opts [map?]})) + +(def coerce-txn + (m/coercer ::txn v/fluree-transformer {:registry registry})) (declare json-ld-node->flakes) @@ -67,22 +65,22 @@ flakes if they didn't already exist." [class-iris {:keys [t next-pid ^clojure.lang.Volatile iris db-before] :as _tx-state}] (go-try - (loop [[class-iri & r] (util/sequential class-iris) - class-sids #{} - class-flakes #{}] - (if class-iri - (if-let [existing (> ( v-map :idx last)}) - flakes (cond - ;; a new node's data is contained, process as another node then link to this one - (jld-reify/node? v-map) - (let [[node-sid node-flakes] (flakes v-map tx-state pid))] - (conj node-flakes (flake/create sid pid node-sid const/$xsd:anyURI t true m))) - - ;; a literal value - (and (some? value) (not= shacl-dt const/$xsd:anyURI)) - (let [[value* dt] (datatype/from-expanded v-map shacl-dt)] - (when validate-fn - (or (validate-fn value*) - (throw (ex-info (str "Value did not pass SHACL validation: " value) - {:status 400 :error :db/shacl-validation})))) - [(flake/create sid pid value* dt t true m)]) - - :else - (throw (ex-info (str "JSON-LD value must be a node or a value, instead found ambiguous value: " v-map) - {:status 400 :error :db/invalid-transaction})))] - (into flakes retractions)))) + (let [retractions (when check-retracts? ;; don't need to check if generated pid during this transaction + (->> ( v-map :idx last)}) + flakes (cond + ;; a new node's data is contained, process as another node then link to this one + (jld-reify/node? v-map) + (let [[node-sid node-flakes] (flakes v-map tx-state pid))] + (conj node-flakes (flake/create sid pid node-sid const/$xsd:anyURI t true m))) + + ;; a literal value + (and (some? value) (not= shacl-dt const/$xsd:anyURI)) + (let [[value* dt] (datatype/from-expanded v-map shacl-dt)] + (when validate-fn + (or (validate-fn value*) + (throw (ex-info (str "Value did not pass SHACL validation: " value) + {:status 400 :error :db/shacl-validation})))) + [(flake/create sid pid value* dt t true m)]) + + :else + (throw (ex-info (str "JSON-LD value must be a node or a value, instead found ambiguous value: " v-map) + {:status 400 :error :db/invalid-transaction})))] + (into flakes retractions)))) (defn list-value? "returns true if json-ld value is a list object." @@ -133,11 +131,11 @@ new-types are a set of newly created types in the transaction." [db sid added-classes] (go-try - (let [type-sids (->> (> ( (db-after staged-map tx-state) - vocab-flakes vocab/refresh-schema - vocab-flakes (db-after staged-map tx-state) + vocab-flakes vocab/refresh-schema + vocab-flakes json-ld - syntax/validate-query - syntax/encode-internal-query - (q-parse/parse-delete db)) - - [s p o] delete - parsed-query (assoc parsed-query :delete [s p o]) - error-ch (async/chan) - flake-ch (async/chan) - where-ch (where/search db parsed-query error-ch)] - (async/pipeline-async 1 - flake-ch - (fn [solution ch] - (let [s* (if (::where/val s) - s - (get solution (::where/var s))) - p* (if (::where/val p) - p - (get solution (::where/var p))) - o* (if (::where/val o) - o - (get solution (::where/var o)))] - (async/pipe - (where/resolve-flake-range db error-ch [s* p* o*]) - ch))) - where-ch) - (let [delete-ch (async/transduce (comp cat - (map (fn [f] - (flake/flip-flake f t)))) - (completing conj) - (flake/sorted-set-by flake/cmp-flakes-spot) - flake-ch) - flakes (async/alt! - error-ch ([e] - (throw e)) - delete-ch ([flakes] - flakes))] - flakes)))) + (let [{:keys [delete] :as parsed-query} + (-> json-ld + syntax/validate-query + syntax/encode-internal-query + (q-parse/parse-delete db)) + + [s p o] delete + parsed-query (assoc parsed-query :delete [s p o]) + error-ch (async/chan) + flake-ch (async/chan) + where-ch (where/search db parsed-query error-ch)] + (async/pipeline-async 1 + flake-ch + (fn [solution ch] + (let [s* (if (::where/val s) + s + (get solution (::where/var s))) + p* (if (::where/val p) + p + (get solution (::where/var p))) + o* (if (::where/val o) + o + (get solution (::where/var o)))] + (async/pipe + (where/resolve-flake-range db error-ch [s* p* o*]) + ch))) + where-ch) + (let [delete-ch (async/transduce (comp cat + (map (fn [f] + (flake/flip-flake f t)))) + (completing conj) + (flake/sorted-set-by flake/cmp-flakes-spot) + flake-ch) + flakes (async/alt! + error-ch ([e] + (throw e)) + delete-ch ([flakes] + flakes))] + flakes)))) (defn flakes->final-db "Takes final set of proposed staged flakes and turns them into a new db value along with performing any final validation and policy enforcement." [tx-state flakes] (go-try - (-> flakes - (final-db tx-state) - flakes + (final-db tx-state) + tx-state db* (assoc opts :issuer issuer)) - flakes (if (and (contains? tx "delete") - (contains? tx "where")) - (final-db tx-state flakes))))) + (let [{tx :subject issuer :issuer} (or (tx-state db* (assoc opts :issuer issuer)) + flakes (if (and (contains? tx "delete") + (contains? tx "where")) + (final-db tx-state flakes))))) diff --git a/src/fluree/db/query/fql/syntax.cljc b/src/fluree/db/query/fql/syntax.cljc index 7207666fd..b9db9fb89 100644 --- a/src/fluree/db/query/fql/syntax.cljc +++ b/src/fluree/db/query/fql/syntax.cljc @@ -1,8 +1,10 @@ (ns fluree.db.query.fql.syntax (:require [clojure.string :as str] + [clojure.walk :as walk] [fluree.db.util.core :as util :refer [pred-ident?]] [fluree.db.util.log :as log] [fluree.db.constants :as const] + [fluree.json-ld :as json-ld] [malli.core :as m] [fluree.db.util.validation :as v] [malli.transform :as mt])) @@ -63,14 +65,38 @@ (defn encode-query-key [k] (if (string? k) - (do - (log/debug "encoding query key:" k) - (let [kebab-k (->kebab-case k)] - (if (str/starts-with? kebab-k "@") - (-> kebab-k (subs 1) keyword) - (keyword kebab-k)))) + (let [kebab-k (->kebab-case k)] + (if (str/starts-with? kebab-k "@") + (-> kebab-k (subs 1) keyword) + (keyword kebab-k))) k)) +(defn decode-query + [q] + (log/debug "decoding query:" q) + ;; TODO: Replace this with a :map-with schema once this lands upstream: + ;; https://github.com/metosin/malli/issues/881 + (walk/postwalk + (fn [v] + (cond + (qualified-keyword? v) (str (namespace v) ":" (name v)) + (= v :context) "@context" + (keyword? v) (name v) + :else v)) + q)) + +(defn analytical-query-results-transformer + [context] + ;; TODO: Replace this with a :map-with schema once this lands upstream: + ;; https://github.com/metosin/malli/issues/881 + (let [kw-context (util/keywordize-keys context)] + (mt/transformer + {:encoders + {:string (fn [s] + (if-let [expanded (json-ld/expand-iri s context)] + (json-ld/compact expanded kw-context) + s))}}))) + (def registry (merge (m/predicate-schemas) @@ -199,7 +225,7 @@ ["where" ::where] ["values" {:optional true} ::values]] ::context ::v/context - ::analytical-query [:map + ::analytical-query [:map {:decode/fluree decode-query} ["where" ::where] ["t" {:optional true} ::t] ["@context" {:optional true} ::context] @@ -247,6 +273,15 @@ (m/encoder ::query {:registry registry} (mt/key-transformer {:encode encode-query-key}))) +(def coerce-analytical-query + (m/coercer ::analytical-query v/fluree-transformer {:registry registry})) + +(defn analytical-query-results-encoder + [context] + (m/encoder ::analytical-query-results + {:registry registry} + (analytical-query-results-transformer context))) + (def valid-query? (m/validator ::query {:registry registry})) diff --git a/src/fluree/db/util/validation.cljc b/src/fluree/db/util/validation.cljc index e99b827c2..6a471c7aa 100644 --- a/src/fluree/db/util/validation.cljc +++ b/src/fluree/db/util/validation.cljc @@ -1,18 +1,64 @@ (ns fluree.db.util.validation - (:require [malli.core :as m])) + (:require [malli.core :as m] + [malli.transform :as mt])) (def value? (complement coll?)) +(defn decode-iri + [v] + (cond + (qualified-keyword? v) (str (namespace v) ":" (name v)) + (keyword? v) (name v) + :else v)) + (def registry (merge - (m/base-schemas) - (m/type-schemas) - (m/comparator-schemas) - (m/predicate-schemas) - {::iri :string - ::val [:fn value?] - ::context [:orn - [:sequence [:sequential [:orn - [:string :string] - [:map map?]]]] - [:map map?]]})) + (m/base-schemas) + (m/type-schemas) + (m/comparator-schemas) + (m/predicate-schemas) + {::iri [:string {:decode/fluree decode-iri}] + ::val [:fn value?] + ::string-key [:string {:decode/fluree name + :encode/fluree keyword}] + ::context-map [:map-of ::iri ::iri] + ::context [:orn + [:sequence [:sequential [:orn + [:string :string] + [:map ::context-map]]]] + [:map ::context-map]] + ::context-key [:= {:decode/fluree #(if (= % :context) + "@context" %)} + "@context"] + ::did [:orn + [:id :string] + [:map [:and + [:map-of ::string-key :any] + [:map + ["id" :string] + ["public" :string] + ["private" :string]]]]] + ::connect-defaults [:map + [:did {:optional true} ::did] + [::m/default [:map-of {:max 1} ::context-key ::context]]] + ::connect-opts [:and + [:map-of ::string-key :any] + [:map + ["method" {:decode/fluree name} :string] + ["defaults" {:optional true} ::connect-defaults]]] + ::create-opts [:maybe + [:and + [:map-of ::string-key :any] + [:map + ["defaults" {:optional true} + [:map-of {:max 1} ::context-key ::context]]]]] + ::create-response [:map-of ::string-key :any]})) + +(def fluree-transformer + (mt/transformer {:name :fluree})) + +(def coerce-connect-opts + (m/coercer ::connect-opts fluree-transformer {:registry registry})) + +(def coerce-create-opts + (m/coercer ::create-opts fluree-transformer {:registry registry})) diff --git a/src/fluree/sdk/clojure.clj b/src/fluree/sdk/clojure.clj new file mode 100644 index 000000000..85573d516 --- /dev/null +++ b/src/fluree/sdk/clojure.clj @@ -0,0 +1,102 @@ +(ns fluree.sdk.clojure + (:require [fluree.db.json-ld.api :as api] + [fluree.db.json-ld.transact :as ftx] + [fluree.db.util.log :as log] + [fluree.db.util.validation :as v] + [fluree.db.query.fql.syntax :as fql] + [fluree.db.query.fql.parse :as fqp] + [fluree.json-ld :as json-ld]) + (:refer-clojure :exclude [load])) + +(defn connect + "Forms connection to ledger, enabling automatic pulls of new updates, event + services, index service. + + Multiple connections to same endpoint will share underlying network connection. + + Options include: + - :defaults - (optional) with any of the following values: + - :did - (optional) DiD information to use, if storing blocks as verifiable + credentials, or issuing queries against a permissioned database. + - :context - (optional) Default @context map to use for ledgers formed with + this connection." + [opts] + (let [opts* (v/coerce-connect-opts opts)] + (log/debug "connect opts:" opts*) + (api/connect opts*))) + +(defn create + "Creates a new json-ld ledger. A connection (conn) must always be supplied. + + Ledger-alias (optional) is a friendly name that is used for: + - When publishing to a naming service that allows multiple pointers for the + same namespace (e.g. IPNS), this becomes a sub-directory off the namespace. + For multiple directories deep, use '/' for a + e.g. the ledgers movies/popular, books/authors, books/best-sellers could + use the same IPNS id (in this example using IPNS DNSLink): + fluree:ipns://my.dns.com/books/authors + fluree:ipns://my.dns.com/books/best-sellers + fluree:ipns://my.dns.com/movies/top-rated + - When combining multiple ledgers, each ledger becomes an individual named + graph which can be referenced by name. + + Options map (opts) can include: + - :defaults + - :did - DiD information to use, if storing blocks as verifiable credentials + - :context - Default @context map to use for ledgers formed with this connection" + ([conn] (create conn nil nil)) + ([conn ledger-alias] (create conn ledger-alias nil)) + ([conn ledger-alias opts] + (let [opts* (v/coerce-create-opts opts)] + (api/create conn ledger-alias opts*)))) + +(defn load + "Loads an existing ledger by its alias (which will be converted to a + connection-specific address first)." + [conn ledger-alias] + (api/load conn ledger-alias)) + +(defn exists? + "Returns a promise with true if the ledger alias or address exists, false + otherwise." + [conn ledger-alias-or-address] + (api/exists? conn ledger-alias-or-address)) + +(defn stage + "Performs a transaction and queues change if valid (does not commit)" + ([db json-ld] (stage db json-ld nil)) + ([db json-ld opts] + (let [json-ld* (ftx/coerce-txn json-ld)] + (api/stage db json-ld* opts)))) + +(defn commit! + ([ledger db] (commit! ledger db nil)) + ([ledger db opts] (api/commit! ledger db opts))) + +(defn db + "Retrieves latest db, or optionally a db at a moment in time + and/or permissioned to a specific identity." + ([ledger] (db ledger nil)) + ([ledger opts] (api/db ledger opts))) + +(defn query + [db query] + (let [context (json-ld/parse-context (fqp/parse-context query db)) + results-encoder (fql/analytical-query-results-encoder context)] + (future + (->> query + fql/coerce-analytical-query + (api/query db) + deref + (log/debug->>val "pre-encoded query results:") + results-encoder)))) + +(comment + ;; TODO: Finish these + (defn multi-query + [db query] + (api/multi-query db query)) + + (defn history + [ledger query] + (api/history ledger query))) diff --git a/test/fluree/db/json_ld/api_test.cljc b/test/fluree/db/json_ld/api_test.cljc index 248a222db..0e50b041f 100644 --- a/test/fluree/db/json_ld/api_test.cljc +++ b/test/fluree/db/json_ld/api_test.cljc @@ -268,75 +268,75 @@ "ex:friends" [{"id" "ex:john"} {"id" "ex:cam"}]}} (set @(fluree/query loaded-db '{"select" {?s ["*"]} - "where" [[?s "rdf:type" "ex:User"]]}))))))) + "where" [[?s "rdf:type" "ex:User"]]})))))))) - (testing "can load with policies" - (with-tmp-dir storage-path - (let [conn @(fluree/connect - {"method" "file" - "storage-path" storage-path - "defaults" - {"@context" (merge test-utils/default-context - {"ex" "http://example.org/ns/"})}}) - ledger-alias "load-policy-test" - ledger @(fluree/create conn ledger-alias) - db @(fluree/stage - (fluree/db ledger) - [{"id" "ex:alice", - "type" "ex:User", - "schema:name" "Alice" - "schema:ssn" "111-11-1111" - "ex:friend" {"id" "ex:john"}} - {"id" "ex:john", - "schema:name" "John" - "type" "ex:User", - "schema:ssn" "888-88-8888"} - {"id" "did:fluree:123" - "ex:user" {"id" "ex:alice"} - "f:role" {"id" "ex:userRole"}}]) - db+policy @(fluree/stage - db - [{"id" "ex:UserPolicy" - "type" ["f:Policy"] - "f:targetClass" {"id" "ex:User"} - "f:allow" [{"id" "ex:globalViewAllow" - "f:targetRole" {"id" "ex:userRole"} - "f:action" [{"id" "f:view"}]}] - "f:property" - [{"f:path" {"id" "schema:ssn"} - "f:allow" - [{"id" "ex:ssnViewRule" - "f:targetRole" {"id" "ex:userRole"} - "f:action" [{"id" "f:view"}] - "f:equals" {"@list" [{"id" "f:$identity"} - {"id" "ex:user"}]}}]}]}]) - db+policy @(fluree/commit! ledger db+policy) - loaded (test-utils/retry-load conn ledger-alias 100) - loaded-db (fluree/db loaded)] - (is (= (:t db) (:t loaded-db))) - (testing "query returns expected policy" - (is (= [{"id" "ex:UserPolicy", - "rdf:type" ["f:Policy"], - "f:allow" - {"id" "ex:globalViewAllow", - "f:action" {"id" "f:view"}, - "f:targetRole" {"_id" 211106232532995}}, - "f:property" - {"id" "_:f211106232532999", - "f:allow" - {"id" "ex:ssnViewRule", - "f:action" {"id" "f:view"}, - "f:targetRole" {"_id" 211106232532995}, - "f:equals" [{"id" "f:$identity"} {"id" "ex:user"}]}, - "f:path" {"id" "schema:ssn"}}, - "f:targetClass" {"id" "ex:User"}}] - @(fluree/query - loaded-db - '{"select" {?s ["*" - {"rdf:type" ["_id"]} - {"f:allow" ["*" {"f:targetRole" ["_id"]}]} - {"f:property" ["*" {"f:allow" ["*" {"f:targetRole" ["_id"]}]}]}]} - "where" [[?s "rdf:type" "f:Policy"]]})))))))))) + (testing "can load with policies" + (with-tmp-dir storage-path + (let [conn @(fluree/connect + {"method" "file" + "storage-path" storage-path + "defaults" + {"@context" (merge test-utils/default-context + {"ex" "http://example.org/ns/"})}}) + ledger-alias "load-policy-test" + ledger @(fluree/create conn ledger-alias) + db @(fluree/stage + (fluree/db ledger) + [{"id" "ex:alice", + "type" "ex:User", + "schema:name" "Alice" + "schema:ssn" "111-11-1111" + "ex:friend" {"id" "ex:john"}} + {"id" "ex:john", + "schema:name" "John" + "type" "ex:User", + "schema:ssn" "888-88-8888"} + {"id" "did:fluree:123" + "ex:user" {"id" "ex:alice"} + "f:role" {"id" "ex:userRole"}}]) + db+policy @(fluree/stage + db + [{"id" "ex:UserPolicy" + "type" ["f:Policy"] + "f:targetClass" {"id" "ex:User"} + "f:allow" [{"id" "ex:globalViewAllow" + "f:targetRole" {"id" "ex:userRole"} + "f:action" [{"id" "f:view"}]}] + "f:property" + [{"f:path" {"id" "schema:ssn"} + "f:allow" + [{"id" "ex:ssnViewRule" + "f:targetRole" {"id" "ex:userRole"} + "f:action" [{"id" "f:view"}] + "f:equals" {"@list" [{"id" "f:$identity"} + {"id" "ex:user"}]}}]}]}]) + db+policy @(fluree/commit! ledger db+policy) + loaded (test-utils/retry-load conn ledger-alias 100) + loaded-db (fluree/db loaded)] + (is (= (:t db) (:t loaded-db))) + (testing "query returns expected policy" + (is (= [{"id" "ex:UserPolicy", + "rdf:type" ["f:Policy"], + "f:allow" + {"id" "ex:globalViewAllow", + "f:action" {"id" "f:view"}, + "f:targetRole" {"_id" 211106232532995}}, + "f:property" + {"id" "_:f211106232532999", + "f:allow" + {"id" "ex:ssnViewRule", + "f:action" {"id" "f:view"}, + "f:targetRole" {"_id" 211106232532995}, + "f:equals" [{"id" "f:$identity"} {"id" "ex:user"}]}, + "f:path" {"id" "schema:ssn"}}, + "f:targetClass" {"id" "ex:User"}}] + @(fluree/query + loaded-db + '{"select" {?s ["*" + {"rdf:type" ["_id"]} + {"f:allow" ["*" {"f:targetRole" ["_id"]}]} + {"f:property" ["*" {"f:allow" ["*" {"f:targetRole" ["_id"]}]}]}]} + "where" [[?s "rdf:type" "f:Policy"]]}))))))))) #?(:clj (deftest load-from-memory-test @@ -573,20 +573,20 @@ loaded-db (fluree/db loaded)] (is (= (:t db) (:t loaded-db))) (testing "query returns expected policy" - (is (= [{"id" "ex:UserPolicy", - "rdf:type" ["f:Policy"], + (is (= [{"id" "ex:UserPolicy" + "rdf:type" ["f:Policy"] "f:allow" - {"id" "ex:globalViewAllow", - "f:action" {"id" "f:view"}, - "f:targetRole" {"_id" 211106232532995}}, + {"id" "ex:globalViewAllow" + "f:action" {"id" "f:view"} + "f:targetRole" {"_id" 211106232532995}} "f:property" - {"id" "_:f211106232532999", + {"id" "_:f211106232532999" "f:allow" - {"id" "ex:ssnViewRule", - "f:action" {"id" "f:view"}, - "f:targetRole" {"_id" 211106232532995}, - "f:equals" [{"id" "f:$identity"} {"id" "ex:user"}]}, - "f:path" {"id" "schema:ssn"}}, + {"id" "ex:ssnViewRule" + "f:action" {"id" "f:view"} + "f:targetRole" {"_id" 211106232532995} + "f:equals" [{"id" "f:$identity"} {"id" "ex:user"}]} + "f:path" {"id" "schema:ssn"}} "f:targetClass" {"id" "ex:User"}}] @(fluree/query loaded-db '{"select" {?s ["*" diff --git a/test/fluree/sdk/clojure_test.clj b/test/fluree/sdk/clojure_test.clj new file mode 100644 index 000000000..60d861fa0 --- /dev/null +++ b/test/fluree/sdk/clojure_test.clj @@ -0,0 +1,430 @@ +(ns fluree.sdk.clojure-test + (:require [clojure.test :refer [deftest is testing]] + [fluree.db.dbproto :as dbproto] + [fluree.sdk.clojure :as fluree] + [fluree.db.test-utils :as test-utils] + [fluree.db.util.core :as util] + [test-with-files.tools :refer [with-tmp-dir] :as twf])) + +(deftest exists?-test + (testing "returns false before committing data to a ledger" + (let [conn (test-utils/create-conn) + ledger-alias "testledger" + check1 @(fluree/exists? conn ledger-alias) + ledger @(fluree/create conn ledger-alias) + check2 @(fluree/exists? conn ledger-alias) + _ @(fluree/stage (fluree/db ledger) + [{:id :f/me + :type :schema/Person + :schema/fname "Me"}]) + check3 @(fluree/exists? conn ledger-alias)] + (is (every? false? [check1 check2 check3])))) + (testing "returns true after committing data to a ledger" + (let [conn (test-utils/create-conn) + ledger-alias "testledger" + ledger @(fluree/create conn ledger-alias) + db @(fluree/stage (fluree/db ledger) + [{:id :f/me + :type :schema/Person + :schema/fname "Me"}])] + @(fluree/commit! ledger db) + (is (test-utils/retry-exists? conn ledger-alias 100)) + (is (not @(fluree/exists? conn "notaledger")))))) + +(deftest create-test + (testing "string ledger context gets correctly merged with keyword conn context" + (let [conn (test-utils/create-conn) + ledger-alias "testledger" + ledger-context {:ex "http://example.com/" + :foo "http://foobar.com/"} + ledger @(fluree/create conn ledger-alias + {:defaults + {:context ["" ledger-context]}}) + merged-context (merge test-utils/default-context + (util/stringify-keys ledger-context))] + (is (= merged-context (dbproto/-default-context (fluree/db ledger))) + (str "merged context is: " (pr-str merged-context)))))) + +(deftest load-from-file-test + (testing "can load a file ledger with single cardinality predicates" + (with-tmp-dir storage-path + (let [conn @(fluree/connect + {:method :file, :storage-path storage-path + :defaults + {:context test-utils/default-context}}) + ledger-alias "load-from-file-test-single-card" + ledger @(fluree/create conn ledger-alias + {:defaults + {:context + ["" {:ex "http://example.org/ns/"}]}}) + db @(fluree/stage + (fluree/db ledger) + [{:id :ex/brian + :type :ex/User + :schema/name "Brian" + :schema/email "brian@example.org" + :schema/age 50 + :ex/favNums 7} + + {:id :ex/cam + :type :ex/User + :schema/name "Cam" + :schema/email "cam@example.org" + :schema/age 34 + :ex/favNums 5 + :ex/friend :ex/brian}]) + db @(fluree/commit! ledger db) + db @(fluree/stage + db + ;; test a retraction + {:f/retract {:id :ex/brian + :ex/favNums 7}}) + _ @(fluree/commit! ledger db) + ;; TODO: Replace this w/ :syncTo equivalent once we have it + loaded (test-utils/retry-load conn ledger-alias 100) + loaded-db (fluree/db loaded)] + (is (= (:t db) (:t loaded-db))) + (is (= (:context ledger) (:context loaded)))))) + + (testing "can load a file ledger with multi-cardinality predicates" + (with-tmp-dir storage-path + (let [conn @(fluree/connect + {:method :file, :storage-path storage-path + :defaults + {:context test-utils/default-context}}) + ledger-alias "load-from-file-test-multi-card" + ledger @(fluree/create conn ledger-alias) + db @(fluree/stage + (fluree/db ledger) + [{:context {:ex "http://example.org/ns/"} + :id :ex/brian + :type :ex/User + :schema/name "Brian" + :schema/email "brian@example.org" + :schema/age 50 + :ex/favNums 7} + + {:context {:ex "http://example.org/ns/"} + :id :ex/alice + :type :ex/User + :schema/name "Alice" + :schema/email "alice@example.org" + :schema/age 50 + :ex/favNums [42 76 9]} + + {:context {:ex "http://example.org/ns/"} + :id :ex/cam + :type :ex/User + :schema/name "Cam" + :schema/email "cam@example.org" + :schema/age 34 + :ex/favNums [5 10] + :ex/friend [:ex/brian :ex/alice]}]) + db @(fluree/commit! ledger db) + db @(fluree/stage + db + ;; test a multi-cardinality retraction + [{:context {:ex "http://example.org/ns/"} + :f/retract {:id :ex/alice + :ex/favNums [42 76 9]}}]) + _ @(fluree/commit! ledger db) + ;; TODO: Replace this w/ :syncTo equivalent once we have it + loaded (test-utils/retry-load conn ledger-alias 100) + loaded-db (fluree/db loaded)] + (is (= (:t db) (:t loaded-db))) + (is (= (:context ledger) (:context loaded)))))) + + (testing "can load a file ledger with its own context" + (with-tmp-dir storage-path #_{::twf/delete-dir false} + #_(println "storage path:" storage-path) + (let [conn-context {:id "@id", :type "@type" + :xsd "http://www.w3.org/2001/XMLSchema#"} + ledger-context {:ex "http://example.com/" + :schema "http://schema.org/"} + conn @(fluree/connect + {:method :file :storage-path storage-path + :defaults {:context conn-context}}) + ledger-alias "load-from-file-with-context" + ledger @(fluree/create conn ledger-alias + {:defaults {:context + ["" ledger-context]}}) + db @(fluree/stage + (fluree/db ledger) + [{:id :ex/wes + :type :ex/User + :schema/name "Wes" + :schema/email "wes@example.org" + :schema/age 42 + :schema/favNums [1 2 3] + :ex/friend {:id :ex/jake + :type :ex/User + :schema/name "Jake" + :schema/email "jake@example.org"}}]) + db @(fluree/commit! ledger db) + loaded (test-utils/retry-load conn ledger-alias 100) + loaded-db (fluree/db loaded) + merged-ctx (merge (util/stringify-keys conn-context) + (util/stringify-keys ledger-context)) + query {:where '[[?p :schema/email "wes@example.org"]] + :select '{?p [:*]}} + results @(fluree/query loaded-db query) + full-type-url "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"] + (is (= (:t db) (:t loaded-db))) + (is (= merged-ctx (dbproto/-default-context (fluree/db loaded)))) + (is (= [{full-type-url [:ex/User] + :id :ex/wes + :schema/age 42 + :schema/email "wes@example.org" + :schema/favNums [1 2 3] + :schema/name "Wes" + :ex/friend {:id :ex/jake}}] + results))))) + + (testing "query returns the correct results from a loaded ledger" + (with-tmp-dir storage-path + (let [conn-context {:id "@id", :type "@type"} + ledger-context {:ex "http://example.com/" + :schema "http://schema.org/"} + conn @(fluree/connect + {:method :file :storage-path storage-path + :defaults {:context conn-context}}) + ledger-alias "load-from-file-query" + ledger @(fluree/create conn ledger-alias + {:defaults {:context + ["" ledger-context]}}) + db @(fluree/stage + (fluree/db ledger) + [{:id :ex/Andrew + :type :schema/Person + :schema/name "Andrew" + :ex/friend {:id :ex/Jonathan + :type :schema/Person + :schema/name "Jonathan"}}]) + query {:select '{?s [:*]} + :where '[[?s :id :ex/Andrew]]} + res1 @(fluree/query db query) + _ @(fluree/commit! ledger db) + loaded (test-utils/retry-load conn ledger-alias 100) + loaded-db (fluree/db loaded) + res2 @(fluree/query loaded-db query)] + (is (= res1 res2))))) + + (testing "can load a ledger with `list` values" + (with-tmp-dir storage-path + (let [conn @(fluree/connect + {:method :file + :storage-path storage-path + :defaults + {:context (merge (util/keywordize-keys + test-utils/default-context) + {:ex "http://example.org/ns/" + :list "@list"})}}) + ledger-alias "load-lists-test" + ledger @(fluree/create conn ledger-alias) + db @(fluree/stage + (fluree/db ledger) + [{:id :ex/alice + :type :ex/User + :ex/friends {:list [:ex/john :ex/cam]}} + {:id :ex/cam + :type :ex/User + :ex/numList {:list [7 8 9 10]}} + {:id :ex/john + :type :ex/User}]) + db @(fluree/commit! ledger db) + loaded (test-utils/retry-load conn ledger-alias 100) + loaded-db (fluree/db loaded)] + (is (= (:t db) (:t loaded-db))) + (testing "query returns expected `list` values" + (is (= #{{:id :ex/cam + :rdf/type [:ex/User] + :ex/numList [7 8 9 10]} + {:id :ex/john :rdf/type [:ex/User]} + {:id :ex/alice + :rdf/type [:ex/User] + :ex/friends [:ex/john :ex/cam]}} + (set + @(fluree/query loaded-db '{:select {?s [:*]} + :where [[?s :rdf/type :ex/User]]}))))))) + + (testing "can load with policies" + (with-tmp-dir storage-path + (let [conn @(fluree/connect + {:method :file + :storage-path storage-path + :defaults + {:context (merge (util/keywordize-keys + test-utils/default-context) + {:ex "http://example.org/ns/" + :list "@list"})}}) + ledger-alias "load-policy-test" + ledger @(fluree/create conn ledger-alias) + db @(fluree/stage + (fluree/db ledger) + [{:id :ex/alice + :type :ex/User + :schema/name "Alice" + :schema/ssn "111-11-1111" + :ex/friend {:id :ex/john}} + {:id :ex/john + :schema/name "John" + :type :ex/User + :schema/ssn "888-88-8888"} + {:id "did:fluree:123" + :ex/user {:id :ex/alice} + :f/role {:id :ex/userRole}}]) + db+policy @(fluree/stage + db + [{:id :ex/UserPolicy + :type [:f/Policy] + :f/targetClass {:id :ex/User} + :f/allow [{:id :ex/globalViewAllow + :f/targetRole {:id :ex/userRole} + :f/action [{:id :f/view}]}] + :f/property + [{:f/path {:id :schema/ssn} + :f/allow + [{:id :ex/ssnViewRule + :f/targetRole {:id :ex/userRole} + :f/action [{:id :f/view}] + :f/equals {:list [{:id :f/$identity} + {:id :ex/user}]}}]}]}]) + db+policy @(fluree/commit! ledger db+policy) + loaded (test-utils/retry-load conn ledger-alias 100) + loaded-db (fluree/db loaded)] + (is (= (:t db) (:t loaded-db))) + (testing "query returns expected policy" + (is (= [{:id :ex/UserPolicy + :rdf/type [:f/Policy] + :f/allow + {:id :ex/globalViewAllow + :f/action {:id :f/view} + ;; TODO: We can likely make "_id" come back as :_id instead + ;; but need to think through where this should happen + :f/targetRole {"_id" 211106232532995}} + :f/property + {:id "_:f211106232532999" + :f/allow + {:id :ex/ssnViewRule + :f/action {:id :f/view} + :f/targetRole {"_id" 211106232532995} + :f/equals [{:id :f/$identity} {:id :ex/user}]} + :f/path {:id :schema/ssn}}, + :f/targetClass {:id :ex/User}}] + @(fluree/query + loaded-db + '{:select {?s [:* + {:rdf/type ["_id"]} + {:f/allow [:* {:f/targetRole ["_id"]}]} + {:f/property [:* {:f/allow [:* {:f/targetRole ["_id"]}]}]}]} + :where [[?s :rdf/type :f/Policy]]})))))))) + + (testing "can load a ledger with `list` values" + (with-tmp-dir storage-path + (let [conn @(fluree/connect + {:method :file + :storage-path storage-path + :defaults + {:context (merge (util/keywordize-keys + test-utils/default-context) + {:ex "http://example.org/ns/" + :list "@list"})}}) + ledger-alias "load-lists-test" + ledger @(fluree/create conn ledger-alias) + db @(fluree/stage + (fluree/db ledger) + [{:id :ex/alice + :type :ex/User + ;; TODO: We can likely add support of inference of + ;; nodes w/ keywords here if we want to + :ex/friends {:list [{:id :ex/john} {:id :ex/cam}]}} + {:id :ex/cam + :type :ex/User + :ex/numList {:list [7 8 9 10]}} + {:id :ex/john + :type :ex/User}]) + db @(fluree/commit! ledger db) + loaded (test-utils/retry-load conn ledger-alias 100) + loaded-db (fluree/db loaded)] + (is (= (:t db) (:t loaded-db))) + (testing "query returns expected `list` values" + (is (= #{{:id :ex/cam + :rdf/type [:ex/User] + :ex/numList [7 8 9 10]} + {:id :ex/john, :rdf/type [:ex/User]} + {:id :ex/alice + :rdf/type [:ex/User] + :ex/friends [{:id :ex/john} {:id :ex/cam}]}} + (set + @(fluree/query loaded-db '{:select {?s [:*]} + :where [[?s :rdf/type :ex/User]]}))))))) + + (testing "can load with policies" + (with-tmp-dir storage-path + (let [conn @(fluree/connect + {:method :file + :storage-path storage-path + :defaults + {:context (merge (util/keywordize-keys + test-utils/default-context) + {:ex "http://example.org/ns/" + :list "@list"})}}) + ledger-alias "load-policy-test" + ledger @(fluree/create conn ledger-alias) + db @(fluree/stage + (fluree/db ledger) + [{:id :ex/alice + :type :ex/User + :schema/name "Alice" + :schema/ssn "111-11-1111" + :ex/friend {:id :ex/john}} + {:id :ex/john + :schema/name "John" + :type :ex/User + :schema/ssn "888-88-8888"} + {:id "did:fluree:123" + :ex/user {:id :ex/alice} + :f/role {:id :ex/userRole}}]) + db+policy @(fluree/stage + db + [{:id :ex/UserPolicy + :type [:f/Policy] + :f/targetClass {:id :ex/User} + :f/allow [{:id :ex/globalViewAllow + :f/targetRole {:id :ex/userRole} + :f/action [{:id :f/view}]}] + :f/property + [{:f/path {:id :schema/ssn} + :f/allow + [{:id :ex/ssnViewRule + :f/targetRole {:id :ex/userRole} + :f/action [{:id :f/view}] + :f/equals {:list [{:id :f/$identity} + {:id :ex/user}]}}]}]}]) + db+policy @(fluree/commit! ledger db+policy) + loaded (test-utils/retry-load conn ledger-alias 100) + loaded-db (fluree/db loaded)] + (is (= (:t db) (:t loaded-db))) + (testing "query returns expected policy" + (is (= [{:id :ex/UserPolicy + :rdf/type [:f/Policy] + :f/allow + {:id :ex/globalViewAllow + :f/action {:id :f/view} + :f/targetRole {"_id" 211106232532995}} + :f/property + {:id "_:f211106232532999" + :f/allow + {:id :ex/ssnViewRule + :f/action {:id :f/view} + :f/targetRole {"_id" 211106232532995} + :f/equals [{:id :f/$identity} {:id :ex/user}]} + :f/path {:id :schema/ssn}} + :f/targetClass {:id :ex/User}}] + @(fluree/query + loaded-db + '{:select {?s [:* + {:rdf/type ["_id"]} + {:f/allow [:* {:f/targetRole ["_id"]}]} + {:f/property [:* {:f/allow [:* {:f/targetRole ["_id"]}]}]}]} + :where [[?s :rdf/type :f/Policy]]})))))))))