mirror of
https://github.com/digital-asset/daml.git
synced 2024-11-05 03:56:26 +03:00
Remove navigator (#18141)
* remove navigator * remove all apart from assistant * remove navigator from assistant * remove navigator from release procedure * remove mentions of navigator completely
This commit is contained in:
parent
6f3bb7f8ef
commit
bde7632208
@ -5,6 +5,5 @@ node_modules/
|
||||
compiler/daml-extension/node_modules/
|
||||
language-support/ts/node_modules/
|
||||
language-support/ts/packages/node_modules/
|
||||
navigator/frontend/node_modules/
|
||||
compatibility/
|
||||
|
||||
|
@ -21,10 +21,6 @@ bazel-daml
|
||||
bazel-out
|
||||
bazel-testlogs
|
||||
|
||||
# ignore generated code
|
||||
navigator/frontend/src/api/Queries.ts
|
||||
navigator/frontend/src/ui-core/src/api/Queries.ts
|
||||
|
||||
# ignore canton code drop
|
||||
canton
|
||||
canton-3x
|
||||
|
@ -55,7 +55,6 @@ NOTICES @garyverhaegen-da @dasormeter
|
||||
|
||||
# Application Runtime
|
||||
/ledger-service/ @filmackay
|
||||
/navigator/ @filmackay
|
||||
/runtime-components/ @filmackay
|
||||
/triggers/service/ @filmackay
|
||||
|
||||
|
@ -732,15 +732,6 @@ yarn_install(
|
||||
yarn_lock = "//compiler/daml-extension:yarn.lock",
|
||||
)
|
||||
|
||||
# TODO use fine-grained managed dependency
|
||||
yarn_install(
|
||||
name = "navigator_frontend_deps",
|
||||
args = ["--frozen-lockfile"],
|
||||
package_json = "//navigator/frontend:package.json",
|
||||
symlink_node_modules = False,
|
||||
yarn_lock = "//navigator/frontend:yarn.lock",
|
||||
)
|
||||
|
||||
# We’ve had a bunch of problems with typescript rules on Windows.
|
||||
# Therefore we’ve disabled them completely for now.
|
||||
# Since we need to @load stuff in @language_support_ts_deps
|
||||
|
@ -160,14 +160,10 @@ commandParser = subparser $ fold
|
||||
|
||||
startCmd = do
|
||||
sandboxPortM <- sandboxPortOpt "sandbox-port" "Port number for the sandbox"
|
||||
shouldOpenBrowser <- flagYesNoAuto "open-browser" True "Open the browser after navigator" idm
|
||||
shouldStartNavigator <- flagYesNoAuto' "start-navigator" "Start navigator as part of daml start. Can be set to true or false. Defaults to true." idm
|
||||
navigatorPort <- navigatorPortOption
|
||||
jsonApiPortM <- jsonApiPortOpt "json-api-port" "Port that the HTTP JSON API should listen on or 'none' to disable it"
|
||||
onStartM <- optional (option str (long "on-start" <> metavar "COMMAND" <> help "Command to run once sandbox and navigator are running."))
|
||||
onStartM <- optional (option str (long "on-start" <> metavar "COMMAND" <> help "Command to run once sandbox is running."))
|
||||
shouldWaitForSignal <- flagYesNoAuto "wait-for-signal" True "Wait for Ctrl+C or interrupt after starting servers." idm
|
||||
sandboxOptions <- many (strOption (long "sandbox-option" <> metavar "SANDBOX_OPTION" <> help "Pass option to sandbox"))
|
||||
navigatorOptions <- many (strOption (long "navigator-option" <> metavar "NAVIGATOR_OPTION" <> help "Pass option to navigator"))
|
||||
scriptOptions <- many (strOption (long "script-option" <> metavar "SCRIPT_OPTION" <> help "Pass option to Daml script interpreter"))
|
||||
shutdownStdinClose <- stdinCloseOpt
|
||||
sandboxPortSpec <- sandboxCantonPortSpecOpt
|
||||
@ -183,12 +179,6 @@ commandParser = subparser $ fold
|
||||
domainAdminApiSpec <- sandboxPortOpt "sandbox-domain-admin-port" "Port number for the canton domain admin API (--sandbox-canton only)"
|
||||
pure SandboxCantonPortSpec {..}
|
||||
|
||||
navigatorPortOption = NavigatorPort <$> option auto
|
||||
(long "navigator-port"
|
||||
<> metavar "PORT_NUM"
|
||||
<> value 7500
|
||||
<> help "Port number for navigator (default is 7500).")
|
||||
|
||||
deployCmdInfo = mconcat
|
||||
[ progDesc $ concat
|
||||
[ "Deploy the current Daml project to a remote Daml ledger. "
|
||||
|
@ -5,10 +5,7 @@ module DA.Daml.Helper.Start
|
||||
|
||||
, withJar
|
||||
, withSandbox
|
||||
, withNavigator
|
||||
|
||||
, StartOptions(..)
|
||||
, NavigatorPort(..)
|
||||
, SandboxPort(..)
|
||||
, SandboxPortSpec(..)
|
||||
, toSandboxPortSpec
|
||||
@ -29,9 +26,6 @@ import System.FilePath
|
||||
import System.Process.Typed
|
||||
import System.IO.Extra
|
||||
import System.Info.Extra
|
||||
import Web.Browser
|
||||
|
||||
import Options.Applicative.Extended (YesNoAuto, determineAutoM)
|
||||
|
||||
import DA.Daml.Helper.Codegen
|
||||
import DA.Daml.Helper.Ledger
|
||||
@ -48,15 +42,8 @@ toSandboxPortSpec n
|
||||
| otherwise = Just (SpecifiedPort (SandboxPort n))
|
||||
|
||||
newtype SandboxPort = SandboxPort { unSandboxPort :: Int }
|
||||
newtype NavigatorPort = NavigatorPort Int
|
||||
newtype JsonApiPort = JsonApiPort { unJsonApiPort :: Int }
|
||||
|
||||
navigatorPortNavigatorArgs :: NavigatorPort -> [String]
|
||||
navigatorPortNavigatorArgs (NavigatorPort p) = ["--port", show p]
|
||||
|
||||
navigatorURL :: NavigatorPort -> String
|
||||
navigatorURL (NavigatorPort p) = "http://localhost:" <> show p
|
||||
|
||||
-- | Use SandboxPortSpec to determine a sandbox port number.
|
||||
-- This is racy thanks to getFreePort, but there's no good alternative at the moment.
|
||||
getPortForSandbox :: SandboxPortSpec -> Maybe SandboxPortSpec -> IO Int
|
||||
@ -92,19 +79,6 @@ withSandbox StartOptions{..} darPath sandboxArgs kont =
|
||||
runLedgerUploadDar (sandboxLedgerFlags sandboxPort) (Just darPath)
|
||||
kont ph (SandboxPort sandboxPort)
|
||||
|
||||
withNavigator :: SandboxPort -> NavigatorPort -> [String] -> (Process () () () -> IO a) -> IO a
|
||||
withNavigator (SandboxPort sandboxPort) navigatorPort args a = do
|
||||
let navigatorArgs = concat
|
||||
[ ["navigator", "server", "localhost", show sandboxPort]
|
||||
, navigatorPortNavigatorArgs navigatorPort
|
||||
, args
|
||||
]
|
||||
withSdkJar navigatorArgs "navigator-logback.xml" $ \ph -> do
|
||||
putStrLn "Waiting for navigator to start: "
|
||||
waitForHttpServer 240 (unsafeProcessHandle ph) (putStr "." *> threadDelay 500000)
|
||||
(navigatorURL navigatorPort) []
|
||||
a ph
|
||||
|
||||
waitForJsonApi :: Process () () () -> JsonApiPort -> IO ()
|
||||
waitForJsonApi sandboxPh (JsonApiPort jsonApiPort) = do
|
||||
putStrLn "Waiting for JSON API to start: "
|
||||
@ -121,14 +95,10 @@ withOptsFromProjectConfig fieldName cliOpts projectConfig = do
|
||||
|
||||
data StartOptions = StartOptions
|
||||
{ sandboxPortM :: Maybe SandboxPortSpec
|
||||
, shouldOpenBrowser :: Bool
|
||||
, shouldStartNavigator :: YesNoAuto
|
||||
, navigatorPort :: NavigatorPort
|
||||
, jsonApiPortM :: Maybe JsonApiPort
|
||||
, onStartM :: Maybe String
|
||||
, shouldWaitForSignal :: Bool
|
||||
, sandboxOptions :: [String]
|
||||
, navigatorOptions :: [String]
|
||||
, scriptOptions :: [String]
|
||||
, sandboxPortSpec :: !SandboxCantonPortSpec
|
||||
}
|
||||
@ -147,13 +117,7 @@ runStart startOptions@StartOptions{..} =
|
||||
mbInitScript :: Maybe String <-
|
||||
requiredE "Failed to parse init-script" $
|
||||
queryProjectConfig ["init-script"] projectConfig
|
||||
shouldStartNavigator :: Bool <-
|
||||
determineAutoM (fmap (fromMaybe True) $
|
||||
requiredE "Failed to parse start-navigator" $
|
||||
queryProjectConfig ["start-navigator"] projectConfig)
|
||||
shouldStartNavigator
|
||||
sandboxOpts <- withOptsFromProjectConfig "sandbox-options" sandboxOptions projectConfig
|
||||
navigatorOpts <- withOptsFromProjectConfig "navigator-options" navigatorOptions projectConfig
|
||||
scriptOpts <- withOptsFromProjectConfig "script-options" scriptOptions projectConfig
|
||||
doBuild
|
||||
doCodegen projectConfig
|
||||
@ -178,19 +142,13 @@ runStart startOptions@StartOptions{..} =
|
||||
runProcess_ procScript
|
||||
doRunInitScript
|
||||
listenForKeyPress projectConfig darPath sandboxPort doRunInitScript
|
||||
withNavigator' shouldStartNavigator sandboxPh sandboxPort navigatorPort navigatorOpts $ \navigatorPh -> do
|
||||
do
|
||||
whenJust onStartM $ \onStart -> runProcess_ (shell onStart)
|
||||
when (shouldStartNavigator && shouldOpenBrowser) $
|
||||
void $ openBrowser (navigatorURL navigatorPort)
|
||||
whenJust jsonApiPortM $ \jsonApiPort -> waitForJsonApi sandboxPh jsonApiPort
|
||||
when shouldWaitForSignal $
|
||||
void $ waitAnyCancel =<< mapM (async . waitExitCode) [navigatorPh,sandboxPh]
|
||||
void $ waitAnyCancel =<< mapM (async . waitExitCode) [sandboxPh]
|
||||
|
||||
where
|
||||
withNavigator' shouldStartNavigator sandboxPh =
|
||||
if shouldStartNavigator
|
||||
then withNavigator
|
||||
else (\_ _ _ f -> f sandboxPh)
|
||||
doCodegen projectConfig =
|
||||
forM_ [minBound :: Lang .. maxBound :: Lang] $ \lang -> do
|
||||
mbOutputPath :: Maybe String <-
|
||||
@ -235,14 +193,3 @@ runStart startOptions@StartOptions{..} =
|
||||
reloadInstructions
|
||||
| isWindows = "\nPress 'r' + 'Enter' to re-build and upload the package to the sandbox.\nPress 'Ctrl-C' to quit."
|
||||
| otherwise = "\nPress 'r' to re-build and upload the package to the sandbox.\nPress 'Ctrl-C' to quit."
|
||||
|
||||
withSdkJar
|
||||
:: [String]
|
||||
-- ^ Commands passed to the assistant and the SDK JAR.
|
||||
-> FilePath
|
||||
-- ^ File name of the logback config.
|
||||
-> (Process () () () -> IO a)
|
||||
-> IO a
|
||||
withSdkJar args logbackConf f = do
|
||||
logbackArg <- getLogbackArg (damlSdkJarFolder </> logbackConf)
|
||||
withJar damlSdkJar [logbackArg] args f
|
||||
|
@ -10,9 +10,7 @@ load("@os_info//:os_info.bzl", "is_windows")
|
||||
load(":util.bzl", "deps")
|
||||
|
||||
scala_deps = [
|
||||
"@maven//:org_apache_pekko_pekko_http_spray_json",
|
||||
"@maven//:com_typesafe_scala_logging_scala_logging",
|
||||
"@maven//:io_spray_spray_json",
|
||||
]
|
||||
|
||||
scala_runtime_deps = [
|
||||
@ -34,8 +32,6 @@ da_scala_binary(
|
||||
resources = glob(["src/main/resources/**/*"]),
|
||||
scala_deps = scala_deps,
|
||||
scala_runtime_deps = scala_runtime_deps,
|
||||
# Navigator resources trigger this incorrectly
|
||||
unused_dependency_checker_mode = "off",
|
||||
visibility = ["//visibility:public"],
|
||||
runtime_deps = runtime_deps,
|
||||
deps = deps("ce"),
|
||||
@ -49,8 +45,6 @@ da_scala_binary(
|
||||
scala_deps = scala_deps,
|
||||
scala_runtime_deps = scala_runtime_deps,
|
||||
tags = ["ee-jar-license"],
|
||||
# Navigator resources trigger this incorrectly
|
||||
unused_dependency_checker_mode = "off",
|
||||
visibility = ["//visibility:public"],
|
||||
runtime_deps = runtime_deps + [
|
||||
"@maven//:com_oracle_database_jdbc_ojdbc8",
|
||||
|
@ -8,7 +8,6 @@ import com.daml.lf.engine.script.{ScriptMain => Script}
|
||||
import com.daml.lf.engine.trigger.{RunnerMain => Trigger}
|
||||
import com.daml.lf.engine.trigger.{ServiceMain => TriggerService}
|
||||
import com.daml.auth.middleware.oauth2.{Main => Oauth2Middleware}
|
||||
import com.daml.navigator.{NavigatorBackend => Navigator}
|
||||
import com.daml.script.export.{Main => Export}
|
||||
|
||||
object SdkMain {
|
||||
@ -22,7 +21,6 @@ object SdkMain {
|
||||
case "codegen" => Codegen.main(rest)
|
||||
case "trigger-service" => TriggerService.main(rest)
|
||||
case "oauth2-middleware" => Oauth2Middleware.main(rest)
|
||||
case "navigator" => Navigator.main(rest)
|
||||
case _ => sys.exit(1)
|
||||
}
|
||||
}
|
||||
|
@ -5,11 +5,8 @@ def deps(edition):
|
||||
return [
|
||||
"//daml-script/runner:script-runner-lib",
|
||||
"//language-support/codegen-main:codegen-main-lib",
|
||||
"//navigator/backend:navigator-library",
|
||||
"//daml-script/export",
|
||||
"//triggers/runner:trigger-runner-lib",
|
||||
"//triggers/service:trigger-service-binary-{}".format(edition),
|
||||
"//triggers/service/auth:oauth2-middleware",
|
||||
"//navigator/backend:backend-resources",
|
||||
"//navigator/backend:frontend-resources",
|
||||
]
|
||||
|
@ -382,7 +382,6 @@ argWhitelist = S.fromList
|
||||
, "clean"
|
||||
, "damlc", "ide", "license", "package", "docs", "visual", "visual-web", "inspect-dar", "validate-dar", "doctest", "lint"
|
||||
, "sandbox", "INFO", "TRACE", "DEBUG", "WARN", "ERROR"
|
||||
, "navigator", "server", "console", "dump-graphql-schema", "create-config", "static", "simulated", "wallclock"
|
||||
, "ledger", "list-parties", "allocate-parties", "upload-dar", "fetch-dar"
|
||||
, "codegen", "java", "js"
|
||||
, "deploy"
|
||||
|
@ -141,7 +141,6 @@ damlStart tmpDir = do
|
||||
let startProc =
|
||||
(shell $ unwords
|
||||
[ "daml start"
|
||||
, "--start-navigator=no"
|
||||
, "--sandbox-port", show $ ledger ports
|
||||
, "--sandbox-admin-api-port", show $ admin ports
|
||||
, "--sandbox-domain-public-port", show $ domainPublic ports
|
||||
@ -413,23 +412,6 @@ damlStartTests getDamlStart =
|
||||
manager <- newManager defaultManagerSettings
|
||||
resp <- httpLbs req manager
|
||||
responseBody resp @?= "{\"status\":\"pass\"}"
|
||||
subtest "Navigator startup" $ do
|
||||
DamlStartResource {projDir, sandboxPort} <- getDamlStart
|
||||
navigatorPort :: Int <- fromIntegral <$> getFreePort
|
||||
-- This test just checks that navigator starts up and returns a 200 response.
|
||||
-- Nevertheless this would have caught a few issues on rules_nodejs upgrades
|
||||
-- where we got a 404 instead.
|
||||
withDamlServiceIn projDir "navigator"
|
||||
[ "server"
|
||||
, "localhost"
|
||||
, show sandboxPort
|
||||
, "--port"
|
||||
, show navigatorPort
|
||||
] $ \ ph -> do
|
||||
waitForHttpServer 240 ph
|
||||
(threadDelay 500000)
|
||||
("http://localhost:" <> show navigatorPort)
|
||||
[]
|
||||
|
||||
subtest "hot reload" $ do
|
||||
DamlStartResource {projDir, jsonApiPort, startStdin, stdoutChan, alice, aliceHeaders} <- getDamlStart
|
||||
|
@ -35,7 +35,6 @@ da_scala_library(
|
||||
"//daml-script:__subpackages__",
|
||||
"//language-support:__subpackages__",
|
||||
"//ledger-service:__subpackages__",
|
||||
"//navigator:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//daml-lf/archive:daml_lf_archive_reader",
|
||||
|
@ -45,10 +45,6 @@ packages/daml-manual/reference/file-structure.html -> /daml/reference/file-struc
|
||||
packages/daml-manual/reference/expressions.html -> /daml/reference/expressions.html
|
||||
packages/sdk-docs-introduction/index.html -> /getting-started/index.html
|
||||
packages/da-docs-example-collateral/index.html -> /examples/collateral/index.html
|
||||
packages/navigator-docs/index.html -> /tools/navigator/index.html
|
||||
packages/navigator-docs/database.html -> /tools/navigator/database.html
|
||||
packages/navigator-docs/backend-licenses.html -> /
|
||||
packages/navigator-docs/frontend-licenses.html -> /
|
||||
packages/quickstart/index.html -> /getting-started/index.html
|
||||
packages/da-docs-ledger-model/ledger-integrity.html -> /concepts/ledger-model/ledger-integrity.html
|
||||
packages/da-docs-ledger-model/index.html -> /concepts/ledger-model/index.html
|
||||
|
@ -1,385 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { DamlLfValue } from '@da/ui-core';
|
||||
|
||||
export const version = {
|
||||
schema: 'navigator-config',
|
||||
major: 2,
|
||||
minor: 0,
|
||||
};
|
||||
|
||||
export const customViews = (userId, party, role) => ({
|
||||
issued_ious: {
|
||||
type: "table-view",
|
||||
title: "Issued Ious",
|
||||
source: {
|
||||
type: "contracts",
|
||||
filter: [
|
||||
{
|
||||
field: "argument.issuer",
|
||||
value: party,
|
||||
},
|
||||
{
|
||||
field: "template.id",
|
||||
value: "Iou:Iou",
|
||||
}
|
||||
],
|
||||
search: "",
|
||||
sort: [
|
||||
{
|
||||
field: "id",
|
||||
direction: "ASCENDING"
|
||||
}
|
||||
]
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
key: "id",
|
||||
title: "Contract ID",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: rowData.id
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 0,
|
||||
alignment: "left"
|
||||
},
|
||||
{
|
||||
key: "argument.owner",
|
||||
title: "Owner",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: DamlLfValue.toJSON(rowData.argument).owner
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 0,
|
||||
alignment: "left"
|
||||
},
|
||||
{
|
||||
key: "argument.currency",
|
||||
title: "Currency",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: DamlLfValue.toJSON(rowData.argument).currency
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 0,
|
||||
alignment: "left"
|
||||
},
|
||||
{
|
||||
key: "argument.amount",
|
||||
title: "Amount",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: DamlLfValue.toJSON(rowData.argument).amount
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 3,
|
||||
alignment: "left"
|
||||
}
|
||||
]
|
||||
},
|
||||
owned_ious: {
|
||||
type: "table-view",
|
||||
title: "Owned Ious",
|
||||
source: {
|
||||
type: "contracts",
|
||||
filter: [
|
||||
{
|
||||
field: "argument.owner",
|
||||
value: party,
|
||||
},
|
||||
{
|
||||
field: "template.id",
|
||||
value: "Iou:Iou",
|
||||
}
|
||||
],
|
||||
search: "",
|
||||
sort: [
|
||||
{
|
||||
field: "id",
|
||||
direction: "ASCENDING"
|
||||
}
|
||||
]
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
key: "id",
|
||||
title: "Contract ID",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: rowData.id
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 0,
|
||||
alignment: "left"
|
||||
},
|
||||
{
|
||||
key: "argument.issuer",
|
||||
title: "Issuer",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: DamlLfValue.toJSON(rowData.argument).issuer
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 0,
|
||||
alignment: "left"
|
||||
},
|
||||
{
|
||||
key: "argument.currency",
|
||||
title: "Currency",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: DamlLfValue.toJSON(rowData.argument).currency
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 0,
|
||||
alignment: "left"
|
||||
},
|
||||
{
|
||||
key: "argument.amount",
|
||||
title: "Amount",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: DamlLfValue.toJSON(rowData.argument).amount
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 3,
|
||||
alignment: "left"
|
||||
}
|
||||
]
|
||||
},
|
||||
transfers: {
|
||||
type: "table-view",
|
||||
title: "Iou Transfers",
|
||||
source: {
|
||||
type: "contracts",
|
||||
filter: [
|
||||
{
|
||||
field: "template.id",
|
||||
value: "Iou:IouTransfer",
|
||||
}
|
||||
],
|
||||
search: "",
|
||||
sort: [
|
||||
{
|
||||
field: "id",
|
||||
direction: "ASCENDING"
|
||||
}
|
||||
]
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
key: "id",
|
||||
title: "Contract ID",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: rowData.id
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 0,
|
||||
alignment: "left"
|
||||
},
|
||||
{
|
||||
key: "argument.iou.owner",
|
||||
title: "Sender",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: DamlLfValue.toJSON(rowData.argument).iou.owner
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 0,
|
||||
alignment: "left"
|
||||
},
|
||||
{
|
||||
key: "argument.iou.newOwner",
|
||||
title: "Receiver",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: DamlLfValue.toJSON(rowData.argument).newOwner
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 0,
|
||||
alignment: "left"
|
||||
},
|
||||
{
|
||||
key: "argument.iou.issuer",
|
||||
title: "Issuer",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: DamlLfValue.toJSON(rowData.argument).iou.issuer
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 0,
|
||||
alignment: "left"
|
||||
},
|
||||
{
|
||||
key: "argument.iou.currency",
|
||||
title: "Currency",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: DamlLfValue.toJSON(rowData.argument).iou.currency
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 0,
|
||||
alignment: "left"
|
||||
},
|
||||
{
|
||||
key: "argument.iou.amount",
|
||||
title: "Amount",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: DamlLfValue.toJSON(rowData.argument).iou.amount
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 3,
|
||||
alignment: "left"
|
||||
}
|
||||
]
|
||||
},
|
||||
trades: {
|
||||
type: "table-view",
|
||||
title: "Trades",
|
||||
source: {
|
||||
type: "contracts",
|
||||
filter: [
|
||||
{
|
||||
field: "template.id",
|
||||
value: "IouTrade:IouTrade@",
|
||||
}
|
||||
],
|
||||
search: "",
|
||||
sort: [
|
||||
{
|
||||
field: "id",
|
||||
direction: "ASCENDING"
|
||||
}
|
||||
]
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
key: "id",
|
||||
title: "Contract ID",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: rowData.id
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 0,
|
||||
alignment: "left"
|
||||
},
|
||||
{
|
||||
key: "argument.buyer",
|
||||
title: "Buyer",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: DamlLfValue.toJSON(rowData.argument).buyer
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 0,
|
||||
alignment: "left"
|
||||
},
|
||||
{
|
||||
key: "argument.seller",
|
||||
title: "Seller",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: DamlLfValue.toJSON(rowData.argument).seller
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 0,
|
||||
alignment: "left"
|
||||
},
|
||||
{
|
||||
key: "argument.baseIssuer",
|
||||
title: "Base Issuer",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: DamlLfValue.toJSON(rowData.argument).baseIssuer
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 0,
|
||||
alignment: "left"
|
||||
},
|
||||
{
|
||||
key: "argument.baseCurrency",
|
||||
title: "Base Currency",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: DamlLfValue.toJSON(rowData.argument).baseCurrency
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 0,
|
||||
alignment: "left"
|
||||
},
|
||||
{
|
||||
key: "argument.baseAmount",
|
||||
title: "Base Amount",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: DamlLfValue.toJSON(rowData.argument).baseAmount
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 3,
|
||||
alignment: "left"
|
||||
},
|
||||
{
|
||||
key: "argument.quoteIssuer",
|
||||
title: "Quote Issuer",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: DamlLfValue.toJSON(rowData.argument).quoteIssuer
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 0,
|
||||
alignment: "left"
|
||||
},
|
||||
{
|
||||
key: "argument.quoteCurrency",
|
||||
title: "Quote Currency",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: DamlLfValue.toJSON(rowData.argument).quoteCurrency
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 0,
|
||||
alignment: "left"
|
||||
},
|
||||
{
|
||||
key: "argument.quoteAmount",
|
||||
title: "Quote Amount",
|
||||
createCell: ({rowData}) => ({
|
||||
type: "text",
|
||||
value: DamlLfValue.toJSON(rowData.argument).quoteAmount
|
||||
}),
|
||||
sortable: true,
|
||||
width: 80,
|
||||
weight: 3,
|
||||
alignment: "left"
|
||||
},
|
||||
]
|
||||
}
|
||||
})
|
2
fmt.sh
2
fmt.sh
@ -175,7 +175,7 @@ if ! diff .bazelrc compatibility/.bazelrc >/dev/null; then
|
||||
diff -u .bazelrc compatibility/.bazelrc
|
||||
fi
|
||||
|
||||
# check akka is not used as dependency except in navigator_maven and deprecated_maven
|
||||
# check akka is not used as dependency except in deprecated_maven
|
||||
for f in $(ls *_install*.json | egrep -v "deprecated"); do
|
||||
if grep -q akka $f; then
|
||||
echo $f contains a dependency to akka
|
||||
|
1
navigator/.gitignore
vendored
1
navigator/.gitignore
vendored
@ -1 +0,0 @@
|
||||
**/src/main/resources/frontend/
|
@ -1,7 +0,0 @@
|
||||
# Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
alias(
|
||||
name = "navigator-binary",
|
||||
actual = "//navigator/backend:navigator-binary",
|
||||
)
|
@ -1,39 +0,0 @@
|
||||
.PHONY: build test start-frontend start-ui-core
|
||||
|
||||
default: build
|
||||
|
||||
build:
|
||||
bazel build //navigator/...
|
||||
|
||||
test:
|
||||
bazel test //navigator/...
|
||||
|
||||
start-backend:
|
||||
$(MAKE) -C backend start
|
||||
|
||||
start-frontend:
|
||||
$(MAKE) -C frontend start
|
||||
|
||||
start-ui-core:
|
||||
$(MAKE) -C frontend start-ui-core
|
||||
|
||||
# Updates licensing documentation.
|
||||
# This documentation includes licensing info for each library that we use.
|
||||
update-license-docs:
|
||||
(cd docs/licenses && ./extract-sbt.py)
|
||||
(cd docs/licenses && ./extract-js.py)
|
||||
|
||||
# CI: Makes sure the frontend code is nicely formatted.
|
||||
lint:
|
||||
$(MAKE) -C frontend lint
|
||||
|
||||
# CI: Makes sure every file has a license header.
|
||||
check-licenses:
|
||||
dade-copyright-headers check navigator
|
||||
|
||||
# CI: Makes sure licensing documentation is up to date.
|
||||
check-license-docs:
|
||||
(cd docs/licenses && ./extract-sbt.py check)
|
||||
(cd docs/licenses && ./extract-js.py check)
|
||||
|
||||
|
@ -1,77 +0,0 @@
|
||||
Navigator
|
||||
=========
|
||||
|
||||
The *Navigator* is a web-app that connects to any Digital Asset ledger and
|
||||
allows the user to inspect contracts, create contracts, and exercise choices.
|
||||
|
||||
The Navigator can be used in development mode (see below) or packaged into a
|
||||
"fat" JAR that includes the compiled frontend assets for distribution.
|
||||
|
||||
Navigator architecture
|
||||
----------------------
|
||||
|
||||
To learn more about developing the different parts of the Navigator see:
|
||||
|
||||
- [Navigator Frontend README](./frontend)
|
||||
- [Navigator Backend README](./backend)
|
||||
|
||||
Building Navigator
|
||||
------------------
|
||||
|
||||
To build a "fat" JAR of the Navigator that includes the pre-compiled front-end
|
||||
assets, run:
|
||||
|
||||
```bash
|
||||
bazel build //navigator/backend:navigator-binary_distribute.jar
|
||||
```
|
||||
|
||||
This produces a "fat" JAR `bazel-bin/navigator/backend/navigator-binary_distribute.jar` which can be run with:
|
||||
|
||||
```bash
|
||||
java -jar bazel-bin/navigator/backend/navigator-binary_distribute.jar
|
||||
```
|
||||
|
||||
Notable things in the Navigator build:
|
||||
|
||||
### backend/src/test/resources/schema.graphql
|
||||
|
||||
Manually written, must be consistent with `backend/src/main/scala/com/digitalasset/navigator/graphql/GraphQLSchema.scala`. Consistency is checked in a test.
|
||||
|
||||
### frontend/src/**/api/Queries.ts
|
||||
|
||||
Generated from `backend/src/test/resources/schema.graphql` with an external codegen tool.
|
||||
Currently, these files are checked in and updated with `make update-graphql-types`.
|
||||
|
||||
### frontend bundled code
|
||||
|
||||
Code from `frontend/src/**/*.ts*`, compiled using TypeScript, and bundled with Webpack.
|
||||
Output includes:
|
||||
- `bundle-[hash].js`: bundled frontend code, name uses content hasing.
|
||||
- `browsercheck-[hash].js`: tiny module for checking browser compatibility, name uses content hasing.
|
||||
- Several image and font files, referenced by the above modules. File names use content hashing.
|
||||
- `index.html`: Single page application main entry, references the above modules.
|
||||
|
||||
Note: Browsers are instructed never to cache `index.html`, and indefinitely cache all other files. This is why content hashing is used.
|
||||
|
||||
### backend binary
|
||||
|
||||
Scala binary, compiled as a fat JAR.
|
||||
Code from `backend/src/**/*.scala`, bundled frontend code is copied to `backend/src/main/resources/frontend`.
|
||||
|
||||
### backend version
|
||||
|
||||
The version is included as resource files in the Navigator fat jar.
|
||||
This is to reduce rebuild times when the version changes.
|
||||
|
||||
### frontend development build
|
||||
|
||||
For developing frontend code, `webpack-dev-server` is used. This serves the current frontend code on a separate port, and does:
|
||||
- Watch `*.ts` files for changes
|
||||
- Perform incremental builds
|
||||
- Send a push notification to the browser, automatically reloading the page when the build is finished.
|
||||
- Forward network requests to a different port, where a Navigator backend is expected to run.
|
||||
|
||||
This is orders of magnitude faster than what the current Bazel build offers, so it is desirable to keep the `webpack-dev-server` setup working.
|
||||
|
||||
Note, the browser is instructed to cache assets based on the SDK version.
|
||||
During development this is too aggressive and you will need to manually refresh to see updates to the front-end.
|
1
navigator/backend/.gitignore
vendored
1
navigator/backend/.gitignore
vendored
@ -1 +0,0 @@
|
||||
target/
|
@ -1,212 +0,0 @@
|
||||
# Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
load(
|
||||
"//bazel_tools:scala.bzl",
|
||||
"da_scala_binary",
|
||||
"da_scala_library",
|
||||
"da_scala_test_suite",
|
||||
)
|
||||
|
||||
navigator_scalacopts = [
|
||||
"-Xelide-below",
|
||||
"CONFIG",
|
||||
]
|
||||
|
||||
# All frontend resource files.
|
||||
# These come in a manually created JAR file, this rule is just wrapping it in
|
||||
# a java_import, so that it is a valid target for the 'resources' property of
|
||||
# scala_binary.
|
||||
java_import(
|
||||
name = "frontend-resources",
|
||||
jars = [
|
||||
"//navigator/frontend:frontend.jar",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
# Static backend resources.
|
||||
java_library(
|
||||
name = "backend-resources",
|
||||
resources = glob(["src/main/resources/**/*"]),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
da_scala_library(
|
||||
name = "navigator-library",
|
||||
srcs = glob([
|
||||
"src/main/scala/**/*.scala",
|
||||
]),
|
||||
scala_deps = [
|
||||
"@maven//:com_chuusai_shapeless",
|
||||
"@maven//:com_github_pureconfig_pureconfig_core",
|
||||
"@maven//:com_github_pureconfig_pureconfig_generic",
|
||||
"@maven//:com_github_scopt_scopt",
|
||||
"@maven//:org_apache_pekko_pekko_actor",
|
||||
"@maven//:org_apache_pekko_pekko_http",
|
||||
"@maven//:org_apache_pekko_pekko_http_core",
|
||||
"@maven//:org_apache_pekko_pekko_http_spray_json",
|
||||
"@maven//:org_apache_pekko_pekko_stream",
|
||||
"@maven//:com_typesafe_scala_logging_scala_logging",
|
||||
"@maven//:io_spray_spray_json",
|
||||
"@maven//:org_sangria_graphql_sangria_ast",
|
||||
"@maven//:org_sangria_graphql_sangria_core",
|
||||
"@maven//:org_sangria_graphql_sangria_derivation",
|
||||
"@maven//:org_sangria_graphql_sangria_parser",
|
||||
"@maven//:org_sangria_graphql_sangria_marshalling_api",
|
||||
"@maven//:org_scalaz_scalaz_core",
|
||||
"@maven//:org_tpolecat_doobie_core",
|
||||
"@maven//:org_tpolecat_doobie_free",
|
||||
"@maven//:org_typelevel_cats_core",
|
||||
"@maven//:org_typelevel_cats_effect",
|
||||
"@maven//:org_typelevel_cats_free",
|
||||
"@maven//:org_typelevel_cats_kernel",
|
||||
],
|
||||
scala_runtime_deps = [
|
||||
"@maven//:org_apache_pekko_pekko_slf4j",
|
||||
],
|
||||
scalacopts = navigator_scalacopts,
|
||||
visibility = ["//visibility:public"],
|
||||
runtime_deps = [
|
||||
"@maven//:ch_qos_logback_logback_classic",
|
||||
"@maven//:org_xerial_sqlite_jdbc",
|
||||
],
|
||||
deps = [
|
||||
"//daml-assistant/scala-daml-project-config",
|
||||
"//daml-lf/api-type-signature",
|
||||
"//daml-lf/archive:daml_lf_archive_reader",
|
||||
"//daml-lf/archive:daml_lf_dev_archive_proto_java",
|
||||
"//daml-lf/data",
|
||||
"//daml-lf/language",
|
||||
"//daml-lf/transaction",
|
||||
"//language-support/scala/bindings",
|
||||
"//ledger-service/lf-value-json",
|
||||
"//ledger/ledger-api-client",
|
||||
"//ledger/ledger-api-common",
|
||||
"//ledger/ledger-api-domain",
|
||||
"//libs-scala/build-info",
|
||||
"//libs-scala/grpc-utils",
|
||||
"//libs-scala/nonempty",
|
||||
"//libs-scala/rs-grpc-bridge",
|
||||
"//libs-scala/rs-grpc-pekko",
|
||||
"//libs-scala/scala-utils",
|
||||
"//observability/tracing",
|
||||
"@maven//:com_typesafe_config",
|
||||
"@maven//:io_grpc_grpc_netty",
|
||||
"@maven//:io_netty_netty_handler",
|
||||
"@maven//:org_slf4j_slf4j_api",
|
||||
],
|
||||
)
|
||||
|
||||
da_scala_library(
|
||||
name = "navigator-tests-library",
|
||||
srcs = glob(
|
||||
["src/test/**/*.scala"],
|
||||
exclude = [
|
||||
"src/test/**/*Spec.scala",
|
||||
"src/test/**/*Test.scala",
|
||||
],
|
||||
),
|
||||
scala_deps = [
|
||||
"@maven//:org_scalaz_scalaz_core",
|
||||
],
|
||||
scala_runtime_deps = [
|
||||
"@maven//:org_apache_pekko_pekko_stream_testkit",
|
||||
],
|
||||
scalacopts = navigator_scalacopts,
|
||||
runtime_deps = [
|
||||
"@maven//:ch_qos_logback_logback_classic",
|
||||
"@maven//:io_grpc_grpc_netty",
|
||||
"@maven//:org_xerial_sqlite_jdbc",
|
||||
],
|
||||
deps = [
|
||||
":navigator-library",
|
||||
"//daml-lf/api-type-signature",
|
||||
"//daml-lf/data",
|
||||
"//daml-lf/transaction",
|
||||
"//language-support/scala/bindings",
|
||||
"//ledger-service/lf-value-json",
|
||||
],
|
||||
)
|
||||
|
||||
da_scala_binary(
|
||||
name = "navigator-binary",
|
||||
main_class = "com.daml.navigator.NavigatorBackend",
|
||||
tags = [
|
||||
"maven_coordinates=com.daml:navigator:__VERSION__",
|
||||
"no_scala_version_suffix",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
deps = [
|
||||
":backend-resources",
|
||||
":frontend-resources",
|
||||
":navigator-library",
|
||||
],
|
||||
)
|
||||
|
||||
# Static test resources.
|
||||
java_library(
|
||||
name = "test-resources",
|
||||
resources = glob(["src/test/resources/**/*"]),
|
||||
)
|
||||
|
||||
da_scala_test_suite(
|
||||
name = "navigator-scala-tests",
|
||||
size = "large",
|
||||
srcs = glob([
|
||||
"src/test/**/*Spec.scala",
|
||||
"src/test/**/*Test.scala",
|
||||
]),
|
||||
scala_deps = [
|
||||
"@maven//:org_apache_pekko_pekko_actor",
|
||||
"@maven//:org_apache_pekko_pekko_http",
|
||||
"@maven//:org_apache_pekko_pekko_http_core",
|
||||
"@maven//:org_apache_pekko_pekko_http_spray_json",
|
||||
"@maven//:org_apache_pekko_pekko_http_testkit",
|
||||
"@maven//:org_apache_pekko_pekko_stream",
|
||||
"@maven//:com_typesafe_scala_logging_scala_logging",
|
||||
"@maven//:io_spray_spray_json",
|
||||
"@maven//:org_sangria_graphql_sangria_ast",
|
||||
"@maven//:org_sangria_graphql_sangria_core",
|
||||
"@maven//:org_sangria_graphql_sangria_derivation",
|
||||
"@maven//:org_sangria_graphql_sangria_parser",
|
||||
"@maven//:org_sangria_graphql_sangria_marshalling_api",
|
||||
"@maven//:org_scalacheck_scalacheck",
|
||||
"@maven//:org_scalatest_scalatest_core",
|
||||
"@maven//:org_scalatest_scalatest_matchers_core",
|
||||
"@maven//:org_scalatest_scalatest_shouldmatchers",
|
||||
"@maven//:org_scalatest_scalatest_wordspec",
|
||||
"@maven//:org_scalatestplus_scalacheck_1_15",
|
||||
"@maven//:org_scalaz_scalaz_core",
|
||||
],
|
||||
scalacopts = navigator_scalacopts,
|
||||
tags = ["cpu:4"],
|
||||
deps = [
|
||||
":navigator-library",
|
||||
":navigator-tests-library",
|
||||
":test-resources",
|
||||
"//bazel_tools/runfiles:scala_runfiles",
|
||||
"//daml-lf/api-type-signature",
|
||||
"//daml-lf/data",
|
||||
"//daml-lf/transaction",
|
||||
"//language-support/scala/bindings",
|
||||
"//ledger-service/lf-value-json",
|
||||
"//ledger/ledger-api-client",
|
||||
"//ledger/ledger-api-common",
|
||||
"//ledger/ledger-api-domain",
|
||||
"//libs-scala/build-info",
|
||||
"//libs-scala/ledger-resources",
|
||||
"//libs-scala/logging-entries",
|
||||
"//libs-scala/ports",
|
||||
"//libs-scala/resources",
|
||||
"//libs-scala/rs-grpc-bridge",
|
||||
"//libs-scala/testing-utils",
|
||||
"//libs-scala/timer-utils",
|
||||
"//test-common/canton/it-lib",
|
||||
"@maven//:com_typesafe_config",
|
||||
],
|
||||
)
|
||||
|
||||
exports_files(["src/main/resources/logback.xml"])
|
@ -1,13 +0,0 @@
|
||||
.PHONY: build test start
|
||||
|
||||
default: build
|
||||
|
||||
build:
|
||||
bazel build //navigator/backend/...
|
||||
|
||||
test:
|
||||
bazel test //navigator/backend/...
|
||||
|
||||
report-licenses:
|
||||
sbt dumpLicenseReport
|
||||
cp target/license-reports/navigator-licenses.csv $(OUT)
|
@ -1,246 +0,0 @@
|
||||
Navigator Backend
|
||||
=================
|
||||
|
||||
The Navigator backend is a Scala application providing
|
||||
|
||||
- a web server that exposes a GraphQL API with some predefined endpoints,
|
||||
such as returning all visible contracts.
|
||||
- a platform client that reacts to events happening in the platform
|
||||
- a simple sign-in API
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
The Navigator backend is written in Scala, making heavy use of
|
||||
[Pekko](http://pekko.io/) and [Sangria](http://sangria-graphql.org/) (for
|
||||
GraphQL). It uses the [Scala Build Tool](http://www.scala-sbt.org/).
|
||||
|
||||
We can build and run a basic the backend using the following commands:
|
||||
|
||||
```bash
|
||||
# Build a distribution archive ("fat jar"):
|
||||
bazel build //navigator/backend:navigator-binary_distribute.jar
|
||||
|
||||
# Run without arguments to show usage:
|
||||
java -jar bazel-bin/navigator/backend/navigator-binary_distribute.jar --help
|
||||
|
||||
# Create a dummy configuration file
|
||||
cat << EOF > navigator.conf
|
||||
users {
|
||||
OPERATOR: {
|
||||
party=OPERATOR
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Start the web server
|
||||
java -jar bazel-bin/navigator/backend/navigator-binary_distribute.jar server -c navigator.conf
|
||||
```
|
||||
|
||||
If you start the server and the configuration file doesn't exist, the server will
|
||||
write a configuration template and quit. You can then edit the configuration file
|
||||
and start again the server.
|
||||
|
||||
Custom UI backends
|
||||
------------------
|
||||
|
||||
## Extend the `UIBackend` base class
|
||||
|
||||
The base class for UI backends is the
|
||||
[`UIBackend`](src/main/scala/com/digitalasset/navigator/backend/UIBackend.scala),
|
||||
a Scala class that implements a ledger indexer and a web-server which exposes a
|
||||
number of default GraphQL endpoints and a simple Session API. It also allows has
|
||||
hooks for adding custom GraphQL endpoints.
|
||||
|
||||
To create a basic UI backend, you can extend `UIBackend` without any custom
|
||||
additions:
|
||||
|
||||
```scala
|
||||
object DefaultUIBackend extends UIBackend {
|
||||
override def customEndpoints: Set[CustomEndpoint[_]] = Set()
|
||||
override val defaultConfigFile: Path = Paths.get("my-app.conf")
|
||||
override def applicationInfo: ApplicationInfo = ApplicationInfo(...)
|
||||
override def banner: Option[String] = Some(...)
|
||||
}
|
||||
```
|
||||
|
||||
You can then run your app with this as the main class.
|
||||
|
||||
## Custom endpoints
|
||||
|
||||
A custom endpoint is an extra endpoint for a UI backend that binds a name to
|
||||
some data that can be represented in GraphQL and a function to calculate that
|
||||
data. For example:
|
||||
|
||||
```scala
|
||||
object ExampleUIBackend extends UIBackend {
|
||||
|
||||
override def customEndpoints: Set[CustomEndpoint[_]] = Set(contractsIdEndpoint)
|
||||
|
||||
final case class TemplateId(id: String)
|
||||
final case class ContractProjection(id: String, template: TemplateId, argument: RecordArgument)
|
||||
|
||||
/** Endpoing for the contract ids */
|
||||
private val contractsIdEndpoint = new CustomEndpoint[ContractProjection] {
|
||||
/** The endpoint to be used as GraphQL top-level for the data served by this */
|
||||
override def endpointName: String = "contract_id"
|
||||
|
||||
/** For each contract in the ledger, extract contract id and template id */
|
||||
override def calculate(ledger: Ledger): Seq[ContractProjection] =
|
||||
ledger.allContracts().map(contract =>
|
||||
ContractProjection(contract.idString, TemplateId(contract.template.idString), contract.argument))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This adds an endpoint called `contract_id` that returns a list of contract
|
||||
projections that include the contract ID, the template ID, and the contract
|
||||
arguments. You would then pass this custom endpoint to a UI backend subclass:
|
||||
|
||||
There are two components of a `CustomEndpoint`:
|
||||
|
||||
- The `endpointName` is the name used to bind this custom endpoint to the root
|
||||
of the GraphQL Schema. Note that each endpoint must have a unique
|
||||
`endpointName` and that default `endpointName`s -- `contracts` or `templates`
|
||||
for example -- cannot be used in custom endpoints either.
|
||||
|
||||
- The `calculate` function is used to extract the data to serve from the
|
||||
[`Ledger`](src/main/scala/com/digitalasset/ui/backend/model/Ledger.scala).
|
||||
This function must create a sequence of instances of a scala *case class*.
|
||||
|
||||
The endpoint `contract_id` becomes available in the root of the graphql schema:
|
||||
|
||||
```graphql
|
||||
type ContractProjection {
|
||||
id: String!
|
||||
template: TemplateId!
|
||||
argument: Value!
|
||||
}
|
||||
|
||||
type TemplateId {
|
||||
id: String!
|
||||
}
|
||||
|
||||
type Query {
|
||||
contract_id: [ContractProjection!]!
|
||||
}
|
||||
```
|
||||
|
||||
The schema is automatically generated by the UI backend by following the
|
||||
structure of a "row" case class. For instance `TemplateId` is a nested structure
|
||||
inside `ContractProjection` in the Scala code and for this reason it will be a
|
||||
nested structure in the GraphQL Schema too.
|
||||
|
||||
## Automatic generation of GraphQL Schemas for custom endpoints
|
||||
|
||||
The code for the GraphQL endpoint is automatically generated at compile time by
|
||||
the UI backend. This means that there are some limitations in what can be
|
||||
exposed as result of the `calculate` function. Specifically:
|
||||
|
||||
- The name of the "row" model case class must be a unique name. Don't use
|
||||
`Contract` or `Template` or any other name already used in the [`GraphQL
|
||||
Schema`](src/main/scala/com/digitalasset/ui/backend/graphql/GraphQLSchema.scala).
|
||||
For example, you can't rename `ContractProjection` to `Contract`.
|
||||
|
||||
- The main "row" class must be modelled as a Scala case class and it must
|
||||
contain fields that are either a case class or Scala primitives. If something
|
||||
is not supported, you won't be able to compile your code.
|
||||
|
||||
If some type is not supported by the generator, it is possible to add support
|
||||
for it by providing either an instance of the typeclass
|
||||
[`GraphQLLeaf`](src/main/scala/com/digitalasset/ui/backend/graphqless/GraphQLLeaf.scala)
|
||||
for simple types, e.g. `String` or `Int`, or an instance of the typeclass
|
||||
[`GraphQLObject`](src/main/scala/com/digitalasset/ui/backend/graphqless/GraphQLObject.scala)
|
||||
for complex types which have fields.
|
||||
|
||||
Common tasks
|
||||
------------
|
||||
|
||||
## How do I query the Backend without a frontend?
|
||||
|
||||
The UI backend exposes a [GraphiQL](https://github.com/graphql/graphiql) page to
|
||||
let the developer run queries interactively. You can find it at the address
|
||||
`<address_of_the_navigator_backend>/graphql`. Note the "Docs" link in the upper
|
||||
right corner and also that you have to be logged in order to run queries. You
|
||||
can either use the frontend in the same browser to login or you can login
|
||||
manually via command line as follows:
|
||||
|
||||
- Send a JSON POST request with the `userId` and make note of
|
||||
the cookie:
|
||||
|
||||
```bash
|
||||
> curl -H "Content-Type: application/json" -d'{ "userId":"PARTY" }' localhost:4000/api/session/ -i
|
||||
HTTP/1.1 200 OK
|
||||
Set-Cookie: session-id=8b4601d4-7113-407b-9b81-7fd5b213a96b; Path=/
|
||||
Server: pekko-http/1.0.0
|
||||
Date: Tue, 13 Jun 2017 16:47:38 GMT
|
||||
Content-Type: application/json
|
||||
Content-Length: 92
|
||||
|
||||
{"type":"session","user":{"id":"BANK1","role":"bank","party":"BANK1","canAdvanceTime":true}}
|
||||
```
|
||||
|
||||
- Open the GraphiQL page in a browser (`localhost:4000/graphql` for example).
|
||||
|
||||
- Write `javascript:document.cookie="session-id=<the_id_recceived>"` in the location bar.
|
||||
|
||||
- You can now use the GraphiQL to query your data.
|
||||
|
||||
We plan to add an option to run the server in unauthenticated mode so that these steps become unnecessary.
|
||||
|
||||
Session API
|
||||
-----------
|
||||
|
||||
In addition to the GraphQL endpoint exposing data, the user needs to act as
|
||||
some party. This is to know which party's "view" on the ledger to expose. The
|
||||
backend supplies the frontend with a list of available users.
|
||||
The chosen user is set in a cookie and therefore
|
||||
persists across reloads.
|
||||
|
||||
```typescript
|
||||
type UserId = string;
|
||||
type Party = string;
|
||||
type Role = string;
|
||||
type User = {
|
||||
id: UserId;
|
||||
party: Party;
|
||||
canAdvanceTime: boolean;
|
||||
role?: Role;
|
||||
};
|
||||
|
||||
type Status = Session | SignIn;
|
||||
type Session = { type: 'session'; user: User; }
|
||||
type SignIn = {
|
||||
type: 'sign-in';
|
||||
method: SignInMethod;
|
||||
error?: 'invalid-credentials';
|
||||
}
|
||||
|
||||
type SignInMethod = SignInSelect
|
||||
type SignInSelect = { type: 'select', users: UserId[] }
|
||||
```
|
||||
|
||||
```bash
|
||||
# Get current session or sign in
|
||||
GET /session/ => Status
|
||||
|
||||
# Sign in
|
||||
POST /session/ -d'{ userId: UserId }' => Status
|
||||
|
||||
# Sign out
|
||||
DELETE /session/ => SignIn
|
||||
```
|
||||
|
||||
Architecture notes
|
||||
------------------
|
||||
|
||||
This section contains short notes for anyone that wishes to make changes to the UI backend library.
|
||||
|
||||
- The backend defines its own internal representation of ledger objects
|
||||
- The backend communicates with the ledger via a JSON-based "ledger API"
|
||||
- The backend communicates with the frontend via a JSON-based "frontend API"
|
||||
- These are the relevant source files:
|
||||
- `model/Model`: Defines the internal representations of ledger objects
|
||||
- `model/Util`: (SDaml package) -> (internal model)
|
||||
- `graphql/JsonType`: (internal model) <-> (frontend API JSON format)
|
||||
- `store/platform/PlatformSubscriber`: (ledger API JSON format) <-> (internal model)
|
1
navigator/backend/scenarios/.gitignore
vendored
1
navigator/backend/scenarios/.gitignore
vendored
@ -1 +0,0 @@
|
||||
.package-database
|
@ -1,44 +0,0 @@
|
||||
### Navigator Testing Scenarios
|
||||
|
||||
The scenario(s) contained in this directory are useful to quickly spin up the sandbox and navigator from `HEAD` and visually inspect the navigator UI or perform other tests.
|
||||
|
||||
As an example, assuming your current working directory is the project root, you can:
|
||||
|
||||
1. open a terminal
|
||||
1. change your working directory to `navigator/backend/scenarios/rental`
|
||||
1. build the project
|
||||
|
||||
daml build
|
||||
|
||||
1. start the sandbox and run a scenario
|
||||
|
||||
bazel run --run_under="cd $PWD && " \
|
||||
//ledger/sandbox-classic:sandbox-classic-binary -- --scenario Main:example dist/rental.dar
|
||||
|
||||
1. open another terminal
|
||||
1. change your working directory to `navigator/backend/scenarios/rental`
|
||||
1. start the navigator
|
||||
|
||||
bazel run --run_under="cd $PWD && " \
|
||||
//navigator/backend:navigator-binary -- server
|
||||
|
||||
1. open a browser
|
||||
1. go to `localhost:4000`
|
||||
1. play around
|
||||
|
||||
#### Usage with TLS
|
||||
|
||||
If you want to use a secure channel (e.g.: to test against an authenticated sandbox), you can use the test certificates in ``//test-common/test-certificates``.
|
||||
|
||||
To use them, run the sandbox as follows:
|
||||
|
||||
bazel build //test-common/test-certificates
|
||||
bazel run --run_under="cd $PWD && " \
|
||||
//ledger/sandbox-classic:sandbox-classic-binary -- --scenario Main:example dist/rental.dar
|
||||
--pem $PWD/bazel-bin/test-common/test-certificates/server.pem --crt $PWD/bazel-bin/test-common/test-certificates/server.crt --cacrt $PWD/bazel-bin/test-common/test-certificates/ca.crt
|
||||
|
||||
And run navigator as follows:
|
||||
|
||||
bazel run --run_under="cd $PWD && " \
|
||||
//navigator/backend:navigator-binary -- server \
|
||||
--pem $PWD/bazel-bin/test-common/test-certificates/client.pem --crt $PWD/bazel-bin/test-common/test-certificates/client.crt --cacrt $PWD/bazel-bin/test-common/test-certificates/ca.crt
|
@ -1,18 +0,0 @@
|
||||
# Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
sdk-version: 0.0.0
|
||||
name: rental
|
||||
source: daml/Main.daml
|
||||
init-script: Main:example
|
||||
parties:
|
||||
- Betina_Beakley
|
||||
- Scrooge_McDuck
|
||||
- OPERATOR
|
||||
version: 1.0.0
|
||||
dependencies:
|
||||
- daml-prim
|
||||
- daml-stdlib
|
||||
- daml-script
|
||||
sandbox-options:
|
||||
- --static-time
|
@ -1,317 +0,0 @@
|
||||
-- Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
-- SPDX-License-Identifier: Apache-2.0
|
||||
{-# LANGUAGE ApplicativeDo #-}
|
||||
|
||||
module Main where
|
||||
|
||||
import DA.Date
|
||||
import DA.Time
|
||||
|
||||
import Daml.Script
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Basic example
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
template RightOfUseAgreement
|
||||
with
|
||||
landlord: Party
|
||||
tenant: Party
|
||||
address: Text
|
||||
expirationDate: Date
|
||||
where
|
||||
signatory landlord
|
||||
signatory tenant
|
||||
|
||||
key (tenant, address): (Party, Text)
|
||||
maintainer key._1
|
||||
|
||||
agreement
|
||||
show landlord <> " promises to let " <>
|
||||
show tenant <> " live at " <> show address <>
|
||||
" until " <> show expirationDate
|
||||
|
||||
template RightOfUseOffer
|
||||
with
|
||||
landlord: Party
|
||||
tenant: Party
|
||||
address: Text
|
||||
expirationDate: Date
|
||||
where
|
||||
signatory landlord
|
||||
observer tenant
|
||||
|
||||
choice Accept: ContractId RightOfUseAgreement
|
||||
controller tenant
|
||||
do
|
||||
create RightOfUseAgreement with landlord; tenant; address; expirationDate
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Helpers for creating lots of contracts
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
{-
|
||||
explode10 : Text -> List Text
|
||||
explode10 t = [
|
||||
t <> "0",
|
||||
t <> "1",
|
||||
t <> "2",
|
||||
t <> "3",
|
||||
t <> "4",
|
||||
t <> "5",
|
||||
t <> "6",
|
||||
t <> "7",
|
||||
t <> "8",
|
||||
t <> "9"
|
||||
]
|
||||
|
||||
explode10s : List Text -> List Text
|
||||
explode10s ts = concat (map explode10 ts)
|
||||
|
||||
oneString: List Text = cons "" nil
|
||||
|
||||
manyStrings = explode10s (explode10s (explode10s oneString))
|
||||
|
||||
pass : RelTime -> Script ()
|
||||
pass d = do
|
||||
t <- getTime
|
||||
setTime (addRelTime t d)
|
||||
|
||||
|
||||
createCounter : Party -> Text -> Script ()
|
||||
createCounter p i = do
|
||||
submit p do (createCmd Counter with index = i; owner = p)
|
||||
pass 1
|
||||
return ()
|
||||
createManyCounters : Party -> Script ()
|
||||
createManyCounters p = do
|
||||
mapS (\i -> createCounter p i) manyStrings
|
||||
return ()
|
||||
-}
|
||||
|
||||
template Counter
|
||||
with
|
||||
index: Text
|
||||
owner: Party
|
||||
where
|
||||
signatory owner
|
||||
|
||||
choice CArchive: ()
|
||||
controller owner
|
||||
do
|
||||
return ()
|
||||
|
||||
choice Recreate: ContractId Counter
|
||||
controller owner
|
||||
do
|
||||
create Counter with index; owner
|
||||
|
||||
|
||||
createCounters : Int -> Text -> Party -> Commands ()
|
||||
createCounters 0 t p = do
|
||||
createCmd Counter with index = t; owner = p
|
||||
pure ()
|
||||
createCounters i t p = do
|
||||
createCounters (i-1) (t <> "0") p
|
||||
createCounters (i-1) (t <> "1") p
|
||||
createCounters (i-1) (t <> "2") p
|
||||
createCounters (i-1) (t <> "3") p
|
||||
createCounters (i-1) (t <> "4") p
|
||||
createCounters (i-1) (t <> "5") p
|
||||
createCounters (i-1) (t <> "6") p
|
||||
createCounters (i-1) (t <> "7") p
|
||||
createCounters (i-1) (t <> "8") p
|
||||
createCounters (i-1) (t <> "9") p
|
||||
pure ()
|
||||
|
||||
-- Creates 10^i contracts
|
||||
createManyCounters : Int -> Party -> Script ()
|
||||
createManyCounters i p = submit p $ createCounters i "" p
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Types
|
||||
--------------------------------------------------------------------------------
|
||||
-- Stdlib types
|
||||
data UserMaybe a = UserNothing | UserJust a deriving (Eq, Show);
|
||||
data UserEither a b = UserLeft a | UserRight b deriving (Eq, Show);
|
||||
|
||||
-- Nested types
|
||||
data Record0 a = Record0
|
||||
with
|
||||
fprimitive: a
|
||||
flistR: [Record1 a]
|
||||
flistV: [UserMaybe a]
|
||||
flistL: [[a]]
|
||||
fvariantR: UserMaybe (Record1 a)
|
||||
fvariantV: UserMaybe (UserMaybe a)
|
||||
fvariantL: UserMaybe ([a])
|
||||
frecord: Record1 a
|
||||
deriving (Eq, Show)
|
||||
|
||||
data Record1 a = Record1
|
||||
with
|
||||
fprimitive: a
|
||||
flistR: [Record2 a]
|
||||
flistV: [UserMaybe a]
|
||||
flistL: [[a]]
|
||||
fvariantR: UserMaybe (Record2 a)
|
||||
fvariantV: UserMaybe (UserMaybe a)
|
||||
fvariantL: UserMaybe ([a])
|
||||
frecord: Record2 a
|
||||
deriving (Eq, Show)
|
||||
|
||||
data Record2 a = Record2
|
||||
with
|
||||
fprimitive: a
|
||||
flistR: [Record3 a]
|
||||
flistV: [UserMaybe a]
|
||||
flistL: [[a]]
|
||||
fvariantR: UserMaybe (Record3 a)
|
||||
fvariantV: UserMaybe (UserMaybe a)
|
||||
fvariantL: UserMaybe ([a])
|
||||
frecord: Record3 a
|
||||
deriving (Eq, Show)
|
||||
|
||||
data Record3 a = Record3
|
||||
with
|
||||
fprimitive: a
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- Example values
|
||||
valDate = date 1970 Jan 1
|
||||
valTime = time valDate 0 0 0
|
||||
valListT: [Text] = ["A"]
|
||||
valMaybeT: UserMaybe Text = UserJust "A"
|
||||
valRecord3T: Record3 Text = Record3 with
|
||||
fprimitive = "A"
|
||||
valRecord2T: Record2 Text = Record2 with
|
||||
fprimitive = "A"
|
||||
flistR = [valRecord3T]
|
||||
flistV = [valMaybeT]
|
||||
flistL = [valListT]
|
||||
fvariantR = UserJust valRecord3T
|
||||
fvariantV = UserJust valMaybeT
|
||||
fvariantL = UserJust valListT
|
||||
frecord = valRecord3T
|
||||
valRecord1T: Record1 Text = Record1 with
|
||||
fprimitive = "A"
|
||||
flistR = [valRecord2T]
|
||||
flistV = [valMaybeT]
|
||||
flistL = [valListT]
|
||||
fvariantR = UserJust valRecord2T
|
||||
fvariantV = UserJust valMaybeT
|
||||
fvariantL = UserJust valListT
|
||||
frecord = valRecord2T
|
||||
valRecord0T: Record0 Text = Record0 with
|
||||
fprimitive = "A"
|
||||
flistR = [valRecord1T]
|
||||
flistV = [valMaybeT]
|
||||
flistL = [valListT]
|
||||
fvariantR = UserJust valRecord1T
|
||||
fvariantV = UserJust valMaybeT
|
||||
fvariantL = UserJust valListT
|
||||
frecord = valRecord1T
|
||||
|
||||
template ParameterShowcase
|
||||
with
|
||||
foperator: Party
|
||||
finteger: Int
|
||||
fdecimal: Decimal
|
||||
ftext: Text
|
||||
fbool: Bool
|
||||
ftime: Time
|
||||
--freltime: RelTime
|
||||
fdate: Date
|
||||
fnestedTypes: Record0 Text
|
||||
fintegerList: [Int]
|
||||
fmaybeInteger: UserMaybe Int
|
||||
foptionInteger: Optional Int
|
||||
where
|
||||
signatory foperator
|
||||
|
||||
choice SCArchive: ()
|
||||
controller foperator
|
||||
do
|
||||
return ()
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Non-serializable types
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- Function
|
||||
data UnserializableF = UnserializableF {f : Int -> Int}
|
||||
|
||||
-- Tuple
|
||||
data UnserializableT = UnserializableT {x : (Int, Int)}
|
||||
|
||||
-- Forall (Daml 1.2)
|
||||
data Box = Box (forall a. a -> a)
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Recursive types
|
||||
--------------------------------------------------------------------------------
|
||||
data RTree a = Leaf a | Node {left: RTree a, right: RTree a} deriving (Eq, Show)
|
||||
data RList a = Nil | Cons {value: a, next: RList a} deriving (Eq, Show)
|
||||
|
||||
template TList
|
||||
with
|
||||
owner: Party
|
||||
value: RList Text
|
||||
where
|
||||
signatory owner
|
||||
|
||||
choice Append: ContractId TList
|
||||
with
|
||||
newValue : Text
|
||||
controller owner
|
||||
do
|
||||
create TList with owner; value = Cons {value=newValue, next=value}
|
||||
|
||||
createRList : Int -> Text -> RList Text
|
||||
createRList 0 t = Nil
|
||||
createRList i t = Cons {value=(t<>show i), next=(createRList (i-1) t)}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Script
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
example = do
|
||||
betina_Beakley <- allocatePartyWithHint "Betina_Beakley" (PartyIdHint "Betina_Beakley")
|
||||
scrooge_McDuck <- allocatePartyWithHint "Scrooge_McDuck" (PartyIdHint "Scrooge_McDuck")
|
||||
operator <- allocatePartyWithHint "OPERATOR" (PartyIdHint "OPERATOR")
|
||||
|
||||
-- Betina_Beakley offers Scrooge_McDuck room at McDuck Manor
|
||||
offer <- submit betina_Beakley do
|
||||
createCmd RightOfUseOffer with
|
||||
landlord = betina_Beakley
|
||||
tenant = scrooge_McDuck
|
||||
address = "McDuck Manor, Duckburg"
|
||||
expirationDate = valDate
|
||||
|
||||
-- Scrooge_McDuck accepts the offer, which creates a RightOfUseAgreement
|
||||
submit scrooge_McDuck do exerciseCmd offer Accept
|
||||
|
||||
-- Operator creates 10^3 contracts
|
||||
createManyCounters 3 operator
|
||||
|
||||
-- Complex contract
|
||||
submit operator do
|
||||
createCmd ParameterShowcase with
|
||||
foperator = operator
|
||||
finteger = 10
|
||||
fdecimal = 10.0
|
||||
ftext = "ABC"
|
||||
fbool = True
|
||||
ftime = valTime
|
||||
--freltime = seconds 120
|
||||
fdate = valDate
|
||||
fnestedTypes = valRecord0T
|
||||
fintegerList = [1, 2, 3]
|
||||
fmaybeInteger = UserJust 10
|
||||
foptionInteger = Some 10
|
||||
|
||||
-- Long recursive value
|
||||
submit operator do
|
||||
createCmd TList with
|
||||
owner = operator
|
||||
value = createRList 45 "X"
|
@ -1,11 +0,0 @@
|
||||
users {
|
||||
Betina_Beakley {
|
||||
party=Betina_Beakley
|
||||
}
|
||||
Scrooge_McDuck {
|
||||
party=Scrooge_McDuck
|
||||
}
|
||||
OPERATOR {
|
||||
party=OPERATOR
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
pekko {
|
||||
loggers = ["org.apache.pekko.event.slf4j.Slf4jLogger"]
|
||||
loglevel = "DEBUG"
|
||||
logging-filter = "org.apache.pekko.event.slf4j.Slf4jLoggingFilter"
|
||||
}
|
||||
|
||||
ui.backend {
|
||||
}
|
@ -1,151 +0,0 @@
|
||||
<!--
|
||||
* LICENSE AGREEMENT For GraphiQL software
|
||||
*
|
||||
* Facebook, Inc. (“Facebook”) owns all right, title and interest, including all
|
||||
* intellectual property and other proprietary rights, in and to the GraphiQL
|
||||
* software. Subject to your compliance with these terms, you are hereby granted a
|
||||
* non-exclusive, worldwide, royalty-free copyright license to (1) use and copy the
|
||||
* GraphiQL software; and (2) reproduce and distribute the GraphiQL software as
|
||||
* part of your own software (“Your Software”). Facebook reserves all rights not
|
||||
* expressly granted to you in this license agreement.
|
||||
*
|
||||
* THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS OR
|
||||
* IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. IN NO
|
||||
* EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICES, DIRECTORS OR EMPLOYEES BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||
* THE USE OF THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* You will include in Your Software (e.g., in the file(s), documentation or other
|
||||
* materials accompanying your software): (1) the disclaimer set forth above; (2)
|
||||
* this sentence; and (3) the following copyright notice:
|
||||
*
|
||||
* Copyright (c) 2015, Facebook, Inc. All rights reserved.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#graphiql {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/graphiql/0.8.0/graphiql.css" />
|
||||
<script src="//cdn.jsdelivr.net/es6-promise/4.0.5/es6-promise.auto.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/react/15.3.2/react.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/react/15.3.2/react-dom.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/graphiql/0.8.0/graphiql.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="graphiql">Loading...</div>
|
||||
|
||||
<script>
|
||||
|
||||
/**
|
||||
* This GraphiQL example illustrates how to use some of GraphiQL's props
|
||||
* in order to enable reading and updating the URL parameters, making
|
||||
* link sharing of queries a little bit easier.
|
||||
*
|
||||
* This is only one example of this kind of feature, GraphiQL exposes
|
||||
* various React params to enable interesting integrations.
|
||||
*/
|
||||
|
||||
// Parse the search string to get url parameters.
|
||||
var search = window.location.search;
|
||||
var parameters = {};
|
||||
search.substr(1).split('&').forEach(function (entry) {
|
||||
var eq = entry.indexOf('=');
|
||||
if (eq >= 0) {
|
||||
parameters[decodeURIComponent(entry.slice(0, eq))] =
|
||||
decodeURIComponent(entry.slice(eq + 1));
|
||||
}
|
||||
});
|
||||
|
||||
// if variables was provided, try to format it.
|
||||
if (parameters.variables) {
|
||||
try {
|
||||
parameters.variables =
|
||||
JSON.stringify(JSON.parse(parameters.variables), null, 2);
|
||||
} catch (e) {
|
||||
// Do nothing, we want to display the invalid JSON as a string, rather
|
||||
// than present an error.
|
||||
}
|
||||
}
|
||||
|
||||
// When the query and variables string is edited, update the URL bar so
|
||||
// that it can be easily shared
|
||||
function onEditQuery(newQuery) {
|
||||
parameters.query = newQuery;
|
||||
updateURL();
|
||||
}
|
||||
|
||||
function onEditVariables(newVariables) {
|
||||
parameters.variables = newVariables;
|
||||
updateURL();
|
||||
}
|
||||
|
||||
function onEditOperationName(newOperationName) {
|
||||
parameters.operationName = newOperationName;
|
||||
updateURL();
|
||||
}
|
||||
|
||||
function updateURL() {
|
||||
var newSearch = '?' + Object.keys(parameters).filter(function (key) {
|
||||
return Boolean(parameters[key]);
|
||||
}).map(function (key) {
|
||||
return encodeURIComponent(key) + '=' +
|
||||
encodeURIComponent(parameters[key]);
|
||||
}).join('&');
|
||||
history.replaceState(null, null, newSearch);
|
||||
}
|
||||
|
||||
// Defines a GraphQL fetcher using the fetch API.
|
||||
function graphQLFetcher(graphQLParams) {
|
||||
return fetch('/api/graphql', {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(graphQLParams),
|
||||
credentials: 'include',
|
||||
}).then(function (response) {
|
||||
return response.text();
|
||||
}).then(function (responseBody) {
|
||||
try {
|
||||
return JSON.parse(responseBody);
|
||||
} catch (error) {
|
||||
return responseBody;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Render <GraphiQL /> into the body.
|
||||
ReactDOM.render(
|
||||
React.createElement(GraphiQL, {
|
||||
fetcher: graphQLFetcher,
|
||||
query: parameters.query,
|
||||
variables: parameters.variables,
|
||||
operationName: parameters.operationName,
|
||||
onEditQuery: onEditQuery,
|
||||
onEditVariables: onEditVariables,
|
||||
onEditOperationName: onEditOperationName
|
||||
}),
|
||||
document.getElementById('graphiql')
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,37 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<target>System.err</target>
|
||||
<encoder>
|
||||
<pattern>%msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>navigator.log</file>
|
||||
<param name="Append" value="false" />
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg %mdc%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
|
||||
<appender-ref ref="FILE"/>
|
||||
</appender>
|
||||
|
||||
<!-- Some important messages are written to stderr -->
|
||||
<logger name="user-facing-logs" level="INFO" additivity="true">
|
||||
<appender-ref ref="STDERR"/>
|
||||
</logger>
|
||||
|
||||
<!-- All log messages are written asynchronously to a file -->
|
||||
<root level="INFO">
|
||||
<appender-ref ref="ASYNC"/>
|
||||
</root>
|
||||
</configuration>
|
@ -1 +0,0 @@
|
||||
org.slf4j.simpleLogger.defaultLogLevel = debug
|
@ -1,17 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator
|
||||
|
||||
import org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
|
||||
import spray.json.{DefaultJsonProtocol, RootJsonFormat}
|
||||
|
||||
case class ApplicationInfo(
|
||||
id: String,
|
||||
name: String,
|
||||
version: String,
|
||||
)
|
||||
|
||||
trait ApplicationInfoJsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
|
||||
implicit val applicationInfoFormat: RootJsonFormat[ApplicationInfo] = jsonFormat3(ApplicationInfo)
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator
|
||||
|
||||
import org.apache.pekko.actor.ActorRef
|
||||
import org.apache.pekko.http.scaladsl.model.StatusCode
|
||||
import org.apache.pekko.http.scaladsl.model.StatusCodes._
|
||||
import com.daml.navigator.graphql._
|
||||
import com.daml.navigator.graphql.SprayMarshallers._
|
||||
import com.daml.navigator.model.PartyState
|
||||
import com.daml.navigator.store.Store.StoreException
|
||||
import com.typesafe.scalalogging.LazyLogging
|
||||
import sangria.ast.Document
|
||||
import sangria.execution._
|
||||
import sangria.parser.QueryParser
|
||||
import sangria.renderer.SchemaRenderer
|
||||
import sangria.schema.Schema
|
||||
import spray.json._
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.Try
|
||||
|
||||
case class ParseResult(ast: Document, operationName: Option[String], variables: JsValue)
|
||||
|
||||
/** Provides a way of executing GraphQL queries.
|
||||
*/
|
||||
trait GraphQLHandler {
|
||||
def schema: Schema[GraphQLContext, Unit]
|
||||
def parse(request: String): Try[ParseResult]
|
||||
def parse(request: JsValue): Try[ParseResult]
|
||||
def executeQuery(parsed: ParseResult, party: PartyState): Future[(StatusCode, JsValue)]
|
||||
def renderSchema: String
|
||||
}
|
||||
|
||||
object GraphQLHandler {
|
||||
type ParseQuery = JsValue => Try[ParseResult]
|
||||
type ExecuteQuery = (ParseResult, PartyState) => Future[(StatusCode, JsValue)]
|
||||
type CustomEndpoints = Set[CustomEndpoint[_]]
|
||||
}
|
||||
|
||||
case class DefaultGraphQLHandler(
|
||||
customEndpoints: GraphQLHandler.CustomEndpoints,
|
||||
platformStore: Option[ActorRef],
|
||||
)(implicit
|
||||
executionContext: ExecutionContext
|
||||
) extends GraphQLHandler
|
||||
with LazyLogging {
|
||||
|
||||
def schema: Schema[GraphQLContext, Unit] = new GraphQLSchema(customEndpoints).QuerySchema
|
||||
|
||||
def parse(request: String): Try[ParseResult] =
|
||||
Try(request.parseJson).flatMap(parse)
|
||||
|
||||
def parse(request: JsValue): Try[ParseResult] =
|
||||
for {
|
||||
fields <- Try(request.asJsObject.fields)
|
||||
JsString(query) <- Try(fields("query"))
|
||||
operationName = fields.get("operationName").collect { case JsString(value) =>
|
||||
value
|
||||
}
|
||||
vars: JsValue = fields.get("variables") match {
|
||||
case Some(obj: JsObject) => obj
|
||||
case _ => JsObject.empty
|
||||
}
|
||||
ast <- QueryParser.parse(query)
|
||||
} yield ParseResult(ast, operationName, vars)
|
||||
|
||||
def executeQuery(parsed: ParseResult, party: PartyState): Future[(StatusCode, JsValue)] = {
|
||||
platformStore.fold[Future[(StatusCode, JsValue)]](
|
||||
Future.successful(InternalServerError -> JsString("Platform store not available"))
|
||||
)(store => {
|
||||
val context = GraphQLContext(party, store)
|
||||
Executor
|
||||
.execute(
|
||||
schema,
|
||||
parsed.ast,
|
||||
context,
|
||||
variables = parsed.variables,
|
||||
operationName = parsed.operationName,
|
||||
exceptionHandler = ExceptionHandler { case (_, StoreException(message)) =>
|
||||
HandledException(message)
|
||||
},
|
||||
)
|
||||
.map(OK -> _)
|
||||
.recover {
|
||||
case error: QueryAnalysisError =>
|
||||
logger.warn(s"GraphQL analysis error ${error.getMessage}.")
|
||||
BadRequest -> error.resolveError
|
||||
case error: ErrorWithResolver =>
|
||||
logger.error("Failed to execute GraphQL query", error)
|
||||
InternalServerError -> error.resolveError
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
def renderSchema: String = SchemaRenderer.renderSchema(schema)
|
||||
}
|
@ -1,123 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator
|
||||
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import org.apache.pekko.actor.ActorRef
|
||||
import org.apache.pekko.pattern.ask
|
||||
import org.apache.pekko.util.Timeout
|
||||
import com.daml.navigator.config.Arguments
|
||||
import com.daml.navigator.store.Store._
|
||||
import spray.json._
|
||||
import DefaultJsonProtocol._
|
||||
import com.daml.navigator.store.Store
|
||||
import com.daml.navigator.time.TimeProviderType
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
/** Provides a way of getting debug information about the application state.
|
||||
* The information is returned in an opaque JSON format,
|
||||
* and is useful for displaying debug information in the frontend.
|
||||
*/
|
||||
trait InfoHandler {
|
||||
def getInfo: Future[JsValue]
|
||||
}
|
||||
|
||||
case class DefaultInfoHandler(arguments: Arguments, platformStore: ActorRef)(implicit
|
||||
executionContext: ExecutionContext
|
||||
) extends InfoHandler {
|
||||
|
||||
private case class Info(arguments: Arguments, appInfo: Either[String, ApplicationStateInfo])
|
||||
private implicit object argumentsWriter extends RootJsonWriter[Arguments] {
|
||||
override def write(obj: Arguments): JsValue = JsObject(
|
||||
"port" -> obj.port.toJson,
|
||||
"assets" -> obj.assets.toJson,
|
||||
"time" -> TimeProviderType.write(obj.time).toJson,
|
||||
"configFile" -> obj.configFile.map(p => p.toString).toJson,
|
||||
"tlsConfig" -> obj.tlsConfig
|
||||
.fold[JsValue](JsNull)(c =>
|
||||
JsObject(
|
||||
"enabled" -> c.enabled.toJson,
|
||||
"privateKeyFile" -> c.privateKeyFile.fold[JsValue](JsNull)(f => JsString(f.toString)),
|
||||
"certChainFile" -> c.certChainFile.fold[JsValue](JsNull)(f => JsString(f.toString)),
|
||||
"trustCollectionFile" -> c.trustCollectionFile.fold[JsValue](JsNull)(f =>
|
||||
JsString(f.toString)
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
private implicit object actorInfoWriter extends RootJsonWriter[PartyActorInfo] {
|
||||
override def write(obj: PartyActorInfo): JsValue = obj match {
|
||||
case _: PartyActorStarting => JsString("Starting")
|
||||
case _: PartyActorStarted => JsString("Started")
|
||||
case info: PartyActorFailed => JsString(s"Failed: ${info.error.getMessage}")
|
||||
}
|
||||
}
|
||||
private implicit object actorResponseWriter extends RootJsonWriter[PartyActorResponse] {
|
||||
override def write(obj: PartyActorResponse): JsValue = obj match {
|
||||
case PartyActorRunning(info) => info.toJson
|
||||
case Store.PartyActorUnresponsive => JsString("Unresponsive")
|
||||
}
|
||||
}
|
||||
private implicit object appInfoWriter extends RootJsonWriter[ApplicationStateInfo] {
|
||||
override def write(obj: ApplicationStateInfo): JsValue = obj match {
|
||||
case info: ApplicationStateConnecting =>
|
||||
JsObject(
|
||||
"status" -> JsString("Connecting"),
|
||||
"platformHost" -> info.platformHost.toJson,
|
||||
"platformPort" -> info.platformPort.toJson,
|
||||
"tls" -> info.tls.toJson,
|
||||
"applicationId" -> info.applicationId.toJson,
|
||||
)
|
||||
case info: ApplicationStateConnected =>
|
||||
JsObject(
|
||||
"status" -> JsString("Connected"),
|
||||
"platformHost" -> info.platformHost.toJson,
|
||||
"platformPort" -> info.platformPort.toJson,
|
||||
"tls" -> info.tls.toJson,
|
||||
"applicationId" -> info.applicationId.toJson,
|
||||
"ledgerId" -> info.ledgerId.toJson,
|
||||
"ledgerTime" -> JsObject(
|
||||
"time" -> DateTimeFormatter.ISO_INSTANT
|
||||
.format(info.ledgerTime.time.getCurrentTime)
|
||||
.toJson,
|
||||
"type" -> TimeProviderType.write(info.ledgerTime.`type`).toJson,
|
||||
),
|
||||
"partyActors" -> JsObject(
|
||||
info.partyActors.map { case (p, s) => p -> s.toJson }.toMap
|
||||
),
|
||||
)
|
||||
case info: ApplicationStateFailed =>
|
||||
JsObject(
|
||||
"status" -> JsString("Failed"),
|
||||
"platformHost" -> info.platformHost.toJson,
|
||||
"platformPort" -> info.platformPort.toJson,
|
||||
"tls" -> info.tls.toJson,
|
||||
"applicationId" -> info.applicationId.toJson,
|
||||
"error" -> info.error.toString.toJson,
|
||||
)
|
||||
}
|
||||
}
|
||||
private implicit object infoWriter extends RootJsonWriter[Info] {
|
||||
override def write(obj: Info): JsValue = JsObject(
|
||||
"arguments" -> obj.arguments.toJson,
|
||||
"appInfo" -> obj.appInfo.fold(_.toJson, _.toJson),
|
||||
)
|
||||
}
|
||||
|
||||
private def getStoreInfo: Future[ApplicationStateInfo] = {
|
||||
implicit val actorTimeout: Timeout = Timeout(5, TimeUnit.SECONDS)
|
||||
(platformStore ? GetApplicationStateInfo).mapTo[ApplicationStateInfo]
|
||||
}
|
||||
|
||||
def getInfo: Future[JsValue] = {
|
||||
getStoreInfo
|
||||
.map(i => Info(arguments, Right(i)))
|
||||
.recover[Info] { case e: Throwable => Info(arguments, Left(e.getMessage)) }
|
||||
.map(i => i.toJson)
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator
|
||||
|
||||
import java.nio.file.{Files, Paths}
|
||||
import java.util.UUID
|
||||
|
||||
import org.apache.pekko.http.scaladsl.model.StatusCodes
|
||||
import org.apache.pekko.http.scaladsl.server.Directives._
|
||||
import org.apache.pekko.http.scaladsl.server.Route
|
||||
import com.daml.buildinfo.BuildInfo
|
||||
|
||||
object NavigatorBackend extends UIBackend {
|
||||
|
||||
private val configFile = "frontend-config.js"
|
||||
override def customEndpoints: Set[CustomEndpoint[_]] = Set()
|
||||
override def customRoutes: List[Route] = List(frontendConfigRoute)
|
||||
override def applicationInfo: ApplicationInfo = ApplicationInfo(
|
||||
id = s"Navigator-${UUID.randomUUID().toString}",
|
||||
name = "Navigator",
|
||||
version = BuildInfo.Version,
|
||||
)
|
||||
override def banner: Option[String] =
|
||||
Some(
|
||||
raw""" _ __ _ __
|
||||
| / |/ /__ __ __(_)__ ____ _/ /____ ____
|
||||
| / / _ `/ |/ / / _ `/ _ `/ __/ _ \/ __/
|
||||
|/_/|_/\_,_/|___/_/\_, /\_,_/\__/\___/_/
|
||||
| /___/
|
||||
|Version """.stripMargin + applicationInfo.version
|
||||
)
|
||||
|
||||
/** Frontend config file */
|
||||
private val frontendConfigRoute: Route = {
|
||||
path("api" / "config") {
|
||||
if (Files.exists(Paths.get(configFile)))
|
||||
getFromFile(configFile)
|
||||
else
|
||||
complete(StatusCodes.NotFound)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator
|
||||
|
||||
import scala.reflect.ClassTag
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/** A reading failure */
|
||||
final case class ReadFailed(from: String)
|
||||
|
||||
/** A simple `Read` typeclass */
|
||||
sealed trait Read[To] {
|
||||
|
||||
/** Convert `from` to a value of type `To`
|
||||
*
|
||||
* @return `Right` wrapping an instance of `To` in case of success in reading, a `Left` wrapping a `ReadFailed`
|
||||
* otherwise
|
||||
*/
|
||||
def from(from: String): Either[ReadFailed, To]
|
||||
}
|
||||
|
||||
object Read {
|
||||
def apply[To](implicit readTo: Read[To]): Read[To] = readTo
|
||||
|
||||
/** Utility function equivalent to calling `new Read` but more convenient */
|
||||
def fromFunction[To](f: String => Either[ReadFailed, To]): Read[To] =
|
||||
new Read[To] {
|
||||
override def from(from: String): Either[ReadFailed, To] = f(from)
|
||||
}
|
||||
|
||||
/** A failure while reading that wraps the failure inside a `Left` */
|
||||
def fail[To](implicit classTag: ClassTag[To]): Either[ReadFailed, To] =
|
||||
Left(ReadFailed(classTag.runtimeClass.getSimpleName))
|
||||
|
||||
/** Catches the exceptions thrown while read a value of type `To` from a `String`
|
||||
*/
|
||||
def fromUnsafeFunction[To](f: String => To)(implicit classTag: ClassTag[To]): Read[To] =
|
||||
fromFunction[To] { str =>
|
||||
Try(f(str)) match {
|
||||
case Success(str) => Right(str)
|
||||
case Failure(_) => Read.fail[To](classTag)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val readString: Read[String] = Read.fromFunction[String](str => Right(str))
|
||||
implicit val readBoolean: Read[Boolean] = Read.fromUnsafeFunction[Boolean](_.toBoolean)
|
||||
implicit val readInt: Read[Int] = Read.fromUnsafeFunction[Int](_.toInt)
|
||||
implicit val readFloat: Read[Float] = Read.fromUnsafeFunction[Float](_.toFloat)
|
||||
implicit val readDouble: Read[Double] = Read.fromUnsafeFunction[Double](_.toDouble)
|
||||
}
|
@ -1,282 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator
|
||||
|
||||
import com.daml.navigator.model.PartyState
|
||||
import scalaz.Tag
|
||||
|
||||
import scala.collection.immutable
|
||||
import spray.json._
|
||||
|
||||
import scala.reflect.ClassTag
|
||||
|
||||
final case class User(
|
||||
id: String,
|
||||
party: PartyState,
|
||||
// TODO: where is `role` used? frontend has some references, but doesn't seem to impact anything?
|
||||
role: Option[String] = None,
|
||||
canAdvanceTime: Boolean = true,
|
||||
)
|
||||
|
||||
sealed trait SignInError
|
||||
case object InvalidCredentials extends SignInError
|
||||
case object NotConnected extends SignInError
|
||||
case object Unresponsive extends SignInError
|
||||
case object Unknown extends SignInError
|
||||
|
||||
sealed abstract class Status
|
||||
final case class Session(user: User) extends Status
|
||||
final case class SignIn(method: SignInMethod, error: Option[SignInError] = None) extends Status
|
||||
|
||||
sealed abstract class SignInMethod
|
||||
final case class SignInSelect(userIds: Set[String]) extends SignInMethod
|
||||
|
||||
case class LoginRequest(userId: String)
|
||||
|
||||
/** Maintains session information for browser clients. */
|
||||
object Session {
|
||||
private var sessions: immutable.Map[String, Session] = immutable.Map()
|
||||
|
||||
def current(sessionId: String): Option[Session] = sessions.get(sessionId)
|
||||
|
||||
def open(
|
||||
sessionId: String,
|
||||
userId: String,
|
||||
userRole: Option[String],
|
||||
state: PartyState,
|
||||
): Session = {
|
||||
val user = Session(User(userId, state, userRole))
|
||||
sessions += sessionId -> user
|
||||
user
|
||||
}
|
||||
|
||||
def close(sessionId: String): Unit = {
|
||||
sessions -= sessionId
|
||||
}
|
||||
|
||||
// method for debugging and testing purposes
|
||||
private[navigator] def clean(): Unit = synchronized {
|
||||
this.sessions = immutable.Map()
|
||||
}
|
||||
}
|
||||
|
||||
object SessionJsonProtocol extends DefaultJsonProtocol {
|
||||
val typeFieldName = "type"
|
||||
val userFieldName = "user"
|
||||
val usersFieldName = "users"
|
||||
val methodFieldName = "method"
|
||||
val errorFieldName = "error"
|
||||
val idFieldName = "id"
|
||||
val roleFieldName = "role"
|
||||
val partyFieldName = "party"
|
||||
val canAdvanceTimeFieldName = "canAdvanceTime"
|
||||
val sessionType = JsString("session")
|
||||
val selectType = JsString("select")
|
||||
val signInType = JsString("sign-in")
|
||||
|
||||
implicit object partyWriter extends RootJsonWriter[PartyState] {
|
||||
override def write(obj: PartyState): JsValue = Tag.unwrap(obj.name).toJson
|
||||
}
|
||||
|
||||
implicit object userWriter extends RootJsonWriter[User] {
|
||||
override def write(obj: User): JsValue =
|
||||
obj.role match {
|
||||
case None =>
|
||||
JsObject(
|
||||
idFieldName -> obj.id.toJson,
|
||||
partyFieldName -> obj.party.toJson,
|
||||
canAdvanceTimeFieldName -> obj.canAdvanceTime.toJson,
|
||||
)
|
||||
case Some(role) =>
|
||||
JsObject(
|
||||
idFieldName -> obj.id.toJson,
|
||||
roleFieldName -> role.toJson,
|
||||
partyFieldName -> obj.party.toJson,
|
||||
canAdvanceTimeFieldName -> obj.canAdvanceTime.toJson,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
implicit object loginRequestFormat extends RootJsonFormat[LoginRequest] {
|
||||
override def write(obj: LoginRequest): JsValue =
|
||||
JsObject("userId" -> obj.userId.toJson)
|
||||
|
||||
override def read(json: JsValue): LoginRequest = {
|
||||
val obj =
|
||||
json.asJsObject(errorMsg =
|
||||
s"LoginRequest should be an object but ${json.getClass.getCanonicalName} found"
|
||||
)
|
||||
obj.fields.get("userId") match {
|
||||
case Some(JsString(userId)) => LoginRequest(userId)
|
||||
case _ =>
|
||||
throw DeserializationException(
|
||||
s"LoginRequest should contain a field called 'userId' of type String, got $json"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicit object sessionWriter extends RootJsonWriter[Session] {
|
||||
override def write(obj: Session): JsValue = {
|
||||
JsObject(
|
||||
typeFieldName -> sessionType,
|
||||
userFieldName -> obj.user.toJson,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
implicit object statusWriter extends RootJsonWriter[Status] {
|
||||
override def write(obj: Status): JsValue =
|
||||
obj match {
|
||||
case session: Session => sessionWriter.write(session)
|
||||
case signIn: SignIn => signInFormat.write(signIn)
|
||||
}
|
||||
}
|
||||
|
||||
implicit object signInSelectJsonFormat extends RootJsonFormat[SignInSelect] {
|
||||
|
||||
def fromFields(fields: Map[String, JsValue]): SignInSelect =
|
||||
fields.get(usersFieldName) match {
|
||||
case None =>
|
||||
throw DeserializationException(
|
||||
s"Cannot decode an instance of " +
|
||||
s"${SignInSelect.getClass.getCanonicalName} because the field $usersFieldName is missing"
|
||||
)
|
||||
case Some(rawUserIds) =>
|
||||
SignInSelect(userIds = rawUserIds.convertTo[Set[String]])
|
||||
}
|
||||
|
||||
override def read(json: JsValue): SignInSelect =
|
||||
deserialize2(json, selectType)(fromFields)
|
||||
|
||||
override def write(obj: SignInSelect): JsValue =
|
||||
JsObject(
|
||||
typeFieldName -> selectType,
|
||||
usersFieldName -> obj.userIds.toJson,
|
||||
)
|
||||
}
|
||||
|
||||
implicit object signInMethodFormat extends JsonFormat[SignInMethod] {
|
||||
override def read(json: JsValue): SignInMethod =
|
||||
deserialize(json) {
|
||||
case (`selectType`, fields) =>
|
||||
signInSelectJsonFormat.fromFields(fields)
|
||||
case _ => throw DeserializationException(s"Unknown sign in method")
|
||||
}
|
||||
|
||||
override def write(obj: SignInMethod): JsValue = obj match {
|
||||
case select: SignInSelect =>
|
||||
select.toJson
|
||||
}
|
||||
}
|
||||
|
||||
implicit object signInFormat extends RootJsonFormat[SignIn] {
|
||||
override def write(obj: SignIn): JsValue =
|
||||
obj.error.fold(
|
||||
JsObject(
|
||||
typeFieldName -> signInType,
|
||||
methodFieldName -> obj.method.toJson,
|
||||
)
|
||||
)(error =>
|
||||
JsObject(
|
||||
typeFieldName -> signInType,
|
||||
methodFieldName -> obj.method.toJson,
|
||||
errorFieldName -> JsString(error match {
|
||||
case InvalidCredentials => "invalid-credentials"
|
||||
case NotConnected => "not-connected"
|
||||
case Unresponsive => "unresponsive"
|
||||
case Unknown => "unknown-error"
|
||||
}),
|
||||
)
|
||||
)
|
||||
|
||||
def fromFields(fields: Map[String, JsValue]): SignIn = {
|
||||
(fields.get(methodFieldName), fields.get(errorFieldName)) match {
|
||||
case (None, _) =>
|
||||
throw DeserializationException(
|
||||
s"Cannot decode an instance of " +
|
||||
s"${SignIn.getClass.getCanonicalName} because the field $methodFieldName is missing"
|
||||
)
|
||||
case (Some(rawMethod), maybeError) =>
|
||||
val method = signInMethodFormat.read(rawMethod)
|
||||
SignIn(
|
||||
method,
|
||||
maybeError.map {
|
||||
case JsString("invalid-credentials") => InvalidCredentials
|
||||
case JsString("not-connected") => NotConnected
|
||||
case JsString("unresponsive") => Unresponsive
|
||||
case _ => Unknown
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override def read(json: JsValue): SignIn =
|
||||
deserialize2(json, signInType)(fromFields)
|
||||
}
|
||||
|
||||
/** Convert a json object following our encoding to an instance of `T` by using a function from the 'type' and the
|
||||
* fields in the json object to the type `T`. Throws `DeserializationException` if the conversion is not possible.
|
||||
*
|
||||
* Our encoding of objects follows the rules:
|
||||
* - the encoded json must be a `JsObject`
|
||||
* - the encoded json must have a field `'type'` with the name of the type encoded. `T` must match it.
|
||||
*/
|
||||
private def deserialize[T](
|
||||
json: JsValue
|
||||
)(f: (JsString, Map[String, JsValue]) => T)(implicit tag: ClassTag[T]): T = {
|
||||
val jsObject =
|
||||
json
|
||||
.asJsObject(errorMsg =
|
||||
s"JSON object required to deserialize an instance of ${tag.runtimeClass.getCanonicalName}"
|
||||
)
|
||||
val typeValue = fieldOrThrow(jsObject, typeFieldName)(jsValueAsJsString)
|
||||
f(typeValue, jsObject.fields)
|
||||
}
|
||||
|
||||
/** Similar to [[deserialize]] but checks that the fields 'type' has the expected type.
|
||||
*/
|
||||
private def deserialize2[T](json: JsValue, typeExpected: JsString)(
|
||||
f: Map[String, JsValue] => T
|
||||
)(implicit tag: ClassTag[T]): T = {
|
||||
deserialize[T](json) { case (typeStr, fields) =>
|
||||
if (typeExpected != typeStr) {
|
||||
throw new DeserializationException(
|
||||
s"Cannot deserialize an instance of ${tag.runtimeClass.getCanonicalName} because the wrong type" +
|
||||
s"${typeStr.prettyPrint} has been found instead of the expected type ${typeExpected.prettyPrint}"
|
||||
)
|
||||
} else {
|
||||
f(fields)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Tries to retrieve a field from a `jsObject` and convert it to `T` if `f` is given. Throws
|
||||
* `DeserializationException` if the `jsObject` doesn't contain the field
|
||||
*/
|
||||
private def fieldOrThrow[T](jsObject: JsObject, fieldName: String)(
|
||||
f: JsValue => T
|
||||
)(implicit tag: ClassTag[T]): T =
|
||||
jsObject.fields
|
||||
.get(fieldName)
|
||||
.map(f)
|
||||
.getOrElse(
|
||||
throw DeserializationException(
|
||||
s"JSON object with a '$fieldName' field is required to deserialize an " +
|
||||
s"instance of ${tag.runtimeClass.getCanonicalName}. jsObject found ${jsObject.compactPrint}"
|
||||
)
|
||||
)
|
||||
|
||||
/** Convert the jsValue to a `JsString` if jsValue is a `JsString`, otherwise throws `DeserializationException`
|
||||
*/
|
||||
private def jsValueAsJsString(jsValue: JsValue): JsString =
|
||||
jsValue match {
|
||||
case js: JsString => js
|
||||
case _ =>
|
||||
throw DeserializationException(
|
||||
s"JsString expected, but found ${jsValue.compactPrint} of type " +
|
||||
jsValue.getClass.getCanonicalName
|
||||
)
|
||||
}
|
||||
}
|
@ -1,403 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator
|
||||
|
||||
import java.nio.file.{Files, Path, Paths}
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.stream.Collectors
|
||||
|
||||
import org.apache.pekko.actor.{ActorSystem, Cancellable}
|
||||
import org.apache.pekko.http.scaladsl.Http
|
||||
import org.apache.pekko.http.scaladsl.model.HttpHeader
|
||||
import org.apache.pekko.http.scaladsl.model.StatusCodes._
|
||||
import org.apache.pekko.http.scaladsl.model.headers.CacheDirectives.{
|
||||
`max-age`,
|
||||
`no-cache`,
|
||||
immutableDirective,
|
||||
}
|
||||
import org.apache.pekko.http.scaladsl.model.headers.{Cookie, EntityTag, HttpCookie, `Cache-Control`}
|
||||
import org.apache.pekko.http.scaladsl.server.Directives._
|
||||
import org.apache.pekko.http.scaladsl.server.Route
|
||||
import org.apache.pekko.http.scaladsl.settings.RoutingSettings
|
||||
import org.apache.pekko.http.scaladsl.settings.ServerSettings
|
||||
import org.apache.pekko.pattern.ask
|
||||
import org.apache.pekko.util.Timeout
|
||||
import com.daml.grpc.GrpcException
|
||||
import com.daml.navigator.SessionJsonProtocol._
|
||||
import com.daml.navigator.config._
|
||||
import com.daml.navigator.graphql.GraphQLContext
|
||||
import com.daml.navigator.graphqless.GraphQLObject
|
||||
import com.daml.navigator.model.{Ledger, PackageRegistry, PartyState}
|
||||
import com.daml.navigator.store.Store
|
||||
import com.daml.navigator.store.Store._
|
||||
import com.daml.navigator.store.platform.PlatformStore
|
||||
import com.daml.scalautil.Statement.discard
|
||||
import com.typesafe.scalalogging.LazyLogging
|
||||
import org.slf4j.LoggerFactory
|
||||
import sangria.schema._
|
||||
import spray.json._
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, ExecutionContext, Future}
|
||||
import scala.util.control.NonFatal
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/** Base abstract class for UI backends.
|
||||
*
|
||||
* A new UI backend can be implemented by extending [[UIBackend]] and by providing
|
||||
* the [[customEndpoints]], [[customRoutes]], [[applicationInfo]] definitions.
|
||||
*/
|
||||
abstract class UIBackend extends LazyLogging with ApplicationInfoJsonSupport {
|
||||
|
||||
def customEndpoints: Set[CustomEndpoint[_]]
|
||||
def customRoutes: List[Route]
|
||||
def applicationInfo: ApplicationInfo
|
||||
def banner: Option[String] = None
|
||||
|
||||
private[this] def userFacingLogger = LoggerFactory.getLogger("user-facing-logs")
|
||||
|
||||
/** Allow subclasses to customize config file name, but don't require it
|
||||
* (supply a default)
|
||||
*/
|
||||
def defaultConfigFile: Path = Paths.get("ui-backend.conf")
|
||||
|
||||
private[navigator] def getRoute(
|
||||
system: ActorSystem,
|
||||
arguments: Arguments,
|
||||
graphQL: GraphQLHandler,
|
||||
info: InfoHandler,
|
||||
getAppState: () => Future[ApplicationStateInfo],
|
||||
): Route = {
|
||||
|
||||
def openSession(userId: String, userRole: Option[String], state: PartyState): Route = {
|
||||
val sessionId = UUID.randomUUID().toString
|
||||
setCookie(HttpCookie("session-id", sessionId, path = Some("/"))) {
|
||||
complete(Session.open(sessionId, userId, userRole, state))
|
||||
}
|
||||
}
|
||||
|
||||
def findSession(httpHeader: HttpHeader): Option[(String, Session)] = httpHeader match {
|
||||
case Cookie(cookies) =>
|
||||
cookies
|
||||
.filter(_.name == "session-id")
|
||||
.flatMap(cookiePair =>
|
||||
Session.current(cookiePair.value).map(cookiePair.value -> _).toList
|
||||
)
|
||||
.headOption
|
||||
case _ =>
|
||||
None
|
||||
}
|
||||
|
||||
def signIn(): Route =
|
||||
onSuccess(getAppState()) {
|
||||
case ApplicationStateConnecting(_, _, _, _) =>
|
||||
complete(SignIn(SignInSelect(Set.empty), Some(NotConnected)))
|
||||
case ApplicationStateConnected(_, _, _, _, _, _, partyActors) =>
|
||||
complete(SignIn(SignInSelect(partyActors.keySet), None))
|
||||
case ApplicationStateFailed(_, _, _, _, _) =>
|
||||
complete(SignIn(SignInSelect(Set.empty), Some(Unknown)))
|
||||
}
|
||||
|
||||
// A resource with content that may change.
|
||||
// Users can quickly switch between Navigator versions, so we don't want to cache this resource for any amount of time.
|
||||
// Note: The server still uses E-Tags, so repeated fetches will complete with a "304: Not Modified".
|
||||
def mutableResource = respondWithHeader(`Cache-Control`(`no-cache`))
|
||||
// Add an ETag with value equal to the version used to build the application.
|
||||
// Use as a cheap ETag for resources that may change between builds.
|
||||
def versionETag = conditional(EntityTag(applicationInfo.version))
|
||||
// A resource that is never going to change. Its path must use some kind of content hash.
|
||||
// Such a resource may be cached indefinitely.
|
||||
def immutableResource =
|
||||
respondWithHeaders(`Cache-Control`(`max-age`(31536000), immutableDirective))
|
||||
val noFileGetConditional = RoutingSettings(system).withFileGetConditional(false)
|
||||
def withoutFileETag = withSettings(noFileGetConditional)
|
||||
|
||||
optionalHeaderValue(findSession) { session =>
|
||||
{
|
||||
// Custom routes
|
||||
customRoutes.foldLeft[Route](reject)(_ ~ _) ~
|
||||
// Built-in API routes
|
||||
pathPrefix("api") {
|
||||
path("about") {
|
||||
complete(applicationInfo)
|
||||
} ~
|
||||
path("session"./) {
|
||||
get {
|
||||
session match {
|
||||
case Some((_, session)) => complete(session)
|
||||
case None => signIn()
|
||||
}
|
||||
} ~
|
||||
post {
|
||||
entity(as[LoginRequest]) { request =>
|
||||
onSuccess(getAppState()) {
|
||||
case ApplicationStateConnecting(_, _, _, _) =>
|
||||
complete(SignIn(SignInSelect(Set.empty), Some(NotConnected)))
|
||||
case ApplicationStateConnected(_, _, _, _, _, _, partyActors) =>
|
||||
partyActors.get(request.userId) match {
|
||||
case Some(resp) =>
|
||||
resp match {
|
||||
case PartyActorRunning(info) =>
|
||||
openSession(request.userId, info.state.userRole, info.state)
|
||||
case Store.PartyActorUnresponsive =>
|
||||
complete(
|
||||
SignIn(SignInSelect(partyActors.keySet), Some(Unresponsive))
|
||||
)
|
||||
}
|
||||
case None =>
|
||||
logger.error(
|
||||
s"Attempt to signin with non-existent user ${request.userId}"
|
||||
)
|
||||
complete(
|
||||
SignIn(SignInSelect(partyActors.keySet), Some(InvalidCredentials))
|
||||
)
|
||||
}
|
||||
case ApplicationStateFailed(
|
||||
_,
|
||||
_,
|
||||
_,
|
||||
_,
|
||||
GrpcException.PERMISSION_DENIED(),
|
||||
) =>
|
||||
logger.warn("Attempt to sign in without valid token")
|
||||
complete(SignIn(SignInSelect(Set.empty), Some(InvalidCredentials)))
|
||||
case ApplicationStateFailed(_, _, _, _, _) =>
|
||||
complete(SignIn(SignInSelect(Set.empty), Some(Unknown)))
|
||||
}
|
||||
}
|
||||
} ~
|
||||
delete {
|
||||
deleteCookie("session-id", path = "/") {
|
||||
session match {
|
||||
case Some((sessionId, sessionUser)) =>
|
||||
Session.close(sessionId)
|
||||
logger.info(s"Logged out user '${sessionUser.user.id}'")
|
||||
case None =>
|
||||
logger.error("Cannot delete session without session-id, cookie not found")
|
||||
}
|
||||
signIn()
|
||||
}
|
||||
}
|
||||
} ~
|
||||
path("info") {
|
||||
mutableResource { complete(info.getInfo) }
|
||||
} ~
|
||||
path("graphql") {
|
||||
get {
|
||||
versionETag { mutableResource { getFromResource("graphiql.html") } }
|
||||
} ~
|
||||
post {
|
||||
authorize(session.isDefined) {
|
||||
entity(as[JsValue]) { request =>
|
||||
logger.debug(s"Handling GraphQL query: $request")
|
||||
graphQL.parse(request) match {
|
||||
case Success(parsed) =>
|
||||
complete(graphQL.executeQuery(parsed, session.get._2.user.party))
|
||||
case Failure(error) =>
|
||||
logger.debug("Cannot execute query: " + error.getMessage)
|
||||
complete((BadRequest, JsObject("error" -> JsString(error.getMessage))))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} ~ {
|
||||
arguments.assets match {
|
||||
case None =>
|
||||
// Serve assets from resource directory at /assets
|
||||
pathPrefix("assets") {
|
||||
// Webpack makes sure all files use content hashing
|
||||
immutableResource { withoutFileETag { getFromResourceDirectory("frontend") } }
|
||||
} ~
|
||||
// Serve index on root and anything else to allow History API to behave
|
||||
// as expected on reloading.
|
||||
versionETag {
|
||||
mutableResource { withoutFileETag { getFromResource("frontend/index.html") } }
|
||||
}
|
||||
case Some(folder) =>
|
||||
// Serve assets under /assets
|
||||
pathPrefix("assets") {
|
||||
mutableResource { getFromDirectory(folder) }
|
||||
} ~
|
||||
// Serve index on root and anything else to allow History API to behave
|
||||
// as expected on reloading.
|
||||
mutableResource { getFromFile(folder + "/index.html") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Factored out for integration tests
|
||||
private[navigator] def setup(arguments: Arguments, config: Config)(implicit
|
||||
system: ActorSystem
|
||||
) = {
|
||||
import system.dispatcher
|
||||
// Read from the access token file or crash
|
||||
val token =
|
||||
arguments.accessTokenFile.map { path =>
|
||||
try {
|
||||
Files.readAllLines(path).stream.collect(Collectors.joining("\n"))
|
||||
} catch {
|
||||
case NonFatal(e) =>
|
||||
throw new RuntimeException(s"Unable to read the access token from $path", e)
|
||||
}
|
||||
}
|
||||
|
||||
val store = system.actorOf(
|
||||
PlatformStore.props(
|
||||
arguments.participantHost,
|
||||
arguments.participantPort,
|
||||
arguments.tlsConfig,
|
||||
token,
|
||||
arguments.time,
|
||||
applicationInfo,
|
||||
arguments.ledgerInboundMessageSizeMax,
|
||||
arguments.enableUserManagement,
|
||||
)
|
||||
)
|
||||
// TODO: usermgmt switching: for now we just poll both user and party mgmt
|
||||
// If no parties are specified, we periodically poll from the party management service.
|
||||
// If parties are specified, we only use those. This allows users to use custom display names
|
||||
// if they are non-unique or use only a subset of parties for performance reasons.
|
||||
// Currently, we subscribe to all available parties. We could change that to do it lazily only on login
|
||||
// but given that Navigator is only a development tool that might not be worth the complexity.
|
||||
val partyRefresh: Option[Cancellable] =
|
||||
if (config.users.isEmpty || arguments.ignoreProjectParties) {
|
||||
Some(
|
||||
system.scheduler
|
||||
.scheduleWithFixedDelay(Duration.Zero, 1.seconds, store, UpdateUsersOrParties)
|
||||
)
|
||||
} else {
|
||||
config.users.foreach { case (displayName, config) =>
|
||||
store ! Subscribe(
|
||||
displayName,
|
||||
config.party,
|
||||
config.role,
|
||||
config.useDatabase,
|
||||
)
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
def graphQL: GraphQLHandler = DefaultGraphQLHandler(customEndpoints, Some(store))
|
||||
def info: InfoHandler = DefaultInfoHandler(arguments, store)
|
||||
val getAppState: () => Future[ApplicationStateInfo] = () => {
|
||||
implicit val actorTimeout: Timeout = Timeout(5, TimeUnit.SECONDS)
|
||||
(store ? GetApplicationStateInfo).mapTo[ApplicationStateInfo]
|
||||
}
|
||||
(graphQL, info, store, getAppState, partyRefresh)
|
||||
}
|
||||
|
||||
private[navigator] def runServer(arguments: Arguments, config: Config): Unit = {
|
||||
|
||||
banner.foreach(println)
|
||||
|
||||
implicit val system: ActorSystem = ActorSystem("da-ui-backend")
|
||||
|
||||
val (graphQL, info, store @ _, getAppState, partyRefresh) = setup(arguments, config)
|
||||
|
||||
val stopServer = if (arguments.startWebServer) {
|
||||
val binding = Http()
|
||||
.newServerAt("0.0.0.0", arguments.port)
|
||||
.withSettings(ServerSettings(system).withTransparentHeadRequests(true))
|
||||
.bind(getRoute(system, arguments, graphQL, info, getAppState))
|
||||
logger.info(s"DA UI backend server listening on port ${arguments.port}")
|
||||
println(s"Frontend running at http://localhost:${arguments.port}.")
|
||||
() => Try(Await.result(binding, 10.seconds).unbind())
|
||||
} else { () =>
|
||||
()
|
||||
}
|
||||
|
||||
val stopPekko = () => Try(Await.result(system.terminate(), 10.seconds))
|
||||
|
||||
discard {
|
||||
sys.addShutdownHook {
|
||||
// Stop the web server, then the Pekko system consuming the ledger API
|
||||
stopServer()
|
||||
partyRefresh.foreach(_.cancel())
|
||||
stopPekko()
|
||||
()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def dumpGraphQLSchema(): Unit = {
|
||||
import ExecutionContext.Implicits.global
|
||||
|
||||
def graphQL: GraphQLHandler = DefaultGraphQLHandler(customEndpoints, None)
|
||||
scala.Console.out.println(graphQL.renderSchema)
|
||||
}
|
||||
|
||||
final def main(rawArgs: Array[String]): Unit =
|
||||
Arguments.parse(rawArgs, defaultConfigFile) foreach run
|
||||
|
||||
private def run(args: Arguments): Unit = {
|
||||
val navigatorConfigFile =
|
||||
args.configFile.fold[ConfigOption](DefaultConfig(defaultConfigFile))(ExplicitConfig(_))
|
||||
|
||||
args.command match {
|
||||
case ShowUsage =>
|
||||
Arguments.showUsage(defaultConfigFile)
|
||||
case DumpGraphQLSchema =>
|
||||
dumpGraphQLSchema()
|
||||
case CreateConfig =>
|
||||
userFacingLogger.info(
|
||||
s"Creating a configuration template file at ${navigatorConfigFile.path.toAbsolutePath()}"
|
||||
)
|
||||
Config.writeTemplateToPath(navigatorConfigFile.path, args.useDatabase)
|
||||
case RunServer =>
|
||||
Config.load(navigatorConfigFile, args.useDatabase) match {
|
||||
case Left(ConfigNotFound(_)) =>
|
||||
val message =
|
||||
s"""No configuration file found!
|
||||
|Please specify a configuration file and restart ${applicationInfo.name}.
|
||||
|Config file path was '$navigatorConfigFile'.
|
||||
|Hint: use the create-config command to create a sample config file.""".stripMargin
|
||||
userFacingLogger.error(message)
|
||||
sys.error("No configuration file found.")
|
||||
case Left(ConfigParseFailed(message)) =>
|
||||
userFacingLogger.error(s"Configuration file could not be parsed: $message")
|
||||
sys.error(message)
|
||||
case Left(ConfigInvalid(message)) =>
|
||||
userFacingLogger.error(s"Configuration file is invalid: $message")
|
||||
sys.error(message)
|
||||
case Right(config) =>
|
||||
runServer(args, config)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A custom endpoint for a backend to serve data of type `T`.
|
||||
*
|
||||
* Note that two `CustomEndpoint`s are considered equal if their `endpointName`s are the same to simplify
|
||||
* registering new `CustomEndpoint`s and validate them
|
||||
*/
|
||||
abstract class CustomEndpoint[T](implicit tGraphQL: GraphQLObject[T]) {
|
||||
|
||||
/** The endpoint to be used as GraphQL top-level for the data served by this */
|
||||
def endpointName: String
|
||||
|
||||
/** Calculate the data to serve within this endpoint */
|
||||
def calculate(ledger: Ledger, templates: PackageRegistry): Seq[T]
|
||||
|
||||
final def endpoint: Field[GraphQLContext, Unit] = {
|
||||
val listOfTType: ListType[T] = ListType(tGraphQL.to[GraphQLContext])
|
||||
val resolve: Context[GraphQLContext, Unit] => Action[GraphQLContext, Seq[T]] =
|
||||
(context: Context[GraphQLContext, Unit]) =>
|
||||
calculate(context.ctx.ledger, context.ctx.templates)
|
||||
Field(endpointName, listOfTType, resolve = resolve)
|
||||
}
|
||||
|
||||
final override def hashCode: Int = endpointName.hashCode
|
||||
|
||||
final override def equals(obj: scala.Any): Boolean = obj match {
|
||||
case that: CustomEndpoint[_] => this.endpointName equals that.endpointName
|
||||
case _ => false
|
||||
}
|
||||
|
||||
override def toString: String = endpointName
|
||||
}
|
@ -1,210 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.config
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.{Path, Paths}
|
||||
|
||||
import com.daml.ledger.api.tls.TlsConfiguration
|
||||
import com.daml.navigator.time.TimeProviderType
|
||||
import scopt.{OptionDef, OptionParser}
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
sealed abstract class Command
|
||||
case object ShowUsage extends Command
|
||||
case object RunServer extends Command
|
||||
case object CreateConfig extends Command
|
||||
case object DumpGraphQLSchema extends Command
|
||||
|
||||
case class Arguments(
|
||||
port: Int = 4000,
|
||||
assets: Option[String] = None,
|
||||
command: Command = ShowUsage,
|
||||
time: TimeProviderType = TimeProviderType.Auto,
|
||||
participantHost: String = "localhost",
|
||||
participantPort: Int = 6865,
|
||||
tlsConfig: Option[TlsConfiguration] = None,
|
||||
accessTokenFile: Option[Path] = None,
|
||||
configFile: Option[Path] = None,
|
||||
startWebServer: Boolean = false,
|
||||
useDatabase: Boolean = false,
|
||||
ledgerInboundMessageSizeMax: Int = 50 * 1024 * 1024, // 50 MiB
|
||||
ignoreProjectParties: Boolean = false,
|
||||
enableUserManagement: Boolean = true,
|
||||
)
|
||||
|
||||
trait ArgumentsHelper { self: OptionParser[Arguments] =>
|
||||
def hostname: OptionDef[String, Arguments] =
|
||||
arg[String]("<host>")
|
||||
.text(
|
||||
s"hostname or IP address of the Ledger API server (default ${Arguments.default.participantHost})"
|
||||
)
|
||||
.optional()
|
||||
.action((ip, arguments) =>
|
||||
arguments.copy(
|
||||
participantHost = ip
|
||||
)
|
||||
)
|
||||
|
||||
def port: OptionDef[Int, Arguments] =
|
||||
arg[Int]("<port>")
|
||||
.text(s"port number of the Ledger API server (default ${Arguments.default.participantPort})")
|
||||
.optional()
|
||||
.action((port, arguments) =>
|
||||
arguments.copy(
|
||||
participantPort = port
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
object Arguments {
|
||||
|
||||
val default = Arguments()
|
||||
|
||||
private def validatePath(path: String, message: String): Either[String, Unit] = {
|
||||
val readable = Try(Paths.get(path).toFile.canRead).getOrElse(false)
|
||||
if (readable) Right(()) else Left(message)
|
||||
}
|
||||
|
||||
private val crtConfig = (path: String, arguments: Arguments) =>
|
||||
arguments.copy(
|
||||
tlsConfig =
|
||||
arguments.tlsConfig.fold(Some(TlsConfiguration(true, Some(new File(path)), None, None)))(
|
||||
c => Some(c.copy(certChainFile = Some(new File(path))))
|
||||
)
|
||||
)
|
||||
|
||||
private def argumentParser(defaultConfigFile: Path) =
|
||||
new OptionParser[Arguments]("navigator") with ArgumentsHelper {
|
||||
help("help").abbr("h").text("prints this usage text")
|
||||
|
||||
opt[Int]("port")
|
||||
.text(s"port number on which the server should listen (default ${Arguments.default.port})")
|
||||
.action((port, arguments) => arguments.copy(port = port))
|
||||
|
||||
opt[String]("assets")
|
||||
.text("folder where frontend assets are available")
|
||||
.action((assets, arguments) => arguments.copy(assets = Some(assets)))
|
||||
|
||||
opt[String]("access-token-file")
|
||||
.text(
|
||||
s"provide the path from which the access token will be read, required to interact with an authenticated ledger, no default"
|
||||
)
|
||||
.action((path, arguments) => arguments.copy(accessTokenFile = Some(Paths.get(path))))
|
||||
.optional()
|
||||
|
||||
opt[String]('c', "config-file")
|
||||
.text(s"set the configuration file default: ${defaultConfigFile}")
|
||||
.action((path, arguments) => arguments.copy(configFile = Some(Paths.get(path))))
|
||||
|
||||
opt[TimeProviderType]('t', "time")
|
||||
.text(s"Time provider. Valid values are: ${TimeProviderType.acceptedValues
|
||||
.mkString(", ")}. Default: ${Arguments.default.time.name}")
|
||||
.action((t, arguments) => arguments.copy(time = t))
|
||||
|
||||
// TODO: the 4 following TLS options can be defined by TlsConfigurationCli instead
|
||||
|
||||
opt[String]("pem")
|
||||
.optional()
|
||||
.text("TLS: The pem file to be used as the private key.")
|
||||
.validate(path => validatePath(path, "The file specified via --pem does not exist"))
|
||||
.action((path, arguments) =>
|
||||
arguments.copy(tlsConfig =
|
||||
arguments.tlsConfig.fold(
|
||||
Some(TlsConfiguration(true, None, Some(new File(path)), None))
|
||||
)(c => Some(c.copy(privateKeyFile = Some(new File(path)))))
|
||||
)
|
||||
)
|
||||
|
||||
opt[String]("crt")
|
||||
.optional()
|
||||
.text("TLS: The crt file to be used as the cert chain. Required for client authentication.")
|
||||
.validate(path => validatePath(path, "The file specified via --crt does not exist"))
|
||||
.action(crtConfig)
|
||||
|
||||
opt[String]("cacrt")
|
||||
.optional()
|
||||
.text("TLS: The crt file to be used as the trusted root CA.")
|
||||
.validate(path => validatePath(path, "The file specified via --cacrt does not exist"))
|
||||
.action((path, arguments) =>
|
||||
arguments.copy(tlsConfig =
|
||||
arguments.tlsConfig.fold(
|
||||
Some(TlsConfiguration(true, None, None, Some(new File(path))))
|
||||
)(c => Some(c.copy(trustCollectionFile = Some(new File(path)))))
|
||||
)
|
||||
)
|
||||
|
||||
opt[Unit]("tls")
|
||||
.optional()
|
||||
.text("TLS: Enable tls. This is redundant if --pem, --crt or --cacrt are set")
|
||||
.action((_, arguments) =>
|
||||
arguments.copy(tlsConfig =
|
||||
arguments.tlsConfig.fold(Some(TlsConfiguration(true, None, None, None)))(Some(_))
|
||||
)
|
||||
)
|
||||
|
||||
opt[Unit]("database")
|
||||
.hidden()
|
||||
.text("EXPERIMENTAL: use an SQLite data store")
|
||||
.action((_, arguments) =>
|
||||
arguments.copy(
|
||||
useDatabase = true
|
||||
)
|
||||
)
|
||||
|
||||
opt[Int]("ledger-api-inbound-message-size-max")
|
||||
.text(
|
||||
s"Maximum message size in bytes from the ledger API. Default is ${Arguments.default.ledgerInboundMessageSizeMax}."
|
||||
)
|
||||
.valueName("<bytes>")
|
||||
.validate(x => Either.cond(x > 0, (), "Buffer size must be positive"))
|
||||
.action((ledgerInboundMessageSizeMax, arguments) => {
|
||||
arguments.copy(
|
||||
ledgerInboundMessageSizeMax = ledgerInboundMessageSizeMax
|
||||
)
|
||||
})
|
||||
|
||||
opt[Unit]("ignore-project-parties")
|
||||
.hidden()
|
||||
.optional()
|
||||
.text(
|
||||
"Ignore the parties specified in the project configuration file and query the ledger for parties instead."
|
||||
)
|
||||
.action((_, arguments) => arguments.copy(ignoreProjectParties = true))
|
||||
|
||||
opt[Boolean]("feature-user-management")
|
||||
.optional()
|
||||
.text(
|
||||
"By default, the login screen is now populated by quering the user mgmt service. Disable to query party mgmt instead (the pre-2.0 default)."
|
||||
)
|
||||
.action((enabled, arguments) => arguments.copy(enableUserManagement = enabled))
|
||||
|
||||
cmd("server")
|
||||
.text("serve data from platform")
|
||||
.action((_, arguments) =>
|
||||
arguments.copy(
|
||||
command = RunServer,
|
||||
startWebServer = true,
|
||||
)
|
||||
)
|
||||
.children(hostname, port)
|
||||
|
||||
cmd("dump-graphql-schema")
|
||||
.text("Early Access (Labs). Dumps the full GraphQL schema to stdout")
|
||||
.action((_, arguments) => arguments.copy(command = DumpGraphQLSchema))
|
||||
|
||||
cmd("create-config")
|
||||
.text("Creates a template configuration file")
|
||||
.action((_, arguments) => arguments.copy(command = CreateConfig))
|
||||
}
|
||||
|
||||
def parse(args: Array[String], defaultConfigFile: Path): Option[Arguments] =
|
||||
this.argumentParser(defaultConfigFile).parse(args, Arguments.default)
|
||||
|
||||
def showUsage(defaultConfigFile: Path): Unit = {
|
||||
val parser = this.argumentParser(defaultConfigFile)
|
||||
parser.displayToOut(parser.usage)
|
||||
}
|
||||
}
|
@ -1,167 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.config
|
||||
|
||||
import java.nio.file.StandardOpenOption._
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
import com.daml.assistant.config.{
|
||||
ProjectConfig,
|
||||
ConfigLoadError => SdkConfigLoadError,
|
||||
ConfigMissing => SdkConfigMissing,
|
||||
ConfigParseError => SdkConfigParseError,
|
||||
}
|
||||
import com.daml.ledger.api.refinements.ApiTypes
|
||||
import com.typesafe.config.{ConfigFactory, ConfigRenderOptions}
|
||||
import org.slf4j.LoggerFactory
|
||||
import pureconfig.{ConfigConvert, ConfigSource, ConfigWriter}
|
||||
import pureconfig.generic.auto._
|
||||
import scalaz.Tag
|
||||
|
||||
import scala.annotation.nowarn
|
||||
|
||||
final case class UserConfig(party: ApiTypes.Party, role: Option[String], useDatabase: Boolean)
|
||||
|
||||
/* The configuration has an empty map as default list of users because you can login as party too */
|
||||
final case class Config(users: Map[String, UserConfig] = Map.empty[String, UserConfig]) {
|
||||
|
||||
def userIds: Set[String] = users.keySet
|
||||
|
||||
def roles: Set[String] = users.values.view.flatMap(_.role.toList).toSet
|
||||
}
|
||||
|
||||
sealed abstract class ConfigReadError extends Product with Serializable {
|
||||
def reason: String
|
||||
}
|
||||
|
||||
final case class ConfigNotFound(reason: String) extends ConfigReadError
|
||||
final case class ConfigInvalid(reason: String) extends ConfigReadError
|
||||
final case class ConfigParseFailed(reason: String) extends ConfigReadError
|
||||
|
||||
sealed abstract class ConfigOption {
|
||||
def path: Path
|
||||
}
|
||||
final case class DefaultConfig(path: Path) extends ConfigOption
|
||||
final case class ExplicitConfig(path: Path) extends ConfigOption
|
||||
|
||||
object Config {
|
||||
|
||||
private[this] val logger = LoggerFactory.getLogger(this.getClass)
|
||||
private[this] def userFacingLogger = LoggerFactory.getLogger("user-facing-logs")
|
||||
|
||||
def load(configOpt: ConfigOption, useDatabase: Boolean): Either[ConfigReadError, Config] = {
|
||||
configOpt match {
|
||||
case ExplicitConfig(configFile) =>
|
||||
// If users specified a config file explicitly, we ignore the SDK config.
|
||||
loadNavigatorConfig(configFile, useDatabase)
|
||||
case DefaultConfig(configFile) =>
|
||||
loadSdkConfig(useDatabase).left
|
||||
.flatMap {
|
||||
case ConfigNotFound(_) =>
|
||||
logger.info("SDK config does not exist. Falling back to Navigator config file.")
|
||||
loadNavigatorConfig(configFile, useDatabase) match {
|
||||
case Left(ConfigNotFound(_)) =>
|
||||
logger.info("No config file found. Using default config")
|
||||
Right(Config())
|
||||
case r => r
|
||||
}
|
||||
case e: ConfigReadError =>
|
||||
logger.warn(s"SDK config exists, but is not usable: ${e.reason}")
|
||||
Left(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def loadNavigatorConfig(
|
||||
configFile: Path,
|
||||
useDatabase: Boolean,
|
||||
): Either[ConfigReadError, Config] = {
|
||||
@nowarn(
|
||||
"msg=local val userConfigConvert .* is never used"
|
||||
) // false positive; macro uses aren't seen
|
||||
implicit val userConfigConvert: ConfigConvert[UserConfig] =
|
||||
mkUserConfigConvert(useDatabase = useDatabase)
|
||||
|
||||
if (Files.exists(configFile)) {
|
||||
logger.info(s"Loading Navigator config file from $configFile")
|
||||
val config = ConfigFactory.parseFileAnySyntax(configFile.toAbsolutePath.toFile)
|
||||
ConfigSource
|
||||
.fromConfig(config)
|
||||
.load[Config]
|
||||
.left
|
||||
.map(e => ConfigParseFailed(e.toList.mkString(", ")))
|
||||
} else {
|
||||
Left(ConfigNotFound(s"File $configFile not found"))
|
||||
}
|
||||
}
|
||||
|
||||
def loadSdkConfig(useDatabase: Boolean): Either[ConfigReadError, Config] = {
|
||||
val partiesE = for {
|
||||
projectConfigPath <- ProjectConfig.projectConfigPath()
|
||||
_ = logger.info(s"Loading SDK config file from $projectConfigPath")
|
||||
projectConfig <- ProjectConfig.loadFromFile(projectConfigPath)
|
||||
result <- projectConfig.parties
|
||||
} yield result
|
||||
|
||||
partiesE match {
|
||||
case Right(Some(parties)) =>
|
||||
Right(
|
||||
Config(
|
||||
parties
|
||||
.map(p => p -> UserConfig(ApiTypes.Party(p), None, useDatabase))
|
||||
.toMap
|
||||
)
|
||||
)
|
||||
case Right(None) =>
|
||||
// Pick up parties from party management service
|
||||
Right(Config())
|
||||
case Left(SdkConfigMissing(reason)) =>
|
||||
Left(ConfigNotFound(reason))
|
||||
case Left(SdkConfigParseError(reason)) =>
|
||||
val message = s"Found a SDK project config file, but it could not be parsed: $reason."
|
||||
Left(ConfigParseFailed(message))
|
||||
case Left(SdkConfigLoadError(reason)) =>
|
||||
val message = s"Found a SDK project config file, but it could not be loaded: $reason."
|
||||
Left(ConfigParseFailed(message))
|
||||
}
|
||||
}
|
||||
|
||||
def template(useDatabase: Boolean): Config =
|
||||
Config(
|
||||
Map(
|
||||
"OPERATOR" -> UserConfig(ApiTypes.Party("party"), None, useDatabase)
|
||||
)
|
||||
)
|
||||
|
||||
def writeTemplateToPath(configFile: Path, useDatabase: Boolean): Unit = {
|
||||
@nowarn(
|
||||
"msg=local val userConfigConvert .* is never used"
|
||||
) // false positive; macro uses aren't seen
|
||||
implicit val userConfigConvert: ConfigConvert[UserConfig] = mkUserConfigConvert(
|
||||
useDatabase = useDatabase
|
||||
)
|
||||
val config = ConfigWriter[Config].to(template(useDatabase))
|
||||
val cro = ConfigRenderOptions
|
||||
.defaults()
|
||||
.setComments(false)
|
||||
.setOriginComments(false)
|
||||
.setFormatted(true)
|
||||
.setJson(false)
|
||||
Files.write(configFile, config.render(cro).getBytes, CREATE_NEW)
|
||||
()
|
||||
}
|
||||
|
||||
final case class UserConfigHelper(password: Option[String], party: String, role: Option[String])
|
||||
|
||||
private[this] def mkUserConfigConvert(useDatabase: Boolean): ConfigConvert[UserConfig] =
|
||||
implicitly[ConfigConvert[UserConfigHelper]].xmap(
|
||||
helper => {
|
||||
helper.password.foreach { _ =>
|
||||
userFacingLogger.warn(s"password field set for user ${helper.party} is deprecated")
|
||||
}
|
||||
UserConfig(ApiTypes.Party(helper.party), helper.role, useDatabase)
|
||||
},
|
||||
conf => UserConfigHelper(None, Tag.unwrap(conf.party), conf.role),
|
||||
)
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.data
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
import com.daml.ledger.api.refinements.ApiTypes
|
||||
import com.daml.navigator.json.ApiCodecVerbose
|
||||
import com.daml.lf.value.json.ApiCodecCompressed
|
||||
import ApiCodecCompressed.JsonImplicits._
|
||||
import com.daml.navigator.model._
|
||||
|
||||
import scala.util.{Failure, Try}
|
||||
import scalaz.syntax.tag._
|
||||
import spray.json._
|
||||
|
||||
final case class CommandRow(
|
||||
id: String,
|
||||
index: Long,
|
||||
workflowId: String,
|
||||
platformTime: String,
|
||||
subclassType: String,
|
||||
template: Option[String],
|
||||
recordArgument: Option[String],
|
||||
contractId: Option[String],
|
||||
interfaceId: Option[String],
|
||||
choice: Option[String],
|
||||
argumentValue: Option[String],
|
||||
) {
|
||||
|
||||
def toCommand(types: PackageRegistry): Try[Command] = {
|
||||
subclassType match {
|
||||
case "CreateCommand" =>
|
||||
(for {
|
||||
tp <- Try(template.get)
|
||||
tid <- Try(parseOpaqueIdentifier(tp).get)
|
||||
recArgJson <- Try(recordArgument.get)
|
||||
anyArg <- Try(
|
||||
ApiCodecCompressed
|
||||
.jsValueToApiValue(recArgJson.parseJson, tid, types.damlLfDefDataType _)
|
||||
)
|
||||
recArg <- Try(anyArg.asInstanceOf[ApiRecord])
|
||||
} yield {
|
||||
CreateCommand(
|
||||
ApiTypes.CommandId(id),
|
||||
index,
|
||||
ApiTypes.WorkflowId(workflowId),
|
||||
Instant.parse(platformTime),
|
||||
tid,
|
||||
recArg,
|
||||
)
|
||||
}).recoverWith { case e: Throwable =>
|
||||
Failure(
|
||||
DeserializationFailed(s"Failed to deserialize CreateCommand from row: $this. Error: $e")
|
||||
)
|
||||
}
|
||||
case "ExerciseCommand" =>
|
||||
(for {
|
||||
tp <- Try(template.get)
|
||||
tid <- Try(parseOpaqueIdentifier(tp).get)
|
||||
iidOp <- Try(interfaceId.map(parseOpaqueIdentifier(_).get))
|
||||
t <- Try(types.template(tid).get)
|
||||
cId <- Try(contractId.get)
|
||||
ch <- Try(choice.get)
|
||||
_ <- Try(t.choices.find(_.name.unwrap == ch).get)
|
||||
argJson <- Try(argumentValue.get)
|
||||
arg <- Try(ApiCodecVerbose.jsValueToApiValue(argJson.parseJson))
|
||||
} yield {
|
||||
ExerciseCommand(
|
||||
ApiTypes.CommandId(id),
|
||||
index,
|
||||
ApiTypes.WorkflowId(workflowId),
|
||||
Instant.parse(platformTime),
|
||||
ApiTypes.ContractId(cId),
|
||||
tid,
|
||||
iidOp,
|
||||
ApiTypes.Choice(ch),
|
||||
arg,
|
||||
)
|
||||
}).recoverWith { case e: Throwable =>
|
||||
Failure(
|
||||
DeserializationFailed(
|
||||
s"Failed to deserialize ExerciseCommand from row: $this. Error: $e"
|
||||
)
|
||||
)
|
||||
}
|
||||
case _ => Failure(DeserializationFailed(s"unknown subclass type for Command: $subclassType"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object CommandRow {
|
||||
|
||||
def fromCommand(c: Command): CommandRow = {
|
||||
c match {
|
||||
case c: CreateCommand =>
|
||||
CommandRow(
|
||||
c.id.unwrap,
|
||||
c.index,
|
||||
c.workflowId.unwrap,
|
||||
c.platformTime.toString,
|
||||
"CreateCommand",
|
||||
Some(c.template.asOpaqueString),
|
||||
Some(c.argument.toJson.compactPrint),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
case e: ExerciseCommand =>
|
||||
CommandRow(
|
||||
c.id.unwrap,
|
||||
c.index,
|
||||
c.workflowId.unwrap,
|
||||
c.platformTime.toString,
|
||||
"ExerciseCommand",
|
||||
Some(e.template.asOpaqueString),
|
||||
None,
|
||||
Some(e.contract.unwrap),
|
||||
e.interfaceId.map(_.asOpaqueString),
|
||||
Some(e.choice.unwrap),
|
||||
Some(ApiCodecVerbose.apiValueToJsValue(e.argument).compactPrint),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.data
|
||||
|
||||
import com.daml.ledger.api.refinements.ApiTypes
|
||||
import com.daml.navigator.model._
|
||||
|
||||
import scala.util.{Failure, Success, Try}
|
||||
import scalaz.syntax.tag._
|
||||
|
||||
final case class CommandStatusRow(
|
||||
commandId: String,
|
||||
isCompleted: Boolean,
|
||||
subclassType: String,
|
||||
code: Option[String],
|
||||
details: Option[String],
|
||||
transactionId: Option[String],
|
||||
) {
|
||||
|
||||
def toCommandStatus(
|
||||
transactionById: ApiTypes.TransactionId => Try[Option[Transaction]]
|
||||
): Try[CommandStatus] = {
|
||||
subclassType match {
|
||||
case "CommandStatusWaiting" =>
|
||||
Success(CommandStatusWaiting())
|
||||
case "CommandStatusError" =>
|
||||
(for {
|
||||
c <- code
|
||||
d <- details
|
||||
} yield {
|
||||
CommandStatusError(c, d)
|
||||
}).fold[Try[CommandStatus]](
|
||||
Failure(
|
||||
DeserializationFailed(s"Failed to deserialize CommandStatusError from row: $this")
|
||||
)
|
||||
)(
|
||||
Success(_)
|
||||
)
|
||||
case "CommandStatusSuccess" =>
|
||||
transactionId.map { tId =>
|
||||
transactionById(ApiTypes.TransactionId(tId))
|
||||
} match {
|
||||
case Some(Success(Some(tx: Transaction))) => Success(CommandStatusSuccess(tx))
|
||||
case Some(Failure(e)) =>
|
||||
Failure(
|
||||
RecordNotFound(
|
||||
s"Failed to load transaction $transactionId for CommandStatus with commandId: $commandId. Exception: ${e.getMessage}"
|
||||
)
|
||||
)
|
||||
case Some(Success(None)) =>
|
||||
Failure(
|
||||
RecordNotFound(
|
||||
s"Failed to load transaction $transactionId for CommandStatus with commandId: $commandId"
|
||||
)
|
||||
)
|
||||
case None =>
|
||||
Failure(
|
||||
DeserializationFailed(s"TransactionId is missing for CommandStatusSuccess row: $this")
|
||||
)
|
||||
}
|
||||
case "CommandStatusUnknown" =>
|
||||
Success(CommandStatusUnknown())
|
||||
case s => Failure(DeserializationFailed(s"unknown subclass type for CommandStatus: $s"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object CommandStatusRow {
|
||||
|
||||
def fromCommandStatus(commandId: ApiTypes.CommandId, cs: CommandStatus): CommandStatusRow = {
|
||||
cs match {
|
||||
case w: CommandStatusWaiting =>
|
||||
CommandStatusRow(commandId.unwrap, w.isCompleted, "CommandStatusWaiting", None, None, None)
|
||||
case e: CommandStatusError =>
|
||||
CommandStatusRow(
|
||||
commandId.unwrap,
|
||||
e.isCompleted,
|
||||
"CommandStatusError",
|
||||
Some(e.code),
|
||||
Some(e.details),
|
||||
None,
|
||||
)
|
||||
case s: CommandStatusSuccess =>
|
||||
CommandStatusRow(
|
||||
commandId.unwrap,
|
||||
s.isCompleted,
|
||||
"CommandStatusSuccess",
|
||||
None,
|
||||
None,
|
||||
Some(s.tx.id.unwrap),
|
||||
)
|
||||
case u: CommandStatusUnknown =>
|
||||
CommandStatusRow(commandId.unwrap, u.isCompleted, "CommandStatusUnknown", None, None, None)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.data
|
||||
|
||||
import com.daml.ledger.api.refinements.ApiTypes
|
||||
import com.daml.lf.value.json.ApiCodecCompressed
|
||||
import ApiCodecCompressed.JsonImplicits._
|
||||
import com.daml.navigator.json.ModelCodec.JsonImplicits._
|
||||
import com.daml.navigator.model._
|
||||
|
||||
import scala.util.{Failure, Try}
|
||||
import scalaz.syntax.tag._
|
||||
import spray.json._
|
||||
|
||||
final case class ContractRow(
|
||||
id: String,
|
||||
templateId: String,
|
||||
archiveTransactionId: Option[String],
|
||||
argument: String,
|
||||
agreementText: Option[String],
|
||||
signatories: String,
|
||||
observers: String,
|
||||
key: Option[String],
|
||||
) {
|
||||
|
||||
def toContract(types: PackageRegistry): Try[Contract] = {
|
||||
(for {
|
||||
id <- Try(ApiTypes.ContractId(id))
|
||||
tid <- Try(parseOpaqueIdentifier(templateId).get)
|
||||
template <- Try(types.template(tid).get)
|
||||
recArgAny <- Try(
|
||||
ApiCodecCompressed.jsValueToApiValue(argument.parseJson, tid, types.damlLfDefDataType _)
|
||||
)
|
||||
recArg <- Try(recArgAny.asInstanceOf[ApiRecord])
|
||||
sig <- Try(signatories.parseJson.convertTo[List[ApiTypes.Party]])
|
||||
obs <- Try(signatories.parseJson.convertTo[List[ApiTypes.Party]])
|
||||
key <- Try(
|
||||
key.map(
|
||||
_.parseJson.convertTo[ApiValue](
|
||||
ApiCodecCompressed.apiValueJsonReader(template.key.get, types.damlLfDefDataType _)
|
||||
)
|
||||
)
|
||||
)
|
||||
} yield {
|
||||
Contract(id, template, recArg, agreementText, sig, obs, key)
|
||||
}).recoverWith { case e: Throwable =>
|
||||
Failure(DeserializationFailed(s"Failed to deserialize Contract from row: $this. Error: $e"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ContractRow {
|
||||
|
||||
def fromContract(c: Contract): ContractRow = {
|
||||
ContractRow(
|
||||
c.id.unwrap,
|
||||
c.template.id.asOpaqueString,
|
||||
None,
|
||||
c.argument.toJson.compactPrint,
|
||||
c.agreementText,
|
||||
c.signatories.toJson.compactPrint,
|
||||
c.observers.toJson.compactPrint,
|
||||
c.key.map(_.toJson.compactPrint),
|
||||
)
|
||||
}
|
||||
}
|
@ -1,472 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.data
|
||||
|
||||
import java.sql.DriverManager
|
||||
import java.util.concurrent.Executors.newWorkStealingPool
|
||||
|
||||
import cats.effect.{Blocker, ContextShift, IO}
|
||||
import cats.implicits._
|
||||
import com.daml.ledger.api.refinements.ApiTypes
|
||||
import com.daml.navigator.model._
|
||||
import com.typesafe.scalalogging.LazyLogging
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
import scalaz.syntax.tag._
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/** This class is responsible for running the queries
|
||||
* and make the transformation between Scala and data store types
|
||||
*/
|
||||
class DatabaseActions extends LazyLogging {
|
||||
|
||||
/** Uncomment the log handler to enable query logging
|
||||
*/
|
||||
// implicit private val lh: LogHandler = doobie.util.log.LogHandler.jdkLogHandler
|
||||
implicit private val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global)
|
||||
|
||||
// How many transactions can be executed in parallel.
|
||||
// 256 comes from https://github.com/scala/scala/blob/v2.12.12/src/library/scala/concurrent/impl/ExecutionContextImpl.scala#L115-L116
|
||||
private val maxConnections = 256
|
||||
|
||||
/** Initializing a new database.
|
||||
* Every :memory: database is distinct from every other.
|
||||
* So, opening two database connections each with the filename
|
||||
* ":memory:" will create two independent in-memory databases.
|
||||
* See https://www.sqlite.org/inmemorydb.html
|
||||
*/
|
||||
private val xa = Transactor.fromConnection[IO](
|
||||
DriverManager.getConnection("jdbc:sqlite::memory:"),
|
||||
Blocker liftExecutorService newWorkStealingPool(maxConnections),
|
||||
)
|
||||
|
||||
/** Creating the tables when initializing the DatabaseActions object
|
||||
*/
|
||||
(Queries.createContractTable.update.run *>
|
||||
Queries.contractIdIndex.update.run *>
|
||||
Queries.contractIsActive.update.run *>
|
||||
Queries.contractTemplateIdIsActive.update.run *>
|
||||
|
||||
Queries.createEventTable.update.run *>
|
||||
Queries.eventIdIndex.update.run *>
|
||||
Queries.eventTransactionIdParentId.update.run *>
|
||||
Queries.eventContractIdSubclass.update.run *>
|
||||
|
||||
Queries.createTransactionTable.update.run *>
|
||||
Queries.transactionIdIndex.update.run *>
|
||||
|
||||
Queries.createCommandStatusTable.update.run *>
|
||||
Queries.commandStatusCommandIdIndex.update.run *>
|
||||
|
||||
Queries.createCommandTable.update.run *>
|
||||
Queries.commandIdIndex.update.run)
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
|
||||
def schema(): Try[String] = {
|
||||
Try(
|
||||
Queries
|
||||
.schema()
|
||||
.query[Option[String]]
|
||||
.to[List]
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
.flatMap(_.toList)
|
||||
.mkString("\n")
|
||||
)
|
||||
}
|
||||
|
||||
private def exec: PreparedStatementIO[SqlQueryResult] = {
|
||||
|
||||
// Read the specified columns from the resultset.
|
||||
def readAll(cols: List[Int]): ResultSetIO[List[List[Object]]] =
|
||||
readOne(cols).whileM[List](HRS.next)
|
||||
|
||||
// Take a list of column offsets and read a parallel list of values.
|
||||
def readOne(cols: List[Int]): ResultSetIO[List[Object]] =
|
||||
cols.traverse(FRS.getObject)
|
||||
|
||||
for {
|
||||
md <- HPS.getMetaData
|
||||
cols = (1 to md.getColumnCount).toList
|
||||
colNames = cols.map(md.getColumnName)
|
||||
data <- HPS.executeQuery(readAll(cols))
|
||||
} yield SqlQueryResult(colNames, data.map(_.map(Option(_).map(_.toString).getOrElse("null"))))
|
||||
}
|
||||
|
||||
/** Returns the given value, logging failures */
|
||||
private def logErrors[T](result: Try[T]): Try[T] = {
|
||||
result.failed.foreach { t =>
|
||||
logger.error("Error executing database action, Navigator may be in a corrupted state", t)
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
def runQuery(query: String): Try[SqlQueryResult] = logErrors {
|
||||
Try(Queries.query(query).execWith(exec).transact(xa).unsafeRunSync())
|
||||
}
|
||||
|
||||
def insertEvent(event: Event): Try[Int] = logErrors {
|
||||
Try {
|
||||
Queries
|
||||
.insertEvent(EventRow.fromEvent(event))
|
||||
.update
|
||||
.run
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
}
|
||||
}
|
||||
|
||||
def eventById(id: ApiTypes.EventId, types: PackageRegistry): Try[Option[Event]] = logErrors {
|
||||
Try {
|
||||
Queries
|
||||
.eventById(id.unwrap)
|
||||
.query[EventRow]
|
||||
.to[List]
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
.headOption
|
||||
.map { data =>
|
||||
data.toEvent(types)
|
||||
}
|
||||
}.flatMap(_.sequence)
|
||||
}
|
||||
|
||||
def eventsByParentId(parentId: ApiTypes.EventId, types: PackageRegistry): Try[List[Event]] =
|
||||
logErrors {
|
||||
Try {
|
||||
Queries
|
||||
.eventsByParentId(parentId.unwrap)
|
||||
.query[EventRow]
|
||||
.to[List]
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
.map { data =>
|
||||
data.toEvent(types)
|
||||
}
|
||||
}.flatMap(_.sequence)
|
||||
}
|
||||
|
||||
def createEventByContractId(
|
||||
id: ApiTypes.ContractId,
|
||||
types: PackageRegistry,
|
||||
): Try[ContractCreated] = logErrors {
|
||||
Try {
|
||||
Queries
|
||||
.eventByTypeAndContractId("ContractCreated", id.unwrap)
|
||||
.query[EventRow]
|
||||
.to[List]
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
.headOption
|
||||
.map(data => (data.toEvent(types), data)) match {
|
||||
case Some((Success(c: ContractCreated), _)) => Success(c)
|
||||
case Some((_, data)) =>
|
||||
Failure(DeserializationFailed(s"Failed to deserialize row as ContractCreated: $data"))
|
||||
case None => Failure(RecordNotFound(s"Create event not found for contractId: $id"))
|
||||
}
|
||||
}.flatten
|
||||
}
|
||||
|
||||
def archiveEventByContractId(
|
||||
id: ApiTypes.ContractId,
|
||||
types: PackageRegistry,
|
||||
): Try[Option[ChoiceExercised]] = logErrors {
|
||||
Try {
|
||||
Queries
|
||||
.eventByTypeAndContractId("ContractArchived", id.unwrap)
|
||||
.query[EventRow]
|
||||
.to[List]
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
.headOption
|
||||
.map { data =>
|
||||
data.toEvent(types) match {
|
||||
case Success(a: ChoiceExercised) => Success(a)
|
||||
case _ =>
|
||||
Failure(DeserializationFailed(s"Failed to deserialize row as ChoiceExercised: $data"))
|
||||
}
|
||||
}
|
||||
}.flatMap(_.sequence)
|
||||
}
|
||||
|
||||
def choiceExercisedEventByContractById(
|
||||
id: ApiTypes.ContractId,
|
||||
types: PackageRegistry,
|
||||
): Try[List[ChoiceExercised]] = logErrors {
|
||||
Try {
|
||||
Queries
|
||||
.eventByTypeAndContractId("ChoiceExercised", id.unwrap)
|
||||
.query[EventRow]
|
||||
.to[List]
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
.map { data =>
|
||||
data.toEvent(types) match {
|
||||
case Success(e: ChoiceExercised) => Success(e)
|
||||
case _ =>
|
||||
Failure(DeserializationFailed(s"Failed to deserialize row as ChoiceExercised: $data"))
|
||||
}
|
||||
}
|
||||
}.flatMap(_.sequence)
|
||||
}
|
||||
|
||||
def insertTransaction(tx: Transaction): Try[Int] = logErrors {
|
||||
Try {
|
||||
Queries
|
||||
.insertTransaction(TransactionRow.fromTransaction(tx))
|
||||
.update
|
||||
.run
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
}
|
||||
}
|
||||
|
||||
def transactionById(
|
||||
id: ApiTypes.TransactionId,
|
||||
types: PackageRegistry,
|
||||
): Try[Option[Transaction]] = logErrors {
|
||||
Try {
|
||||
Queries
|
||||
.topLevelEventsByTransactionId(id.unwrap)
|
||||
.query[EventRow]
|
||||
.to[List]
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
.map { data =>
|
||||
data.toEvent(types)
|
||||
}
|
||||
.sequence
|
||||
.map { events =>
|
||||
Queries
|
||||
.transactionById(id.unwrap)
|
||||
.query[TransactionRow]
|
||||
.to[List]
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
.headOption
|
||||
.map { data =>
|
||||
data.toTransaction(events)
|
||||
}
|
||||
}
|
||||
}.flatten
|
||||
}
|
||||
|
||||
def lastTransaction(types: PackageRegistry): Try[Option[Transaction]] = logErrors {
|
||||
Try {
|
||||
val txData = Queries
|
||||
.lastTransaction()
|
||||
.query[TransactionRow]
|
||||
.to[List]
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
.headOption
|
||||
|
||||
txData.map { tx =>
|
||||
Queries
|
||||
.topLevelEventsByTransactionId(tx.id)
|
||||
.query[EventRow]
|
||||
.to[List]
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
.map { data =>
|
||||
data.toEvent(types)
|
||||
}
|
||||
.sequence
|
||||
.map { events =>
|
||||
tx.toTransaction(events)
|
||||
}
|
||||
}
|
||||
}.flatMap(_.sequence)
|
||||
}
|
||||
|
||||
def upsertCommandStatus(commandId: ApiTypes.CommandId, cs: CommandStatus): Try[Int] = logErrors {
|
||||
Try {
|
||||
Queries
|
||||
.upsertCommandStatus(CommandStatusRow.fromCommandStatus(commandId, cs))
|
||||
.update
|
||||
.run
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
}
|
||||
}
|
||||
|
||||
def updateCommandStatus(commandId: ApiTypes.CommandId, cs: CommandStatus): Try[Int] = logErrors {
|
||||
Try {
|
||||
Queries
|
||||
.updateCommandStatus(CommandStatusRow.fromCommandStatus(commandId, cs))
|
||||
.update
|
||||
.run
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
}
|
||||
}
|
||||
|
||||
def commandStatusByCommandId(
|
||||
commandId: ApiTypes.CommandId,
|
||||
types: PackageRegistry,
|
||||
): Try[Option[CommandStatus]] = logErrors {
|
||||
Try {
|
||||
Queries
|
||||
.commandStatusByCommandId(commandId.unwrap)
|
||||
.query[CommandStatusRow]
|
||||
.to[List]
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
.headOption
|
||||
.map { data =>
|
||||
data.toCommandStatus(transactionById(_, types))
|
||||
}
|
||||
}.flatMap(_.sequence)
|
||||
}
|
||||
|
||||
def insertCommand(cmd: Command): Try[Int] = logErrors {
|
||||
Try {
|
||||
Queries
|
||||
.insertCommand(CommandRow.fromCommand(cmd))
|
||||
.update
|
||||
.run
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
}
|
||||
}
|
||||
|
||||
def commandById(id: ApiTypes.CommandId, types: PackageRegistry): Try[Option[Command]] =
|
||||
logErrors {
|
||||
Try {
|
||||
Queries
|
||||
.commandById(id.unwrap)
|
||||
.query[CommandRow]
|
||||
.to[List]
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
.headOption
|
||||
.map(_.toCommand(types))
|
||||
}.flatMap(_.sequence)
|
||||
}
|
||||
|
||||
def allCommands(types: PackageRegistry): Try[List[Command]] = logErrors {
|
||||
Try {
|
||||
Queries
|
||||
.allCommands()
|
||||
.query[CommandRow]
|
||||
.to[List]
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
.map(_.toCommand(types))
|
||||
}.flatMap(_.sequence)
|
||||
}
|
||||
|
||||
def insertContract(contract: Contract): Try[Int] = logErrors {
|
||||
Try {
|
||||
Queries
|
||||
.insertContract(ContractRow.fromContract(contract))
|
||||
.update
|
||||
.run
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
}
|
||||
}
|
||||
|
||||
def archiveContract(
|
||||
contractId: ApiTypes.ContractId,
|
||||
archiveTransactionId: ApiTypes.TransactionId,
|
||||
): Try[Int] = logErrors {
|
||||
Try {
|
||||
Queries
|
||||
.archiveContract(contractId.unwrap, archiveTransactionId.unwrap)
|
||||
.update
|
||||
.run
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
}
|
||||
}
|
||||
|
||||
def contractCount(): Try[Int] = logErrors {
|
||||
Try {
|
||||
Queries
|
||||
.contractCount()
|
||||
.query[Int]
|
||||
.unique
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
}
|
||||
}
|
||||
|
||||
def activeContractCount(): Try[Int] = logErrors {
|
||||
Try {
|
||||
Queries
|
||||
.activeContractCount()
|
||||
.query[Int]
|
||||
.unique
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
}
|
||||
}
|
||||
|
||||
def contract(id: ApiTypes.ContractId, types: PackageRegistry): Try[Option[Contract]] = logErrors {
|
||||
Try {
|
||||
Queries
|
||||
.contract(id.unwrap)
|
||||
.query[ContractRow]
|
||||
.to[List]
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
.headOption
|
||||
.map(_.toContract(types))
|
||||
}.flatMap(_.sequence)
|
||||
}
|
||||
|
||||
def contracts(types: PackageRegistry): Try[List[Contract]] = logErrors {
|
||||
Try {
|
||||
Queries.contracts
|
||||
.query[ContractRow]
|
||||
.to[List]
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
.map(_.toContract(types))
|
||||
}.flatMap(_.sequence)
|
||||
}
|
||||
|
||||
def activeContracts(types: PackageRegistry): Try[List[Contract]] = logErrors {
|
||||
Try {
|
||||
Queries.activeContracts
|
||||
.query[ContractRow]
|
||||
.to[List]
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
.map(_.toContract(types))
|
||||
}.flatMap(_.sequence)
|
||||
}
|
||||
|
||||
def contractsForTemplate(tId: DamlLfIdentifier, types: PackageRegistry): Try[List[Contract]] =
|
||||
logErrors {
|
||||
Try {
|
||||
Queries
|
||||
.contractsForTemplate(tId.asOpaqueString)
|
||||
.query[ContractRow]
|
||||
.to[List]
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
.map(_.toContract(types))
|
||||
}.flatMap(_.sequence)
|
||||
}
|
||||
|
||||
def activeContractsForTemplate(
|
||||
tId: DamlLfIdentifier,
|
||||
types: PackageRegistry,
|
||||
): Try[List[Contract]] = logErrors {
|
||||
Try {
|
||||
Queries
|
||||
.activeContractsForTemplate(tId.asOpaqueString)
|
||||
.query[ContractRow]
|
||||
.to[List]
|
||||
.transact(xa)
|
||||
.unsafeRunSync()
|
||||
.map(_.toContract(types))
|
||||
}.flatMap(_.sequence)
|
||||
}
|
||||
}
|
@ -1,176 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.data
|
||||
|
||||
import com.daml.ledger.api.refinements.ApiTypes
|
||||
import com.daml.lf.value.json.ApiCodecCompressed
|
||||
import ApiCodecCompressed.JsonImplicits._
|
||||
import com.daml.navigator.json.ModelCodec.JsonImplicits._
|
||||
import com.daml.navigator.json.DamlLfCodec.JsonImplicits._
|
||||
import com.daml.navigator.model._
|
||||
|
||||
import scala.util.{Failure, Try}
|
||||
import scalaz.syntax.tag._
|
||||
import spray.json._
|
||||
|
||||
final case class EventRow(
|
||||
id: String,
|
||||
transactionId: String,
|
||||
workflowId: String,
|
||||
parentId: Option[String],
|
||||
contractId: String,
|
||||
witnessParties: String,
|
||||
subclassType: String,
|
||||
templateId: Option[String],
|
||||
recordArgument: Option[String],
|
||||
choice: Option[String],
|
||||
argumentValue: Option[String],
|
||||
actingParties: Option[String],
|
||||
isConsuming: Option[Boolean],
|
||||
agreementText: Option[String],
|
||||
signatories: String,
|
||||
observers: String,
|
||||
key: Option[String],
|
||||
) {
|
||||
|
||||
def toEvent(types: PackageRegistry): Try[Event] = {
|
||||
subclassType match {
|
||||
case "ContractCreated" =>
|
||||
(for {
|
||||
wp <- Try(witnessParties.parseJson.convertTo[List[ApiTypes.Party]])
|
||||
sig <- Try(signatories.parseJson.convertTo[List[ApiTypes.Party]])
|
||||
obs <- Try(observers.parseJson.convertTo[List[ApiTypes.Party]])
|
||||
tpStr <- Try(templateId.get)
|
||||
tp <- Try(parseOpaqueIdentifier(tpStr).get)
|
||||
recArgJson <- Try(recordArgument.get)
|
||||
recArgAny <- Try(
|
||||
ApiCodecCompressed
|
||||
.jsValueToApiValue(recArgJson.parseJson, tp, types.damlLfDefDataType _)
|
||||
)
|
||||
recArg <- Try(recArgAny.asInstanceOf[ApiRecord])
|
||||
template <- types
|
||||
.template(tp)
|
||||
.fold[Try[Template]](
|
||||
Failure(new RuntimeException(s"No template in package registry with identifier $tp"))
|
||||
)(Try(_))
|
||||
key <- Try(
|
||||
key.map(
|
||||
_.parseJson.convertTo[ApiValue](
|
||||
ApiCodecCompressed.apiValueJsonReader(template.key.get, types.damlLfDefDataType _)
|
||||
)
|
||||
)
|
||||
)
|
||||
} yield {
|
||||
ContractCreated(
|
||||
ApiTypes.EventId(id),
|
||||
parentId.map(ApiTypes.EventId(_)),
|
||||
ApiTypes.TransactionId(transactionId),
|
||||
wp,
|
||||
ApiTypes.WorkflowId(workflowId),
|
||||
ApiTypes.ContractId(contractId),
|
||||
tp,
|
||||
recArg,
|
||||
agreementText,
|
||||
sig,
|
||||
obs,
|
||||
key,
|
||||
)
|
||||
}).recoverWith { case e: Throwable =>
|
||||
Failure(
|
||||
DeserializationFailed(
|
||||
s"Failed to deserialize ContractCreated from row: $this. Error: $e"
|
||||
)
|
||||
)
|
||||
}
|
||||
case "ChoiceExercised" =>
|
||||
(for {
|
||||
wp <- Try(witnessParties.parseJson.convertTo[List[ApiTypes.Party]])
|
||||
chc <- Try(choice.get)
|
||||
argJson <- Try(argumentValue.get)
|
||||
tp <- Try(templateId.get)
|
||||
tid <- Try(parseOpaqueIdentifier(tp).get)
|
||||
t <- Try(types.template(tid).get)
|
||||
choiceType <- Try(
|
||||
t.choices.find(c => ApiTypes.Choice.unwrap(c.name) == chc).get.parameter
|
||||
)
|
||||
arg <- Try(
|
||||
ApiCodecCompressed
|
||||
.jsValueToApiValue(argJson.parseJson, choiceType, types.damlLfDefDataType _)
|
||||
)
|
||||
apJson <- Try(actingParties.get)
|
||||
ap <- Try(apJson.parseJson.convertTo[List[ApiTypes.Party]])
|
||||
consuming <- Try(isConsuming.get)
|
||||
} yield {
|
||||
ChoiceExercised(
|
||||
ApiTypes.EventId(id),
|
||||
parentId.map(ApiTypes.EventId(_)),
|
||||
ApiTypes.TransactionId(transactionId),
|
||||
wp,
|
||||
ApiTypes.WorkflowId(workflowId),
|
||||
ApiTypes.ContractId(contractId),
|
||||
tid,
|
||||
ApiTypes.Choice(chc),
|
||||
arg,
|
||||
ap,
|
||||
consuming,
|
||||
)
|
||||
}).recoverWith { case e: Throwable =>
|
||||
Failure(
|
||||
DeserializationFailed(
|
||||
s"Failed to deserialize ChoiceExercised from row: $this. Error: $e"
|
||||
)
|
||||
)
|
||||
}
|
||||
case s => Failure(DeserializationFailed(s"unknown subclass type for Event: $s"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object EventRow {
|
||||
|
||||
def fromEvent(event: Event): EventRow = {
|
||||
event match {
|
||||
case c: ContractCreated =>
|
||||
EventRow(
|
||||
c.id.unwrap,
|
||||
c.transactionId.unwrap,
|
||||
c.workflowId.unwrap,
|
||||
c.parentId.map(_.unwrap),
|
||||
c.contractId.unwrap,
|
||||
c.witnessParties.toJson.compactPrint,
|
||||
"ContractCreated",
|
||||
Some(c.templateId.asOpaqueString),
|
||||
Some(c.argument.toJson.compactPrint),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
c.agreementText,
|
||||
c.signatories.toJson.compactPrint,
|
||||
c.observers.toJson.compactPrint,
|
||||
c.key.map(_.toJson.compactPrint),
|
||||
)
|
||||
case e: ChoiceExercised =>
|
||||
EventRow(
|
||||
e.id.unwrap,
|
||||
e.transactionId.unwrap,
|
||||
e.workflowId.unwrap,
|
||||
e.parentId.map(_.unwrap),
|
||||
e.contractId.unwrap,
|
||||
e.witnessParties.toJson.compactPrint,
|
||||
"ChoiceExercised",
|
||||
Some(e.templateId.asOpaqueString),
|
||||
None,
|
||||
Some(e.choice.unwrap),
|
||||
Some(e.argument.toJson.compactPrint),
|
||||
Some(e.actingParties.toJson.compactPrint),
|
||||
Some(e.consuming),
|
||||
None,
|
||||
"[]",
|
||||
"[]",
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,275 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.data
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
|
||||
object Queries {
|
||||
|
||||
private def createIndex(table: String, columns: List[String], name: String): Fragment =
|
||||
Fragment.const(s"CREATE INDEX $name ON ${table} (${columns.mkString(", ")})")
|
||||
|
||||
val createContractTable: Fragment = sql"""
|
||||
CREATE TABLE
|
||||
contract
|
||||
(
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
template_id TEXT NOT NULL,
|
||||
archive_transaction_id TEXT DEFAULT NULL,
|
||||
argument JSON NOT NULL,
|
||||
agreement_text TEXT DEFAULT NULL,
|
||||
signatories JSON DEFAULT NULL,
|
||||
observers JSON DEFAULT NULL,
|
||||
contract_key JSON DEFAULT NULL
|
||||
)
|
||||
"""
|
||||
|
||||
val contractIdIndex = createIndex("contract", List("id"), "contract_id_idx")
|
||||
val contractIsActive =
|
||||
createIndex("contract", List("archive_transaction_id"), "contract_active_idx")
|
||||
val contractTemplateIdIsActive = createIndex(
|
||||
"contract",
|
||||
List("template_id", "archive_transaction_id"),
|
||||
"contract_tmplt_active_idx",
|
||||
)
|
||||
|
||||
val createEventTable: Fragment = sql"""
|
||||
CREATE TABLE
|
||||
event
|
||||
(
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
transaction_id TEXT NOT NULL,
|
||||
workflow_id TEXT NOT NULL,
|
||||
parent_id TEXT DEFAULT NULL,
|
||||
contract_id TEXT NOT NULL,
|
||||
witness_parties JSON NOT NULL,
|
||||
subclass_type TEXT NOT NULL,
|
||||
template_id TEXT DEFAULT NULL,
|
||||
record_argument JSON DEFAULT NULL,
|
||||
choice TEXT DEFAULT NULL,
|
||||
argument_value JSON DEFAULT NULL,
|
||||
acting_parties JSON DEFAULT NULL,
|
||||
is_consuming INTEGER DEFAULT NULL,
|
||||
agreement_text TEXT DEFAULT NULL,
|
||||
signatories JSON DEFAULT NULL,
|
||||
observers JSON DEFAULT NULL,
|
||||
contract_key JSON DEFAULT NULL
|
||||
)
|
||||
"""
|
||||
|
||||
val eventIdIndex = createIndex("event", List("id"), "event_id_idx")
|
||||
val eventTransactionIdParentId =
|
||||
createIndex("event", List("transaction_id", "parent_id"), "event_tx_id_idx")
|
||||
val eventContractIdSubclass =
|
||||
createIndex("event", List("contract_id", "subclass_type"), "event_cid_idx")
|
||||
|
||||
val createTransactionTable: Fragment = sql"""
|
||||
CREATE TABLE
|
||||
transactions
|
||||
(
|
||||
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
id TEXT UNIQUE NOT NULL,
|
||||
command_id TEXT DEFAULT NULL,
|
||||
effective_at TEXT NOT NULL,
|
||||
offset TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
|
||||
val transactionIdIndex = createIndex("transactions", List("id"), "tx_id_idx")
|
||||
|
||||
val createCommandStatusTable: Fragment = sql"""
|
||||
CREATE TABLE
|
||||
command_status
|
||||
(
|
||||
command_id TEXT PRIMARY KEY NOT NULL,
|
||||
is_completed INTEGER NOT NULL,
|
||||
subclass_type TEXT NOT NULL,
|
||||
code TEXT DEFAULT NULL,
|
||||
details TEXT DEFAULT NULL,
|
||||
transaction_id TEXT DEFAULT NULL
|
||||
)
|
||||
"""
|
||||
|
||||
val commandStatusCommandIdIndex =
|
||||
createIndex("command_status", List("command_id"), "cmd_st_id_idx")
|
||||
|
||||
val createCommandTable: Fragment = sql"""
|
||||
CREATE TABLE
|
||||
command
|
||||
(
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
idx INTEGER UNIQUE NOT NULL,
|
||||
workflow_id TEXT NOT NULL,
|
||||
platform_time TEXT NOT NULL,
|
||||
subclass_type TEXT NOT NULL,
|
||||
template TEXT DEFAULT NULL,
|
||||
record_argument JSON DEFAULT NULL,
|
||||
contract_id TEXT DEFAULT NULL,
|
||||
choice TEXT DEFAULT NULL,
|
||||
argument_value JSON DEFAULT NULL
|
||||
)
|
||||
"""
|
||||
|
||||
val commandIdIndex = createIndex("command", List("id"), "cmd_id_idx")
|
||||
|
||||
def schema(): Fragment =
|
||||
sql"""SELECT sql FROM sqlite_master
|
||||
ORDER BY tbl_name, type DESC, name
|
||||
"""
|
||||
|
||||
def query(query: String): Fragment = {
|
||||
Fragment.const(query)
|
||||
}
|
||||
|
||||
def insertEvent(row: EventRow): Fragment =
|
||||
sql"""
|
||||
INSERT INTO
|
||||
event
|
||||
(id, transaction_id, workflow_id, parent_id, contract_id, witness_parties, subclass_type,
|
||||
template_id, record_argument, choice, argument_value, acting_parties, is_consuming, agreement_text, signatories, observers, contract_key)
|
||||
VALUES
|
||||
(${row.id}, ${row.transactionId}, ${row.workflowId}, ${row.parentId}, ${row.contractId}, ${row.witnessParties}, ${row.subclassType},
|
||||
${row.templateId}, ${row.recordArgument}, ${row.choice}, ${row.argumentValue}, ${row.actingParties}, ${row.isConsuming}, ${row.agreementText}, ${row.signatories}, ${row.observers}, ${row.key})
|
||||
"""
|
||||
|
||||
def eventById(id: String): Fragment =
|
||||
sql"""
|
||||
SELECT * FROM event WHERE id = $id LIMIT 1
|
||||
"""
|
||||
|
||||
def eventsByParentId(id: String): Fragment =
|
||||
sql"""
|
||||
SELECT * FROM event WHERE parent_id = $id
|
||||
"""
|
||||
|
||||
def topLevelEventsByTransactionId(id: String): Fragment =
|
||||
sql"""
|
||||
SELECT * FROM event WHERE transaction_id = $id AND parent_id is NULL
|
||||
"""
|
||||
|
||||
def eventByTypeAndContractId(subclassType: String, contractId: String): Fragment =
|
||||
sql"""
|
||||
SELECT * FROM event WHERE subclass_type = $subclassType and contract_id = $contractId
|
||||
"""
|
||||
|
||||
def insertTransaction(row: TransactionRow): Fragment =
|
||||
sql"""
|
||||
INSERT INTO
|
||||
transactions
|
||||
(id, command_id, effective_at, offset)
|
||||
VALUES
|
||||
(${row.id}, ${row.commandId}, ${row.effectiveAt}, ${row.offset})
|
||||
"""
|
||||
|
||||
def transactionById(id: String): Fragment =
|
||||
sql"""
|
||||
SELECT * FROM transactions WHERE id = $id LIMIT 1
|
||||
"""
|
||||
|
||||
def lastTransaction(): Fragment =
|
||||
sql"""
|
||||
SELECT * FROM transactions ORDER BY seq DESC LIMIT 1
|
||||
"""
|
||||
|
||||
def upsertCommandStatus(row: CommandStatusRow): Fragment =
|
||||
sql"""
|
||||
INSERT INTO
|
||||
command_status
|
||||
(command_id, is_completed, subclass_type, code, details, transaction_id)
|
||||
VALUES
|
||||
(${row.commandId}, ${row.isCompleted}, ${row.subclassType}, ${row.code}, ${row.details}, ${row.transactionId})
|
||||
ON CONFLICT(command_id)
|
||||
DO UPDATE
|
||||
SET is_completed = ${row.isCompleted},
|
||||
subclass_type = ${row.subclassType},
|
||||
code = ${row.code},
|
||||
details = ${row.details},
|
||||
transaction_id = ${row.transactionId}
|
||||
"""
|
||||
|
||||
def updateCommandStatus(row: CommandStatusRow): Fragment =
|
||||
sql"""
|
||||
UPDATE command_status
|
||||
SET is_completed = ${row.isCompleted},
|
||||
subclass_type = ${row.subclassType},
|
||||
code = ${row.code},
|
||||
details = ${row.details},
|
||||
transaction_id = ${row.transactionId}
|
||||
WHERE command_id = ${row.commandId}
|
||||
"""
|
||||
|
||||
def commandStatusByCommandId(commandId: String): Fragment =
|
||||
sql"""
|
||||
SELECT * FROM command_status WHERE command_id = $commandId LIMIT 1
|
||||
"""
|
||||
|
||||
def insertCommand(row: CommandRow): Fragment =
|
||||
sql"""
|
||||
INSERT INTO
|
||||
command
|
||||
(id, idx, workflow_id, platform_time, subclass_type, template, record_argument, contract_id, choice, argument_value)
|
||||
VALUES
|
||||
(${row.id}, ${row.index}, ${row.workflowId}, ${row.platformTime}, ${row.subclassType}, ${row.template}, ${row.recordArgument}, ${row.contractId}, ${row.choice}, ${row.argumentValue})
|
||||
"""
|
||||
|
||||
def commandById(id: String): Fragment =
|
||||
sql"""
|
||||
SELECT * FROM command WHERE id = $id LIMIT 1
|
||||
"""
|
||||
|
||||
def allCommands(): Fragment =
|
||||
sql"""
|
||||
SELECT * FROM command
|
||||
"""
|
||||
|
||||
def insertContract(row: ContractRow): Fragment =
|
||||
sql"""
|
||||
INSERT INTO
|
||||
contract
|
||||
(id, template_id, archive_transaction_id, argument, agreement_text, signatories, observers, contract_key)
|
||||
VALUES
|
||||
(${row.id}, ${row.templateId}, ${row.archiveTransactionId}, ${row.argument}, ${row.agreementText}, ${row.signatories}, ${row.observers}, ${row.key})
|
||||
"""
|
||||
|
||||
def archiveContract(contractId: String, archiveTransactionId: String): Fragment =
|
||||
sql"""
|
||||
UPDATE contract SET archive_transaction_id = $archiveTransactionId WHERE id = $contractId
|
||||
"""
|
||||
|
||||
def contractCount(): Fragment =
|
||||
sql"""
|
||||
SELECT COUNT(*) FROM contract
|
||||
"""
|
||||
|
||||
def activeContractCount(): Fragment =
|
||||
sql"""
|
||||
SELECT COUNT(*) FROM contract WHERE archive_transaction_id IS NULL
|
||||
"""
|
||||
|
||||
def contract(id: String): Fragment =
|
||||
sql"""
|
||||
SELECT * FROM contract WHERE id = $id LIMIT 1
|
||||
"""
|
||||
|
||||
def contracts: Fragment =
|
||||
sql"""
|
||||
SELECT * FROM contract
|
||||
"""
|
||||
|
||||
def activeContracts: Fragment =
|
||||
sql"""
|
||||
SELECT * FROM contract WHERE archive_transaction_id IS NULL
|
||||
"""
|
||||
|
||||
def contractsForTemplate(tId: String): Fragment =
|
||||
sql"""
|
||||
SELECT * FROM contract WHERE template_id = $tId
|
||||
"""
|
||||
|
||||
def activeContractsForTemplate(tId: String): Fragment =
|
||||
sql"""
|
||||
SELECT * FROM contract WHERE template_id = $tId and archive_transaction_id IS NULL
|
||||
"""
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.data
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
import com.daml.ledger.api.refinements.ApiTypes
|
||||
import com.daml.navigator.model.{Event, Transaction}
|
||||
|
||||
import scalaz.syntax.tag._
|
||||
|
||||
final case class TransactionRow(
|
||||
autoInc: Option[Int],
|
||||
id: String,
|
||||
commandId: Option[String],
|
||||
effectiveAt: String,
|
||||
offset: String,
|
||||
) {
|
||||
|
||||
def toTransaction(events: List[Event]): Transaction = {
|
||||
Transaction(
|
||||
ApiTypes.TransactionId(id),
|
||||
commandId.map(ApiTypes.CommandId(_)),
|
||||
Instant.parse(effectiveAt),
|
||||
offset,
|
||||
events,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object TransactionRow {
|
||||
|
||||
def fromTransaction(tx: Transaction): TransactionRow = {
|
||||
TransactionRow(
|
||||
None,
|
||||
tx.id.unwrap,
|
||||
tx.commandId.map(_.unwrap),
|
||||
tx.effectiveAt.toString,
|
||||
tx.offset,
|
||||
)
|
||||
}
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.dotnot
|
||||
|
||||
/** A zipper for a property-like structure, such as dot-notation
|
||||
*
|
||||
* For instance, given the property `foo.bar.baz.doh`, a cursor `PropertyCursor(List("foo"), "bar", List("baz", "doh"))`
|
||||
* is pointing at the `"bar"` part of the property.
|
||||
*
|
||||
* @param reversedBefore the part of the property prior to the cursor
|
||||
* @param current the piece of the property currently pointed by the cursor
|
||||
* @param after the part of the property after the cursor
|
||||
*/
|
||||
final case class PropertyCursor(
|
||||
reversedBefore: List[String],
|
||||
current: String,
|
||||
after: List[String],
|
||||
) {
|
||||
|
||||
/** @return whether [[current]] is the last */
|
||||
def isLast: Boolean = after.isEmpty
|
||||
|
||||
/** @return a cursor pointing at the previous part of the property, if possible */
|
||||
def prev: Option[PropertyCursor] =
|
||||
reversedBefore match {
|
||||
case Nil => None
|
||||
case head :: tail => Some(PropertyCursor(tail, head, current :: after))
|
||||
}
|
||||
|
||||
/** @return a cursor pointing at the next part of the property, if possible */
|
||||
def next: Option[PropertyCursor] =
|
||||
after match {
|
||||
case Nil => None
|
||||
case head :: tail => Some(PropertyCursor(current :: reversedBefore, head, tail))
|
||||
}
|
||||
|
||||
/** Change the part pointed by the cursor */
|
||||
def withCurrent(current: String): PropertyCursor =
|
||||
this.copy(current = current)
|
||||
|
||||
/** Change the part of the property after the cursor */
|
||||
def withAfter(newAfter: List[String]): PropertyCursor =
|
||||
this.copy(after = newAfter)
|
||||
|
||||
override def toString: String =
|
||||
(reversedBefore.reverse ++ (("[" + current + "]") +: after)).mkString(".")
|
||||
|
||||
def ensureLast[A](target: String)(r: => A): Either[DotNotFailure, A] =
|
||||
if (isLast) Right(r) else Left(MustBeLastPart(target, this, current))
|
||||
}
|
||||
|
||||
object PropertyCursor {
|
||||
|
||||
/** @return a [[PropertyCursor]] created by splitting the input on dot */
|
||||
def fromString(str: String): PropertyCursor = {
|
||||
val Array(current, after @ _*) = str.split("\\.", -1)
|
||||
PropertyCursor(List.empty, current, after.toList)
|
||||
}
|
||||
}
|
@ -1,260 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator
|
||||
|
||||
/** dotnot(ation) is a simple library to implement a string-based dot-notation system
|
||||
* to access properties. A property is something in dot notation `branch1.branch2.leaf`
|
||||
* similar to the dot-notation in scala to reference a field.
|
||||
*
|
||||
* The idea is to represent all the possible paths from the root of the model down
|
||||
* to the leafs with a nice DSL.
|
||||
*
|
||||
* Naming:
|
||||
* - `Tree`: the model is seen as a `Tree` where each portion can either be a `Leaf` or a `Branch`
|
||||
* - `Leaf`: a node of the Tree that contains only one value (e.g. a primitive field, like `Int`)
|
||||
* - `Branch`: a non-leaf part of the model (e.g. an object with fields)
|
||||
* - `root`: the entry point and root of the Tree
|
||||
* - `path` or `property`: the list of all the pieces of the `Tree` from the `root` to a `leaf`
|
||||
* - `dotnot handler for k`: the set of all the paths and actions to be performed on an instance of `k`
|
||||
*/
|
||||
package dotnot {
|
||||
|
||||
sealed trait DotNotFailure {
|
||||
def cursor: PropertyCursor
|
||||
def value: String
|
||||
}
|
||||
final case class CannotBeEmpty(target: String, cursor: PropertyCursor, value: String)
|
||||
extends DotNotFailure
|
||||
final case class MustBeLastPart(target: String, cursor: PropertyCursor, value: String)
|
||||
extends DotNotFailure
|
||||
final case class MustNotBeLastPart(target: String, cursor: PropertyCursor, value: String)
|
||||
extends DotNotFailure
|
||||
final case class TypeCoercionFailure(
|
||||
target: String,
|
||||
expectedType: String,
|
||||
cursor: PropertyCursor,
|
||||
value: String,
|
||||
) extends DotNotFailure
|
||||
final case class UnknownProperty(target: String, cursor: PropertyCursor, value: String)
|
||||
extends DotNotFailure
|
||||
final case class MatchNotFoundForValue(target: String, cursor: PropertyCursor, value: String)
|
||||
extends DotNotFailure
|
||||
final case class UnknownType(name: String, cursor: PropertyCursor, value: String)
|
||||
extends DotNotFailure
|
||||
|
||||
final case class NameMatcherToAction[T, R, C](matcher: NameMatcher, action: Action[T, R, C])
|
||||
final case class ValueMatcherToAction[T, R, C](matcher: ValueMatcher, action: Action[T, R, C])
|
||||
|
||||
/** Utility for debugging purposes, implements an equalsTo function with a toString defined */
|
||||
final case class equalsTo[T](t: T) extends Function[T, Boolean] {
|
||||
def apply(t2: T): Boolean = t == t2
|
||||
override def toString: String = s"""_ == $t"""
|
||||
}
|
||||
|
||||
final case object constTrue extends Function[String, Boolean] {
|
||||
def apply(s: String): Boolean = true
|
||||
override def toString: String = s"""_ => true"""
|
||||
}
|
||||
|
||||
sealed trait OnTreeBase[T, R, C] {
|
||||
|
||||
def target: String
|
||||
def onTree: OnTreeReady[T, R, C]
|
||||
|
||||
def onLeafP(nameMatcher: NameMatcher): OnLeaf[T, R, C] =
|
||||
OnLeaf[T, R, C](onTree, nameMatcher)
|
||||
|
||||
def onLeaf(name: String): OnLeaf[T, R, C] =
|
||||
onLeafP(equalsTo(name))
|
||||
|
||||
/** Match a branch by predicate on the name and delegate handling the subtree from that branch
|
||||
*
|
||||
* @param nameMatcher matcher of the branch name
|
||||
* @param project the projection from the parent model of type `T` to the child model
|
||||
* `P` where `P` is the type of the field of an instance of `T`
|
||||
* @param delegate the dotnot handler for the subtree
|
||||
*/
|
||||
def onBranchP[P](
|
||||
nameMatcher: NameMatcher,
|
||||
project: T => P,
|
||||
delegate: OnTreeReady[P, R, C],
|
||||
): OnTreeReady[T, R, C] = {
|
||||
val action: (T, PropertyCursor, String, C) => Either[DotNotFailure, R] =
|
||||
(t: T, cursor: PropertyCursor, value: String, context: C) => {
|
||||
cursor.next match {
|
||||
case None =>
|
||||
Left(MustNotBeLastPart(target, cursor, value))
|
||||
case Some(nextCursor) =>
|
||||
val p = project(t)
|
||||
delegate.run(p, nextCursor, value, context)
|
||||
}
|
||||
}
|
||||
val nameMatcherToAction = NameMatcherToAction[T, R, C](nameMatcher, action)
|
||||
onTree.copy(nameMatcherToActions = onTree.nameMatcherToActions :+ nameMatcherToAction)
|
||||
}
|
||||
|
||||
/** Same as [[onBranchP]] but match the branch by name */
|
||||
def onBranch[P](
|
||||
name: String,
|
||||
project: T => P,
|
||||
delegate: OnTreeReady[P, R, C],
|
||||
): OnTreeReady[T, R, C] =
|
||||
if (name == "*") {
|
||||
onBranchP[P](constTrue, project, delegate)
|
||||
} else {
|
||||
onBranchP[P](equalsTo(name), project, delegate)
|
||||
}
|
||||
|
||||
/** Given a collection, match one the first element `e` with `getName(e)` equal to
|
||||
* the value currently pointed by the cursor and then delegate handling that element
|
||||
* to `delegate`
|
||||
*
|
||||
* @param getName a function to read the name of `E`
|
||||
* @param delegate the dotnot handler for `e`
|
||||
*/
|
||||
def onElements[E](getName: E => String, delegate: OnTreeReady[E, R, C])(implicit
|
||||
ev: T <:< Iterable[E]
|
||||
): OnTreeReady[T, R, C] = {
|
||||
val action: (T, PropertyCursor, String, C) => Either[DotNotFailure, R] =
|
||||
(t: T, cursor: PropertyCursor, value: String, context: C) => {
|
||||
cursor.next match {
|
||||
case None =>
|
||||
Left(MustNotBeLastPart(target, cursor, value))
|
||||
case Some(nextCursor) =>
|
||||
t.find(getName(_).toLowerCase == cursor.current.toLowerCase) match {
|
||||
case None =>
|
||||
Left(UnknownProperty(target, cursor, value))
|
||||
case Some(e) =>
|
||||
delegate.run(e, nextCursor, value, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
val nameMatcherToAction = NameMatcherToAction[T, R, C](constTrue, action)
|
||||
onTree.copy(nameMatcherToActions = onTree.nameMatcherToActions :+ nameMatcherToAction)
|
||||
}
|
||||
|
||||
def run(t: T, cursor: PropertyCursor, value: String, context: C): Either[DotNotFailure, R] = {
|
||||
onTree.nameMatcherToActions
|
||||
.find(_.matcher(cursor.current))
|
||||
.map(_.action)
|
||||
.orElse(onTree.default) match {
|
||||
case None =>
|
||||
Left(UnknownProperty(target, cursor, value))
|
||||
case Some(action) =>
|
||||
action(t, cursor, value, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final case class OnTree[T, R, C](target: String) extends OnTreeBase[T, R, C] {
|
||||
|
||||
def onTree: OnTreeReady[T, R, C] =
|
||||
OnTreeReady[T, R, C](target, Vector(), None)
|
||||
|
||||
}
|
||||
|
||||
final case class OnTreeReady[T, R, C](
|
||||
target: String,
|
||||
nameMatcherToActions: Vector[NameMatcherToAction[T, R, C]],
|
||||
default: Option[Action[T, R, C]],
|
||||
) extends OnTreeBase[T, R, C] {
|
||||
|
||||
override def onTree: OnTreeReady[T, R, C] = this
|
||||
|
||||
}
|
||||
|
||||
sealed trait OnLeafBase[T, R, C] {
|
||||
def onTreeOld: OnTreeReady[T, R, C]
|
||||
def leafMatcher: NameMatcher
|
||||
def valueMatcherToActions: Vector[ValueMatcherToAction[T, R, C]]
|
||||
def default: Option[Action[T, R, C]]
|
||||
|
||||
def onValueP(valueMatcher: ValueMatcher): OnValue[T, R, C] =
|
||||
OnValue[T, R, C](
|
||||
OnLeafReady(onTreeOld, leafMatcher, valueMatcherToActions, default),
|
||||
valueMatcher,
|
||||
)
|
||||
|
||||
def onValue(value: String): OnValue[T, R, C] =
|
||||
onValueP(equalsTo(value))
|
||||
|
||||
def onAnyValue: OnValue[T, R, C] =
|
||||
onValueP(constTrue)
|
||||
}
|
||||
|
||||
final case class OnLeaf[T, R, C](onTreeOld: OnTreeReady[T, R, C], leafMatcher: NameMatcher)
|
||||
extends OnLeafBase[T, R, C] {
|
||||
override def valueMatcherToActions: Vector[ValueMatcherToAction[T, R, C]] = Vector()
|
||||
override def default: Option[Action[T, R, C]] = None
|
||||
}
|
||||
|
||||
final case class OnLeafReady[T, R, C](
|
||||
onTreeOld: OnTreeReady[T, R, C],
|
||||
leafMatcher: NameMatcher,
|
||||
valueMatcherToActions: Vector[ValueMatcherToAction[T, R, C]],
|
||||
default: Option[Action[T, R, C]],
|
||||
) extends OnLeafBase[T, R, C]
|
||||
with OnTreeBase[T, R, C] {
|
||||
|
||||
override def target: String = onTreeOld.target
|
||||
|
||||
private def nameAction: NameMatcherToAction[T, R, C] = {
|
||||
val action: Action[T, R, C] = (t: T, cursor: PropertyCursor, value: String, context: C) => {
|
||||
valueMatcherToActions.find(_.matcher(value)).map(_.action).orElse(default) match {
|
||||
case None =>
|
||||
Left(MatchNotFoundForValue(onTreeOld.target, cursor, value))
|
||||
case Some(action) =>
|
||||
action(t, cursor, value, context)
|
||||
}
|
||||
}
|
||||
NameMatcherToAction[T, R, C](leafMatcher, action)
|
||||
}
|
||||
|
||||
private def onTreeUpdated: OnTreeReady[T, R, C] =
|
||||
onTreeOld.copy(nameMatcherToActions = onTreeOld.nameMatcherToActions :+ nameAction)
|
||||
|
||||
override def onTree: OnTreeReady[T, R, C] = onTreeUpdated
|
||||
}
|
||||
|
||||
final case class OnValue[T, R, C](onLeaf: OnLeafReady[T, R, C], valueMatcher: ValueMatcher) {
|
||||
|
||||
def perform[K](f: (T, K) => R)(implicit readK: Read[K]): OnLeafReady[T, R, C] = {
|
||||
val action: Action[T, R, C] = (t: T, cursor: PropertyCursor, value: String, _: C) => {
|
||||
if (cursor.isLast) {
|
||||
Right(f(t, readK.from(value).toOption.get))
|
||||
} else {
|
||||
Left(MustBeLastPart(onLeaf.onTreeOld.target, cursor, value))
|
||||
}
|
||||
}
|
||||
val valueMatcherToAction = ValueMatcherToAction(valueMatcher, action)
|
||||
onLeaf.copy(valueMatcherToActions = onLeaf.valueMatcherToActions :+ valueMatcherToAction)
|
||||
}
|
||||
|
||||
def const(r: R): OnLeafReady[T, R, C] =
|
||||
perform[String]((_: T, _: String) => r)
|
||||
}
|
||||
}
|
||||
|
||||
package object dotnot {
|
||||
|
||||
type NameMatcher = String => Boolean
|
||||
type ValueMatcher = String => Boolean
|
||||
type Action[T, R, C] = (T, PropertyCursor, String, C) => Either[DotNotFailure, R]
|
||||
|
||||
/** Create a dot-notation handler for a type `T` with name `name` and with return
|
||||
* type from the leafs of type `R`
|
||||
*/
|
||||
def root[T, R, C](name: String): OnTree[T, R, C] =
|
||||
OnTree[T, R, C](name)
|
||||
|
||||
/** Utility method that creates a "opaque" dot-notation handler, which is
|
||||
* a handler that bypass the library and uses the given function `f` to
|
||||
* access the underlying structure of type `T` and to produce a result of
|
||||
* type `R`.
|
||||
*
|
||||
* Useful for when dotnot is not useful
|
||||
*/
|
||||
def opaque[T, R, C](name: String)(f: Action[T, R, C]): OnTreeReady[T, R, C] =
|
||||
OnTreeReady[T, R, C](name, Vector(), Some(f))
|
||||
}
|
@ -1,921 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.graphql
|
||||
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import org.apache.pekko.actor.ActorRef
|
||||
import org.apache.pekko.pattern.ask
|
||||
import org.apache.pekko.util.Timeout
|
||||
import com.daml.ledger.api.refinements.ApiTypes
|
||||
import com.daml.navigator.CustomEndpoint
|
||||
import com.daml.navigator.model._
|
||||
import com.daml.navigator.query._
|
||||
import com.daml.navigator.store.Store._
|
||||
import com.daml.navigator.time.{TimeProviderType, TimeProviderWithType}
|
||||
import com.daml.navigator.model.converter.GenericConversionError
|
||||
import sangria.ast.StringValue
|
||||
import sangria.macros.derive.GraphQLDeprecated
|
||||
import sangria.schema.InputObjectType.DefaultInput
|
||||
import sangria.schema._
|
||||
import sangria.validation.{ValueCoercionViolation, Violation}
|
||||
import scalaz.Tag
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
case class GraphQLContext(party: PartyState, store: ActorRef) {
|
||||
def ledger: Ledger = party.ledger
|
||||
def templates: PackageRegistry = party.packageRegistry
|
||||
}
|
||||
|
||||
case class LedgerTime(time: TimeProviderWithType, id: String = "CURRENT")
|
||||
|
||||
case class UserFacingError(message: String)
|
||||
extends Throwable
|
||||
with sangria.execution.UserFacingError {
|
||||
override def getMessage: String = message
|
||||
}
|
||||
|
||||
private object Implicits {
|
||||
|
||||
implicit final class `ExtendedInterfaceType Ops`[Ctx, Val](
|
||||
private val self: InterfaceType[Ctx, Val]
|
||||
) extends AnyVal {
|
||||
// Hack around https://github.com/scala/bug/issues/11662 which
|
||||
// otherwise results in the implicit PossibleObject constructor
|
||||
// being ambiguous due to the also implicit apply method.
|
||||
def withPossibleObjectTypes(possible: () => List[ObjectType[Ctx, _]]) =
|
||||
self.withPossibleTypes(() => possible().map(new PossibleObject[Ctx, Val](_)))
|
||||
}
|
||||
}
|
||||
|
||||
/** Schema definition for the UI backend GraphQL API. */
|
||||
@SuppressWarnings(
|
||||
Array("org.wartremover.warts.JavaSerializable", "org.wartremover.warts.Serializable")
|
||||
)
|
||||
final class GraphQLSchema(customEndpoints: Set[CustomEndpoint[_]]) {
|
||||
|
||||
import Implicits._
|
||||
|
||||
implicit private val actorTimeout: Timeout = Timeout(60, TimeUnit.SECONDS)
|
||||
implicit private val executionContext: ExecutionContext = ExecutionContext.global
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Scalar and enum types
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
val DirectionType = EnumType(
|
||||
"Direction",
|
||||
values = List(
|
||||
EnumValue("ASCENDING", value = SortDirection.ASCENDING),
|
||||
EnumValue("DESCENDING", value = SortDirection.DESCENDING),
|
||||
),
|
||||
)
|
||||
|
||||
val TimeTypeType = EnumType(
|
||||
"TimeType",
|
||||
values = List(
|
||||
EnumValue("static", value = TimeProviderType.Static),
|
||||
EnumValue("wallclock", value = TimeProviderType.WallClock),
|
||||
EnumValue("simulated", value = TimeProviderType.Simulated),
|
||||
),
|
||||
)
|
||||
|
||||
val SortingType = ObjectType(
|
||||
"Sorting",
|
||||
fields[Unit, SortCriterion](
|
||||
Field("field", StringType, resolve = _.value.field),
|
||||
Field("direction", DirectionType, resolve = _.value.direction),
|
||||
),
|
||||
)
|
||||
|
||||
val TimeType: ScalarType[Instant] = ScalarType[Instant](
|
||||
"Time",
|
||||
coerceOutput = (instant, _) => DateTimeFormatter.ISO_INSTANT.format(instant),
|
||||
coerceUserInput = {
|
||||
case s: String => parseInstant(s)
|
||||
case _ => Left(InstantCoercionViolation)
|
||||
},
|
||||
coerceInput = {
|
||||
case StringValue(s, _, _, _, _) => parseInstant(s)
|
||||
case _ => Left(InstantCoercionViolation)
|
||||
},
|
||||
)
|
||||
|
||||
val PartyType = StringAliasType("Party")
|
||||
val OffsetType = StringAliasType("LedgerOffset")
|
||||
val CommandIdType: ScalarType[ApiTypes.CommandId] = StringAliasType.tagged("CommandId")
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Composite types
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
val LedgerTimeType = ObjectType(
|
||||
"LedgerTime",
|
||||
fields[Unit, TimeProviderWithType](
|
||||
Field("id", IDType, resolve = _ => "CURRENT"),
|
||||
Field("time", TimeType, resolve = _.value.time.getCurrentTime),
|
||||
Field("type", TimeTypeType, resolve = _.value.`type`),
|
||||
),
|
||||
)
|
||||
|
||||
// noinspection ForwardReference
|
||||
val NodeType: InterfaceType[GraphQLContext, Node[_]] = InterfaceType[GraphQLContext, Node[_]](
|
||||
"Node",
|
||||
fields[GraphQLContext, Node[_]](
|
||||
Field("id", IDType, resolve = _.value.idString)
|
||||
),
|
||||
).withPossibleObjectTypes(() =>
|
||||
List(
|
||||
TransactionType,
|
||||
TemplateType,
|
||||
ContractType,
|
||||
CreateCommandType,
|
||||
ExerciseCommandType,
|
||||
CreatedEventType,
|
||||
ExercisedEventType,
|
||||
DamlLfDefDataTypeType,
|
||||
)
|
||||
)
|
||||
|
||||
// noinspection ForwardReference
|
||||
val DamlLfNodeType: InterfaceType[GraphQLContext, DamlLfNode] = InterfaceType(
|
||||
"DamlLfNode",
|
||||
fields[GraphQLContext, DamlLfNode](
|
||||
Field("id", IDType, resolve = _.value.idString),
|
||||
Field("package", StringType, resolve = _.value.id.packageId),
|
||||
Field("module", StringType, resolve = _.value.id.qualifiedName.module.toString()),
|
||||
Field("name", StringType, resolve = _.value.id.qualifiedName.name.toString()),
|
||||
),
|
||||
).withPossibleObjectTypes(() =>
|
||||
List(
|
||||
DamlLfDefDataTypeType,
|
||||
TemplateType,
|
||||
)
|
||||
)
|
||||
|
||||
val ChoiceType = ObjectType(
|
||||
"Choice",
|
||||
fields[Unit, Choice](
|
||||
Field(
|
||||
"name",
|
||||
StringType,
|
||||
resolve = context => Tag.unwrap[String, ApiTypes.ChoiceTag](context.value.name),
|
||||
),
|
||||
Field("parameter", JsonType.DamlLfTypeType, resolve = _.value.parameter),
|
||||
Field("consuming", BooleanType, resolve = _.value.consuming),
|
||||
Field(
|
||||
"inheritedInterface",
|
||||
OptionType(StringType),
|
||||
resolve = _.value.inheritedInterface.map(_.asOpaqueString),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// noinspection ForwardReference
|
||||
val TemplateType: ObjectType[GraphQLContext, Template] = ObjectType(
|
||||
"Template",
|
||||
interfaces[GraphQLContext, Template](NodeType, DamlLfNodeType),
|
||||
() =>
|
||||
fields[GraphQLContext, Template](
|
||||
Field("id", IDType, resolve = _.value.idString),
|
||||
Field("topLevelDecl", StringType, resolve = _.value.topLevelDecl),
|
||||
Field("parameter", JsonType.DamlLfTypeType, resolve = _.value.parameter),
|
||||
Field(
|
||||
"parameterDef",
|
||||
DamlLfDefDataTypeType,
|
||||
resolve = context =>
|
||||
context.ctx.templates
|
||||
.damlLfDefDataType(context.value.id)
|
||||
.map(DamlLfDefDataTypeBoxed(context.value.id, _))
|
||||
.get,
|
||||
),
|
||||
Field("choices", ListType(ChoiceType), resolve = _.value.choices),
|
||||
Field(
|
||||
"contracts",
|
||||
ContractPaginationType,
|
||||
arguments =
|
||||
SearchArg :: FilterArg :: IncludeArchivedArg :: CountArg :: StartArg :: SortArg :: Nil,
|
||||
resolve = context =>
|
||||
buildTemplateContractPager(context).fetch(context.ctx.ledger, context.ctx.templates),
|
||||
),
|
||||
Field(
|
||||
"implementedInterfaces",
|
||||
ListType(StringType),
|
||||
resolve = _.value.implementedInterfaces.toList.map(_.asOpaqueString),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val TemplateEdgeType = ObjectType(
|
||||
"TemplateEdge",
|
||||
fields[GraphQLContext, Template](
|
||||
Field("node", TemplateType, resolve = _.value),
|
||||
Field("cursor", StringType, resolve = _.value.idString),
|
||||
),
|
||||
)
|
||||
|
||||
val TemplatePaginationType = ObjectType(
|
||||
"TemplatePagination",
|
||||
fields[GraphQLContext, Page[Template]](
|
||||
Field("beforeCount", IntType, resolve = _.value.offset),
|
||||
Field("totalCount", IntType, resolve = _.value.total),
|
||||
Field("sortings", OptionType(ListType(SortingType)), resolve = _.value.sortedLike),
|
||||
Field("edges", ListType(TemplateEdgeType), resolve = _.value.rows),
|
||||
),
|
||||
)
|
||||
|
||||
// noinspection ForwardReference
|
||||
val EventType: InterfaceType[GraphQLContext, Event] = InterfaceType(
|
||||
"Event",
|
||||
() =>
|
||||
fields[GraphQLContext, Event](
|
||||
Field("id", IDType, resolve = _.value.idString),
|
||||
Field(
|
||||
"parent",
|
||||
OptionType(EventType),
|
||||
resolve = context =>
|
||||
context.value.parentId.flatMap(pid =>
|
||||
context.ctx.ledger.event(pid, context.ctx.templates)
|
||||
),
|
||||
),
|
||||
Field(
|
||||
"transaction",
|
||||
TransactionType,
|
||||
resolve = context =>
|
||||
context.ctx.ledger.transaction(context.value.transactionId, context.ctx.templates).get,
|
||||
),
|
||||
Field(
|
||||
"witnessParties",
|
||||
ListType(PartyType),
|
||||
resolve = _.value.witnessParties.map(Tag.unwrap),
|
||||
),
|
||||
Field(
|
||||
"workflowId",
|
||||
StringType,
|
||||
resolve = context => Tag.unwrap[String, ApiTypes.WorkflowIdTag](context.value.workflowId),
|
||||
),
|
||||
),
|
||||
).withPossibleObjectTypes(() => List(CreatedEventType, ExercisedEventType))
|
||||
|
||||
// noinspection ForwardReference
|
||||
val CreatedEventType: ObjectType[GraphQLContext, ContractCreated] = ObjectType(
|
||||
"CreatedEvent",
|
||||
interfaces[GraphQLContext, ContractCreated](NodeType, EventType),
|
||||
() =>
|
||||
fields[GraphQLContext, ContractCreated](
|
||||
Field(
|
||||
"transaction",
|
||||
TransactionType,
|
||||
resolve = context =>
|
||||
context.ctx.ledger.transaction(context.value.transactionId, context.ctx.templates).get,
|
||||
),
|
||||
Field(
|
||||
"contract",
|
||||
ContractType,
|
||||
resolve = context =>
|
||||
context.ctx.ledger.contract(context.value.contractId, context.ctx.templates).get,
|
||||
),
|
||||
Field("argument", JsonType.ApiRecordType, resolve = _.value.argument),
|
||||
),
|
||||
)
|
||||
|
||||
// noinspection ForwardReference
|
||||
val ExercisedEventType: ObjectType[GraphQLContext, ChoiceExercised] = ObjectType(
|
||||
"ExercisedEvent",
|
||||
interfaces[GraphQLContext, ChoiceExercised](NodeType, EventType),
|
||||
() =>
|
||||
fields[GraphQLContext, ChoiceExercised](
|
||||
Field(
|
||||
"transaction",
|
||||
TransactionType,
|
||||
resolve = context =>
|
||||
context.ctx.ledger.transaction(context.value.transactionId, context.ctx.templates).get,
|
||||
),
|
||||
Field(
|
||||
"contract",
|
||||
ContractType,
|
||||
resolve = context =>
|
||||
context.ctx.ledger.contract(context.value.contractId, context.ctx.templates).get,
|
||||
),
|
||||
Field(
|
||||
"choice",
|
||||
StringType,
|
||||
resolve = context => Tag.unwrap[String, ApiTypes.ChoiceTag](context.value.choice),
|
||||
),
|
||||
Field("argument", JsonType.ApiValueType, resolve = _.value.argument),
|
||||
Field(
|
||||
"actingParties",
|
||||
ListType(PartyType),
|
||||
resolve = _.value.actingParties.map(Tag.unwrap),
|
||||
),
|
||||
Field("consuming", BooleanType, resolve = _.value.consuming),
|
||||
Field(
|
||||
"children",
|
||||
ListType(EventType),
|
||||
resolve =
|
||||
context => context.ctx.ledger.childEvents(context.value.id, context.ctx.templates),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val TransactionType: ObjectType[GraphQLContext, Transaction] = ObjectType(
|
||||
"Transaction",
|
||||
interfaces[GraphQLContext, Transaction](NodeType),
|
||||
() =>
|
||||
fields[GraphQLContext, Transaction](
|
||||
Field("id", IDType, resolve = _.value.idString),
|
||||
Field("offset", OffsetType, resolve = context => context.value.offset),
|
||||
Field("effectiveAt", TimeType, resolve = _.value.effectiveAt),
|
||||
Field("commandId", OptionType(CommandIdType), resolve = _.value.commandId),
|
||||
Field("events", ListType(EventType), resolve = _.value.events),
|
||||
),
|
||||
)
|
||||
|
||||
// noinspection ForwardReference
|
||||
val ContractType = ObjectType(
|
||||
"Contract",
|
||||
interfaces[GraphQLContext, Contract](NodeType),
|
||||
() =>
|
||||
fields[GraphQLContext, Contract](
|
||||
Field("id", IDType, resolve = _.value.idString),
|
||||
Field("template", TemplateType, resolve = _.value.template),
|
||||
Field(
|
||||
"createEvent",
|
||||
CreatedEventType,
|
||||
resolve =
|
||||
context => context.ctx.ledger.createEventOf(context.value, context.ctx.templates),
|
||||
),
|
||||
Field(
|
||||
"archiveEvent",
|
||||
OptionType(ExercisedEventType),
|
||||
resolve =
|
||||
context => context.ctx.ledger.archiveEventOf(context.value, context.ctx.templates),
|
||||
),
|
||||
Field(
|
||||
"exerciseEvents",
|
||||
ListType(ExercisedEventType),
|
||||
resolve =
|
||||
context => context.ctx.ledger.exercisedEventsOf(context.value, context.ctx.templates),
|
||||
),
|
||||
Field("argument", JsonType.ApiRecordType, resolve = _.value.argument),
|
||||
Field("agreementText", OptionType(StringType), resolve = _.value.agreementText),
|
||||
Field("signatories", ListType(StringType), resolve = _.value.signatories.map(Tag.unwrap)),
|
||||
Field("observers", ListType(StringType), resolve = _.value.observers.map(Tag.unwrap)),
|
||||
Field("key", OptionType(JsonType.ApiValueType), resolve = _.value.key),
|
||||
),
|
||||
)
|
||||
|
||||
val ContractEdgeType = ObjectType(
|
||||
"ContractEdge",
|
||||
fields[Unit, Contract](
|
||||
Field("node", ContractType, resolve = _.value),
|
||||
Field("cursor", StringType, resolve = _.value.idString),
|
||||
),
|
||||
)
|
||||
|
||||
val ContractPaginationType = ObjectType(
|
||||
"ContractPagination",
|
||||
fields[Unit, Page[Contract]](
|
||||
Field("beforeCount", IntType, resolve = _.value.offset),
|
||||
Field("totalCount", IntType, resolve = _.value.total),
|
||||
Field("sortings", OptionType(ListType(SortingType)), resolve = _.value.sortedLike),
|
||||
Field("edges", ListType(ContractEdgeType), resolve = _.value.rows),
|
||||
),
|
||||
)
|
||||
|
||||
val FilterCriterionType = InputObjectType(
|
||||
"FilterCriterion",
|
||||
List(
|
||||
InputField("field", StringType),
|
||||
InputField("value", StringType),
|
||||
),
|
||||
)
|
||||
|
||||
val SortCriterionType = InputObjectType(
|
||||
"SortCriterion",
|
||||
List(
|
||||
InputField("field", StringType),
|
||||
InputField("direction", DirectionType),
|
||||
),
|
||||
)
|
||||
|
||||
// noinspection ForwardReference
|
||||
val CommandStatusType: InterfaceType[GraphQLContext, CommandStatus] = InterfaceType(
|
||||
"CommandStatus",
|
||||
() =>
|
||||
fields[GraphQLContext, CommandStatus](
|
||||
Field("completed", BooleanType, resolve = _.value.isCompleted)
|
||||
),
|
||||
).withPossibleObjectTypes(() =>
|
||||
List(
|
||||
CommandStatusErrorType,
|
||||
CommandStatusWaitingType,
|
||||
CommandStatusSuccessType,
|
||||
CommandStatusUnknownType,
|
||||
)
|
||||
)
|
||||
|
||||
val CommandStatusErrorType: ObjectType[GraphQLContext, CommandStatusError] = ObjectType(
|
||||
"CommandStatusError",
|
||||
interfaces[GraphQLContext, CommandStatusError](CommandStatusType),
|
||||
() =>
|
||||
fields[GraphQLContext, CommandStatusError](
|
||||
Field("code", StringType, resolve = _.value.code),
|
||||
Field("details", StringType, resolve = _.value.details),
|
||||
),
|
||||
)
|
||||
|
||||
val CommandStatusWaitingType: ObjectType[GraphQLContext, CommandStatusWaiting] = ObjectType(
|
||||
"CommandStatusWaiting",
|
||||
interfaces[GraphQLContext, CommandStatusWaiting](CommandStatusType),
|
||||
() =>
|
||||
fields[GraphQLContext, CommandStatusWaiting](
|
||||
),
|
||||
)
|
||||
|
||||
val CommandStatusUnknownType: ObjectType[GraphQLContext, CommandStatusUnknown] = ObjectType(
|
||||
"CommandStatusUnknown",
|
||||
interfaces[GraphQLContext, CommandStatusUnknown](CommandStatusType),
|
||||
() =>
|
||||
fields[GraphQLContext, CommandStatusUnknown](
|
||||
),
|
||||
)
|
||||
|
||||
val CommandStatusSuccessType: ObjectType[GraphQLContext, CommandStatusSuccess] = ObjectType(
|
||||
"CommandStatusSuccess",
|
||||
interfaces[GraphQLContext, CommandStatusSuccess](CommandStatusType),
|
||||
() =>
|
||||
fields[GraphQLContext, CommandStatusSuccess](
|
||||
Field("transaction", TransactionType, resolve = _.value.tx)
|
||||
),
|
||||
)
|
||||
|
||||
// noinspection ForwardReference
|
||||
val CommandType: InterfaceType[GraphQLContext, Command] = InterfaceType(
|
||||
"Command",
|
||||
() =>
|
||||
fields[GraphQLContext, Command](
|
||||
Field("id", IDType, resolve = _.value.idString),
|
||||
Field("index", IntType, resolve = _.value.index.toInt),
|
||||
Field(
|
||||
"workflowId",
|
||||
IDType,
|
||||
resolve = context => Tag.unwrap[String, ApiTypes.WorkflowIdTag](context.value.workflowId),
|
||||
),
|
||||
Field("platformTime", TimeType, resolve = _.value.platformTime),
|
||||
Field(
|
||||
"status",
|
||||
CommandStatusType,
|
||||
resolve = context =>
|
||||
context.ctx.ledger
|
||||
.statusOf(context.value.id, context.ctx.templates)
|
||||
.getOrElse(CommandStatusUnknown()),
|
||||
),
|
||||
),
|
||||
).withPossibleObjectTypes(() => List(CreateCommandType, ExerciseCommandType))
|
||||
|
||||
val CreateCommandType: ObjectType[GraphQLContext, CreateCommand] = ObjectType(
|
||||
"CreateCommand",
|
||||
interfaces[GraphQLContext, CreateCommand](NodeType, CommandType),
|
||||
() =>
|
||||
fields[GraphQLContext, CreateCommand](
|
||||
Field(
|
||||
"template",
|
||||
OptionType(TemplateType),
|
||||
resolve = context => context.ctx.templates.template(context.value.template),
|
||||
),
|
||||
Field("templateId", StringType, resolve = context => context.value.template.asOpaqueString),
|
||||
Field("argument", JsonType.ApiRecordType, resolve = _.value.argument),
|
||||
),
|
||||
)
|
||||
|
||||
val ExerciseCommandType: ObjectType[GraphQLContext, ExerciseCommand] = ObjectType(
|
||||
"ExerciseCommand",
|
||||
interfaces[GraphQLContext, ExerciseCommand](NodeType, CommandType),
|
||||
() =>
|
||||
fields[GraphQLContext, ExerciseCommand](
|
||||
Field(
|
||||
"contract",
|
||||
OptionType(ContractType),
|
||||
resolve =
|
||||
context => context.ctx.ledger.contract(context.value.contract, context.ctx.templates),
|
||||
),
|
||||
Field(
|
||||
"contractId",
|
||||
StringType,
|
||||
resolve = context => Tag.unwrap[String, ApiTypes.ContractIdTag](context.value.contract),
|
||||
),
|
||||
Field(
|
||||
"interfaceId",
|
||||
OptionType(StringType),
|
||||
resolve = _.value.interfaceId.map(_.asOpaqueString),
|
||||
),
|
||||
Field(
|
||||
"choice",
|
||||
StringType,
|
||||
resolve = context => Tag.unwrap[String, ApiTypes.ChoiceTag](context.value.choice),
|
||||
),
|
||||
Field("argument", JsonType.ApiValueType, resolve = _.value.argument),
|
||||
),
|
||||
)
|
||||
|
||||
val CommandEdgeType = ObjectType(
|
||||
"CommandEdge",
|
||||
fields[GraphQLContext, Command](
|
||||
Field("node", CommandType, resolve = _.value),
|
||||
Field("cursor", StringType, resolve = _.value.idString),
|
||||
),
|
||||
)
|
||||
|
||||
val CommandPaginationType = ObjectType(
|
||||
"CommandPagination",
|
||||
fields[GraphQLContext, Page[Command]](
|
||||
Field("beforeCount", IntType, resolve = _.value.offset),
|
||||
Field("totalCount", IntType, resolve = _.value.total),
|
||||
Field("sortings", OptionType(ListType(SortingType)), resolve = _.value.sortedLike),
|
||||
Field("edges", ListType(CommandEdgeType), resolve = _.value.rows),
|
||||
),
|
||||
)
|
||||
|
||||
// noinspection ForwardReference
|
||||
val DamlLfDefDataTypeType: ObjectType[GraphQLContext, DamlLfDefDataTypeBoxed] = ObjectType(
|
||||
"DamlLfDefDataType",
|
||||
interfaces[GraphQLContext, DamlLfDefDataTypeBoxed](NodeType, DamlLfNodeType),
|
||||
() =>
|
||||
fields[GraphQLContext, DamlLfDefDataTypeBoxed](
|
||||
Field("dataType", JsonType.DamlLfDataTypeType, resolve = _.value.value.dataType),
|
||||
Field("typeVars", ListType(StringType), resolve = _.value.value.typeVars),
|
||||
Field(
|
||||
"dependencies",
|
||||
ListType(DamlLfDefDataTypeType),
|
||||
arguments = DepthArg :: Nil,
|
||||
resolve = context =>
|
||||
context.ctx.templates
|
||||
.typeDependencies(context.value.value, context.arg(DepthArg).getOrElse(Int.MaxValue))
|
||||
.toList
|
||||
.map(t => DamlLfDefDataTypeBoxed(t._1, t._2)),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val DamlLfFieldWithTypeType = ObjectType(
|
||||
"DamlLfFieldWithType",
|
||||
fields[GraphQLContext, DamlLfFieldWithType](
|
||||
Field("name", StringType, resolve = _.value._1),
|
||||
Field("type", JsonType.DamlLfTypeType, resolve = _.value._2),
|
||||
),
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Queries
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
val TypeArg = Argument("typename", StringType)
|
||||
val IDArg = Argument("id", IDType)
|
||||
val IDListArg = Argument("ids", ListInputType(IDType))
|
||||
|
||||
@GraphQLDeprecated("use filter intead")
|
||||
val SearchArg = Argument("search", OptionInputType(StringType))
|
||||
|
||||
val FilterArg = Argument("filter", OptionInputType(ListInputType(FilterCriterionType)))
|
||||
val CountArg = Argument("count", OptionInputType(IntType))
|
||||
val StartArg = Argument("start", OptionInputType(StringType))
|
||||
val SortArg = Argument("sort", OptionInputType(ListInputType(SortCriterionType)))
|
||||
val IncludeArchivedArg = Argument("includeArchived", OptionInputType(BooleanType))
|
||||
val DeclarationArg = Argument("topLevelDecl", StringType)
|
||||
val DepthArg = Argument("depth", OptionInputType(IntType))
|
||||
|
||||
val QueryType = ObjectType(
|
||||
"Query",
|
||||
fields[GraphQLContext, Unit](
|
||||
Field(
|
||||
"parties",
|
||||
ListType(PartyType),
|
||||
arguments = SearchArg :: Nil,
|
||||
resolve = context =>
|
||||
(context.ctx.store ? GetParties(context.arg(SearchArg).getOrElse("")))
|
||||
.mapTo[PartyList]
|
||||
.map(response => Tag.unsubst(response.parties)),
|
||||
),
|
||||
Field(
|
||||
"ledgerTime",
|
||||
LedgerTimeType,
|
||||
resolve = context =>
|
||||
(context.ctx.store ? ReportCurrentTime)
|
||||
.mapTo[Try[TimeProviderWithType]]
|
||||
.map(t => t.get),
|
||||
),
|
||||
Field(
|
||||
"latestTransaction",
|
||||
OptionType(TransactionType),
|
||||
resolve = context => context.ctx.ledger.latestTransaction(context.ctx.templates),
|
||||
),
|
||||
Field(
|
||||
"node",
|
||||
OptionType(NodeType),
|
||||
arguments = TypeArg :: IDArg :: Nil,
|
||||
resolve = context =>
|
||||
context.arg(TypeArg) match {
|
||||
case ContractType.name =>
|
||||
context.ctx.ledger
|
||||
.contract(ApiTypes.ContractId(context.arg(IDArg)), context.ctx.templates)
|
||||
case TemplateType.name =>
|
||||
context.ctx.templates.templateByStringId(TemplateStringId(context.arg(IDArg)))
|
||||
case CommandType.name =>
|
||||
context.ctx.ledger
|
||||
.command(ApiTypes.CommandId(context.arg(IDArg)), context.ctx.templates)
|
||||
case EventType.name =>
|
||||
context.ctx.ledger.event(ApiTypes.EventId(context.arg(IDArg)), context.ctx.templates)
|
||||
case TransactionType.name =>
|
||||
context.ctx.ledger
|
||||
.transaction(ApiTypes.TransactionId(context.arg(IDArg)), context.ctx.templates)
|
||||
case DamlLfDefDataTypeType.name =>
|
||||
for {
|
||||
id <- parseOpaqueIdentifier(context.arg(IDArg))
|
||||
ddt <- context.ctx.templates.damlLfDefDataType(id)
|
||||
} yield DamlLfDefDataTypeBoxed(id, ddt)
|
||||
case _ => None
|
||||
},
|
||||
),
|
||||
Field(
|
||||
"template",
|
||||
ListType(TemplateType),
|
||||
arguments = DeclarationArg :: Nil,
|
||||
resolve = context => context.ctx.templates.templatesByName(context.arg(DeclarationArg)),
|
||||
),
|
||||
Field(
|
||||
"nodes",
|
||||
ListType(NodeType),
|
||||
arguments = TypeArg :: IDListArg :: Nil,
|
||||
resolve = context => {
|
||||
val ledger = context.ctx.ledger
|
||||
val ids = context.arg(IDListArg).toSet
|
||||
context.arg(TypeArg) match {
|
||||
case ContractType.name =>
|
||||
ids
|
||||
.map(id => ApiTypes.ContractId(id.toString))
|
||||
.flatMap(ledger.contract(_, context.ctx.templates).toList)
|
||||
.toSeq
|
||||
case TemplateType.name =>
|
||||
ids
|
||||
.map(id => TemplateStringId(id.toString))
|
||||
.flatMap(context.ctx.templates.templateByStringId(_).toList)
|
||||
.toSeq
|
||||
case CommandType.name =>
|
||||
ids
|
||||
.map(id => ApiTypes.CommandId(id.toString))
|
||||
.flatMap(context.ctx.ledger.command(_, context.ctx.templates).toList)
|
||||
.toSeq
|
||||
case EventType.name =>
|
||||
ids
|
||||
.map(id => ApiTypes.EventId(id.toString))
|
||||
.flatMap(context.ctx.ledger.event(_, context.ctx.templates).toList)
|
||||
.toSeq
|
||||
case TransactionType.name =>
|
||||
ids
|
||||
.map(id => ApiTypes.TransactionId(id.toString))
|
||||
.flatMap(context.ctx.ledger.transaction(_, context.ctx.templates).toList)
|
||||
.toSeq
|
||||
case _ => Seq.empty[Node[_]]
|
||||
}
|
||||
},
|
||||
),
|
||||
Field(
|
||||
"templates",
|
||||
TemplatePaginationType,
|
||||
arguments = SearchArg :: FilterArg :: CountArg :: StartArg :: SortArg :: Nil,
|
||||
resolve =
|
||||
context => buildTemplatePager(context).fetch(context.ctx.ledger, context.ctx.templates),
|
||||
),
|
||||
Field(
|
||||
"contracts",
|
||||
ContractPaginationType,
|
||||
arguments =
|
||||
SearchArg :: FilterArg :: IncludeArchivedArg :: CountArg :: StartArg :: SortArg :: Nil,
|
||||
resolve =
|
||||
context => buildContractPager(context).fetch(context.ctx.ledger, context.ctx.templates),
|
||||
),
|
||||
Field(
|
||||
"commands",
|
||||
CommandPaginationType,
|
||||
arguments = SearchArg :: FilterArg :: CountArg :: StartArg :: SortArg :: Nil,
|
||||
resolve =
|
||||
context => buildCommandPager(context).fetch(context.ctx.ledger, context.ctx.templates),
|
||||
),
|
||||
Field(
|
||||
"commandStatus",
|
||||
OptionType(CommandStatusType),
|
||||
arguments = IDArg :: Nil,
|
||||
resolve = context =>
|
||||
context.ctx.ledger
|
||||
.statusOf(ApiTypes.CommandId(context.arg(IDArg)), context.ctx.templates)
|
||||
.getOrElse(CommandStatusUnknown()),
|
||||
),
|
||||
) ++ customEndpoints.map(_.endpoint),
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Mutations
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
val TimeArgument = Argument("time", TimeType)
|
||||
val TemplateIdArgument = Argument("templateId", IDType)
|
||||
val ContractIdArgument = Argument("contractId", IDType)
|
||||
val ChoiceIdArgument = Argument("choiceId", IDType)
|
||||
val InterfaceIdArgument = Argument("interfaceId", OptionInputType(IDType))
|
||||
val AnyArgument = Argument("argument", OptionInputType(JsonType.ApiValueType))
|
||||
|
||||
val MutationType = ObjectType(
|
||||
"Mutation",
|
||||
fields[GraphQLContext, Unit](
|
||||
Field(
|
||||
"advanceTime",
|
||||
LedgerTimeType,
|
||||
arguments = TimeArgument :: Nil,
|
||||
resolve = context =>
|
||||
(context.ctx.store ? AdvanceTime(context.arg(TimeArgument)))
|
||||
.mapTo[Try[TimeProviderWithType]]
|
||||
.map(t => t.get),
|
||||
),
|
||||
Field(
|
||||
"create",
|
||||
CommandIdType,
|
||||
arguments = TemplateIdArgument :: AnyArgument :: Nil,
|
||||
resolve = context => {
|
||||
val command = CreateContract(
|
||||
context.ctx.party,
|
||||
TemplateStringId(context.arg(TemplateIdArgument)),
|
||||
context.arg(AnyArgument).collect({ case r: ApiRecord => r }).orNull,
|
||||
)
|
||||
wrapError((context.ctx.store ? command).mapTo[Try[ApiTypes.CommandId]])
|
||||
},
|
||||
),
|
||||
Field(
|
||||
"exercise",
|
||||
CommandIdType,
|
||||
arguments =
|
||||
ContractIdArgument :: InterfaceIdArgument :: ChoiceIdArgument :: AnyArgument :: Nil,
|
||||
resolve = context => {
|
||||
val command = ExerciseChoice(
|
||||
context.ctx.party,
|
||||
ApiTypes.ContractId(context.arg(ContractIdArgument)),
|
||||
context.arg(InterfaceIdArgument).map(InterfaceStringId(_)),
|
||||
ApiTypes.Choice(context.arg(ChoiceIdArgument)),
|
||||
context.arg(AnyArgument).orNull,
|
||||
)
|
||||
wrapError((context.ctx.store ? command).mapTo[Try[ApiTypes.CommandId]])
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val QuerySchema = Schema(QueryType, Some(MutationType))
|
||||
|
||||
case object InstantCoercionViolation extends ValueCoercionViolation("ISO-8601 timestamp expected")
|
||||
|
||||
private def buildContractPager(context: Context[GraphQLContext, _]): Pager[Contract] =
|
||||
buildContractPager(
|
||||
context,
|
||||
if (shouldIncludeArchived(context)) AllContractsPager else ActiveContractsPager,
|
||||
)
|
||||
|
||||
private def buildTemplateContractPager(
|
||||
context: Context[GraphQLContext, Template]
|
||||
): Pager[Contract] =
|
||||
buildContractPager(
|
||||
context,
|
||||
if (shouldIncludeArchived(context)) {
|
||||
new TemplateContractPager(context.value)
|
||||
} else {
|
||||
new ActiveTemplateContractPager(context.value)
|
||||
},
|
||||
)
|
||||
|
||||
private def shouldIncludeArchived(context: Context[_, _]): Boolean =
|
||||
context.arg(IncludeArchivedArg).getOrElse(false)
|
||||
|
||||
def contractSearchToFilter(search: String): FilterCriterionBase =
|
||||
OrFilterCriterion(
|
||||
List(
|
||||
FilterCriterion("id", search),
|
||||
FilterCriterion("template.id", search),
|
||||
FilterCriterion("template.topLevelDecl", search),
|
||||
)
|
||||
)
|
||||
|
||||
private def buildContractPager(
|
||||
context: Context[GraphQLContext, _],
|
||||
base: Pager[Contract],
|
||||
): Pager[Contract] = {
|
||||
val ps = context.ctx.templates.damlLfDefDataType(_)
|
||||
val searched = context
|
||||
.arg(SearchArg)
|
||||
.map(arg => new ContractFilter(contractSearchToFilter(arg), ps, base))
|
||||
.getOrElse(base)
|
||||
val filtered = context
|
||||
.arg(FilterArg)
|
||||
.map(arg => new ContractFilter(filterCriteria(arg), ps, searched))
|
||||
.getOrElse(searched)
|
||||
val sorted = context
|
||||
.arg(SortArg)
|
||||
.map(arg => new ContractSorter(sortCriteria(arg), ps, filtered))
|
||||
.getOrElse(filtered)
|
||||
buildNodePager(context, sorted)
|
||||
}
|
||||
|
||||
def templateSearchToFilter(search: String): FilterCriterionBase =
|
||||
OrFilterCriterion(List(FilterCriterion("id", search), FilterCriterion("topLevelDecl", search)))
|
||||
|
||||
private def buildTemplatePager(
|
||||
context: Context[GraphQLContext, _],
|
||||
base: Pager[Template] = TemplatePager,
|
||||
): Pager[Template] = {
|
||||
val ps = context.ctx.templates.damlLfDefDataType(_)
|
||||
val searched = context
|
||||
.arg(SearchArg)
|
||||
.map(arg => new TemplateFilter(templateSearchToFilter(arg), ps, base))
|
||||
.getOrElse(base)
|
||||
val filtered = context
|
||||
.arg(FilterArg)
|
||||
.map(arg => new TemplateFilter(filterCriteria(arg), ps, searched))
|
||||
.getOrElse(searched)
|
||||
val sorted = context
|
||||
.arg(SortArg)
|
||||
.map(arg => new TemplateSorter(sortCriteria(arg), ps, filtered))
|
||||
.getOrElse(filtered)
|
||||
buildNodePager(context, sorted)
|
||||
}
|
||||
|
||||
def commandSearchToFilter(search: String): FilterCriterionBase =
|
||||
OrFilterCriterion(List(FilterCriterion("id", search), FilterCriterion("workflowId", search)))
|
||||
|
||||
private def buildCommandPager(
|
||||
context: Context[GraphQLContext, _],
|
||||
base: Pager[Command] = CommandPager,
|
||||
): Pager[Command] = {
|
||||
val ps = context.ctx.templates.damlLfDefDataType(_)
|
||||
val searched = context
|
||||
.arg(SearchArg)
|
||||
.map(arg => new CommandFilter(commandSearchToFilter(arg), ps, base))
|
||||
.getOrElse(base)
|
||||
val filtered = context
|
||||
.arg(FilterArg)
|
||||
.map(arg => new CommandFilter(filterCriteria(arg), ps, searched))
|
||||
.getOrElse(searched)
|
||||
val sorted = context
|
||||
.arg(SortArg)
|
||||
.map(arg => new CommandSorter(sortCriteria(arg), ps, filtered))
|
||||
.getOrElse(filtered)
|
||||
buildNodePager(context, sorted)
|
||||
}
|
||||
|
||||
private def buildNodePager[N <: Node[_]](context: Context[_, _], sorted: Pager[N]): Pager[N] = {
|
||||
val shifted = context.arg(StartArg).map(new ShiftingPager(_, sorted)).getOrElse(sorted)
|
||||
val bounded = context.arg(CountArg).map(new BoundingPager(_, shifted)).getOrElse(shifted)
|
||||
bounded
|
||||
}
|
||||
|
||||
def sortCriteria(arg: Seq[DefaultInput]): List[SortCriterion] =
|
||||
arg
|
||||
.map(input =>
|
||||
SortCriterion(
|
||||
input("field").toString,
|
||||
SortDirection.withName(input("direction").toString),
|
||||
)
|
||||
)
|
||||
.toList
|
||||
|
||||
def filterCriteria(arg: Seq[DefaultInput]): AndFilterCriterion =
|
||||
AndFilterCriterion(
|
||||
arg.view
|
||||
.map(input =>
|
||||
FilterCriterion(
|
||||
input("field").toString,
|
||||
input("value").toString,
|
||||
)
|
||||
)
|
||||
.toList
|
||||
)
|
||||
|
||||
private def parseInstant(s: String): Either[Violation, Instant] = Try(Instant.parse(s)) match {
|
||||
case Success(instant) => Right(instant)
|
||||
case Failure(_) => Left(InstantCoercionViolation)
|
||||
}
|
||||
|
||||
private def mapError(error: Throwable): Throwable = error match {
|
||||
case GenericConversionError(message) => UserFacingError(message)
|
||||
case _ => error
|
||||
}
|
||||
|
||||
private def wrapError[T](value: Try[T]): Try[T] = {
|
||||
value.recoverWith({ case e: Throwable => Failure(mapError(e)) })
|
||||
}
|
||||
|
||||
private def wrapError[T](value: Future[Try[T]]): Future[T] = {
|
||||
value
|
||||
.recoverWith({ case e: Throwable => Future.failed(mapError(e)) })
|
||||
.flatMap(v => Future.fromTry(wrapError(v)))
|
||||
}
|
||||
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.graphql
|
||||
|
||||
import com.daml.navigator.model._
|
||||
import com.daml.navigator.json.ApiCodecVerbose.JsonImplicits._
|
||||
import com.daml.navigator.json.DamlLfCodec.JsonImplicits._
|
||||
import sangria.ast
|
||||
import sangria.schema._
|
||||
import sangria.validation.ValueCoercionViolation
|
||||
import spray.json._
|
||||
|
||||
/** Custom GraphQL scalar type for raw JSON values.
|
||||
*
|
||||
* The custom ScalarType is necessary to deliver raw JSON values from the platform to the client
|
||||
* through the Sangria GraphQL mechanism. We currently use such raw JSON values for contract arguments
|
||||
* and choice parameters. Their internal structure varies enough that they do not lend themselves well
|
||||
* to GraphQL queries, and transmitting them in full has no significant effects on performance (<100kB).
|
||||
* There is still the minor inconvenience that we include a transfer format in our model but this may
|
||||
* change when we introduce a unified approach to generate all transfer data formats and their
|
||||
* marshalling/unmarshalling.
|
||||
*
|
||||
* Adapted from https://gist.github.com/OlegIlyenko/5b96f4b54f656aac226d3c4bc33fd2a6
|
||||
*/
|
||||
@SuppressWarnings(
|
||||
Array(
|
||||
"org.wartremover.warts.Product",
|
||||
"org.wartremover.warts.Serializable",
|
||||
"org.wartremover.warts.Any",
|
||||
)
|
||||
)
|
||||
object JsonType {
|
||||
|
||||
case object JsonCoercionViolation extends ValueCoercionViolation("Not valid JSON")
|
||||
|
||||
// TODO: handle exceptions
|
||||
private def coerceJsonInput[T](
|
||||
v: sangria.ast.Value
|
||||
)(implicit fmt: JsonFormat[T]): Either[sangria.validation.Violation, T] = v match {
|
||||
case ast.StringValue(jsonStr, _, _, _, _) =>
|
||||
jsonStr.parseJson match {
|
||||
case jsValue: JsValue => Right(jsValue.convertTo[T])
|
||||
case _ => Left(JsonCoercionViolation)
|
||||
}
|
||||
case _ =>
|
||||
Left(JsonCoercionViolation)
|
||||
}
|
||||
|
||||
private def coerceUserJsonInput[T](
|
||||
v: Any
|
||||
)(implicit fmt: JsonFormat[T]): Either[sangria.validation.Violation, T] = v match {
|
||||
case jsValue: JsValue => Right(jsValue.convertTo[T])
|
||||
case _ => Left(JsonCoercionViolation)
|
||||
}
|
||||
|
||||
private def newScalarType[T](name: String)(implicit fmt: JsonFormat[T]): ScalarType[T] =
|
||||
ScalarType(
|
||||
name,
|
||||
coerceOutput = (value, _) => value.toJson,
|
||||
coerceUserInput = coerceUserJsonInput[T],
|
||||
coerceInput = coerceJsonInput[T],
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Untyped JSON values???
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
def apply(name: String, description: Option[String] = None): ScalarType[JsValue] =
|
||||
ScalarType[JsValue](
|
||||
name,
|
||||
description = description,
|
||||
coerceOutput = (value, _) => value,
|
||||
coerceUserInput = {
|
||||
case v: String => Right(JsString(v): JsValue)
|
||||
case v: Boolean => Right(JsBoolean(v))
|
||||
case v: Int => Right(JsNumber(v))
|
||||
case v: Long => Right(JsNumber(v))
|
||||
case v: Float => Right(JsNumber(v.toDouble): JsValue)
|
||||
case v: Double => Right(JsNumber(v): JsValue)
|
||||
case v: BigInt => Right(JsNumber(v))
|
||||
case v: BigDecimal => Right(JsNumber(v))
|
||||
case v: JsValue => Right(v)
|
||||
},
|
||||
coerceInput = {
|
||||
case ast.StringValue(jsonStr, _, _, _, _) =>
|
||||
Right(jsonStr.parseJson)
|
||||
case _ =>
|
||||
Left(JsonCoercionViolation)
|
||||
},
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Ledger API values
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
val ApiValueType: ScalarType[ApiValue] = newScalarType[ApiValue]("DamlLfValue")
|
||||
val ApiRecordType: ScalarType[ApiRecord] = newScalarType[ApiRecord]("DamlLfValueRecord")
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Daml-LF types
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
val DamlLfTypeType: ScalarType[DamlLfType] = newScalarType[DamlLfType]("DamlLfType")
|
||||
val DamlLfDataTypeType: ScalarType[DamlLfDataType] =
|
||||
newScalarType[DamlLfDataType]("DamlLfDataType")
|
||||
|
||||
}
|
@ -1,137 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.graphql
|
||||
|
||||
import sangria.marshalling._
|
||||
import spray.json._
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
/** Custom marshallers and unmarshallers for dealing with raw JSON values.
|
||||
*
|
||||
* The declared implicit objects replace the ones you would usually import from `sangria.marshalling.sprayJson`.
|
||||
* In fact they are largely identical to those except for the additional support for non-scalar JSON values.
|
||||
*
|
||||
* The entire object is only relevant as long as we use raw JSON values in the model.
|
||||
*
|
||||
* Adapted from https://gist.github.com/OlegIlyenko/5b96f4b54f656aac226d3c4bc33fd2a6
|
||||
*/
|
||||
@SuppressWarnings(Array("org.wartremover.warts.Serializable"))
|
||||
object SprayMarshallers {
|
||||
implicit object CustomSprayJsonResultMarshaller extends ResultMarshaller {
|
||||
type Node = JsValue
|
||||
type MapBuilder = ArrayMapBuilder[Node]
|
||||
|
||||
def emptyMapNode(keys: Seq[String]) = new ArrayMapBuilder[Node](keys)
|
||||
def addMapNodeElem(
|
||||
builder: MapBuilder,
|
||||
key: String,
|
||||
value: Node,
|
||||
optional: Boolean,
|
||||
): ArrayMapBuilder[JsValue] = builder.add(key, value)
|
||||
|
||||
def mapNode(builder: MapBuilder) = JsObject(builder.toMap)
|
||||
def mapNode(keyValues: Seq[(String, JsValue)]) = JsObject(keyValues: _*)
|
||||
|
||||
def arrayNode(values: Vector[JsValue]) = JsArray(values)
|
||||
def optionalArrayNodeValue(value: Option[JsValue]): JsValue = value match {
|
||||
case Some(v) => v
|
||||
case None => nullNode
|
||||
}
|
||||
|
||||
def scalarNode(value: Any, typeName: String, info: Set[ScalarValueInfo]): JsValue =
|
||||
value match {
|
||||
case v: String => JsString(v)
|
||||
case v: Boolean => JsBoolean(v)
|
||||
case v: Int => JsNumber(v)
|
||||
case v: Long => JsNumber(v)
|
||||
case v: Float => JsNumber(v.toDouble)
|
||||
case v: Double => JsNumber(v)
|
||||
case v: BigInt => JsNumber(v)
|
||||
case v: BigDecimal => JsNumber(v)
|
||||
case v: JsValue => v
|
||||
case v => throw new IllegalArgumentException("Unsupported scalar value: " + v.toString)
|
||||
}
|
||||
|
||||
def enumNode(value: String, typeName: String) = JsString(value)
|
||||
|
||||
def nullNode = JsNull
|
||||
|
||||
def renderCompact(node: JsValue): String = node.compactPrint
|
||||
def renderPretty(node: JsValue): String = node.prettyPrint
|
||||
}
|
||||
|
||||
implicit object SprayJsonMarshallerForType extends ResultMarshallerForType[JsValue] {
|
||||
val marshaller = CustomSprayJsonResultMarshaller
|
||||
}
|
||||
|
||||
implicit object CustomSprayJsonInputUnmarshaller extends InputUnmarshaller[JsValue] {
|
||||
def getRootMapValue(node: JsValue, key: String): Option[JsValue] =
|
||||
node.asInstanceOf[JsObject].fields get key
|
||||
|
||||
def isListNode(node: JsValue): Boolean = node.isInstanceOf[JsArray]
|
||||
def getListValue(node: JsValue): Seq[JsValue] = node.asInstanceOf[JsArray].elements
|
||||
|
||||
def isMapNode(node: JsValue): Boolean = node.isInstanceOf[JsObject]
|
||||
def getMapValue(node: JsValue, key: String): Option[JsValue] =
|
||||
node.asInstanceOf[JsObject].fields get key
|
||||
def getMapKeys(node: JsValue): Iterable[String] = node.asInstanceOf[JsObject].fields.keys
|
||||
|
||||
def isDefined(node: JsValue): Boolean = node != JsNull
|
||||
def getScalarValue(node: JsValue): Any = node match {
|
||||
case JsBoolean(b) => b
|
||||
case JsNumber(d) => d.toBigIntExact getOrElse d
|
||||
case JsString(s) => s
|
||||
case n => n
|
||||
}
|
||||
|
||||
def getScalaScalarValue(node: JsValue): Any = getScalarValue(node)
|
||||
|
||||
def isEnumNode(node: JsValue): Boolean = node.isInstanceOf[JsString]
|
||||
|
||||
def isScalarNode(node: JsValue) = true
|
||||
|
||||
def isVariableNode(node: JsValue) = false
|
||||
def getVariableName(node: JsValue) =
|
||||
throw new IllegalArgumentException("variables are not supported")
|
||||
|
||||
def render(node: JsValue): String = node.compactPrint
|
||||
}
|
||||
|
||||
private object CustomSprayJsonToInput extends ToInput[JsValue, JsValue] {
|
||||
def toInput(value: JsValue): (JsValue, CustomSprayJsonInputUnmarshaller.type) =
|
||||
(value, CustomSprayJsonInputUnmarshaller)
|
||||
}
|
||||
|
||||
private object CustomSprayJsonFromInput extends FromInput[JsValue] {
|
||||
val marshaller = CustomSprayJsonResultMarshaller
|
||||
def fromResult(node: marshaller.Node): JsValue = node
|
||||
}
|
||||
|
||||
implicit def sprayJsonToInput[T <: JsValue]: ToInput[T, JsValue] =
|
||||
CustomSprayJsonToInput.asInstanceOf[ToInput[T, JsValue]]
|
||||
|
||||
implicit def sprayJsonFromInput[T <: JsValue]: FromInput[T] =
|
||||
CustomSprayJsonFromInput.asInstanceOf[FromInput[T]]
|
||||
|
||||
implicit def sprayJsonWriterToInput[T: JsonWriter]: ToInput[T, JsValue] =
|
||||
new ToInput[T, JsValue] {
|
||||
def toInput(value: T): (JsValue, CustomSprayJsonInputUnmarshaller.type) =
|
||||
implicitly[JsonWriter[T]].write(value) -> CustomSprayJsonInputUnmarshaller
|
||||
}
|
||||
|
||||
implicit def sprayJsonReaderFromInput[T: JsonReader]: FromInput[T] =
|
||||
new FromInput[T] {
|
||||
val marshaller = CustomSprayJsonResultMarshaller
|
||||
def fromResult(node: marshaller.Node): T =
|
||||
try implicitly[JsonReader[T]].read(node)
|
||||
catch {
|
||||
case e: DeserializationException => throw InputParsingError(Vector(e.msg))
|
||||
}
|
||||
}
|
||||
|
||||
implicit object CustomSprayJsonInputParser extends InputParser[JsValue] {
|
||||
def parse(str: String) = Try(str.parseJson)
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.graphql
|
||||
|
||||
import sangria.ast.StringValue
|
||||
import sangria.schema.ScalarType
|
||||
import sangria.validation.StringCoercionViolation
|
||||
|
||||
import scalaz.{@@, Tag}
|
||||
|
||||
/** GraphQL alias type for string value. */
|
||||
@SuppressWarnings(Array("org.wartremover.warts.Any"))
|
||||
object StringAliasType {
|
||||
def apply(name: String, description: Option[String] = None): ScalarType[String] =
|
||||
ScalarType(
|
||||
name,
|
||||
description = description,
|
||||
coerceOutput = sangria.schema.valueOutput,
|
||||
coerceUserInput = {
|
||||
case s: String => Right(s)
|
||||
case _ => Left(StringCoercionViolation)
|
||||
},
|
||||
coerceInput = {
|
||||
case StringValue(s, _, _, _, _) => Right(s)
|
||||
case _ => Left(StringCoercionViolation)
|
||||
},
|
||||
)
|
||||
|
||||
def tagged[T](name: String, description: Option[String] = None): ScalarType[String @@ T] =
|
||||
ScalarType(
|
||||
name,
|
||||
description = description,
|
||||
coerceOutput = (value, _) => Tag.unwrap(value),
|
||||
coerceUserInput = {
|
||||
case s: String => Right(Tag.of[T](s))
|
||||
case _ => Left(StringCoercionViolation)
|
||||
},
|
||||
coerceInput = {
|
||||
case StringValue(s, _, _, _, _) => Right(Tag.of[T](s))
|
||||
case _ => Left(StringCoercionViolation)
|
||||
},
|
||||
)
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.graphqless
|
||||
|
||||
import sangria.schema.{Context, Field}
|
||||
import shapeless.{::, HList, HNil, LabelledGeneric, Lazy, Witness}
|
||||
import shapeless.labelled.FieldType
|
||||
import shapeless.ops.record.Selector
|
||||
|
||||
trait GraphQLFields[T, Repr] {
|
||||
def fields[Ctx]: List[Field[Ctx, T]]
|
||||
}
|
||||
|
||||
object GraphQLFields {
|
||||
def apply[T, Repr](implicit graphQLFields: GraphQLFields[T, Repr]): GraphQLFields[T, Repr] =
|
||||
graphQLFields
|
||||
}
|
||||
|
||||
@SuppressWarnings(Array("org.wartremover.warts.Any"))
|
||||
trait DerivedGraphQLFields {
|
||||
|
||||
def mkField[Ctx, C, Repr <: HList, K <: Symbol, V](implicit
|
||||
kw: Witness.Aux[K],
|
||||
vField: Lazy[GraphQLOutputType[V]],
|
||||
gen: LabelledGeneric.Aux[C, Repr],
|
||||
selector: Selector.Aux[Repr, K, V],
|
||||
): Field[Ctx, C] =
|
||||
Field(
|
||||
name = kw.value.name,
|
||||
fieldType = vField.value.outputType[Ctx],
|
||||
resolve = (context: Context[Ctx, C]) => selector(gen.to(context.value)),
|
||||
)
|
||||
|
||||
implicit def singleFieldGraphQLField[C, Repr <: HList, K <: Symbol, V](implicit
|
||||
kw: Witness.Aux[K],
|
||||
vField: Lazy[GraphQLOutputType[V]],
|
||||
gen: LabelledGeneric.Aux[C, Repr],
|
||||
selector: Selector.Aux[Repr, K, V],
|
||||
): GraphQLFields[C, FieldType[K, V] :: HNil] = new GraphQLFields[C, FieldType[K, V] :: HNil] {
|
||||
|
||||
override def fields[Ctx]: List[Field[Ctx, C]] =
|
||||
List(mkField[Ctx, C, Repr, K, V])
|
||||
}
|
||||
|
||||
implicit def productGraphQLField[C, Repr <: HList, K <: Symbol, V, T <: HList](implicit
|
||||
kw: Witness.Aux[K],
|
||||
vField: Lazy[GraphQLOutputType[V]],
|
||||
tGraphQLField: Lazy[GraphQLFields[C, T]],
|
||||
gen: LabelledGeneric.Aux[C, Repr],
|
||||
selector: Selector.Aux[Repr, K, V],
|
||||
): GraphQLFields[C, FieldType[K, V] :: T] = new GraphQLFields[C, FieldType[K, V] :: T] {
|
||||
override def fields[Ctx]: List[Field[Ctx, C]] = {
|
||||
val head = mkField[Ctx, C, Repr, K, V]
|
||||
val tail = tGraphQLField.value.fields[Ctx]
|
||||
head +: tail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object DerivedGraphQLFields extends DerivedGraphQLFields
|
@ -1,32 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.graphqless
|
||||
|
||||
import sangria.schema._
|
||||
|
||||
trait GraphQLLeaf[T] {
|
||||
def to: OutputType[T] with LeafType with Named
|
||||
}
|
||||
|
||||
object GraphQLLeaf {
|
||||
def apply[T](implicit toGraphQLLeaf: GraphQLLeaf[T]): GraphQLLeaf[T] = toGraphQLLeaf
|
||||
def instance[T](toImpl: OutputType[T] with LeafType with Named): GraphQLLeaf[T] =
|
||||
new GraphQLLeaf[T] {
|
||||
override def to: OutputType[T] with LeafType with Named = toImpl
|
||||
}
|
||||
}
|
||||
|
||||
trait PrimitiveGraphQLLeaf {
|
||||
implicit val bigDecimalGraphQLLeaf: GraphQLLeaf[BigDecimal] =
|
||||
GraphQLLeaf.instance[BigDecimal](BigDecimalType)
|
||||
implicit val bigIntTypeGraphQLLeaf: GraphQLLeaf[BigInt] = GraphQLLeaf.instance[BigInt](BigIntType)
|
||||
implicit val booleanToGraphQLLeaf: GraphQLLeaf[Boolean] =
|
||||
GraphQLLeaf.instance[Boolean](BooleanType)
|
||||
implicit val doubleToGraphQLLeaf: GraphQLLeaf[Double] = GraphQLLeaf.instance[Double](FloatType)
|
||||
implicit val intToGraphQLLeaf: GraphQLLeaf[Int] = GraphQLLeaf.instance[Int](IntType)
|
||||
implicit val longToGraphQLLeaf: GraphQLLeaf[Long] = GraphQLLeaf.instance[Long](LongType)
|
||||
implicit val stringToGraphQLLeaf: GraphQLLeaf[String] = GraphQLLeaf.instance[String](StringType)
|
||||
}
|
||||
|
||||
object PrimitiveGraphQLLeaf extends PrimitiveGraphQLLeaf
|
@ -1,39 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.graphqless
|
||||
|
||||
import sangria.schema._
|
||||
import shapeless._
|
||||
|
||||
import scala.annotation.nowarn
|
||||
import scala.reflect.ClassTag
|
||||
|
||||
trait GraphQLObject[T] {
|
||||
def to[Ctx]: ObjectType[Ctx, T]
|
||||
}
|
||||
|
||||
object GraphQLObject {
|
||||
def apply[T](implicit graphQLObject: GraphQLObject[T]): GraphQLObject[T] = graphQLObject
|
||||
}
|
||||
|
||||
trait DerivedGraphQLObject {
|
||||
|
||||
@nowarn("msg=parameter generic .*is never used") // used to calculate Repr0 tparam only
|
||||
implicit def caseClassGraphQLObject[C, Repr0 <: HList](implicit
|
||||
classTag: ClassTag[C],
|
||||
generic: LabelledGeneric.Aux[C, Repr0],
|
||||
graphQLFields: GraphQLFields[C, Repr0],
|
||||
): GraphQLObject[C] = new GraphQLObject[C] {
|
||||
override def to[Ctx]: ObjectType[Ctx, C] = {
|
||||
val objectName = classTag.runtimeClass.getSimpleName
|
||||
val reprFields = graphQLFields.fields[Ctx]
|
||||
ObjectType(
|
||||
name = objectName,
|
||||
fields = reprFields,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object DerivedGraphQLObject extends DerivedGraphQLObject
|
@ -1,34 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.graphqless
|
||||
|
||||
import sangria.schema.OutputType
|
||||
|
||||
trait GraphQLOutputType[T] {
|
||||
def outputType[Ctx]: OutputType[T]
|
||||
}
|
||||
|
||||
object GraphQLOutputType {
|
||||
def apply[T](implicit graphQLOutputType: GraphQLOutputType[T]): GraphQLOutputType[T] =
|
||||
graphQLOutputType
|
||||
}
|
||||
|
||||
trait DerivedGraphQLOutputType {
|
||||
|
||||
implicit def graphQLLeafToGraphQLOutputType[T](implicit
|
||||
graphQLLeaf: GraphQLLeaf[T]
|
||||
): GraphQLOutputType[T] = new GraphQLOutputType[T] {
|
||||
override def outputType[Ctx]: OutputType[T] =
|
||||
graphQLLeaf.to
|
||||
}
|
||||
|
||||
implicit def graphQLObjectToGraphQLOutputType[T](implicit
|
||||
graphQLObject: GraphQLObject[T]
|
||||
): GraphQLOutputType[T] = new GraphQLOutputType[T] {
|
||||
override def outputType[Ctx]: OutputType[T] =
|
||||
graphQLObject.to[Ctx]
|
||||
}
|
||||
}
|
||||
|
||||
object DerivedGraphQLOutputType extends DerivedGraphQLOutputType
|
@ -1,10 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator
|
||||
|
||||
package object graphqless
|
||||
extends DerivedGraphQLObject
|
||||
with DerivedGraphQLFields
|
||||
with PrimitiveGraphQLLeaf
|
||||
with DerivedGraphQLOutputType
|
@ -1,276 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.json
|
||||
|
||||
import com.daml.lf.data.{Decimal => LfDecimal, FrontStack, ImmArray, Ref, SortedLookupList}
|
||||
import com.daml.navigator.{model => Model}
|
||||
import com.daml.lf.value.{Value => V}
|
||||
import com.daml.lf.value.json.ApiValueImplicits._
|
||||
import com.daml.navigator.json.DamlLfCodec.JsonImplicits._
|
||||
import com.daml.navigator.json.Util._
|
||||
import com.daml.navigator.model.DamlLfIdentifier
|
||||
import spray.json._
|
||||
|
||||
/** A verbose encoding of API values.
|
||||
*
|
||||
* The encoded values include type information and can be losslessly encoded.
|
||||
*/
|
||||
object ApiCodecVerbose {
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Constants used in the encoding
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
private[this] final val propType: String = "type"
|
||||
private[this] final val propValue: String = "value"
|
||||
private[this] final val propLabel: String = "label"
|
||||
private[this] final val propFields: String = "fields"
|
||||
private[this] final val propId: String = "id"
|
||||
private[this] final val propConstructor: String = "constructor"
|
||||
private[this] final val tagText: String = "text"
|
||||
private[this] final val tagInt64: String = "int64"
|
||||
private[this] final val tagNumeric: String = "numeric"
|
||||
private[this] final val tagBool: String = "bool"
|
||||
private[this] final val tagContractId: String = "contractid"
|
||||
private[this] final val tagTimestamp: String = "timestamp"
|
||||
private[this] final val tagDate: String = "date"
|
||||
private[this] final val tagParty: String = "party"
|
||||
private[this] final val tagUnit: String = "unit"
|
||||
private[this] final val tagOptional: String = "optional"
|
||||
private[this] final val tagList: String = "list"
|
||||
private[this] final val tagTextMap: String = "textmap"
|
||||
private[this] final val tagGenMap: String = "genmap"
|
||||
private[this] final val tagRecord: String = "record"
|
||||
private[this] final val tagVariant: String = "variant"
|
||||
private[this] final val tagEnum: String = "enum"
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Encoding
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
def apiValueToJsValue(value: Model.ApiValue): JsValue = value match {
|
||||
case v: Model.ApiRecord => apiRecordToJsValue(v)
|
||||
case v: Model.ApiVariant => apiVariantToJsValue(v)
|
||||
case v: V.ValueEnum => apiEnumToJsValue(v)
|
||||
case v: Model.ApiList => apiListToJsValue(v)
|
||||
case V.ValueText(v) => JsObject(propType -> JsString(tagText), propValue -> JsString(v))
|
||||
case V.ValueInt64(v) =>
|
||||
JsObject(propType -> JsString(tagInt64), propValue -> JsString((v: Long).toString))
|
||||
case V.ValueNumeric(v) =>
|
||||
JsObject(propType -> JsString(tagNumeric), propValue -> JsString(v.toUnscaledString))
|
||||
case V.ValueBool(v) => JsObject(propType -> JsString(tagBool), propValue -> JsBoolean(v))
|
||||
case V.ValueContractId(v) =>
|
||||
JsObject(propType -> JsString(tagContractId), propValue -> JsString(v.coid))
|
||||
case v: V.ValueTimestamp =>
|
||||
JsObject(propType -> JsString(tagTimestamp), propValue -> JsString(v.toIso8601))
|
||||
case v: V.ValueDate =>
|
||||
JsObject(propType -> JsString(tagDate), propValue -> JsString(v.toIso8601))
|
||||
case V.ValueParty(v) =>
|
||||
JsObject(propType -> JsString(tagParty), propValue -> JsString(v))
|
||||
case V.ValueUnit => JsObject(propType -> JsString(tagUnit))
|
||||
case V.ValueOptional(None) => JsObject(propType -> JsString(tagOptional), propValue -> JsNull)
|
||||
case V.ValueOptional(Some(v)) =>
|
||||
JsObject(propType -> JsString(tagOptional), propValue -> apiValueToJsValue(v))
|
||||
case v: Model.ApiMap => apiTextMapToJsValue(v)
|
||||
case v: Model.ApiGenMap => apiGenMapToJsValue(v)
|
||||
}
|
||||
|
||||
def apiListToJsValue(value: Model.ApiList): JsValue =
|
||||
JsObject(
|
||||
propType -> JsString(tagList),
|
||||
propValue -> JsArray(value.values.map(apiValueToJsValue).toImmArray.toSeq: _*),
|
||||
)
|
||||
|
||||
def apiTextMapToJsValue(value: Model.ApiMap): JsValue =
|
||||
JsObject(
|
||||
propType -> JsString(tagTextMap),
|
||||
propValue -> JsArray(value.value.toImmArray.toSeq.toVector.map { case (k, v) =>
|
||||
JsObject("key" -> JsString(k), "value" -> apiValueToJsValue(v))
|
||||
}),
|
||||
)
|
||||
|
||||
def apiGenMapToJsValue(value: Model.ApiGenMap): JsValue =
|
||||
JsObject(
|
||||
propType -> JsString(tagGenMap),
|
||||
propValue -> JsArray(value.entries.toSeq.toVector.map { case (k, v) =>
|
||||
JsObject("key" -> apiValueToJsValue(k), "value" -> apiValueToJsValue(v))
|
||||
}),
|
||||
)
|
||||
|
||||
def apiVariantToJsValue(value: Model.ApiVariant): JsValue =
|
||||
JsObject(
|
||||
propType -> JsString(tagVariant),
|
||||
propId -> value.tycon.map(_.toJson).getOrElse(JsNull),
|
||||
propConstructor -> JsString(value.variant),
|
||||
propValue -> apiValueToJsValue(value.value),
|
||||
)
|
||||
|
||||
def apiEnumToJsValue(value: V.ValueEnum): JsValue =
|
||||
JsObject(
|
||||
propType -> JsString(tagEnum),
|
||||
propId -> value.tycon.map(_.toJson).getOrElse(JsNull),
|
||||
propConstructor -> JsString(value.value),
|
||||
)
|
||||
|
||||
def apiRecordToJsValue(value: Model.ApiRecord): JsValue =
|
||||
JsObject(
|
||||
propType -> JsString(tagRecord),
|
||||
propId -> value.tycon.map(_.toJson).getOrElse(JsNull),
|
||||
propFields -> JsArray(value.fields.toSeq.zipWithIndex.map { case ((oflabel, fvalue), ix) =>
|
||||
JsObject(
|
||||
propLabel -> JsString(oflabel getOrElse (ix: Int).toString),
|
||||
propValue -> apiValueToJsValue(fvalue),
|
||||
)
|
||||
}.toVector),
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Decoding
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
private[this] def jsValueToApiRecordField(value: JsValue): Model.ApiRecordField = {
|
||||
val label = strField(value, propLabel, "ApiRecordField")
|
||||
val avalue = jsValueToApiValue(anyField(value, propValue, "ApiRecordField"))
|
||||
(Some(assertDE(Ref.Name fromString label)), avalue)
|
||||
}
|
||||
|
||||
def jsValueToApiValue(value: JsValue): Model.ApiValue =
|
||||
strField(value, propType, "ApiValue") match {
|
||||
case `tagRecord` => jsValueToApiRecord(value)
|
||||
case `tagVariant` => jsValueToApiVariant(value)
|
||||
case `tagEnum` => jsValueToApiEnum(value)
|
||||
case `tagList` =>
|
||||
V.ValueList(arrayField(value, propValue, "ApiList").map(jsValueToApiValue).to(FrontStack))
|
||||
case `tagText` => V.ValueText(strField(value, propValue, "ApiText"))
|
||||
case `tagInt64` => V.ValueInt64(strField(value, propValue, "ApiInt64").toLong)
|
||||
case `tagNumeric` =>
|
||||
V.ValueNumeric(assertDE(LfDecimal fromString strField(value, propValue, "ApiNumeric")))
|
||||
case `tagBool` => V.ValueBool(boolField(value, propValue, "ApiBool"))
|
||||
case `tagContractId` =>
|
||||
V.ValueContractId(
|
||||
assertDE(V.ContractId.fromString(strField(value, propValue, "ApiContractId")))
|
||||
)
|
||||
case `tagTimestamp` =>
|
||||
V.ValueTimestamp.fromIso8601(strField(value, propValue, "ApiTimestamp"))
|
||||
case `tagDate` => V.ValueDate.fromIso8601(strField(value, propValue, "ApiDate"))
|
||||
case `tagParty` =>
|
||||
V.ValueParty(assertDE(Ref.Party fromString strField(value, propValue, "ApiParty")))
|
||||
case `tagUnit` => V.ValueUnit
|
||||
case `tagOptional` =>
|
||||
anyField(value, propValue, "ApiOptional") match {
|
||||
case JsNull => V.ValueNone
|
||||
case v => V.ValueOptional(Some(jsValueToApiValue(v)))
|
||||
}
|
||||
case `tagTextMap` =>
|
||||
V.ValueTextMap(
|
||||
SortedLookupList
|
||||
.fromImmArray(
|
||||
arrayField(value, propValue, "ApiMap").view.map(jsValueToMapEntry).to(ImmArray)
|
||||
)
|
||||
.fold(
|
||||
err => deserializationError(s"Can't read ${value.prettyPrint} as ApiValue, $err'"),
|
||||
identity,
|
||||
)
|
||||
)
|
||||
case `tagGenMap` =>
|
||||
V.ValueGenMap(
|
||||
arrayField(value, propValue, "ApiGenMap").view.map(jsValueToGenMapEntry).to(ImmArray)
|
||||
)
|
||||
case t =>
|
||||
deserializationError(s"Can't read ${value.prettyPrint} as ApiValue, unknown type '$t'")
|
||||
}
|
||||
|
||||
def jsValueToApiRecord(value: JsValue): Model.ApiRecord =
|
||||
strField(value, propType, "ApiRecord") match {
|
||||
case `tagRecord` =>
|
||||
V.ValueRecord(
|
||||
asObject(value, "ApiRecord").fields
|
||||
.get(propId)
|
||||
.flatMap(_.convertTo[Option[DamlLfIdentifier]]),
|
||||
arrayField(value, propFields, "ApiRecord").map(jsValueToApiRecordField).to(ImmArray),
|
||||
)
|
||||
case t =>
|
||||
deserializationError(
|
||||
s"Can't read ${value.prettyPrint} as ApiRecord, type '$t' is not a record"
|
||||
)
|
||||
}
|
||||
|
||||
def jsValueToMapEntry(value: JsValue): (String, Model.ApiValue) = {
|
||||
val translation = value match {
|
||||
case JsObject(map) =>
|
||||
for {
|
||||
key <- map.get("key").collect { case JsString(s) => s }
|
||||
value <- map.get("value").map(jsValueToApiValue)
|
||||
} yield key -> value
|
||||
case _ => None
|
||||
}
|
||||
|
||||
translation.getOrElse(deserializationError(s"Can't read ${value.prettyPrint} as a map entry"))
|
||||
}
|
||||
|
||||
def jsValueToGenMapEntry(value: JsValue): (Model.ApiValue, Model.ApiValue) = {
|
||||
val translation = value match {
|
||||
case JsObject(genMap) =>
|
||||
for {
|
||||
key <- genMap.get("key").map(jsValueToApiValue)
|
||||
value <- genMap.get("value").map(jsValueToApiValue)
|
||||
} yield key -> value
|
||||
case _ => None
|
||||
}
|
||||
|
||||
translation.getOrElse(deserializationError(s"Can't read ${value.prettyPrint} as a map entry"))
|
||||
}
|
||||
|
||||
def jsValueToApiVariant(value: JsValue): Model.ApiVariant =
|
||||
strField(value, propType, "ApiVariant") match {
|
||||
case `tagVariant` =>
|
||||
V.ValueVariant(
|
||||
asObject(value, "ApiVariant").fields
|
||||
.get(propId)
|
||||
.flatMap(_.convertTo[Option[DamlLfIdentifier]]),
|
||||
assertDE(Ref.Name fromString strField(value, propConstructor, "ApiVariant")),
|
||||
jsValueToApiValue(anyField(value, propValue, "ApiVariant")),
|
||||
)
|
||||
case t =>
|
||||
deserializationError(
|
||||
s"Can't read ${value.prettyPrint} as ApiVariant, type '$t' is not a variant"
|
||||
)
|
||||
}
|
||||
|
||||
def jsValueToApiEnum(value: JsValue): V.ValueEnum =
|
||||
strField(value, propType, "ApiEnum") match {
|
||||
case `tagEnum` =>
|
||||
V.ValueEnum(
|
||||
asObject(value, "ApiEnum").fields
|
||||
.get(propId)
|
||||
.flatMap(_.convertTo[Option[DamlLfIdentifier]]),
|
||||
assertDE(Ref.Name fromString strField(value, propConstructor, "ApiEnum")),
|
||||
)
|
||||
case t =>
|
||||
deserializationError(
|
||||
s"Can't read ${value.prettyPrint} as ApiEnum, type '$t' is not a enum"
|
||||
)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Implicits that can be imported for .parseJson and .toJson functions
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
object JsonImplicits extends DefaultJsonProtocol {
|
||||
implicit object ApiValueJsonFormat extends RootJsonFormat[Model.ApiValue] {
|
||||
def write(v: Model.ApiValue): JsValue = apiValueToJsValue(v)
|
||||
def read(v: JsValue): Model.ApiValue = jsValueToApiValue(v)
|
||||
}
|
||||
|
||||
implicit object ApiRecordJsonFormat extends RootJsonFormat[Model.ApiRecord] {
|
||||
def write(v: Model.ApiRecord): JsValue = apiRecordToJsValue(v)
|
||||
def read(v: JsValue): Model.ApiRecord = jsValueToApiRecord(v)
|
||||
}
|
||||
|
||||
implicit object ApiVariantJsonFormat extends RootJsonFormat[Model.ApiVariant] {
|
||||
def write(v: Model.ApiVariant): JsValue = apiVariantToJsValue(v)
|
||||
def read(v: JsValue): Model.ApiVariant = jsValueToApiVariant(v)
|
||||
}
|
||||
}
|
||||
|
||||
private[this] def assertDE[A](ea: Either[String, A]): A =
|
||||
ea.fold(deserializationError(_), identity)
|
||||
}
|
@ -1,258 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.json
|
||||
|
||||
import com.daml.lf.data.{Ref => DamlLfRef, Numeric => DamlLfNumeric}
|
||||
import com.daml.navigator.json.Util._
|
||||
import com.daml.navigator.{model => Model}
|
||||
import spray.json._
|
||||
|
||||
/** An encoding of Daml-LF types.
|
||||
*
|
||||
* The types are encoded as-is (no type variables substitution, no type reference resolution).
|
||||
*/
|
||||
object DamlLfCodec {
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Constants used in the encoding
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
private[this] final val propType: String = "type"
|
||||
private[this] final val propValue: String = "value"
|
||||
private[this] final val propName: String = "name"
|
||||
private[this] final val propModule: String = "module"
|
||||
private[this] final val propPackage: String = "package"
|
||||
private[this] final val propArgs: String = "args"
|
||||
private[this] final val propVars: String = "vars"
|
||||
private[this] final val propFields: String = "fields"
|
||||
private[this] final val propConstructors: String = "constructors"
|
||||
private[this] final val propScale: String = "scale"
|
||||
|
||||
private[this] final val tagTypeCon: String = "typecon"
|
||||
private[this] final val tagTypeVar: String = "typevar"
|
||||
private[this] final val tagTypePrim: String = "primitive"
|
||||
private[this] final val tagTypeList: String = "list"
|
||||
private[this] final val tagTypeBool: String = "bool"
|
||||
private[this] final val tagTypeNumeric: String = "numeric"
|
||||
private[this] final val tagTypeInt64: String = "int64"
|
||||
private[this] final val tagTypeContractId: String = "contractid"
|
||||
private[this] final val tagTypeDate: String = "date"
|
||||
private[this] final val tagTypeParty: String = "party"
|
||||
private[this] final val tagTypeText: String = "text"
|
||||
private[this] final val tagTypeTimestamp: String = "timestamp"
|
||||
private[this] final val tagTypeUnit: String = "unit"
|
||||
private[this] final val tagTypeRecord: String = "record"
|
||||
private[this] final val tagTypeVariant: String = "variant"
|
||||
private[this] final val tagTypeEnum: String = "enum"
|
||||
private[this] final val tagTypeOptional: String = "optional"
|
||||
private[this] final val tagTypeTextMap: String = "textmap"
|
||||
private[this] final val tagTypeGenMap: String = "genmap"
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Encoding
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
def damlLfTypeToJsValue(value: Model.DamlLfType): JsValue = value match {
|
||||
case typeCon: Model.DamlLfTypeCon => damlLfTypeConToJsValue(typeCon)
|
||||
case typePrim: Model.DamlLfTypePrim => damlLfPrimToJsValue(typePrim)
|
||||
case typeVar: Model.DamlLfTypeVar => damlLfTypeVarToJsValue(typeVar)
|
||||
case typeNum: Model.DamlLfTypeNumeric => damlLfNumericToJsValue(typeNum)
|
||||
}
|
||||
|
||||
def damlLfTypeConToJsValue(value: Model.DamlLfTypeCon): JsValue = {
|
||||
val id =
|
||||
Model.DamlLfIdentifier(value.name.identifier.packageId, value.name.identifier.qualifiedName)
|
||||
JsObject(
|
||||
propType -> JsString(tagTypeCon),
|
||||
propName -> damlLfIdentifierToJsValue(id),
|
||||
propArgs -> JsArray(value.typArgs.map(damlLfTypeToJsValue).toVector),
|
||||
)
|
||||
}
|
||||
|
||||
def damlLfTypeVarToJsValue(value: Model.DamlLfTypeVar): JsValue = JsObject(
|
||||
propType -> JsString(tagTypeVar),
|
||||
propName -> JsString(value.name),
|
||||
)
|
||||
|
||||
def damlLfPrimToJsValue(value: Model.DamlLfTypePrim): JsValue = JsObject(
|
||||
propType -> JsString(tagTypePrim),
|
||||
propName -> damlLfPrimTypeToJsValue(value.typ),
|
||||
propArgs -> JsArray(value.typArgs.map(damlLfTypeToJsValue).toVector),
|
||||
)
|
||||
|
||||
def damlLfPrimTypeToJsValue(value: Model.DamlLfPrimType): JsString = value match {
|
||||
case Model.DamlLfPrimType.List => JsString(tagTypeList)
|
||||
case Model.DamlLfPrimType.ContractId => JsString(tagTypeContractId)
|
||||
case Model.DamlLfPrimType.Bool => JsString(tagTypeBool)
|
||||
case Model.DamlLfPrimType.Int64 => JsString(tagTypeInt64)
|
||||
case Model.DamlLfPrimType.Date => JsString(tagTypeDate)
|
||||
case Model.DamlLfPrimType.Party => JsString(tagTypeParty)
|
||||
case Model.DamlLfPrimType.Text => JsString(tagTypeText)
|
||||
case Model.DamlLfPrimType.Timestamp => JsString(tagTypeTimestamp)
|
||||
case Model.DamlLfPrimType.Optional => JsString(tagTypeOptional)
|
||||
case Model.DamlLfPrimType.TextMap => JsString(tagTypeTextMap)
|
||||
case Model.DamlLfPrimType.GenMap => JsString(tagTypeGenMap)
|
||||
case Model.DamlLfPrimType.Unit => JsString(tagTypeUnit)
|
||||
}
|
||||
|
||||
def damlLfNumericToJsValue(typeNum: Model.DamlLfTypeNumeric): JsValue = JsObject(
|
||||
propType -> JsString(tagTypeNumeric),
|
||||
propScale -> JsNumber(typeNum.scale),
|
||||
)
|
||||
|
||||
def damlLfIdentifierToJsValue(value: Model.DamlLfIdentifier): JsValue = JsObject(
|
||||
propName -> JsString(value.qualifiedName.name.toString()),
|
||||
propModule -> JsString(value.qualifiedName.module.toString()),
|
||||
propPackage -> JsString(value.packageId),
|
||||
)
|
||||
|
||||
def damlLfDataTypeToJsValue(value: Model.DamlLfDataType): JsValue = value match {
|
||||
case r: Model.DamlLfRecord =>
|
||||
JsObject(
|
||||
propType -> JsString(tagTypeRecord),
|
||||
propFields -> JsArray(
|
||||
r.fields
|
||||
.map(f => JsObject(propName -> JsString(f._1), propValue -> damlLfTypeToJsValue(f._2)))
|
||||
.toVector
|
||||
),
|
||||
)
|
||||
case v: Model.DamlLfVariant =>
|
||||
JsObject(
|
||||
propType -> JsString(tagTypeVariant),
|
||||
propFields -> JsArray(
|
||||
v.fields
|
||||
.map(f => JsObject(propName -> JsString(f._1), propValue -> damlLfTypeToJsValue(f._2)))
|
||||
.toVector
|
||||
),
|
||||
)
|
||||
case e: Model.DamlLfEnum =>
|
||||
JsObject(
|
||||
propType -> JsString(tagTypeEnum),
|
||||
propConstructors -> JsArray(e.constructors.map(JsString(_)).toVector),
|
||||
)
|
||||
}
|
||||
|
||||
def damlLfDefDataTypeToJsValue(value: Model.DamlLfDefDataType): JsValue = JsObject(
|
||||
propType -> damlLfDataTypeToJsValue(value.dataType),
|
||||
propVars -> JsArray(value.typeVars.map(JsString(_)).toVector),
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Decoding
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def jsValueToDamlLfType(value: JsValue): Model.DamlLfType =
|
||||
strField(value, propType, "DamlLfType") match {
|
||||
case `tagTypeCon` =>
|
||||
Model.DamlLfTypeCon(
|
||||
Model.DamlLfTypeConName(
|
||||
jsValueToDamlLfIdentifier(anyField(value, propName, "DamlLfTypeCon"))
|
||||
),
|
||||
Model.DamlLfImmArraySeq(
|
||||
arrayField(value, propArgs, "DamlLfTypeCon").map(jsValueToDamlLfType): _*
|
||||
),
|
||||
)
|
||||
case `tagTypeVar` => Model.DamlLfTypeVar(nameField(value, propName, "DamlLfTypeVar"))
|
||||
case `tagTypePrim` =>
|
||||
Model.DamlLfTypePrim(
|
||||
jsValueToDamlLfPrimType(strField(value, propName, "DamlLfTypePrim")),
|
||||
Model.DamlLfImmArraySeq(
|
||||
arrayField(value, propArgs, "DamlLfTypePrim").map(jsValueToDamlLfType): _*
|
||||
),
|
||||
)
|
||||
case `tagTypeNumeric` =>
|
||||
DamlLfNumeric.Scale
|
||||
.fromLong(intField(value, propScale, "DamlLfTypeNumeric"))
|
||||
.fold[Model.DamlLfTypeNumeric](deserializationError(_), Model.DamlLfTypeNumeric)
|
||||
}
|
||||
|
||||
def jsValueToDamlLfPrimType(value: String): Model.DamlLfPrimType = value match {
|
||||
case `tagTypeList` => Model.DamlLfPrimType.List
|
||||
case `tagTypeContractId` => Model.DamlLfPrimType.ContractId
|
||||
case `tagTypeBool` => Model.DamlLfPrimType.Bool
|
||||
case `tagTypeInt64` => Model.DamlLfPrimType.Int64
|
||||
case `tagTypeDate` => Model.DamlLfPrimType.Date
|
||||
case `tagTypeParty` => Model.DamlLfPrimType.Party
|
||||
case `tagTypeText` => Model.DamlLfPrimType.Text
|
||||
case `tagTypeTimestamp` => Model.DamlLfPrimType.Timestamp
|
||||
case `tagTypeUnit` => Model.DamlLfPrimType.Unit
|
||||
case `tagTypeOptional` => Model.DamlLfPrimType.Optional
|
||||
case `tagTypeTextMap` => Model.DamlLfPrimType.TextMap
|
||||
}
|
||||
|
||||
def jsValueToDamlLfDataType(value: JsValue): Model.DamlLfDataType =
|
||||
strField(value, propType, "DamlLfDefDataType") match {
|
||||
case `tagTypeRecord` =>
|
||||
val fields = arrayField(value, propFields, "DamlLfRecord")
|
||||
Model.DamlLfRecord(
|
||||
Model.DamlLfImmArraySeq(
|
||||
fields.map(f =>
|
||||
(
|
||||
nameField(f, propName, "DamlLfRecord"),
|
||||
jsValueToDamlLfType(anyField(f, propValue, "DamlLfRecord")),
|
||||
)
|
||||
): _*
|
||||
)
|
||||
)
|
||||
case `tagTypeVariant` =>
|
||||
val constructors = arrayField(value, propFields, "DamlLfVariant")
|
||||
Model.DamlLfVariant(
|
||||
Model.DamlLfImmArraySeq(
|
||||
constructors.map(f =>
|
||||
(
|
||||
nameField(f, propName, "DamlLfVariant"),
|
||||
jsValueToDamlLfType(anyField(f, propValue, "DamlLfVariant")),
|
||||
)
|
||||
): _*
|
||||
)
|
||||
)
|
||||
case `tagTypeEnum` =>
|
||||
val constructors = arrayField(value, propConstructors, "DamlLfEnum")
|
||||
Model.DamlLfEnum(Model.DamlLfImmArraySeq(constructors: _*).map(asName(_, "DamlLfEnum")))
|
||||
case t =>
|
||||
deserializationError(
|
||||
s"Can't read ${value.prettyPrint} as DamlLfDataType, unknown type '$t'"
|
||||
)
|
||||
}
|
||||
|
||||
def jsValueToDamlLfDefDataType(value: JsValue): Model.DamlLfDefDataType = {
|
||||
val vars =
|
||||
arrayField(value, propVars, "DamlLfDefDataType").map(v => asName(v, "DamlLfDefDataType"))
|
||||
val dataType = jsValueToDamlLfDataType(anyField(value, propType, "DamlLfDefDataType"))
|
||||
|
||||
Model.DamlLfDefDataType(Model.DamlLfImmArraySeq(vars: _*), dataType)
|
||||
}
|
||||
|
||||
def jsValueToDamlLfIdentifier(value: JsValue): Model.DamlLfIdentifier =
|
||||
Model.DamlLfIdentifier(
|
||||
DamlLfRef.PackageId.assertFromString(strField(value, propPackage, "DamlLfIdentifier")),
|
||||
Model.DamlLfQualifiedName(
|
||||
Model.DamlLfDottedName.assertFromString(strField(value, propModule, "DamlLfIdentifier")),
|
||||
Model.DamlLfDottedName.assertFromString(strField(value, propName, "DamlLfIdentifier")),
|
||||
),
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Implicits that can be imported for .parseJson and .toJson functions
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
object JsonImplicits extends DefaultJsonProtocol {
|
||||
implicit object DamlLfTypeJsonFormat extends RootJsonFormat[Model.DamlLfType] {
|
||||
def write(v: Model.DamlLfType): JsValue = damlLfTypeToJsValue(v)
|
||||
def read(v: JsValue): Model.DamlLfType = jsValueToDamlLfType(v)
|
||||
}
|
||||
|
||||
implicit object DamlLfDataTypeJsonFormat extends RootJsonFormat[Model.DamlLfDataType] {
|
||||
def write(v: Model.DamlLfDataType): JsValue = damlLfDataTypeToJsValue(v)
|
||||
def read(v: JsValue): Model.DamlLfDataType = jsValueToDamlLfDataType(v)
|
||||
}
|
||||
|
||||
implicit object DamlLfDefDataTypeJsonFormat extends RootJsonFormat[Model.DamlLfDefDataType] {
|
||||
def write(v: Model.DamlLfDefDataType): JsValue = damlLfDefDataTypeToJsValue(v)
|
||||
def read(v: JsValue): Model.DamlLfDefDataType = jsValueToDamlLfDefDataType(v)
|
||||
}
|
||||
|
||||
implicit object DamlLfIdentifierJsonFormat extends RootJsonFormat[Model.DamlLfIdentifier] {
|
||||
def write(v: Model.DamlLfIdentifier): JsValue = damlLfIdentifierToJsValue(v)
|
||||
def read(v: JsValue): Model.DamlLfIdentifier = jsValueToDamlLfIdentifier(v)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.json
|
||||
|
||||
import com.daml.ledger.api.refinements.ApiTypes
|
||||
import com.daml.navigator.model._
|
||||
import com.daml.navigator.json.Util._
|
||||
import com.daml.navigator.json.DamlLfCodec.JsonImplicits._
|
||||
import spray.json._
|
||||
|
||||
/** An encoding of Model types.
|
||||
*/
|
||||
object ModelCodec {
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Constants used in the encoding
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Encoding
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
/*
|
||||
def eventToJsValue(event: Event): JsValue = {
|
||||
import JsonImplicits._
|
||||
|
||||
val fields: List[JsField] = event match {
|
||||
case c: ContractCreated => c.toJson.asJsObject.fields.toList :+ propType -> JsString(contractCreatedTag)
|
||||
case a: ContractArchived => a.toJson.asJsObject.fields.toList :+ propType -> JsString(contractArchivedTag)
|
||||
case e: ChoiceExercised => e.toJson.asJsObject.fields.toList :+ propType -> JsString(choiceExercisedTag)
|
||||
}
|
||||
JsObject(fields: _*)
|
||||
}
|
||||
*/
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Decoding
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
/*
|
||||
def jsValueToEvent(value: JsValue): Event = {
|
||||
import JsonImplicits._
|
||||
|
||||
strField(value, propType, "Event") match {
|
||||
case `contractCreatedTag` => value.convertTo[ContractCreated]
|
||||
case `contractArchivedTag` => value.convertTo[ContractArchived]
|
||||
case `choiceExercisedTag` => value.convertTo[ChoiceExercised]
|
||||
case subclassType => deserializationError(s"Unknown Event type: $subclassType")
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Implicits that can be imported to convert from and to JSON
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
object JsonImplicits extends DefaultJsonProtocol {
|
||||
|
||||
implicit object ContractIdJsonFormat extends RootJsonFormat[ApiTypes.ContractId] {
|
||||
def write(value: ApiTypes.ContractId): JsValue = JsString(ApiTypes.ContractId.unwrap(value))
|
||||
def read(value: JsValue): ApiTypes.ContractId =
|
||||
ApiTypes.ContractId(asString(value, "ContractId"))
|
||||
}
|
||||
|
||||
implicit object EventIdJsonFormat extends RootJsonFormat[ApiTypes.EventId] {
|
||||
def write(value: ApiTypes.EventId): JsValue = JsString(ApiTypes.EventId.unwrap(value))
|
||||
def read(value: JsValue): ApiTypes.EventId = ApiTypes.EventId(asString(value, "EventId"))
|
||||
}
|
||||
|
||||
implicit object TransactionIdJsonFormat extends RootJsonFormat[ApiTypes.TransactionId] {
|
||||
def write(value: ApiTypes.TransactionId): JsValue =
|
||||
JsString(ApiTypes.TransactionId.unwrap(value))
|
||||
def read(value: JsValue): ApiTypes.TransactionId =
|
||||
ApiTypes.TransactionId(asString(value, "TransactionId"))
|
||||
}
|
||||
|
||||
implicit object WorkflowIdJsonFormat extends RootJsonFormat[ApiTypes.WorkflowId] {
|
||||
def write(value: ApiTypes.WorkflowId): JsValue = JsString(ApiTypes.WorkflowId.unwrap(value))
|
||||
def read(value: JsValue): ApiTypes.WorkflowId =
|
||||
ApiTypes.WorkflowId(asString(value, "WorkflowId"))
|
||||
}
|
||||
|
||||
implicit object ChoiceJsonFormat extends RootJsonFormat[ApiTypes.Choice] {
|
||||
def write(value: ApiTypes.Choice): JsValue = JsString(ApiTypes.Choice.unwrap(value))
|
||||
def read(value: JsValue): ApiTypes.Choice = ApiTypes.Choice(asString(value, "Choice"))
|
||||
}
|
||||
|
||||
implicit object PartyJsonFormat extends RootJsonFormat[ApiTypes.Party] {
|
||||
def write(value: ApiTypes.Party): JsValue = JsString(ApiTypes.Party.unwrap(value))
|
||||
def read(value: JsValue): ApiTypes.Party = ApiTypes.Party(asString(value, "Party"))
|
||||
}
|
||||
|
||||
implicit val PartyListJsonFormat: RootJsonFormat[List[ApiTypes.Party]] =
|
||||
listFormat[ApiTypes.Party]
|
||||
implicit val choiceFormat: RootJsonFormat[Choice] = jsonFormat5(Choice.apply)
|
||||
implicit val templateFormat: RootJsonFormat[Template] = jsonFormat4(Template.apply)
|
||||
}
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.json
|
||||
|
||||
import com.daml.lf.data.Ref
|
||||
import spray.json._
|
||||
|
||||
/** JSON encoding utils
|
||||
*/
|
||||
object Util {
|
||||
def strField(obj: JsValue, name: String, as: String): String =
|
||||
asObject(obj, as).fields.get(name) match {
|
||||
case Some(JsString(v)) =>
|
||||
v
|
||||
case Some(_) =>
|
||||
deserializationError(s"Can't read ${obj.prettyPrint} as $as, field '$name' is not a string")
|
||||
case None =>
|
||||
deserializationError(s"Can't read ${obj.prettyPrint} as $as, missing field '$name'")
|
||||
}
|
||||
|
||||
def nameField(obj: JsValue, name: String, as: String): Ref.Name =
|
||||
Ref.Name
|
||||
.fromString(strField(obj, name, as))
|
||||
.fold(
|
||||
err => deserializationError(s"Can't read ${obj.prettyPrint} as $as, $err"),
|
||||
identity,
|
||||
)
|
||||
|
||||
def intField(obj: JsValue, name: String, as: String): Long =
|
||||
asObject(obj, as).fields.get(name) match {
|
||||
case Some(JsNumber(v)) => v.toLongExact
|
||||
case Some(_) =>
|
||||
deserializationError(
|
||||
s"Can't read ${obj.prettyPrint} as $as, field '$name' is not an integer"
|
||||
)
|
||||
case None =>
|
||||
deserializationError(s"Can't read ${obj.prettyPrint} as $as, missing field '$name'")
|
||||
}
|
||||
|
||||
def boolField(obj: JsValue, name: String, as: String): Boolean =
|
||||
asObject(obj, as).fields.get(name) match {
|
||||
case Some(JsBoolean(v)) => v
|
||||
case Some(_) =>
|
||||
deserializationError(
|
||||
s"Can't read ${obj.prettyPrint} as $as, field '$name' is not a boolean"
|
||||
)
|
||||
case None =>
|
||||
deserializationError(s"Can't read ${obj.prettyPrint} as $as, missing field '$name'")
|
||||
}
|
||||
|
||||
def arrayField(obj: JsValue, name: String, as: String): List[JsValue] =
|
||||
asObject(obj, as).fields.get(name) match {
|
||||
case Some(JsArray(v)) => v.toList
|
||||
case Some(_) =>
|
||||
deserializationError(s"Can't read ${obj.prettyPrint} as $as, field '$name' is not an array")
|
||||
case None =>
|
||||
deserializationError(s"Can't read ${obj.prettyPrint} as $as, missing field '$name'")
|
||||
}
|
||||
|
||||
def objectField(obj: JsValue, name: String, as: String): Map[String, JsValue] =
|
||||
asObject(obj, as).fields.get(name) match {
|
||||
case Some(JsObject(v)) => v
|
||||
case Some(_) =>
|
||||
deserializationError(s"Can't read ${obj.prettyPrint} as $as, field '$name' is not an array")
|
||||
case None =>
|
||||
deserializationError(s"Can't read ${obj.prettyPrint} as $as, missing field '$name'")
|
||||
}
|
||||
|
||||
def anyField(obj: JsValue, name: String, as: String): JsValue =
|
||||
asObject(obj, as).fields.get(name) match {
|
||||
case Some(v: JsValue) => v
|
||||
case _ => deserializationError(s"Can't read ${obj.prettyPrint} as $as, missing field '$name'")
|
||||
}
|
||||
|
||||
def asObject(value: JsValue, as: String): JsObject = value match {
|
||||
case obj: JsObject => obj
|
||||
case _ =>
|
||||
deserializationError(s"Can't read ${value.prettyPrint} as $as, value is not an object")
|
||||
}
|
||||
|
||||
def asString(value: JsValue, as: String): String = value match {
|
||||
case JsString(s) => s
|
||||
case _ => deserializationError(s"Can't read ${value.prettyPrint} as $as, value is not a string")
|
||||
}
|
||||
|
||||
def asName(value: JsValue, as: String): Ref.Name = value match {
|
||||
case JsString(v) =>
|
||||
Ref.Name
|
||||
.fromString(v)
|
||||
.fold(
|
||||
err => deserializationError(s"Can't read ${value.prettyPrint} as $as, $err"),
|
||||
identity,
|
||||
)
|
||||
case _ => deserializationError(s"Can't read ${value.prettyPrint} as $as, value is not a string")
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.model
|
||||
|
||||
sealed trait DatabaseError extends Throwable
|
||||
|
||||
final case class NoDatabaseUsed() extends DatabaseError {
|
||||
override def getMessage: String = "No database is used."
|
||||
}
|
||||
|
||||
final case class DeserializationFailed(msg: String) extends DatabaseError {
|
||||
override def getMessage: String = s"Deserialization failed while reading from the db: $msg"
|
||||
}
|
||||
|
||||
final case class RecordNotFound(msg: String) extends DatabaseError {
|
||||
override def getMessage: String = s"Record not found: $msg"
|
||||
}
|
@ -1,361 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.model
|
||||
|
||||
import com.daml.ledger.api.refinements.ApiTypes
|
||||
import com.daml.navigator.data.DatabaseActions
|
||||
import com.typesafe.scalalogging.LazyLogging
|
||||
|
||||
import scala.collection.immutable.LazyList
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/** In-memory projection of ledger events. */
|
||||
case class Ledger(
|
||||
private val forParty: ApiTypes.Party,
|
||||
private val lastTransaction: Option[Transaction],
|
||||
private val useDatabase: Boolean,
|
||||
private val transactionById: Map[ApiTypes.TransactionId, Transaction] = Map.empty,
|
||||
private val eventById: Map[ApiTypes.EventId, Event] = Map.empty,
|
||||
private val contractById: Map[ApiTypes.ContractId, Contract] = Map.empty,
|
||||
private val activeContractById: Map[ApiTypes.ContractId, Contract] = Map.empty,
|
||||
private val createEventByContractId: Map[ApiTypes.ContractId, ContractCreated] = Map.empty,
|
||||
private val archiveEventByContractId: Map[ApiTypes.ContractId, ChoiceExercised] = Map.empty,
|
||||
private val choiceExercisedEventByContractById: Map[ApiTypes.ContractId, List[
|
||||
ChoiceExercised
|
||||
]] = Map.empty,
|
||||
private val contractsByTemplateId: Map[DamlLfIdentifier, Set[Contract]] = Map.empty,
|
||||
private val commandById: Map[ApiTypes.CommandId, Command] = Map.empty,
|
||||
private val statusByCommandId: Map[ApiTypes.CommandId, CommandStatus] = Map.empty,
|
||||
private val db: DatabaseActions = new DatabaseActions,
|
||||
) extends LazyLogging {
|
||||
|
||||
private def logErrorAndDefaultTo[A](result: Try[A], default: A): A = {
|
||||
result match {
|
||||
case Success(a) => a
|
||||
case Failure(e) =>
|
||||
logger.error(e.getMessage)
|
||||
default
|
||||
}
|
||||
}
|
||||
|
||||
def latestTransaction(types: PackageRegistry): Option[Transaction] = {
|
||||
if (useDatabase) {
|
||||
logErrorAndDefaultTo(db.lastTransaction(types), None)
|
||||
} else {
|
||||
lastTransaction
|
||||
}
|
||||
}
|
||||
|
||||
def withCommandStatus(commandId: ApiTypes.CommandId, result: CommandStatus): Ledger = {
|
||||
if (useDatabase) {
|
||||
db.upsertCommandStatus(commandId, result)
|
||||
this
|
||||
} else {
|
||||
copy(
|
||||
statusByCommandId = statusByCommandId + (commandId -> result)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def withCommand(command: Command): Ledger = {
|
||||
if (useDatabase) {
|
||||
db.insertCommand(command)
|
||||
db.upsertCommandStatus(command.id, CommandStatusWaiting())
|
||||
this
|
||||
} else {
|
||||
copy(
|
||||
commandById = commandById + (command.id -> command),
|
||||
statusByCommandId = statusByCommandId + (command.id -> CommandStatusWaiting()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def withTransaction(tx: Transaction, packageRegistry: PackageRegistry): Ledger = {
|
||||
val ledger0 = this
|
||||
.withLatestTransaction(tx)
|
||||
.withTransactionResult(tx)
|
||||
|
||||
tx.events.foldLeft(ledger0) { (ledger, event) =>
|
||||
ledger.withEvent(event, packageRegistry)
|
||||
}
|
||||
}
|
||||
|
||||
private def withLatestTransaction(tx: Transaction): Ledger = {
|
||||
if (useDatabase) {
|
||||
db.insertTransaction(tx)
|
||||
this
|
||||
} else {
|
||||
copy(
|
||||
lastTransaction = Some(tx),
|
||||
transactionById = transactionById + (tx.id -> tx),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def withTransactionResult(tx: Transaction): Ledger = {
|
||||
// Note: only store the status for commands submitted by this client
|
||||
if (useDatabase) {
|
||||
tx.commandId.foreach(db.updateCommandStatus(_, CommandStatusSuccess(tx)))
|
||||
this
|
||||
} else {
|
||||
tx.commandId
|
||||
.filter(commandById.contains)
|
||||
.fold(this) { commandId =>
|
||||
copy(statusByCommandId = statusByCommandId + (commandId -> CommandStatusSuccess(tx)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def withEvent(event: Event, packageRegistry: PackageRegistry): Ledger =
|
||||
event match {
|
||||
case event: ContractCreated =>
|
||||
packageRegistry.template(event.templateId).fold(this) { template =>
|
||||
val contract = Contract(
|
||||
event.contractId,
|
||||
template,
|
||||
event.argument,
|
||||
event.agreementText,
|
||||
event.signatories,
|
||||
event.observers,
|
||||
event.key,
|
||||
)
|
||||
withContractCreatedInEvent(contract, event)
|
||||
}
|
||||
|
||||
case event: ChoiceExercised =>
|
||||
withChoiceExercisedInEvent(event.contractId, event)
|
||||
}
|
||||
|
||||
private def withContractCreatedInEvent(contract: Contract, event: ContractCreated): Ledger = {
|
||||
val isStakeHolder =
|
||||
contract.signatories.contains(forParty) || contract.observers.contains(forParty)
|
||||
if (useDatabase) {
|
||||
if (isStakeHolder) db.insertContract(contract)
|
||||
db.insertEvent(event)
|
||||
this
|
||||
} else {
|
||||
val contractUpdated =
|
||||
if (isStakeHolder)
|
||||
copy(
|
||||
contractById = contractById + (contract.id -> contract),
|
||||
activeContractById = activeContractById + (contract.id -> contract),
|
||||
contractsByTemplateId = contractsByTemplateIdWith(contract),
|
||||
createEventByContractId = createEventByContractId + (contract.id -> event),
|
||||
)
|
||||
else
|
||||
this
|
||||
contractUpdated.copy(eventById = eventById + (event.id -> event))
|
||||
}
|
||||
}
|
||||
|
||||
private def contractsByTemplateIdWith(contract: Contract) = {
|
||||
val templateId = contract.template.id
|
||||
val entryWithContract =
|
||||
templateId -> (contractsByTemplateId.getOrElse(templateId, Set.empty) + contract)
|
||||
contractsByTemplateId + entryWithContract
|
||||
}
|
||||
|
||||
private def withChoiceExercisedInEvent(
|
||||
contractId: ApiTypes.ContractId,
|
||||
event: ChoiceExercised,
|
||||
): Ledger = {
|
||||
if (useDatabase) {
|
||||
if (event.consuming) {
|
||||
db.archiveContract(contractId, event.transactionId)
|
||||
}
|
||||
db.insertEvent(event)
|
||||
this
|
||||
} else {
|
||||
val prevExercises: List[ChoiceExercised] =
|
||||
choiceExercisedEventByContractById.getOrElse(contractId, List.empty)
|
||||
copy(
|
||||
archiveEventByContractId =
|
||||
if (event.consuming) archiveEventByContractId + (contractId -> event)
|
||||
else archiveEventByContractId,
|
||||
activeContractById =
|
||||
if (event.consuming) activeContractById - contractId else activeContractById,
|
||||
choiceExercisedEventByContractById =
|
||||
choiceExercisedEventByContractById + (contractId -> (event :: prevExercises)),
|
||||
eventById = eventById + (event.id -> event),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def allContractsCount: Int = {
|
||||
if (useDatabase) {
|
||||
logErrorAndDefaultTo(db.contractCount(), 0)
|
||||
} else {
|
||||
contractById.size
|
||||
}
|
||||
}
|
||||
|
||||
def activeContractsCount: Int = {
|
||||
if (useDatabase) {
|
||||
logErrorAndDefaultTo(db.activeContractCount(), 0)
|
||||
} else {
|
||||
activeContractById.size
|
||||
}
|
||||
}
|
||||
|
||||
def contract(id: ApiTypes.ContractId, types: PackageRegistry): Option[Contract] = {
|
||||
if (useDatabase) {
|
||||
logErrorAndDefaultTo(db.contract(id, types), None)
|
||||
} else {
|
||||
contractById.get(id)
|
||||
}
|
||||
}
|
||||
|
||||
def event(id: ApiTypes.EventId, types: PackageRegistry): Option[Event] = {
|
||||
if (useDatabase) {
|
||||
logErrorAndDefaultTo(db.eventById(id, types), None)
|
||||
} else {
|
||||
eventById.get(id)
|
||||
}
|
||||
}
|
||||
|
||||
def childEvents(id: ApiTypes.EventId, types: PackageRegistry): List[Event] = {
|
||||
if (useDatabase) {
|
||||
logErrorAndDefaultTo(db.eventsByParentId(id, types), List.empty)
|
||||
} else {
|
||||
(for {
|
||||
ev <- eventById.get(id)
|
||||
tx <- transactionById.get(ev.transactionId)
|
||||
} yield {
|
||||
tx.events.filter(_.parentId.contains(id))
|
||||
}).getOrElse(List.empty)
|
||||
}
|
||||
}
|
||||
|
||||
def transaction(id: ApiTypes.TransactionId, types: PackageRegistry): Option[Transaction] = {
|
||||
if (useDatabase) {
|
||||
logErrorAndDefaultTo(db.transactionById(id, types), None)
|
||||
} else {
|
||||
transactionById.get(id)
|
||||
}
|
||||
}
|
||||
|
||||
def command(id: ApiTypes.CommandId, types: PackageRegistry): Option[Command] = {
|
||||
if (useDatabase) {
|
||||
logErrorAndDefaultTo(db.commandById(id, types), None)
|
||||
} else {
|
||||
commandById.get(id)
|
||||
}
|
||||
}
|
||||
|
||||
def commandStatus(id: ApiTypes.CommandId, types: PackageRegistry): Option[CommandStatus] = {
|
||||
if (useDatabase) {
|
||||
logErrorAndDefaultTo(db.commandStatusByCommandId(id, types), None)
|
||||
} else {
|
||||
statusByCommandId.get(id)
|
||||
}
|
||||
}
|
||||
|
||||
def createEventOf(contract: Contract, types: PackageRegistry): Try[ContractCreated] = {
|
||||
if (useDatabase) {
|
||||
db.createEventByContractId(contract.id, types)
|
||||
} else {
|
||||
Success(createEventByContractId(contract.id))
|
||||
}
|
||||
}
|
||||
|
||||
def archiveEventOf(contract: Contract, types: PackageRegistry): Option[ChoiceExercised] = {
|
||||
if (useDatabase) {
|
||||
logErrorAndDefaultTo(db.archiveEventByContractId(contract.id, types), None)
|
||||
} else {
|
||||
archiveEventByContractId.get(contract.id)
|
||||
}
|
||||
}
|
||||
|
||||
def exercisedEventsOf(contract: Contract, types: PackageRegistry): List[ChoiceExercised] = {
|
||||
if (useDatabase) {
|
||||
logErrorAndDefaultTo(db.choiceExercisedEventByContractById(contract.id, types), List.empty)
|
||||
} else {
|
||||
choiceExercisedEventByContractById.getOrElse(contract.id, List.empty)
|
||||
}
|
||||
}
|
||||
|
||||
def allContracts(types: PackageRegistry): LazyList[Contract] = {
|
||||
if (useDatabase) {
|
||||
logErrorAndDefaultTo(db.contracts(types), List.empty[Contract]).to(LazyList)
|
||||
} else {
|
||||
contractById.values.to(LazyList)
|
||||
}
|
||||
}
|
||||
|
||||
def activeContracts(types: PackageRegistry): LazyList[Contract] = {
|
||||
if (useDatabase) {
|
||||
logErrorAndDefaultTo(db.activeContracts(types), List.empty[Contract]).to(LazyList)
|
||||
} else {
|
||||
activeContractById.values.to(LazyList)
|
||||
}
|
||||
}
|
||||
|
||||
def activeTemplateContractsOf(template: Template, types: PackageRegistry): LazyList[Contract] = {
|
||||
if (useDatabase) {
|
||||
logErrorAndDefaultTo(
|
||||
db.activeContractsForTemplate(template.id, types),
|
||||
List.empty[Contract],
|
||||
).to(LazyList)
|
||||
} else {
|
||||
templateContractsOf(template, types).filter(contract =>
|
||||
activeContractById.contains(contract.id)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def templateContractsOf(template: Template, types: PackageRegistry): LazyList[Contract] = {
|
||||
if (useDatabase) {
|
||||
logErrorAndDefaultTo(
|
||||
db.contractsForTemplate(template.id, types),
|
||||
List.empty[Contract],
|
||||
).to(LazyList)
|
||||
} else {
|
||||
contractsByTemplateId.getOrElse(template.id, Set.empty).to(LazyList)
|
||||
}
|
||||
}
|
||||
|
||||
def allCommands(types: PackageRegistry): LazyList[Command] = {
|
||||
if (useDatabase) {
|
||||
logErrorAndDefaultTo(db.allCommands(types), List.empty[Command]).to(LazyList)
|
||||
} else {
|
||||
commandById.values.to(LazyList)
|
||||
}
|
||||
}
|
||||
|
||||
def statusOf(commandId: ApiTypes.CommandId, types: PackageRegistry): Option[CommandStatus] = {
|
||||
if (useDatabase) {
|
||||
logErrorAndDefaultTo(db.commandStatusByCommandId(commandId, types), None)
|
||||
} else {
|
||||
statusByCommandId.get(commandId)
|
||||
}
|
||||
}
|
||||
|
||||
def databaseSchema(): Try[String] =
|
||||
if (useDatabase) {
|
||||
db.schema()
|
||||
} else {
|
||||
Failure(NoDatabaseUsed())
|
||||
}
|
||||
|
||||
def runQuery(query: String): Try[SqlQueryResult] =
|
||||
if (useDatabase) {
|
||||
db.runQuery(query)
|
||||
} else {
|
||||
Failure(NoDatabaseUsed())
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
def resultOf(commandId: ApiTypes.CommandId, types: PackageRegistry): Option[Result] =
|
||||
for {
|
||||
status <- statusOf(commandId, types)
|
||||
result <- status match {
|
||||
case _: CommandStatusWaiting => None
|
||||
case cmd: CommandStatusSuccess => Some(Result(commandId, Right(cmd.tx)))
|
||||
case cmd: CommandStatusError =>
|
||||
Some(Result(commandId, Left(new Error(cmd.code, cmd.details, ""))))
|
||||
case _: CommandStatusUnknown =>
|
||||
Some(Result(commandId, Left(new Error("INTERNAL", "Command status unknown", ""))))
|
||||
}
|
||||
} yield result
|
||||
}
|
@ -1,218 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.model
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
import com.daml.lf.data.{Ref => DamlLfRef}
|
||||
import com.daml.ledger.api.refinements.ApiTypes
|
||||
import scalaz.{@@, Tag}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Node
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
/** Identifiable entity. */
|
||||
sealed trait Node[T] {
|
||||
def id: T
|
||||
def idString: String
|
||||
}
|
||||
|
||||
sealed trait TaggedNode[IdTag] extends Node[String @@ IdTag] {
|
||||
def id: String @@ IdTag
|
||||
override def idString: String = Tag.unwrap(id)
|
||||
}
|
||||
|
||||
sealed trait DamlLfNode extends Node[DamlLfIdentifier] {
|
||||
def id: DamlLfIdentifier
|
||||
override def idString: String = id.asOpaqueString
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Commands
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
// Status of a command - waiting, failed, succeeded, or unknown
|
||||
sealed trait CommandStatus {
|
||||
def isCompleted: Boolean
|
||||
}
|
||||
|
||||
// Waiting for command completion
|
||||
final case class CommandStatusWaiting() extends CommandStatus {
|
||||
def isCompleted = false
|
||||
}
|
||||
|
||||
// Completed with an error
|
||||
final case class CommandStatusError(code: String, details: String) extends CommandStatus {
|
||||
def isCompleted = true
|
||||
}
|
||||
|
||||
// Completed successfully
|
||||
final case class CommandStatusSuccess(tx: Transaction) extends CommandStatus {
|
||||
def isCompleted = true
|
||||
}
|
||||
|
||||
// Command status tracking failed, status unknown
|
||||
final case class CommandStatusUnknown() extends CommandStatus {
|
||||
def isCompleted = false
|
||||
}
|
||||
|
||||
sealed trait Command extends TaggedNode[ApiTypes.CommandIdTag] {
|
||||
|
||||
/** Order in which the command was submitted. */
|
||||
def index: Long
|
||||
def workflowId: ApiTypes.WorkflowId
|
||||
def platformTime: Instant
|
||||
}
|
||||
|
||||
final case class CreateCommand(
|
||||
id: ApiTypes.CommandId,
|
||||
index: Long,
|
||||
workflowId: ApiTypes.WorkflowId,
|
||||
platformTime: Instant,
|
||||
template: DamlLfIdentifier,
|
||||
argument: ApiRecord,
|
||||
) extends Command
|
||||
|
||||
/** @param template
|
||||
* The template of the given contract. Not required for the ledger API, but we keep
|
||||
* this denormalized information so that it's easier to serialize/deserialize the
|
||||
* choice argument.
|
||||
*/
|
||||
final case class ExerciseCommand(
|
||||
id: ApiTypes.CommandId,
|
||||
index: Long,
|
||||
workflowId: ApiTypes.WorkflowId,
|
||||
platformTime: Instant,
|
||||
contract: ApiTypes.ContractId,
|
||||
template: DamlLfIdentifier,
|
||||
interfaceId: Option[DamlLfIdentifier],
|
||||
choice: ApiTypes.Choice,
|
||||
argument: ApiValue,
|
||||
) extends Command
|
||||
|
||||
case class Error(code: String, details: String, parameters: String)
|
||||
final case class Result(id: ApiTypes.CommandId, errorOrTx: Either[Error, Transaction])
|
||||
extends TaggedNode[ApiTypes.CommandIdTag]
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Daml Package
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
case class DamlLfPackage(
|
||||
id: DamlLfRef.PackageId,
|
||||
typeDefs: Map[DamlLfIdentifier, DamlLfDefDataType],
|
||||
templates: Map[DamlLfIdentifier, Template],
|
||||
interfaces: Map[DamlLfIdentifier, Interface],
|
||||
)
|
||||
|
||||
/** A boxed DefDataType that also includes the ID of the type.
|
||||
* This is useful for the GraphQL schema.
|
||||
*/
|
||||
final case class DamlLfDefDataTypeBoxed(id: DamlLfIdentifier, value: DamlLfDefDataType)
|
||||
extends DamlLfNode
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Transactions
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
/** Transaction. */
|
||||
final case class Transaction(
|
||||
id: ApiTypes.TransactionId,
|
||||
commandId: Option[ApiTypes.CommandId],
|
||||
effectiveAt: Instant,
|
||||
offset: String,
|
||||
events: List[Event],
|
||||
) extends TaggedNode[ApiTypes.TransactionIdTag]
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Events
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
sealed trait Event extends TaggedNode[ApiTypes.EventIdTag] {
|
||||
def workflowId: ApiTypes.WorkflowId
|
||||
|
||||
/** Id of the parent event in the transaction tree. */
|
||||
def parentId: Option[ApiTypes.EventId]
|
||||
|
||||
/** Id of the transaction tree containing this event. */
|
||||
def transactionId: ApiTypes.TransactionId
|
||||
|
||||
/** Determines which parties are notified of this event. */
|
||||
def witnessParties: List[ApiTypes.Party]
|
||||
}
|
||||
|
||||
final case class ContractCreated(
|
||||
id: ApiTypes.EventId,
|
||||
parentId: Option[ApiTypes.EventId],
|
||||
transactionId: ApiTypes.TransactionId,
|
||||
witnessParties: List[ApiTypes.Party],
|
||||
workflowId: ApiTypes.WorkflowId,
|
||||
contractId: ApiTypes.ContractId,
|
||||
templateId: DamlLfIdentifier,
|
||||
argument: ApiRecord,
|
||||
agreementText: Option[String],
|
||||
signatories: List[ApiTypes.Party],
|
||||
observers: List[ApiTypes.Party],
|
||||
key: Option[ApiValue],
|
||||
) extends Event
|
||||
|
||||
final case class ChoiceExercised(
|
||||
id: ApiTypes.EventId,
|
||||
parentId: Option[ApiTypes.EventId],
|
||||
transactionId: ApiTypes.TransactionId,
|
||||
witnessParties: List[ApiTypes.Party],
|
||||
workflowId: ApiTypes.WorkflowId,
|
||||
contractId: ApiTypes.ContractId,
|
||||
templateId: DamlLfIdentifier,
|
||||
choice: ApiTypes.Choice,
|
||||
argument: ApiValue,
|
||||
actingParties: List[ApiTypes.Party],
|
||||
consuming: Boolean,
|
||||
) extends Event
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Contract
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
/** Active contract. */
|
||||
final case class Contract(
|
||||
id: ApiTypes.ContractId,
|
||||
template: Template,
|
||||
argument: ApiRecord,
|
||||
agreementText: Option[String],
|
||||
signatories: List[ApiTypes.Party],
|
||||
observers: List[ApiTypes.Party],
|
||||
key: Option[ApiValue],
|
||||
) extends TaggedNode[ApiTypes.ContractIdTag]
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Template
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
/** Template for instantiating contracts. */
|
||||
final case class Template(
|
||||
id: DamlLfIdentifier,
|
||||
choices: List[Choice],
|
||||
key: Option[DamlLfType],
|
||||
implementedInterfaces: Set[DamlLfIdentifier],
|
||||
) extends DamlLfNode {
|
||||
def topLevelDecl: String = id.qualifiedName.toString()
|
||||
def parameter: DamlLfTypeCon = DamlLfTypeCon(DamlLfTypeConName(id), DamlLfImmArraySeq())
|
||||
}
|
||||
|
||||
/** Interfaces. */
|
||||
final case class Interface(
|
||||
id: DamlLfIdentifier,
|
||||
choices: List[Choice],
|
||||
) extends DamlLfNode
|
||||
|
||||
/** Template choice. */
|
||||
case class Choice(
|
||||
name: ApiTypes.Choice,
|
||||
parameter: DamlLfType,
|
||||
returnType: DamlLfType,
|
||||
consuming: Boolean,
|
||||
inheritedInterface: Option[DamlLfIdentifier] = None,
|
||||
)
|
@ -1,184 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.model
|
||||
|
||||
import com.daml.navigator.{model => Model}
|
||||
import com.daml.ledger.api.refinements.ApiTypes
|
||||
import com.daml.lf.{typesig => DamlLfIface}
|
||||
import com.daml.lf.data.{Ref => DamlLfRef}
|
||||
|
||||
/** Manages a set of known Daml-LF packages. */
|
||||
case class PackageRegistry(
|
||||
private val packageState: PackageState = PackageState(Map.empty),
|
||||
|
||||
// These are just projections from `packageState` for performance. `packageState` is the source of truth
|
||||
private val packages: Map[DamlLfRef.PackageId, DamlLfPackage] = Map.empty,
|
||||
private val templates: Map[DamlLfIdentifier, Template] = Map.empty,
|
||||
private val typeDefs: Map[DamlLfIdentifier, DamlLfDefDataType] = Map.empty,
|
||||
private val interfaces: Map[DamlLfIdentifier, Interface] = Map.empty,
|
||||
) {
|
||||
// TODO (#13969) ignores inherited choices; interfaces aren't handled at all
|
||||
private[this] def template(
|
||||
packageId: DamlLfRef.PackageId,
|
||||
qname: DamlLfQualifiedName,
|
||||
t: DamlLfIface.DefTemplate[DamlLfIface.Type],
|
||||
): Template = Template(
|
||||
DamlLfIdentifier(packageId, qname),
|
||||
t.tChoices.resolvedChoices.toList.flatMap { case (choiceName, resolvedChoices) =>
|
||||
resolvedChoices.map { case (interfaceIdOption, templateChoice) =>
|
||||
choice(choiceName, templateChoice, interfaceIdOption)
|
||||
}
|
||||
},
|
||||
t.key,
|
||||
t.implementedInterfaces.toSet,
|
||||
)
|
||||
|
||||
private[this] def interface(
|
||||
packageId: DamlLfRef.PackageId,
|
||||
qname: DamlLfQualifiedName,
|
||||
interface: DamlLfIface.DefInterface[DamlLfIface.Type],
|
||||
): Interface = {
|
||||
Interface(
|
||||
DamlLfIdentifier(packageId, qname),
|
||||
interface.choices.toList.map(c => choice(c._1, c._2)),
|
||||
)
|
||||
}
|
||||
|
||||
private[this] def choice(
|
||||
name: String,
|
||||
c: DamlLfIface.TemplateChoice[DamlLfIface.Type],
|
||||
inheritedInterface: Option[DamlLfIdentifier] = None,
|
||||
): Model.Choice = Model.Choice(
|
||||
ApiTypes.Choice(name),
|
||||
c.param,
|
||||
c.returnType,
|
||||
c.consuming,
|
||||
inheritedInterface,
|
||||
)
|
||||
|
||||
def withPackages(interfaces: List[DamlLfIface.PackageSignature]): PackageRegistry = {
|
||||
val newPackageStore = packageState.append(interfaces.map(p => p.packageId -> p).toMap)
|
||||
|
||||
val newPackages = newPackageStore.packages.values.map { p =>
|
||||
val typeDefs = p.typeDecls.map { case (qname, td) =>
|
||||
DamlLfIdentifier(p.packageId, qname) -> td.`type`
|
||||
}
|
||||
val templates = p.typeDecls.collect {
|
||||
case (qname, DamlLfIface.PackageSignature.TypeDecl.Template(r @ _, t)) =>
|
||||
DamlLfIdentifier(p.packageId, qname) -> template(p.packageId, qname, t)
|
||||
}
|
||||
val interfaces = p.interfaces.map { case (qname, defInterface) =>
|
||||
DamlLfIdentifier(p.packageId, qname) -> interface(p.packageId, qname, defInterface)
|
||||
}
|
||||
p.packageId -> DamlLfPackage(p.packageId, typeDefs, templates, interfaces)
|
||||
}.toMap
|
||||
|
||||
copy(
|
||||
packages = newPackages,
|
||||
templates = newPackages.flatMap(_._2.templates),
|
||||
typeDefs = newPackages.flatMap(_._2.typeDefs),
|
||||
interfaces = newPackages.flatMap(_._2.interfaces),
|
||||
)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Packages
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def packageCount: Int =
|
||||
packages.size
|
||||
|
||||
def pack(id: DamlLfRef.PackageId): Option[DamlLfPackage] =
|
||||
packages.get(id)
|
||||
|
||||
def allPackages(): LazyList[DamlLfPackage] =
|
||||
packages.values.to(LazyList)
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Templates
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def template(id: DamlLfIdentifier): Option[Template] =
|
||||
templates.get(id)
|
||||
|
||||
def templateByIdentifier(id: ApiTypes.TemplateId): Option[Template] = {
|
||||
val damlId = ApiTypes.TemplateId.unwrap(id).asDaml
|
||||
templates.get(damlId)
|
||||
}
|
||||
|
||||
def templateByStringId(id: TemplateStringId): Option[Template] = {
|
||||
parseOpaqueIdentifier(TemplateStringId.unwrap(id)).flatMap(lfid => templates.get(lfid))
|
||||
}
|
||||
|
||||
def templateCount: Int =
|
||||
templates.size
|
||||
|
||||
def allTemplates(): LazyList[Template] =
|
||||
templates.values.to(LazyList)
|
||||
|
||||
def templatesByName(topLevelDecl: String): Seq[Template] =
|
||||
templates.toList
|
||||
.filter(t => t._2.topLevelDecl == topLevelDecl)
|
||||
.map(_._2)
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Types
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def damlLfDefDataType(id: DamlLfIdentifier): Option[DamlLfDefDataType] =
|
||||
typeDefs.get(id)
|
||||
|
||||
/** Returns a list of all user defined types required to evaluate the given user defined type.
|
||||
* maxDepth defines the maximum depth of instantiate() calls (i.e., recursive type lookups)
|
||||
*/
|
||||
def typeDependencies(
|
||||
typ: DamlLfDefDataType,
|
||||
maxDepth: Int = Int.MaxValue,
|
||||
): Map[DamlLfIdentifier, DamlLfDefDataType] = {
|
||||
def foldType(
|
||||
typ: DamlLfType,
|
||||
deps: Map[DamlLfIdentifier, DamlLfDefDataType],
|
||||
instantiatesRemaining: Int,
|
||||
): Map[DamlLfIdentifier, DamlLfDefDataType] = {
|
||||
typ match {
|
||||
case DamlLfTypeVar(_) | DamlLfTypeNumeric(_) => deps
|
||||
case DamlLfTypePrim(_, vars) =>
|
||||
vars.foldLeft(deps)((r, v) => foldType(v, r, instantiatesRemaining))
|
||||
case DamlLfTypeCon(name, vars) =>
|
||||
deps.get(name.identifier) match {
|
||||
// Dependency already added
|
||||
case Some(_) => deps
|
||||
// New dependency
|
||||
case None =>
|
||||
if (instantiatesRemaining > 0) {
|
||||
damlLfDefDataType(name.identifier).fold(deps)(ddt => {
|
||||
val r1 = deps + (name.identifier -> ddt)
|
||||
val r2 = foldDataType(ddt, r1, instantiatesRemaining - 1)
|
||||
vars.foldLeft(r2)((r, v) => foldType(v, r, instantiatesRemaining - 1))
|
||||
})
|
||||
} else {
|
||||
deps
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def foldDataType(
|
||||
ddt: DamlLfDefDataType,
|
||||
deps: Map[DamlLfIdentifier, DamlLfDefDataType],
|
||||
instantiatesRemaining: Int,
|
||||
): Map[DamlLfIdentifier, DamlLfDefDataType] = {
|
||||
ddt.dataType match {
|
||||
case DamlLfRecord(fields) =>
|
||||
fields.foldLeft(deps)((r, field) => foldType(field._2, r, instantiatesRemaining))
|
||||
case DamlLfVariant(fields) =>
|
||||
fields.foldLeft(deps)((r, field) => foldType(field._2, r, instantiatesRemaining))
|
||||
case DamlLfEnum(_) =>
|
||||
deps
|
||||
}
|
||||
}
|
||||
|
||||
foldDataType(typ, Map.empty, maxDepth)
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.model
|
||||
|
||||
import com.daml.lf.data.Ref
|
||||
import com.daml.lf.typesig
|
||||
|
||||
import scala.collection.immutable.Map
|
||||
|
||||
final case class PackageState(packages: PackageState.PackageStore) {
|
||||
import PackageState.PackageStore
|
||||
def append(diff: PackageStore): PackageState = {
|
||||
val newPackages = resolveChoicesIn(
|
||||
appendAndResolveRetroactiveInterfaces(diff)
|
||||
)
|
||||
copy(packages = newPackages)
|
||||
}
|
||||
|
||||
private[this] def appendAndResolveRetroactiveInterfaces(diff: PackageStore): PackageStore = {
|
||||
def lookupIf(packageStore: PackageStore, pkId: Ref.PackageId) =
|
||||
packageStore
|
||||
.get(pkId)
|
||||
.map((_, { newSig: typesig.PackageSignature => packageStore.updated(pkId, newSig) }))
|
||||
|
||||
val (packageStore2, diffElems) =
|
||||
typesig.PackageSignature.resolveRetroImplements(packages, diff.values.toSeq)(lookupIf)
|
||||
packageStore2 ++ diffElems.view.map(p => (p.packageId, p))
|
||||
}
|
||||
|
||||
private[this] def resolveChoicesIn(diff: PackageStore): PackageStore = {
|
||||
def lookupIf(pkgId: Ref.PackageId) = (packages get pkgId) orElse (diff get pkgId)
|
||||
val findIface = typesig.PackageSignature.findInterface(Function unlift lookupIf)
|
||||
diff.transform((_, iface) => iface resolveChoicesAndFailOnUnresolvableChoices findIface)
|
||||
}
|
||||
}
|
||||
|
||||
object PackageState {
|
||||
type PackageStore = Map[String, typesig.PackageSignature]
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.model
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import com.daml.lf.{typesig => DamlLfIface}
|
||||
import com.daml.ledger.api.refinements.ApiTypes
|
||||
|
||||
import scala.collection.immutable.LazyList
|
||||
import scalaz.Tag
|
||||
|
||||
import java.net.URLEncoder
|
||||
|
||||
case class State(ledger: Ledger, packageRegistry: PackageRegistry)
|
||||
|
||||
/** A DA party and its ledger view(s). */
|
||||
class PartyState(
|
||||
val name: ApiTypes.Party,
|
||||
// A named role specified in our config. The role is an arbitrary string chosen by the user. It gets passed to the frontend, which can display a custom theme or custom views depending on the role.
|
||||
val userRole: Option[String] = None,
|
||||
val useDatabase: Boolean = false,
|
||||
) {
|
||||
def actorName: String =
|
||||
"party-" + URLEncoder.encode(ApiTypes.Party.unwrap(name), "UTF-8")
|
||||
|
||||
private val stateRef: AtomicReference[State] = new AtomicReference(
|
||||
State(Ledger(name, None, useDatabase), new PackageRegistry)
|
||||
)
|
||||
|
||||
def ledger: Ledger = stateRef.get.ledger
|
||||
def packageRegistry: PackageRegistry = stateRef.get.packageRegistry
|
||||
|
||||
def addLatestTransaction(tx: Transaction): Unit = {
|
||||
stateRef.updateAndGet(state =>
|
||||
state.copy(ledger = state.ledger.withTransaction(tx, packageRegistry))
|
||||
)
|
||||
()
|
||||
}
|
||||
|
||||
def addCommand(cmd: Command): Unit = {
|
||||
stateRef.updateAndGet(state => state.copy(ledger = state.ledger.withCommand(cmd)))
|
||||
()
|
||||
}
|
||||
|
||||
def addCommandStatus(id: ApiTypes.CommandId, status: CommandStatus): Unit = {
|
||||
stateRef.updateAndGet(state => state.copy(ledger = state.ledger.withCommandStatus(id, status)))
|
||||
()
|
||||
}
|
||||
|
||||
def addPackages(packs: List[DamlLfIface.PackageSignature]): Unit = {
|
||||
stateRef.updateAndGet(state =>
|
||||
state.copy(packageRegistry = packageRegistry.withPackages(packs))
|
||||
)
|
||||
()
|
||||
}
|
||||
|
||||
override def hashCode(): Int = Tag.unwrap(name).hashCode()
|
||||
|
||||
override def equals(obj: scala.Any): Boolean = obj match {
|
||||
case that: PartyState => Tag.unwrap(this.name) equals Tag.unwrap(that.name)
|
||||
case _ => false
|
||||
}
|
||||
|
||||
override def toString: String = "Party(" + name.toString + ")"
|
||||
|
||||
def contracts(): LazyList[Contract] = this.ledger.allContracts(this.packageRegistry)
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.model
|
||||
|
||||
final case class SqlQueryResult(columnNames: List[String], rows: List[List[String]])
|
@ -1,23 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.model.converter
|
||||
|
||||
import com.daml.navigator.model.DamlLfIdentifier
|
||||
|
||||
sealed trait ConversionError extends Throwable
|
||||
|
||||
/** Conversion failed because a type is missing. */
|
||||
final case class TypeNotFoundError(id: DamlLfIdentifier) extends ConversionError {
|
||||
override def getMessage: String = s"Type $id not found"
|
||||
}
|
||||
|
||||
/** A required field is missing. */
|
||||
final case class RequiredFieldDoesNotExistError(name: String) extends ConversionError {
|
||||
override def getMessage: String = s"Required field $name is missing"
|
||||
}
|
||||
|
||||
/** Generic conversion error, with a human readable error string. */
|
||||
final case class GenericConversionError(error: String) extends ConversionError {
|
||||
override def getMessage: String = error
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.model.converter
|
||||
|
||||
case object Converter {
|
||||
|
||||
/** Returns the sequence of values, or the last error encountered */
|
||||
def sequence[E, T](xs: Seq[Either[E, T]]): Either[E, List[T]] =
|
||||
xs.foldRight(Right(Nil): Either[E, List[T]]) { (e, acc) =>
|
||||
for (xs <- acc; x <- e) yield x :: xs
|
||||
}
|
||||
|
||||
/** Returns the value of a required protobuf3 field, or RequiredFieldDoesNotExistError if it doesn't exist. */
|
||||
def checkExists[T](
|
||||
fieldName: String,
|
||||
maybeElement: Option[T],
|
||||
): Either[ConversionError, T] =
|
||||
maybeElement match {
|
||||
case Some(element) => Right(element)
|
||||
case None => Left(RequiredFieldDoesNotExistError(fieldName))
|
||||
}
|
||||
|
||||
def checkExists[T](
|
||||
maybeElement: Option[T],
|
||||
error: ConversionError,
|
||||
): Either[ConversionError, T] =
|
||||
maybeElement match {
|
||||
case Some(element) => Right(element)
|
||||
case None => Left(error)
|
||||
}
|
||||
}
|
@ -1,529 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.model.converter
|
||||
|
||||
import java.time.Instant
|
||||
import com.daml.lf.data.Ref
|
||||
import com.daml.lf.typesig
|
||||
import com.daml.lf.value.{Value => V}
|
||||
import com.daml.ledger.api.{v1 => V1}
|
||||
import com.daml.ledger.api.refinements.ApiTypes
|
||||
import com.daml.ledger.api.validation.NoLoggingValueValidator.{validateRecord, validateValue}
|
||||
import com.daml.navigator.{model => Model}
|
||||
import com.daml.navigator.model.{
|
||||
DamlLfIdentifier,
|
||||
IdentifierApiConversions,
|
||||
IdentifierDamlConversions,
|
||||
}
|
||||
import com.daml.platform.participant.util.LfEngineToApi.{lfValueToApiRecord, lfValueToApiValue}
|
||||
import scalaz.Tag
|
||||
import scalaz.syntax.bifunctor._
|
||||
import scalaz.syntax.traverse._
|
||||
import scalaz.syntax.std.option._
|
||||
import scalaz.std.either._
|
||||
import scalaz.std.option._
|
||||
|
||||
case object LedgerApiV1 {
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Types
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
case class Context(party: ApiTypes.Party, templates: Model.PackageRegistry)
|
||||
|
||||
private type Result[X] = Either[ConversionError, X]
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Read methods (V1 -> Model)
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
/*
|
||||
def readTransaction(tx: V1.transaction.Transaction, ctx: Context): Result[Model.Transaction] = {
|
||||
for {
|
||||
events <- Converter.sequence(tx.events.map(ev =>
|
||||
readEvent(ev, ApiTypes.TransactionId(tx.transactionId), ctx, List.empty, ApiTypes.WorkflowId(tx.workflowId), None)))
|
||||
effectiveAt <- Converter.checkExists("Transaction.effectiveAt", tx.effectiveAt)
|
||||
offset <- readLedgerOffset(tx.offset)
|
||||
} yield {
|
||||
Model.Transaction(
|
||||
id = ApiTypes.TransactionId(tx.transactionId),
|
||||
commandId = if (tx.commandId.isEmpty) None else Some(ApiTypes.CommandId(tx.commandId)),
|
||||
effectiveAt = Instant.ofEpochSecond(effectiveAt.seconds, effectiveAt.nanos),
|
||||
offset = offset,
|
||||
events = events
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def readEvent(
|
||||
event: V1.event.Event,
|
||||
transactionId: ApiTypes.TransactionId,
|
||||
ctx: Context,
|
||||
parentWitnessParties: List[ApiTypes.Party],
|
||||
workflowId: ApiTypes.WorkflowId,
|
||||
parentId: Option[ApiTypes.EventId] = None
|
||||
): Result[Model.Event] = {
|
||||
event match {
|
||||
case V1.event.Event(V1.event.Event.Event.Created(ev)) =>
|
||||
readEventCreated(ev, transactionId, parentWitnessParties, workflowId, parentId, ctx)
|
||||
|
||||
case V1.event.Event(V1.event.Event.Event.Exercised(ev)) =>
|
||||
// This case should be removed from the protobuf, Transactions never contain Exercised events
|
||||
Left(GenericConversionError("Exercised event found in GetTransactions"))
|
||||
|
||||
case V1.event.Event(V1.event.Event.Event.Archived(ev)) =>
|
||||
readEventArchived(ev, transactionId, parentWitnessParties, workflowId, parentId, ctx)
|
||||
|
||||
case V1.event.Event(V1.event.Event.Event.Empty) =>
|
||||
Left(RequiredFieldDoesNotExistError("Event.value"))
|
||||
}
|
||||
}
|
||||
|
||||
private def readEventArchived(
|
||||
event: V1.event.ArchivedEvent,
|
||||
transactionId: ApiTypes.TransactionId,
|
||||
parentWitnessParties: List[ApiTypes.Party],
|
||||
workflowId: ApiTypes.WorkflowId,
|
||||
parentId: Option[ApiTypes.EventId],
|
||||
ctx: Context
|
||||
): Result[Model.Event] = {
|
||||
val witnessParties = parentWitnessParties ++ ApiTypes.Party.subst(event.witnessParties)
|
||||
Right(
|
||||
Model.ContractArchived(
|
||||
id = ApiTypes.EventId(event.eventId),
|
||||
parentId = parentId,
|
||||
transactionId = transactionId,
|
||||
witnessParties = witnessParties,
|
||||
workflowId = workflowId,
|
||||
contractId = ApiTypes.ContractId(event.contractId)
|
||||
)
|
||||
)
|
||||
}
|
||||
*/
|
||||
|
||||
def readTransactionTree(
|
||||
tx: V1.transaction.TransactionTree,
|
||||
ctx: Context,
|
||||
): Result[Model.Transaction] = {
|
||||
for {
|
||||
events <- Converter
|
||||
.sequence(
|
||||
tx.rootEventIds
|
||||
.map(evid =>
|
||||
readTreeEvent(
|
||||
tx.eventsById(evid),
|
||||
ApiTypes.TransactionId(tx.transactionId),
|
||||
tx.eventsById,
|
||||
ctx,
|
||||
ApiTypes.WorkflowId(tx.workflowId),
|
||||
None,
|
||||
)
|
||||
)
|
||||
)
|
||||
.map(_.flatten)
|
||||
effectiveAt <- Converter.checkExists("Transaction.effectiveAt", tx.effectiveAt)
|
||||
offset <- readLedgerOffset(tx.offset)
|
||||
} yield {
|
||||
Model.Transaction(
|
||||
id = ApiTypes.TransactionId(tx.transactionId),
|
||||
commandId = if (tx.commandId.isEmpty) None else Some(ApiTypes.CommandId(tx.commandId)),
|
||||
effectiveAt = Instant.ofEpochSecond(effectiveAt.seconds, effectiveAt.nanos.toLong),
|
||||
offset = offset,
|
||||
events = events,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def readTreeEvent(
|
||||
event: V1.transaction.TreeEvent,
|
||||
transactionId: ApiTypes.TransactionId,
|
||||
eventsById: Map[String, V1.transaction.TreeEvent],
|
||||
ctx: Context,
|
||||
workflowId: ApiTypes.WorkflowId,
|
||||
parentId: Option[ApiTypes.EventId],
|
||||
): Result[List[Model.Event]] = {
|
||||
event match {
|
||||
case V1.transaction.TreeEvent(V1.transaction.TreeEvent.Kind.Created(ev)) =>
|
||||
readEventCreated(ev, transactionId, workflowId, parentId, ctx).map(List(_))
|
||||
|
||||
case V1.transaction.TreeEvent(V1.transaction.TreeEvent.Kind.Exercised(ev)) =>
|
||||
readEventExercised(ev, transactionId, eventsById, workflowId, parentId, ctx)
|
||||
|
||||
case V1.transaction.TreeEvent(V1.transaction.TreeEvent.Kind.Empty) =>
|
||||
Left(RequiredFieldDoesNotExistError("TreeEvent.value"))
|
||||
}
|
||||
}
|
||||
|
||||
private def getTemplate(
|
||||
id: Model.DamlLfIdentifier,
|
||||
ctx: Context,
|
||||
): Result[Model.Template] =
|
||||
ctx.templates
|
||||
.template(id)
|
||||
.map(Right(_))
|
||||
.getOrElse(Left(TypeNotFoundError(id)))
|
||||
|
||||
private def readLedgerOffset(offset: String): Result[String] = {
|
||||
// Ledger offset may change to become a number in the future
|
||||
// Try(BigInt(offset)).toEither
|
||||
// .left.map(t => GenericConversionError(s"Could not parse ledger offset '$offset'"))
|
||||
Right(offset)
|
||||
}
|
||||
|
||||
private def readEventCreated(
|
||||
event: V1.event.CreatedEvent,
|
||||
transactionId: ApiTypes.TransactionId,
|
||||
workflowId: ApiTypes.WorkflowId,
|
||||
parentId: Option[ApiTypes.EventId],
|
||||
ctx: Context,
|
||||
): Result[Model.Event] = {
|
||||
val witnessParties = ApiTypes.Party.subst(event.witnessParties.toList)
|
||||
val signatories = ApiTypes.Party.subst(event.signatories.toList)
|
||||
val observers = ApiTypes.Party.subst(event.observers.toList)
|
||||
for {
|
||||
templateId <- Converter.checkExists("CreatedEvent.templateId", event.templateId)
|
||||
templateIdentifier = templateId.asDaml
|
||||
template <- getTemplate(templateIdentifier, ctx)
|
||||
arguments <- Converter.checkExists("CreatedEvent.arguments", event.createArguments)
|
||||
arg <- readRecordArgument(arguments, templateIdentifier, ctx)
|
||||
keyResult = event.contractKey
|
||||
.traverse(k => readArgument(k, template.key.get, ctx))
|
||||
key <- keyResult
|
||||
} yield Model.ContractCreated(
|
||||
id = ApiTypes.EventId(event.eventId),
|
||||
parentId = parentId,
|
||||
transactionId = transactionId,
|
||||
witnessParties = witnessParties,
|
||||
workflowId = workflowId,
|
||||
contractId = ApiTypes.ContractId(event.contractId),
|
||||
templateId = templateIdentifier,
|
||||
argument = arg,
|
||||
agreementText = event.agreementText,
|
||||
signatories = signatories,
|
||||
observers = observers,
|
||||
key = key,
|
||||
)
|
||||
}
|
||||
|
||||
private def readEventExercised(
|
||||
event: V1.event.ExercisedEvent,
|
||||
transactionId: ApiTypes.TransactionId,
|
||||
eventsById: Map[String, V1.transaction.TreeEvent],
|
||||
workflowId: ApiTypes.WorkflowId,
|
||||
parentId: Option[ApiTypes.EventId],
|
||||
ctx: Context,
|
||||
): Result[List[Model.Event]] = {
|
||||
val witnessParties = ApiTypes.Party.subst(event.witnessParties.toList)
|
||||
for {
|
||||
templateId <- Converter.checkExists("ExercisedEvent.templateId", event.templateId)
|
||||
templateIdentifier = templateId.asDaml
|
||||
template <- getTemplate(templateId.asDaml, ctx)
|
||||
argument <- Converter.checkExists("ExercisedEvent.arguments", event.choiceArgument)
|
||||
choice <- Converter.checkExists(
|
||||
template.choices.find(c => ApiTypes.Choice.unwrap(c.name) == event.choice),
|
||||
GenericConversionError(s"Choice '${event.choice}' not found"),
|
||||
)
|
||||
modelArgument <- readArgument(argument, choice.parameter, ctx)
|
||||
children <- Converter
|
||||
.sequence(
|
||||
event.childEventIds
|
||||
.map(childId =>
|
||||
readTreeEvent(
|
||||
eventsById(childId),
|
||||
transactionId,
|
||||
eventsById,
|
||||
ctx,
|
||||
workflowId,
|
||||
Some(ApiTypes.EventId(event.eventId)),
|
||||
)
|
||||
)
|
||||
)
|
||||
.map(_.flatten)
|
||||
} yield Model.ChoiceExercised(
|
||||
id = ApiTypes.EventId(event.eventId),
|
||||
parentId = parentId,
|
||||
transactionId = transactionId,
|
||||
witnessParties = witnessParties,
|
||||
workflowId = workflowId,
|
||||
contractId = ApiTypes.ContractId(event.contractId),
|
||||
templateId = templateIdentifier,
|
||||
choice = ApiTypes.Choice(event.choice),
|
||||
argument = modelArgument,
|
||||
consuming = event.consuming,
|
||||
actingParties = event.actingParties.map(ApiTypes.Party(_)).toList,
|
||||
) :: children
|
||||
}
|
||||
|
||||
private def readRecordArgument(
|
||||
value: V1.value.Record,
|
||||
typId: Model.DamlLfIdentifier,
|
||||
ctx: Context,
|
||||
): Result[Model.ApiRecord] =
|
||||
for {
|
||||
lfr <- validateRecord(value).leftMap(sre => GenericConversionError(sre.getMessage))
|
||||
cidMapped <- lfr match {
|
||||
case r: Model.ApiRecord => Right(r)
|
||||
case v => Left(GenericConversionError(s"validating record produced non-record $v"))
|
||||
}
|
||||
filled <- fillInRecordTI(
|
||||
cidMapped,
|
||||
Model.DamlLfTypeCon(Model.DamlLfTypeConName(typId), Model.DamlLfImmArraySeq()),
|
||||
ctx,
|
||||
)
|
||||
} yield filled
|
||||
|
||||
private def fillInRecordTI(
|
||||
value: Model.ApiRecord,
|
||||
typ: Model.DamlLfType,
|
||||
ctx: Context,
|
||||
): Result[Model.ApiRecord] =
|
||||
for {
|
||||
typeCon <- asTypeCon(typ, value)
|
||||
ddt <- ctx.templates
|
||||
.damlLfDefDataType(typeCon.name.identifier)
|
||||
.toRight(GenericConversionError(s"Unknown type ${typeCon.name.identifier}"))
|
||||
dt <- typeCon.instantiate(ddt) match {
|
||||
case r @ typesig.Record(_) => Right(r)
|
||||
case typesig.Variant(_) | typesig.Enum(_) =>
|
||||
Left(GenericConversionError(s"Record expected"))
|
||||
}
|
||||
fields <- value.fields zip dt.fields.toImmArray traverseU {
|
||||
case ((von, vv), (tn, fieldType)) =>
|
||||
for {
|
||||
_ <- von.cata(
|
||||
vn =>
|
||||
Either.cond(
|
||||
(vn: String) == (tn: String),
|
||||
(),
|
||||
GenericConversionError(s"field order mismatch: expected $tn, got $vn"),
|
||||
),
|
||||
Right(()),
|
||||
)
|
||||
newVv <- fillInTypeInfo(vv, fieldType, ctx)
|
||||
} yield (Some(tn), newVv)
|
||||
}
|
||||
} yield V.ValueRecord(Some(typeCon.name.identifier), fields)
|
||||
|
||||
private def fillInListTI(
|
||||
list: Model.ApiList,
|
||||
typ: Model.DamlLfType,
|
||||
ctx: Context,
|
||||
): Result[Model.ApiList] =
|
||||
for {
|
||||
elementType <- typ match {
|
||||
case Model.DamlLfTypePrim(Model.DamlLfPrimType.List, Seq(t)) =>
|
||||
Right(t)
|
||||
case _ => Left(GenericConversionError(s"Cannot read $list as $typ"))
|
||||
}
|
||||
values <- list.values traverse (fillInTypeInfo(_, elementType, ctx))
|
||||
} yield V.ValueList(values)
|
||||
|
||||
private def fillInTextMapTI(
|
||||
textMap: Model.ApiMap,
|
||||
typ: Model.DamlLfType,
|
||||
ctx: Context,
|
||||
): Result[Model.ApiMap] =
|
||||
for {
|
||||
elementType <- typ match {
|
||||
case Model.DamlLfTypePrim(Model.DamlLfPrimType.TextMap, Seq(t)) =>
|
||||
Right(t)
|
||||
case _ => Left(GenericConversionError(s"Cannot read $textMap as $typ"))
|
||||
}
|
||||
values <- textMap.value traverse (fillInTypeInfo(_, elementType, ctx))
|
||||
} yield V.ValueTextMap(values)
|
||||
|
||||
private def fillInGenMapTI(
|
||||
genMap: Model.ApiGenMap,
|
||||
typ: Model.DamlLfType,
|
||||
ctx: Context,
|
||||
): Result[Model.ApiGenMap] =
|
||||
for {
|
||||
types <- typ match {
|
||||
case Model.DamlLfTypePrim(Model.DamlLfPrimType.GenMap, Seq(kT, vT)) =>
|
||||
Right((kT, vT))
|
||||
case _ => Left(GenericConversionError(s"Cannot read $genMap as $typ"))
|
||||
}
|
||||
(keyType, valueType) = types
|
||||
values <- genMap.entries.toSeq traverse { case (k, v) =>
|
||||
for {
|
||||
key <- fillInTypeInfo(k, keyType, ctx)
|
||||
value <- fillInTypeInfo(v, valueType, ctx)
|
||||
} yield key -> value
|
||||
}
|
||||
} yield V.ValueGenMap(values.toImmArray)
|
||||
|
||||
private def fillInOptionalTI(
|
||||
opt: Model.ApiOptional,
|
||||
typ: Model.DamlLfType,
|
||||
ctx: Context,
|
||||
): Result[Model.ApiOptional] =
|
||||
for {
|
||||
optType <- typ match {
|
||||
case Model.DamlLfTypePrim(Model.DamlLfPrimType.Optional, Seq(t)) =>
|
||||
Right(t)
|
||||
case _ => Left(GenericConversionError(s"Cannot read $opt as $typ"))
|
||||
}
|
||||
value <- opt.value traverse (fillInTypeInfo(_, optType, ctx))
|
||||
} yield V.ValueOptional(value)
|
||||
|
||||
private def asTypeCon(
|
||||
typ: Model.DamlLfType,
|
||||
selector: Model.ApiValue,
|
||||
): Result[Model.DamlLfTypeCon] =
|
||||
typ match {
|
||||
case t @ Model.DamlLfTypeCon(_, _) => Right(t)
|
||||
case _ => Left(GenericConversionError(s"Cannot read $selector as $typ"))
|
||||
}
|
||||
|
||||
private def fillInVariantTI(
|
||||
variant: Model.ApiVariant,
|
||||
typ: Model.DamlLfType,
|
||||
ctx: Context,
|
||||
): Result[Model.ApiVariant] =
|
||||
for {
|
||||
typeCon <- asTypeCon(typ, variant)
|
||||
ddt <- ctx.templates
|
||||
.damlLfDefDataType(typeCon.name.identifier)
|
||||
.toRight(GenericConversionError(s"Unknown type ${typeCon.name.identifier}"))
|
||||
dt <- typeCon.instantiate(ddt) match {
|
||||
case v @ typesig.Variant(_) => Right(v)
|
||||
case typesig.Record(_) | typesig.Enum(_) =>
|
||||
Left(GenericConversionError(s"Variant expected"))
|
||||
}
|
||||
constructor = variant.variant
|
||||
choice <- dt.fields
|
||||
.collectFirst { case (`constructor`, cargTyp) => cargTyp }
|
||||
.toRight(GenericConversionError(s"Unknown enum constructor $constructor"))
|
||||
value = variant.value
|
||||
argument <- fillInTypeInfo(value, choice, ctx)
|
||||
} yield variant.copy(tycon = Some(typeCon.name.identifier), value = argument)
|
||||
|
||||
private def fillInEnumTI(
|
||||
enumeration: V.ValueEnum,
|
||||
typ: Model.DamlLfType,
|
||||
): Result[V.ValueEnum] =
|
||||
for {
|
||||
typeCon <- asTypeCon(typ, enumeration)
|
||||
} yield enumeration.copy(tycon = Some(typeCon.name.identifier))
|
||||
|
||||
private def readArgument(
|
||||
value: V1.value.Value,
|
||||
typ: Model.DamlLfType,
|
||||
ctx: Context,
|
||||
): Result[Model.ApiValue] =
|
||||
validateValue(value)
|
||||
.leftMap(sre => GenericConversionError(sre.getMessage))
|
||||
.flatMap(vv => fillInTypeInfo(vv, typ, ctx))
|
||||
|
||||
/** Add `tycon`s and record field names where absent. */
|
||||
private def fillInTypeInfo(
|
||||
value: Model.ApiValue,
|
||||
typ: Model.DamlLfType,
|
||||
ctx: Context,
|
||||
): Result[Model.ApiValue] =
|
||||
value match {
|
||||
case v: V.ValueEnum => fillInEnumTI(v, typ)
|
||||
case _: V.ValueCidlessLeaf | _: V.ValueContractId => Right(value)
|
||||
case v: Model.ApiOptional => fillInOptionalTI(v, typ, ctx)
|
||||
case v: Model.ApiMap => fillInTextMapTI(v, typ, ctx)
|
||||
case v: Model.ApiGenMap => fillInGenMapTI(v, typ, ctx)
|
||||
case v: Model.ApiList => fillInListTI(v, typ, ctx)
|
||||
case v: Model.ApiRecord => fillInRecordTI(v, typ, ctx)
|
||||
case v: Model.ApiVariant => fillInVariantTI(v, typ, ctx)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Write methods (Model -> V1)
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def writeArgument(value: Model.ApiValue): Result[V1.value.Value] =
|
||||
lfValueToApiValue(verbose = true, value) leftMap GenericConversionError
|
||||
|
||||
def writeRecordArgument(value: Model.ApiRecord): Result[V1.value.Record] =
|
||||
lfValueToApiRecord(verbose = true, value) leftMap GenericConversionError
|
||||
|
||||
/** Write a composite command consisting of just the given command */
|
||||
def writeCommands(
|
||||
party: Model.PartyState,
|
||||
command: Model.Command,
|
||||
ledgerId: String,
|
||||
applicationId: Ref.LedgerString,
|
||||
): Result[V1.commands.Commands] = {
|
||||
for {
|
||||
ledgerCommand <- writeCommand(party, command)
|
||||
} yield {
|
||||
V1.commands.Commands(
|
||||
ledgerId,
|
||||
Tag.unwrap(command.workflowId),
|
||||
applicationId,
|
||||
Tag.unwrap(command.id),
|
||||
Tag.unwrap(party.name),
|
||||
List(ledgerCommand),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def writeCommand(
|
||||
party: Model.PartyState,
|
||||
command: Model.Command,
|
||||
): Result[V1.commands.Command] = {
|
||||
command match {
|
||||
case cmd: Model.CreateCommand =>
|
||||
writeCreateContract(party, cmd.template, cmd.argument)
|
||||
case cmd: Model.ExerciseCommand =>
|
||||
writeExerciseChoice(party, cmd.contract, cmd.interfaceId, cmd.choice, cmd.argument)
|
||||
}
|
||||
}
|
||||
|
||||
def writeCreateContract(
|
||||
party: Model.PartyState,
|
||||
templateId: Model.DamlLfIdentifier,
|
||||
value: Model.ApiRecord,
|
||||
): Result[V1.commands.Command] = {
|
||||
for {
|
||||
template <- Converter.checkExists(
|
||||
party.packageRegistry.template(templateId),
|
||||
GenericConversionError(s"Template '$templateId' not found"),
|
||||
)
|
||||
argument <- writeRecordArgument(value)
|
||||
} yield {
|
||||
V1.commands.Command(
|
||||
V1.commands.Command.Command.Create(
|
||||
V1.commands.CreateCommand(
|
||||
Some(template.id.asApi),
|
||||
Some(argument),
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def writeExerciseChoice(
|
||||
party: Model.PartyState,
|
||||
contractId: ApiTypes.ContractId,
|
||||
interfaceId: Option[DamlLfIdentifier],
|
||||
choiceId: ApiTypes.Choice,
|
||||
value: Model.ApiValue,
|
||||
): Result[V1.commands.Command] = {
|
||||
for {
|
||||
contract <- Converter.checkExists(
|
||||
party.ledger.contract(contractId, party.packageRegistry),
|
||||
GenericConversionError(s"Contract '${Tag.unwrap(contractId)}' not found"),
|
||||
)
|
||||
_ <- Converter.checkExists(
|
||||
contract.template.choices.find(c => c.name == choiceId),
|
||||
GenericConversionError(s"Choice '${Tag.unwrap(choiceId)}' not found"),
|
||||
)
|
||||
argument <- writeArgument(value)
|
||||
} yield {
|
||||
V1.commands.Command(
|
||||
V1.commands.Command.Command.Exercise(
|
||||
V1.commands.ExerciseCommand(
|
||||
interfaceId.orElse(Some(contract.template.id)).map(_.asApi),
|
||||
Tag.unwrap(contractId),
|
||||
Tag.unwrap(choiceId),
|
||||
Some(argument),
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator
|
||||
|
||||
import scalaz.{@@, Tag}
|
||||
import com.daml.lf.{data => DamlLfData}
|
||||
import com.daml.lf.data.{Ref => DamlLfRef}
|
||||
import com.daml.lf.{typesig => DamlLfIface}
|
||||
import com.daml.lf.value.json.NavigatorModelAliases
|
||||
import com.daml.ledger.api.{v1 => ApiV1}
|
||||
import com.daml.ledger.api.refinements.ApiTypes
|
||||
|
||||
package object model extends NavigatorModelAliases[String] {
|
||||
|
||||
/** An opaque identifier used for templates.
|
||||
* Templates are usually identified using a composite type (see [[DamlLfIdentifier]]).
|
||||
*/
|
||||
type TemplateStringId = String @@ TemplateStringIdTag
|
||||
val TemplateStringId = Tag.of[TemplateStringIdTag]
|
||||
|
||||
type InterfaceStringId = String @@ InterfaceStringIdTag
|
||||
val InterfaceStringId = Tag.of[InterfaceStringIdTag]
|
||||
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// Types used in the ledger API
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
|
||||
type EventId = ApiTypes.EventId
|
||||
type ContractId = ApiTypes.ContractId
|
||||
type TemplateId = ApiTypes.TemplateId
|
||||
type Party = DamlLfRef.Party
|
||||
type CommandId = ApiTypes.CommandId
|
||||
type WorkflowId = ApiTypes.WorkflowId
|
||||
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// Types used in Daml-LF
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
|
||||
/** A dot-separated list of strings */
|
||||
type DamlLfDottedName = DamlLfRef.DottedName
|
||||
val DamlLfDottedName = DamlLfRef.DottedName
|
||||
|
||||
/** A qualified name, referencing entities from the same Daml-LF package */
|
||||
type DamlLfQualifiedName = DamlLfRef.QualifiedName
|
||||
val DamlLfQualifiedName = DamlLfRef.QualifiedName
|
||||
|
||||
type DamlLfTypeConNameOrPrimType = DamlLfIface.TypeConNameOrPrimType
|
||||
|
||||
type DamlLfImmArraySeq[T] = DamlLfData.ImmArray.ImmArraySeq[T]
|
||||
val DamlLfImmArraySeq = DamlLfData.ImmArray.ImmArraySeq
|
||||
|
||||
type DamlLfImmArray[T] = DamlLfData.ImmArray[T]
|
||||
val DamlLfImmArray = DamlLfData.ImmArray
|
||||
|
||||
type DamlLfFieldWithType = DamlLfIface.FieldWithType
|
||||
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// Conversion between API Identifier, Daml-LF Identifier, and String
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
implicit class IdentifierApiConversions(val id: ApiV1.value.Identifier) extends AnyVal {
|
||||
def asDaml: DamlLfRef.Identifier =
|
||||
DamlLfRef.Identifier(
|
||||
DamlLfRef.PackageId.assertFromString(id.packageId),
|
||||
DamlLfRef.QualifiedName(
|
||||
DamlLfRef.DottedName.assertFromString(id.moduleName),
|
||||
DamlLfRef.DottedName.assertFromString(id.entityName),
|
||||
),
|
||||
)
|
||||
|
||||
/** An opaque unique string for this identifier */
|
||||
def asOpaqueString: String = id.asDaml.asOpaqueString
|
||||
}
|
||||
|
||||
implicit class IdentifierDamlConversions(val id: DamlLfRef.Identifier) extends AnyVal {
|
||||
def asApi: ApiV1.value.Identifier =
|
||||
ApiV1.value.Identifier(
|
||||
id.packageId,
|
||||
id.qualifiedName.module.toString(),
|
||||
id.qualifiedName.name.toString(),
|
||||
)
|
||||
|
||||
/** An opaque unique string for this identifier */
|
||||
def asOpaqueString: String =
|
||||
opaqueIdentifier(id.qualifiedName.toString, id.packageId)
|
||||
}
|
||||
|
||||
private[this] def opaqueIdentifier(qualifiedName: String, packageId: String): String =
|
||||
s"$qualifiedName@$packageId"
|
||||
|
||||
private[this] val opaqueIdentifierRegex = "([^@]*)@([^@]*)".r
|
||||
def parseOpaqueIdentifier(id: String): Option[DamlLfRef.Identifier] = {
|
||||
id match {
|
||||
case opaqueIdentifierRegex(qualifiedName, packageId) =>
|
||||
Some(
|
||||
DamlLfRef.Identifier(
|
||||
DamlLfRef.PackageId.assertFromString(packageId),
|
||||
DamlLfRef.QualifiedName.assertFromString(qualifiedName),
|
||||
)
|
||||
)
|
||||
case _ =>
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
def parseTemplateOpaqueIdentifier(id: TemplateStringId): Option[DamlLfRef.Identifier] =
|
||||
parseOpaqueIdentifier(TemplateStringId.unwrap(id))
|
||||
|
||||
def parseInterfaceOpaqueIdentifier(id: InterfaceStringId): Option[DamlLfRef.Identifier] =
|
||||
parseOpaqueIdentifier(InterfaceStringId.unwrap(id))
|
||||
}
|
||||
|
||||
package model {
|
||||
sealed trait TemplateStringIdTag
|
||||
sealed trait InterfaceStringIdTag
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.query
|
||||
|
||||
import com.daml.navigator.dotnot.{OnTreeReady, PropertyCursor}
|
||||
import com.daml.navigator.model._
|
||||
import com.daml.navigator.query.filter._
|
||||
import com.typesafe.scalalogging.LazyLogging
|
||||
|
||||
sealed abstract class FilteringPager[N <: Node[_]](
|
||||
criterion: FilterCriterionBase,
|
||||
filter: OnTreeReady[N, Boolean, DamlLfTypeLookup],
|
||||
ps: DamlLfTypeLookup,
|
||||
) extends PagerDecorator[N]
|
||||
with LazyLogging {
|
||||
|
||||
final override def decorate(page: Page[N], ledger: Ledger): Page[N] = {
|
||||
val included = page.rows.filter(isIncluded)
|
||||
page.copy(total = included.size, rows = included)
|
||||
}
|
||||
|
||||
private[query] def isIncluded(node: N): Boolean = {
|
||||
def loop(criterion: FilterCriterionBase): Boolean = {
|
||||
criterion match {
|
||||
case AndFilterCriterion(criteria) =>
|
||||
criteria.forall(loop)
|
||||
case OrFilterCriterion(criteria) =>
|
||||
criteria.exists(loop)
|
||||
case criterion: FilterCriterion =>
|
||||
matchCriterion(node)(criterion)
|
||||
}
|
||||
}
|
||||
loop(criterion)
|
||||
}
|
||||
|
||||
private[query] def matchCriterion(node: N)(criterion: FilterCriterion): Boolean = {
|
||||
val cursor = PropertyCursor.fromString(criterion.field)
|
||||
filter.run(node, cursor, criterion.value, ps) match {
|
||||
case Left(failure) =>
|
||||
logger.error(
|
||||
s"Cannot match $node and " +
|
||||
s"criterion $criterion: $failure. Excluding it."
|
||||
)
|
||||
false
|
||||
case Right(isMatching) =>
|
||||
isMatching
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ContractFilter(
|
||||
criterion: FilterCriterionBase,
|
||||
ps: DamlLfTypeLookup,
|
||||
val delegate: Pager[Contract],
|
||||
) extends FilteringPager[Contract](criterion, contractFilter, ps)
|
||||
|
||||
final class TemplateFilter(
|
||||
criterion: FilterCriterionBase,
|
||||
ps: DamlLfTypeLookup,
|
||||
val delegate: Pager[Template],
|
||||
) extends FilteringPager[Template](criterion, templateFilter, ps)
|
||||
|
||||
final class CommandFilter(
|
||||
criterion: FilterCriterionBase,
|
||||
ps: DamlLfTypeLookup,
|
||||
val delegate: Pager[Command],
|
||||
) extends FilteringPager[Command](criterion, commandFilter, ps)
|
@ -1,82 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.query
|
||||
|
||||
import com.daml.navigator.model._
|
||||
|
||||
case class Page[N <: Node[_]](offset: Int, total: Int, rows: Seq[N], sortedLike: Seq[SortCriterion])
|
||||
|
||||
@SuppressWarnings(Array("org.wartremover.warts.Enumeration"))
|
||||
object SortDirection extends Enumeration {
|
||||
val ASCENDING, DESCENDING = Value
|
||||
}
|
||||
|
||||
case class SortCriterion(field: String, direction: SortDirection.Value)
|
||||
|
||||
sealed abstract class FilterCriterionBase
|
||||
final case class OrFilterCriterion(filters: List[FilterCriterionBase]) extends FilterCriterionBase
|
||||
final case class AndFilterCriterion(filters: List[FilterCriterionBase]) extends FilterCriterionBase
|
||||
final case class FilterCriterion(field: String, value: String) extends FilterCriterionBase
|
||||
|
||||
trait Pager[N <: Node[_]] {
|
||||
def fetch(ledger: Ledger, templates: PackageRegistry): Page[N]
|
||||
}
|
||||
|
||||
final object ActiveContractsPager extends Pager[Contract] {
|
||||
override def fetch(ledger: Ledger, templates: PackageRegistry): Page[Contract] =
|
||||
Page(0, ledger.activeContractsCount, ledger.activeContracts(templates), Seq.empty)
|
||||
}
|
||||
|
||||
final object AllContractsPager extends Pager[Contract] {
|
||||
override def fetch(ledger: Ledger, templates: PackageRegistry): Page[Contract] =
|
||||
Page(0, ledger.allContractsCount, ledger.allContracts(templates), Seq.empty)
|
||||
}
|
||||
|
||||
final object TemplatePager extends Pager[Template] {
|
||||
override def fetch(ledger: Ledger, templates: PackageRegistry): Page[Template] =
|
||||
Page(0, templates.templateCount, templates.allTemplates(), Seq.empty)
|
||||
}
|
||||
|
||||
final class TemplateContractPager(template: Template) extends Pager[Contract] {
|
||||
override def fetch(ledger: Ledger, templates: PackageRegistry): Page[Contract] = {
|
||||
val contracts = ledger.templateContractsOf(template, templates)
|
||||
Page(0, contracts.size, contracts, Seq.empty)
|
||||
}
|
||||
}
|
||||
|
||||
final class ActiveTemplateContractPager(template: Template) extends Pager[Contract] {
|
||||
override def fetch(ledger: Ledger, templates: PackageRegistry): Page[Contract] = {
|
||||
val contracts = ledger.activeTemplateContractsOf(template, templates)
|
||||
Page(0, contracts.size, contracts, Seq.empty)
|
||||
}
|
||||
}
|
||||
|
||||
final object CommandPager extends Pager[Command] {
|
||||
override def fetch(ledger: Ledger, templates: PackageRegistry): Page[Command] = {
|
||||
val commands = ledger.allCommands(templates)
|
||||
Page(0, commands.size, commands, Seq.empty)
|
||||
}
|
||||
}
|
||||
|
||||
trait PagerDecorator[N <: Node[_]] extends Pager[N] {
|
||||
override def fetch(ledger: Ledger, templates: PackageRegistry): Page[N] =
|
||||
decorate(delegate.fetch(ledger, templates), ledger)
|
||||
|
||||
def delegate: Pager[N]
|
||||
|
||||
def decorate(page: Page[N], ledger: Ledger): Page[N]
|
||||
}
|
||||
|
||||
class BoundingPager[N <: Node[_]](size: Int, val delegate: Pager[N]) extends PagerDecorator[N] {
|
||||
override def decorate(page: Page[N], ledger: Ledger): Page[N] =
|
||||
page.copy(rows = page.rows.take(size))
|
||||
}
|
||||
|
||||
class ShiftingPager[N <: Node[_]](previous: String, val delegate: Pager[N])
|
||||
extends PagerDecorator[N] {
|
||||
override def decorate(page: Page[N], ledger: Ledger): Page[N] = {
|
||||
val shiftedRows = page.rows.dropWhile(_.id != previous).drop(1)
|
||||
page.copy(offset = page.rows.size - shiftedRows.size, rows = shiftedRows)
|
||||
}
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.query
|
||||
|
||||
import com.daml.navigator.dotnot.{OnTreeReady, PropertyCursor}
|
||||
import com.daml.navigator.model._
|
||||
import com.daml.navigator.query.project._
|
||||
import com.typesafe.scalalogging.LazyLogging
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
sealed abstract class SortingPager[N <: Node[_]](
|
||||
criteria: List[SortCriterion],
|
||||
project: OnTreeReady[N, ProjectValue, DamlLfTypeLookup],
|
||||
ps: DamlLfTypeLookup,
|
||||
) extends PagerDecorator[N]
|
||||
with LazyLogging {
|
||||
|
||||
final override def decorate(page: Page[N], ledger: Ledger): Page[N] =
|
||||
page.copy(rows = sort(page.rows), sortedLike = criteria)
|
||||
|
||||
def sort(rows: Seq[N]): Seq[N] =
|
||||
rows.sortBy(projectAll)(ordering)
|
||||
|
||||
private val ordering =
|
||||
new Ordering[List[Option[ProjectValue]]] {
|
||||
val optionProjectValueOrdering = Ordering[Option[ProjectValue]]
|
||||
|
||||
override def compare(l1: List[Option[ProjectValue]], l2: List[Option[ProjectValue]]): Int = {
|
||||
@tailrec
|
||||
def loop(
|
||||
l1: List[Option[ProjectValue]],
|
||||
l2: List[Option[ProjectValue]],
|
||||
criteria: List[SortCriterion],
|
||||
): Int = {
|
||||
l1 match {
|
||||
case Nil => 0
|
||||
case x1 :: xs1 =>
|
||||
val x2 :: xs2 = l2
|
||||
val c :: cs = criteria
|
||||
optionProjectValueOrdering.compare(x1, x2) match {
|
||||
case 0 => loop(xs1, xs2, cs)
|
||||
case x =>
|
||||
c.direction match {
|
||||
case SortDirection.DESCENDING => x * (-1)
|
||||
case SortDirection.ASCENDING => x
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
loop(l1, l2, criteria)
|
||||
}
|
||||
}
|
||||
|
||||
private def projectAll(node: N): List[Option[ProjectValue]] =
|
||||
criteria.map(project(node))
|
||||
|
||||
private def project(node: N)(criterion: SortCriterion): Option[ProjectValue] = {
|
||||
val cursor = PropertyCursor.fromString(criterion.field)
|
||||
val x = project.run(node, cursor, "", ps)
|
||||
x match {
|
||||
case Left(failure) =>
|
||||
logger.error(s"Cannot project $node with criterion $criterion: $failure. Using None.")
|
||||
None
|
||||
case Right(value) =>
|
||||
Some(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ContractSorter(
|
||||
val criteria: List[SortCriterion],
|
||||
ps: DamlLfTypeLookup,
|
||||
val delegate: Pager[Contract],
|
||||
) extends SortingPager[Contract](criteria, contractProject, ps)
|
||||
|
||||
final class TemplateSorter(
|
||||
val criteria: List[SortCriterion],
|
||||
ps: DamlLfTypeLookup,
|
||||
val delegate: Pager[Template],
|
||||
) extends SortingPager[Template](criteria, templateProject, ps)
|
||||
|
||||
final class CommandSorter(
|
||||
val criteria: List[SortCriterion],
|
||||
ps: DamlLfTypeLookup,
|
||||
val delegate: Pager[Command],
|
||||
) extends SortingPager[Command](criteria, commandProject, ps)
|
@ -1,339 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.query
|
||||
|
||||
import com.daml.lf.value.json.ApiValueImplicits._
|
||||
import com.daml.lf.value.{Value => V}
|
||||
import com.daml.navigator.dotnot._
|
||||
import com.daml.navigator.model._
|
||||
import scalaz.Tag
|
||||
import scalaz.syntax.tag._
|
||||
|
||||
import scala.annotation.nowarn
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
package object filter {
|
||||
|
||||
object checkContained extends ((String, String) => Boolean) {
|
||||
|
||||
override def apply(v1: String, v2: String): Boolean =
|
||||
v1.toLowerCase contains v2.toLowerCase
|
||||
|
||||
override def toString(): String =
|
||||
"target.contains(value)"
|
||||
}
|
||||
|
||||
object checkOptionalContained extends ((Option[String], String) => Boolean) {
|
||||
|
||||
override def apply(v1: Option[String], v2: String): Boolean =
|
||||
v1.exists(_.toLowerCase contains v2.toLowerCase)
|
||||
|
||||
override def toString(): String =
|
||||
"target.exists(_.contains(value))"
|
||||
}
|
||||
|
||||
def checkParameter(
|
||||
rootParam: DamlLfType,
|
||||
cursor: PropertyCursor,
|
||||
expectedValue: String,
|
||||
ps: DamlLfTypeLookup,
|
||||
): Either[DotNotFailure, Boolean] = {
|
||||
|
||||
@annotation.tailrec
|
||||
def loop(parameter: DamlLfType, cursor: PropertyCursor): Either[DotNotFailure, Boolean] =
|
||||
parameter match {
|
||||
case tc: DamlLfTypeCon =>
|
||||
val nextOrResult =
|
||||
(ps(tc.name.identifier).map(tc.instantiate(_)), cursor.next) match {
|
||||
case (Some(DamlLfRecord(fields)), Some(nextCursor)) =>
|
||||
fields
|
||||
.collectFirst { case (nextCursor.current, fType) =>
|
||||
fType -> nextCursor
|
||||
}
|
||||
.toLeft(false)
|
||||
case (Some(DamlLfVariant(fields)), Some(nextCursor)) =>
|
||||
fields
|
||||
.collectFirst { case (nextCursor.current, fType) =>
|
||||
fType -> nextCursor
|
||||
}
|
||||
.toLeft(false)
|
||||
case (Some(DamlLfEnum(constructors)), _) =>
|
||||
Right(constructors.exists(checkContained(_, expectedValue)))
|
||||
case (None, _) | (_, None) =>
|
||||
Right(false)
|
||||
}
|
||||
|
||||
nextOrResult match {
|
||||
case Right(r) => Right(r)
|
||||
case Left((typ, nextCursor)) => loop(typ, nextCursor)
|
||||
}
|
||||
|
||||
case DamlLfTypeVar(name) => Right(checkContained(name, expectedValue))
|
||||
case DamlLfTypePrim(DamlLfPrimType.Bool, _) => Right(checkContained("bool", expectedValue))
|
||||
case DamlLfTypeNumeric(_) => Right(checkContained("decimal", expectedValue))
|
||||
case DamlLfTypePrim(DamlLfPrimType.Int64, _) =>
|
||||
Right(checkContained("int64", expectedValue))
|
||||
case DamlLfTypePrim(DamlLfPrimType.Date, _) => Right(checkContained("date", expectedValue))
|
||||
case DamlLfTypePrim(DamlLfPrimType.Text, _) => Right(checkContained("text", expectedValue))
|
||||
case DamlLfTypePrim(DamlLfPrimType.Party, _) =>
|
||||
Right(checkContained("party", expectedValue))
|
||||
case DamlLfTypePrim(DamlLfPrimType.Timestamp, _) =>
|
||||
Right(checkContained("timestamp", expectedValue))
|
||||
case DamlLfTypePrim(DamlLfPrimType.Unit, _) => Right(checkContained("unit", expectedValue))
|
||||
case DamlLfTypePrim(DamlLfPrimType.List, _) => Right(checkContained("list", expectedValue))
|
||||
case DamlLfTypePrim(DamlLfPrimType.ContractId, _) =>
|
||||
Right(checkContained("contractid", expectedValue))
|
||||
case DamlLfTypePrim(DamlLfPrimType.Optional, _) =>
|
||||
Right(checkContained("optional", expectedValue))
|
||||
case DamlLfTypePrim(DamlLfPrimType.TextMap, _) =>
|
||||
Right(checkContained("textmap", expectedValue))
|
||||
case DamlLfTypePrim(DamlLfPrimType.GenMap, _) =>
|
||||
Right(checkContained("genmap", expectedValue))
|
||||
|
||||
}
|
||||
|
||||
loop(rootParam, cursor.prev.get)
|
||||
}
|
||||
|
||||
def checkOptionalValue(
|
||||
rootArgument: Option[ApiValue],
|
||||
cursor: PropertyCursor,
|
||||
expectedValue: String,
|
||||
ps: DamlLfTypeLookup,
|
||||
): Either[DotNotFailure, Boolean] =
|
||||
rootArgument.fold[Either[DotNotFailure, Boolean]](Right(false))(
|
||||
checkValue(_, cursor, expectedValue, ps)
|
||||
)
|
||||
|
||||
@nowarn("msg=parameter ps .* is never used") // conform to opaque's signature
|
||||
def checkValue(
|
||||
rootArgument: ApiValue,
|
||||
cursor: PropertyCursor,
|
||||
expectedValue: String,
|
||||
ps: DamlLfTypeLookup,
|
||||
): Either[DotNotFailure, Boolean] = {
|
||||
|
||||
@annotation.tailrec
|
||||
def loop(argument: ApiValue, cursor: PropertyCursor): Either[DotNotFailure, Boolean] =
|
||||
argument match {
|
||||
case V.ValueContractId(value) =>
|
||||
cursor.ensureLast("ContractId")(checkContained(value.coid, expectedValue))
|
||||
case V.ValueInt64(value) =>
|
||||
cursor.ensureLast("Int64")(checkContained(value.toString, expectedValue))
|
||||
case V.ValueNumeric(value) =>
|
||||
cursor.ensureLast("Numeric")(checkContained(value.toUnscaledString, expectedValue))
|
||||
case V.ValueText(value) =>
|
||||
cursor.ensureLast("Text")(checkContained(value, expectedValue))
|
||||
case V.ValueParty(value) =>
|
||||
cursor.ensureLast("Party")(checkContained(value, expectedValue))
|
||||
case V.ValueBool(value) =>
|
||||
cursor.ensureLast("Bool")(checkContained(value.toString, expectedValue))
|
||||
case V.ValueUnit =>
|
||||
cursor.ensureLast("Unit")(expectedValue == "")
|
||||
case t: V.ValueTimestamp =>
|
||||
cursor.ensureLast("Timestamp")(checkContained(t.toIso8601, expectedValue))
|
||||
case t: V.ValueDate =>
|
||||
cursor.ensureLast("Date")(checkContained(t.toIso8601, expectedValue))
|
||||
case V.ValueRecord(_, fields) =>
|
||||
cursor.next match {
|
||||
case None => Right(false)
|
||||
case Some(nextCursor) =>
|
||||
val current: String = nextCursor.current
|
||||
fields.toSeq.collectFirst { case (Some(`current`), value) => value } match {
|
||||
case Some(nextField) => loop(nextField, nextCursor)
|
||||
case None => Right(false)
|
||||
}
|
||||
}
|
||||
case V.ValueVariant(_, constructor, value) =>
|
||||
cursor.next match {
|
||||
case None => Right(false)
|
||||
case Some(nextCursor) =>
|
||||
nextCursor.current match {
|
||||
case "__constructor" => Right(checkContained(constructor, expectedValue))
|
||||
case "__value" => loop(value, nextCursor)
|
||||
case `constructor` => loop(value, nextCursor)
|
||||
case _ => Right(false)
|
||||
}
|
||||
}
|
||||
case V.ValueEnum(_, constructor) =>
|
||||
cursor.next match {
|
||||
case None => Right(false)
|
||||
case Some(nextCursor) =>
|
||||
nextCursor.current match {
|
||||
case "__constructor" => Right(checkContained(constructor, expectedValue))
|
||||
case _ => Right(false)
|
||||
}
|
||||
}
|
||||
case V.ValueList(elements) =>
|
||||
cursor.next match {
|
||||
case None => Right(false)
|
||||
case Some(nextCursor) =>
|
||||
Try(nextCursor.current.toInt) match {
|
||||
case Success(index) => loop(elements.slowApply(index), nextCursor)
|
||||
case Failure(_) =>
|
||||
Left(TypeCoercionFailure("list index", "int", cursor, cursor.current))
|
||||
}
|
||||
}
|
||||
|
||||
case V.ValueOptional(optValue) =>
|
||||
(cursor.next, optValue) match {
|
||||
case (None, None) => Right(expectedValue == "None")
|
||||
case (None, Some(_)) => Right(expectedValue == "Some")
|
||||
case (Some(nextCursor), Some(value)) if nextCursor.current == "Some" =>
|
||||
loop(value, nextCursor)
|
||||
case (Some(nextCursor), None) if nextCursor.current == "None" => Right(true)
|
||||
case (Some(_), _) => Right(false)
|
||||
}
|
||||
case V.ValueTextMap(textMap) =>
|
||||
cursor.next match {
|
||||
case None => Right(false)
|
||||
case Some(nextCursor) =>
|
||||
textMap.toImmArray.toSeq.collectFirst {
|
||||
case (k, v) if k == nextCursor.current => v
|
||||
} match {
|
||||
case None => Right(false)
|
||||
case Some(v) => loop(v, nextCursor)
|
||||
}
|
||||
}
|
||||
case V.ValueGenMap(entries) =>
|
||||
cursor.next match {
|
||||
case None =>
|
||||
Right(false)
|
||||
case Some(nextCursor) =>
|
||||
Try(nextCursor.current.toInt) match {
|
||||
case Success(index) =>
|
||||
nextCursor.next match {
|
||||
case Some(nextNextCursor) if nextNextCursor.current == "key" =>
|
||||
loop(entries(index)._1, nextNextCursor)
|
||||
case Some(nextNextCursor) if nextNextCursor.current == "value" =>
|
||||
loop(entries(index)._2, nextNextCursor)
|
||||
case Some(_) => Left(UnknownProperty("genmap", nextCursor, expectedValue))
|
||||
case None =>
|
||||
Right(false)
|
||||
}
|
||||
case Failure(_) =>
|
||||
Left(TypeCoercionFailure("GenMap index", "int", cursor, cursor.current))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
loop(rootArgument, cursor.prev.get)
|
||||
}
|
||||
|
||||
lazy val parameterFilter =
|
||||
opaque[DamlLfType, Boolean, DamlLfTypeLookup]("parameter")((t, c, e, p) =>
|
||||
checkParameter(t, c, e, p)
|
||||
)
|
||||
|
||||
lazy val parameterIdFilter =
|
||||
opaque[DamlLfIdentifier, Boolean, DamlLfTypeLookup]("parameter")((id, c, e, p) =>
|
||||
checkParameter(DamlLfTypeCon(DamlLfTypeConName(id), DamlLfImmArraySeq()), c, e, p)
|
||||
)
|
||||
|
||||
lazy val argumentFilter =
|
||||
opaque[ApiValue, Boolean, DamlLfTypeLookup]("argument")(checkValue)
|
||||
|
||||
lazy val keyFilter =
|
||||
opaque[Option[ApiValue], Boolean, DamlLfTypeLookup]("key")(checkOptionalValue)
|
||||
|
||||
lazy val templateFilter =
|
||||
root[Template, Boolean, DamlLfTypeLookup]("template")
|
||||
.onLeaf("id")
|
||||
.onValue("*")
|
||||
.const(true)
|
||||
.onAnyValue
|
||||
.perform[String]((contract, id) => checkContained(contract.idString, id))
|
||||
.onLeaf("topLevelDecl")
|
||||
.onValue("*")
|
||||
.const(true)
|
||||
.onAnyValue
|
||||
.perform[String]((template, topLevelDecl) =>
|
||||
checkContained(template.topLevelDecl, topLevelDecl)
|
||||
)
|
||||
.onBranch("parameter", _.id, parameterIdFilter)
|
||||
.onBranch("choices", _.choices, choicesFilter)
|
||||
// .onStar(check all fields)
|
||||
|
||||
lazy val contractFilter =
|
||||
root[Contract, Boolean, DamlLfTypeLookup]("contract")
|
||||
.onLeaf("id")
|
||||
.onValue("*")
|
||||
.const(true)
|
||||
.onAnyValue
|
||||
.perform[String]((contract, id) => checkContained(contract.id.unwrap, id.toLowerCase))
|
||||
.onBranch("template", _.template, templateFilter)
|
||||
.onBranch("argument", _.argument, argumentFilter)
|
||||
.onBranch("key", _.key, keyFilter)
|
||||
.onLeaf("agreementText")
|
||||
.onValue("*")
|
||||
.const(true)
|
||||
.onAnyValue
|
||||
.perform[String]((contract, agree) => checkOptionalContained(contract.agreementText, agree))
|
||||
.onTree
|
||||
.onLeaf("signatories")
|
||||
.onValue("*")
|
||||
.const(true)
|
||||
.onAnyValue
|
||||
.perform[String]((contract, signatory) =>
|
||||
contract.signatories.map(Tag.unwrap).exists(checkContained(_, signatory))
|
||||
)
|
||||
.onTree
|
||||
.onLeaf("observers")
|
||||
.onValue("*")
|
||||
.const(true)
|
||||
.onAnyValue
|
||||
.perform[String]((contract, observer) =>
|
||||
contract.observers.map(Tag.unwrap).exists(checkContained(_, observer))
|
||||
)
|
||||
.onTree
|
||||
// .onStar(check all fields)
|
||||
|
||||
lazy val choicesFilter =
|
||||
root[Seq[Choice], Boolean, DamlLfTypeLookup]("choices")
|
||||
.onElements[Choice](choice => Tag.unwrap(choice.name), choiceFilter)
|
||||
|
||||
lazy val choiceFilter =
|
||||
root[Choice, Boolean, DamlLfTypeLookup]("choice")
|
||||
.onLeaf("name")
|
||||
.onValue("*")
|
||||
.const(true)
|
||||
.onAnyValue
|
||||
.perform[String]((choice, name) => checkContained(Tag.unwrap(choice.name), name.toLowerCase))
|
||||
.onLeaf("consuming")
|
||||
.onAnyValue
|
||||
.perform[Boolean]((choice, consuming) => choice.consuming == consuming)
|
||||
.onBranch("parameter", _.parameter, parameterFilter)
|
||||
// .onStar(check all fields)
|
||||
.onTree
|
||||
|
||||
lazy val commandFilter =
|
||||
root[Command, Boolean, DamlLfTypeLookup]("command")
|
||||
.onLeaf("id")
|
||||
.onValue("*")
|
||||
.const(true)
|
||||
.onAnyValue
|
||||
.perform[String]((command, id) => checkContained(command.id.unwrap, id.toLowerCase))
|
||||
.onLeaf("index")
|
||||
.onValue("*")
|
||||
.const(true)
|
||||
.onAnyValue
|
||||
.perform[String]((command, index) =>
|
||||
checkContained(command.index.toString, index.toLowerCase)
|
||||
)
|
||||
.onLeaf("platformTime")
|
||||
.onValue("*")
|
||||
.const(true)
|
||||
.onAnyValue
|
||||
.perform[String]((command, time) =>
|
||||
checkContained(command.platformTime.toString, time.toLowerCase)
|
||||
)
|
||||
.onLeaf("workflowId")
|
||||
.onValue("*")
|
||||
.const(true)
|
||||
.onAnyValue
|
||||
.perform[String]((command, id) => checkContained(command.workflowId.unwrap, id.toLowerCase))
|
||||
.onTree
|
||||
// .onStar(check all fields)
|
||||
}
|
@ -1,303 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.query
|
||||
|
||||
import com.daml.lf.value.json.ApiValueImplicits._
|
||||
import com.daml.lf.value.{Value => V}
|
||||
import com.daml.navigator.dotnot._
|
||||
import com.daml.navigator.model._
|
||||
import scalaz.Tag
|
||||
import scalaz.syntax.tag._
|
||||
|
||||
import scala.annotation.nowarn
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
@SuppressWarnings(Array("org.wartremover.warts.Product", "org.wartremover.warts.Serializable"))
|
||||
object project {
|
||||
|
||||
/** ADT of primitives used as result of a projection over contracts or templates */
|
||||
sealed abstract class ProjectValue
|
||||
final case class StringValue(value: String) extends ProjectValue
|
||||
final case class NumberValue(value: BigDecimal) extends ProjectValue
|
||||
final case class BooleanValue(value: Boolean) extends ProjectValue
|
||||
|
||||
implicit val projectValueOrdering: Ordering[ProjectValue] = new Ordering[ProjectValue] {
|
||||
override def compare(x: ProjectValue, y: ProjectValue): Int =
|
||||
x match {
|
||||
case StringValue(s) =>
|
||||
y match {
|
||||
case StringValue(s2) => s compareTo s2
|
||||
case _ => 1
|
||||
}
|
||||
case NumberValue(n) =>
|
||||
y match {
|
||||
case NumberValue(n2) => n compare n2
|
||||
case _: StringValue => -1
|
||||
case _: BooleanValue => 1
|
||||
}
|
||||
case BooleanValue(b) =>
|
||||
y match {
|
||||
case BooleanValue(b2) => b compareTo b2
|
||||
case _ => -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def checkParameter(
|
||||
rootParam: DamlLfType,
|
||||
cursor: PropertyCursor,
|
||||
value: String,
|
||||
ps: DamlLfTypeLookup,
|
||||
): Either[DotNotFailure, ProjectValue] = {
|
||||
|
||||
@annotation.tailrec
|
||||
def loop(
|
||||
parameter: DamlLfType,
|
||||
cursor: PropertyCursor,
|
||||
ps: DamlLfTypeLookup,
|
||||
): Either[DotNotFailure, ProjectValue] =
|
||||
parameter match {
|
||||
case tc: DamlLfTypeCon =>
|
||||
val tyCon = tc.name.identifier
|
||||
ps(tyCon) match {
|
||||
case Some(ddt) =>
|
||||
val next = tc.instantiate(ddt) match {
|
||||
case DamlLfEnum(_) =>
|
||||
Right(StringValue(tyCon.toString))
|
||||
case DamlLfRecord(fields) =>
|
||||
Left("record" -> fields)
|
||||
case DamlLfVariant(constructors) =>
|
||||
Left("variant" -> constructors)
|
||||
}
|
||||
next match {
|
||||
case Left((description, fields)) =>
|
||||
cursor.next match {
|
||||
case Some(nextCursor) =>
|
||||
fields.collectFirst { case (nextCursor.current, fieldType) =>
|
||||
fieldType
|
||||
} match {
|
||||
case Some(nextType) =>
|
||||
loop(nextType, nextCursor, ps)
|
||||
case None =>
|
||||
Left(UnknownProperty(description, nextCursor, value))
|
||||
}
|
||||
case None =>
|
||||
Left(MustNotBeLastPart("DataType", cursor, value))
|
||||
}
|
||||
case Right(result) =>
|
||||
Right(result)
|
||||
}
|
||||
case None =>
|
||||
Left(UnknownType(tyCon.toString, cursor, value))
|
||||
}
|
||||
case DamlLfTypeVar(name) => Right(StringValue(name))
|
||||
case DamlLfTypePrim(DamlLfPrimType.Bool, _) => Right(StringValue("bool"))
|
||||
case DamlLfTypeNumeric(_) => Right(StringValue("decimal"))
|
||||
case DamlLfTypePrim(DamlLfPrimType.Int64, _) => Right(StringValue("int64"))
|
||||
case DamlLfTypePrim(DamlLfPrimType.Date, _) => Right(StringValue("date"))
|
||||
case DamlLfTypePrim(DamlLfPrimType.Text, _) => Right(StringValue("text"))
|
||||
case DamlLfTypePrim(DamlLfPrimType.Party, _) => Right(StringValue("party"))
|
||||
case DamlLfTypePrim(DamlLfPrimType.Timestamp, _) => Right(StringValue("timestamp"))
|
||||
case DamlLfTypePrim(DamlLfPrimType.Unit, _) => Right(StringValue("unit"))
|
||||
case DamlLfTypePrim(DamlLfPrimType.List, _) => Right(StringValue("list"))
|
||||
case DamlLfTypePrim(DamlLfPrimType.ContractId, _) => Right(StringValue("contractid"))
|
||||
case DamlLfTypePrim(DamlLfPrimType.Optional, _) => Right(StringValue("optional"))
|
||||
case DamlLfTypePrim(DamlLfPrimType.TextMap, _) => Right(StringValue("textmap"))
|
||||
case DamlLfTypePrim(DamlLfPrimType.GenMap, _) => Right(StringValue("genmap"))
|
||||
}
|
||||
|
||||
loop(rootParam, cursor.prev.get, ps)
|
||||
}
|
||||
|
||||
def checkOptionalValue(
|
||||
rootArgument: Option[ApiValue],
|
||||
cursor: PropertyCursor,
|
||||
expectedValue: String,
|
||||
ps: DamlLfTypeLookup,
|
||||
): Either[DotNotFailure, ProjectValue] =
|
||||
rootArgument.fold[Either[DotNotFailure, ProjectValue]](Right(StringValue("")))(
|
||||
checkValue(_, cursor, expectedValue, ps)
|
||||
)
|
||||
|
||||
@nowarn("msg=parameter ps .* is never used") // conforms to `opaque`'s signature
|
||||
def checkValue(
|
||||
rootArgument: ApiValue,
|
||||
cursor: PropertyCursor,
|
||||
expectedValue: String,
|
||||
ps: DamlLfTypeLookup,
|
||||
): Either[DotNotFailure, ProjectValue] = {
|
||||
|
||||
@annotation.tailrec
|
||||
def loop(argument: ApiValue, cursor: PropertyCursor): Either[DotNotFailure, ProjectValue] =
|
||||
argument match {
|
||||
case V.ValueContractId(value) => cursor.ensureLast("contractid")(StringValue(value.coid))
|
||||
case V.ValueInt64(value) => cursor.ensureLast("int64")(NumberValue(value))
|
||||
case V.ValueNumeric(value) =>
|
||||
cursor.ensureLast("numeric")(StringValue(value.toUnscaledString))
|
||||
case V.ValueText(value) => cursor.ensureLast("text")(StringValue(value))
|
||||
case V.ValueParty(value) => cursor.ensureLast("party")(StringValue(value))
|
||||
case V.ValueBool(value) => cursor.ensureLast("bool")(BooleanValue(value))
|
||||
case V.ValueUnit => cursor.ensureLast("unit")(StringValue(""))
|
||||
case t: V.ValueTimestamp => cursor.ensureLast("timestamp")(StringValue(t.toIso8601))
|
||||
case t: V.ValueDate => cursor.ensureLast("date")(StringValue(t.toIso8601))
|
||||
case V.ValueRecord(_, fields) =>
|
||||
cursor.next match {
|
||||
case None => Left(MustNotBeLastPart("record", cursor, expectedValue))
|
||||
case Some(nextCursor) =>
|
||||
val current: String = nextCursor.current
|
||||
fields.toSeq.collectFirst { case (Some(`current`), value) => value } match {
|
||||
case Some(nextField) => loop(nextField, nextCursor)
|
||||
case None => Left(UnknownProperty("record", nextCursor, expectedValue))
|
||||
}
|
||||
}
|
||||
case V.ValueVariant(_, constructor, value) =>
|
||||
cursor.next match {
|
||||
case None => Left(MustNotBeLastPart("variant", cursor, expectedValue))
|
||||
case Some(nextCursor) =>
|
||||
nextCursor.current match {
|
||||
case "__constructor" => Right(StringValue(constructor))
|
||||
case "__value" => loop(value, nextCursor)
|
||||
case `constructor` => loop(value, nextCursor)
|
||||
case _ => Left(UnknownProperty("variant", nextCursor, expectedValue))
|
||||
}
|
||||
}
|
||||
case V.ValueEnum(_, constructor) =>
|
||||
cursor.next match {
|
||||
case None => Left(MustNotBeLastPart("enum", cursor, expectedValue))
|
||||
case Some(nextCursor) =>
|
||||
nextCursor.current match {
|
||||
case "__constructor" => Right(StringValue(constructor))
|
||||
case _ => Left(UnknownProperty("enum", nextCursor, expectedValue))
|
||||
}
|
||||
}
|
||||
case V.ValueOptional(optValue) =>
|
||||
(cursor.next, optValue) match {
|
||||
case (None, None) => Right(StringValue("None"))
|
||||
case (None, Some(_)) => Right(StringValue("Some"))
|
||||
case (Some(nextCursor), Some(value)) if nextCursor.current == "Some" =>
|
||||
loop(value, nextCursor)
|
||||
case (Some(nextCursor), None) if nextCursor.current == "None" => Right(StringValue(""))
|
||||
case (Some(nextCursor), _) =>
|
||||
Left(UnknownProperty("optional", nextCursor, expectedValue))
|
||||
}
|
||||
case V.ValueList(elements) =>
|
||||
cursor.next match {
|
||||
case None => Left(MustNotBeLastPart("list", cursor, expectedValue))
|
||||
case Some(nextCursor) =>
|
||||
Try(nextCursor.current.toInt) match {
|
||||
case Success(index) => loop(elements.slowApply(index), nextCursor)
|
||||
case Failure(_) =>
|
||||
Left(TypeCoercionFailure("list index", "int", cursor, cursor.current))
|
||||
}
|
||||
}
|
||||
case V.ValueTextMap(textMap) =>
|
||||
cursor.next match {
|
||||
case None => Left(MustNotBeLastPart("textmap", cursor, expectedValue))
|
||||
case Some(nextCursor) =>
|
||||
textMap.toImmArray.toSeq.collectFirst {
|
||||
case (k, v) if k == nextCursor.current => v
|
||||
} match {
|
||||
case Some(v) => loop(v, nextCursor)
|
||||
case None =>
|
||||
Left(UnknownProperty(nextCursor.current, nextCursor, expectedValue))
|
||||
}
|
||||
}
|
||||
case V.ValueGenMap(entries) =>
|
||||
cursor.next match {
|
||||
case None =>
|
||||
Left(MustNotBeLastPart("genmap", cursor, expectedValue))
|
||||
case Some(nextCursor) =>
|
||||
Try(nextCursor.current.toInt) match {
|
||||
case Success(index) =>
|
||||
nextCursor.next match {
|
||||
case Some(nextNextCursor) if nextNextCursor.current == "key" =>
|
||||
loop(entries(index)._1, nextNextCursor)
|
||||
case Some(nextNextCursor) if nextNextCursor.current == "value" =>
|
||||
loop(entries(index)._2, nextNextCursor)
|
||||
case Some(_) => Left(UnknownProperty("genmap", nextCursor, expectedValue))
|
||||
case None =>
|
||||
Left(UnknownProperty(nextCursor.current, nextCursor, expectedValue))
|
||||
}
|
||||
case Failure(_) =>
|
||||
Left(TypeCoercionFailure("GenMap index", "int", cursor, cursor.current))
|
||||
}
|
||||
}
|
||||
}
|
||||
loop(rootArgument, cursor.prev.get)
|
||||
}
|
||||
|
||||
lazy val parameterProject =
|
||||
opaque[DamlLfType, ProjectValue, DamlLfTypeLookup]("parameter")((t, c, e, p) =>
|
||||
checkParameter(t, c, e, p)
|
||||
)
|
||||
|
||||
lazy val parameterIdProject =
|
||||
opaque[DamlLfIdentifier, ProjectValue, DamlLfTypeLookup]("parameter")((id, c, e, p) =>
|
||||
checkParameter(DamlLfTypeCon(DamlLfTypeConName(id), DamlLfImmArraySeq()), c, e, p)
|
||||
)
|
||||
|
||||
lazy val argumentProject =
|
||||
opaque[ApiValue, ProjectValue, DamlLfTypeLookup]("argument")(checkValue)
|
||||
|
||||
lazy val keyProject =
|
||||
opaque[Option[ApiValue], ProjectValue, DamlLfTypeLookup]("key")(checkOptionalValue)
|
||||
|
||||
lazy val templateProject =
|
||||
root[Template, ProjectValue, DamlLfTypeLookup]("template")
|
||||
.onLeaf("id")
|
||||
.onAnyValue
|
||||
.perform[String]((contract, _) => StringValue(contract.idString))
|
||||
.onBranch("parameter", _.id, parameterIdProject)
|
||||
.onBranch("choices", _.choices, choicesProject)
|
||||
|
||||
lazy val contractProject =
|
||||
root[Contract, ProjectValue, DamlLfTypeLookup]("contract")
|
||||
.onLeaf("id")
|
||||
.onAnyValue
|
||||
.perform[String]((contract, _) => StringValue(contract.id.unwrap))
|
||||
.onBranch("template", _.template, templateProject)
|
||||
.onBranch("argument", _.argument, argumentProject)
|
||||
.onBranch("key", _.key, keyProject)
|
||||
.onLeaf("agreementText")
|
||||
.onAnyValue
|
||||
.perform[String]((contract, _) => StringValue(contract.agreementText.getOrElse("")))
|
||||
.onLeaf("signatories")
|
||||
.onAnyValue
|
||||
.perform[String]((contract, _) => StringValue(contract.signatories.mkString))
|
||||
.onLeaf("observers")
|
||||
.onAnyValue
|
||||
.perform[String]((contract, _) => StringValue(contract.observers.mkString))
|
||||
.onTree
|
||||
|
||||
lazy val choicesProject =
|
||||
root[Seq[Choice], ProjectValue, DamlLfTypeLookup]("choices")
|
||||
.onElements[Choice](choice => Tag.unwrap(choice.name), choiceProject)
|
||||
|
||||
lazy val choiceProject =
|
||||
root[Choice, ProjectValue, DamlLfTypeLookup]("choice")
|
||||
.onLeaf("name")
|
||||
.onAnyValue
|
||||
.perform[String]((choice, _) => StringValue(Tag.unwrap(choice.name)))
|
||||
.onLeaf("consuming")
|
||||
.onAnyValue
|
||||
.perform[String]((choice, _) => BooleanValue(choice.consuming))
|
||||
.onBranch("parameter", _.parameter, parameterProject)
|
||||
.onTree
|
||||
|
||||
lazy val commandProject =
|
||||
root[Command, ProjectValue, DamlLfTypeLookup]("command")
|
||||
.onLeaf("id")
|
||||
.onAnyValue
|
||||
.perform[String]((command, _) => StringValue(Tag.unwrap(command.id)))
|
||||
.onLeaf("index")
|
||||
.onAnyValue
|
||||
.perform[String]((command, _) => NumberValue(command.index))
|
||||
.onLeaf("workflowId")
|
||||
.onAnyValue
|
||||
.perform[String]((command, _) => StringValue(Tag.unwrap(command.workflowId)))
|
||||
.onLeaf("platformTime")
|
||||
.onAnyValue
|
||||
.perform[String]((command, _) => StringValue(command.platformTime.toString))
|
||||
.onTree
|
||||
}
|
@ -1,138 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.store
|
||||
|
||||
import java.time.Instant
|
||||
import com.daml.ledger.api.domain.{PartyDetails, User}
|
||||
import com.daml.navigator.model._
|
||||
import com.daml.ledger.api.refinements.ApiTypes
|
||||
import com.daml.navigator.time.TimeProviderWithType
|
||||
|
||||
trait ActorStatus
|
||||
|
||||
/** Actor reports working at full health */
|
||||
case object ActorRunning extends ActorStatus
|
||||
|
||||
/** Actor is still starting up */
|
||||
case object ActorStarting extends ActorStatus
|
||||
|
||||
/** Actor reports a permanently failed state */
|
||||
case class ActorFailed(error: Throwable) extends ActorStatus
|
||||
|
||||
/** Actor did not respond within a reasonable time */
|
||||
case object ActorUnresponsive extends ActorStatus
|
||||
|
||||
object Store {
|
||||
|
||||
/** Reinitialize the platform connection and reset all local state `Unit` */
|
||||
case object ResetConnection
|
||||
|
||||
case object UpdateUsersOrParties
|
||||
case class UpdatedUsers(details: Seq[User])
|
||||
|
||||
case class UpdatedParties(details: List[PartyDetails])
|
||||
|
||||
/** Request to subscribe a party to the store (without response to sender). */
|
||||
case class Subscribe(
|
||||
displayName: String,
|
||||
name: ApiTypes.Party,
|
||||
userRole: Option[String] = None,
|
||||
useDatabase: Boolean = false,
|
||||
)
|
||||
|
||||
/** Request to create a contract instance for a template and respond with a `scala.util.Try[CommandId]`. */
|
||||
case class CreateContract(party: PartyState, templateId: TemplateStringId, argument: ApiRecord)
|
||||
|
||||
/** Request to exercise a choice on a contract and respond with a `scala.util.Try[CommandId]`. */
|
||||
case class ExerciseChoice(
|
||||
party: PartyState,
|
||||
contractId: ApiTypes.ContractId,
|
||||
interfaceId: Option[InterfaceStringId],
|
||||
choiceId: ApiTypes.Choice,
|
||||
argument: ApiValue,
|
||||
)
|
||||
|
||||
/** Request to respond with a `scala.util.Try[TimeProviderWithType]` with the current store time. */
|
||||
case object ReportCurrentTime
|
||||
|
||||
/** Request to advance time to the specified instant and respond with updated store time as a
|
||||
* `scala.util.Try[TimeProviderWithType]`.
|
||||
*/
|
||||
case class AdvanceTime(to: Instant)
|
||||
|
||||
/** An indication that the store was not able to execute a request. */
|
||||
case class StoreException(message: String) extends Exception(message)
|
||||
|
||||
/** Request diagnostic information about the state of the application and respond with a [[ApplicationStateInfo]]. */
|
||||
case object GetApplicationStateInfo
|
||||
|
||||
/** Request a list of all parties that have an active actor on the system matching the provided search string */
|
||||
final case class GetParties(search: String)
|
||||
|
||||
/** Response to a request for parties */
|
||||
final case class PartyList(parties: List[ApiTypes.Party])
|
||||
|
||||
/** Diagnostic information about the state of the application */
|
||||
sealed trait ApplicationStateInfo {
|
||||
def platformHost: String
|
||||
def platformPort: Int
|
||||
def tls: Boolean
|
||||
def applicationId: String
|
||||
}
|
||||
|
||||
/** Application is still connecting to the ledger */
|
||||
final case class ApplicationStateConnecting(
|
||||
platformHost: String,
|
||||
platformPort: Int,
|
||||
tls: Boolean,
|
||||
applicationId: String,
|
||||
) extends ApplicationStateInfo
|
||||
|
||||
/** Application is still connecting to the ledger */
|
||||
final case class ApplicationStateConnected(
|
||||
platformHost: String,
|
||||
platformPort: Int,
|
||||
tls: Boolean,
|
||||
applicationId: String,
|
||||
ledgerId: String,
|
||||
ledgerTime: TimeProviderWithType,
|
||||
// `partyActors`'s keys are passed to the frontend as possible user ids to log in as
|
||||
partyActors: Map[String, PartyActorResponse],
|
||||
) extends ApplicationStateInfo
|
||||
|
||||
/** Application failed to start up */
|
||||
final case class ApplicationStateFailed(
|
||||
platformHost: String,
|
||||
platformPort: Int,
|
||||
tls: Boolean,
|
||||
applicationId: String,
|
||||
error: Throwable,
|
||||
) extends ApplicationStateInfo
|
||||
|
||||
/** Request diagnostic information about a party and respond with a [[PartyActorInfo]]. */
|
||||
case object GetPartyActorInfo
|
||||
|
||||
sealed trait PartyActorResponse
|
||||
|
||||
final case class PartyActorRunning(info: PartyActorInfo) extends PartyActorResponse
|
||||
final case object PartyActorUnresponsive extends PartyActorResponse
|
||||
|
||||
/** Diagnostic information about a party */
|
||||
sealed trait PartyActorInfo {
|
||||
def party: ApiTypes.Party = state.name
|
||||
def state: PartyState
|
||||
}
|
||||
|
||||
/** Actor still starting up */
|
||||
final case class PartyActorStarting(state: PartyState) extends PartyActorInfo
|
||||
|
||||
/** Actor running and consuming the transaction stream */
|
||||
final case class PartyActorStarted(state: PartyState) extends PartyActorInfo
|
||||
|
||||
/** Actor permanently failed */
|
||||
final case class PartyActorFailed(
|
||||
state: PartyState,
|
||||
error: Throwable,
|
||||
) extends PartyActorInfo
|
||||
}
|
@ -1,552 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.store.platform
|
||||
|
||||
import java.time.{Duration, Instant}
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import org.apache.pekko.actor.{Actor, ActorLogging, ActorRef, Props, Scheduler, Stash}
|
||||
import org.apache.pekko.pattern.{ask, pipe}
|
||||
import org.apache.pekko.stream.Materializer
|
||||
import org.apache.pekko.util.Timeout
|
||||
import com.daml.grpc.GrpcException
|
||||
import com.daml.grpc.adapter.{PekkoExecutionSequencerPool, ExecutionSequencerFactory}
|
||||
import com.daml.ledger.api.domain.Feature
|
||||
import com.daml.ledger.api.refinements.{ApiTypes, IdGenerator}
|
||||
import com.daml.ledger.api.tls.TlsConfiguration
|
||||
import com.daml.ledger.api.v1.testing.time_service.TimeServiceGrpc
|
||||
import com.daml.ledger.client.LedgerClient
|
||||
import com.daml.ledger.client.configuration.{
|
||||
CommandClientConfiguration,
|
||||
LedgerClientChannelConfiguration,
|
||||
LedgerClientConfiguration,
|
||||
LedgerIdRequirement,
|
||||
}
|
||||
import com.daml.ledger.client.services.testing.time.StaticTime
|
||||
import com.daml.lf.data.Ref
|
||||
import com.daml.navigator.ApplicationInfo
|
||||
import com.daml.navigator.model._
|
||||
import com.daml.navigator.store.Store._
|
||||
import com.daml.navigator.time._
|
||||
import com.daml.navigator.util.RetryHelper
|
||||
import io.grpc.Channel
|
||||
import io.grpc.netty.GrpcSslContexts
|
||||
import io.netty.handler.ssl.SslContext
|
||||
import org.slf4j.LoggerFactory
|
||||
import scalaz.syntax.tag._
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.{Failure, Random, Success, Try}
|
||||
|
||||
object PlatformStore {
|
||||
def props(
|
||||
platformHost: String,
|
||||
platformPort: Int,
|
||||
tlsConfig: Option[TlsConfiguration],
|
||||
accessToken: Option[String],
|
||||
timeProviderType: TimeProviderType,
|
||||
applicationInfo: ApplicationInfo,
|
||||
ledgerMaxInbound: Int,
|
||||
enableUserManagement: Boolean,
|
||||
): Props =
|
||||
Props(
|
||||
classOf[PlatformStore],
|
||||
platformHost,
|
||||
platformPort,
|
||||
tlsConfig,
|
||||
accessToken,
|
||||
timeProviderType,
|
||||
applicationInfo,
|
||||
ledgerMaxInbound,
|
||||
enableUserManagement,
|
||||
)
|
||||
|
||||
type PlatformTime = Instant
|
||||
|
||||
case class ConnectionResult(
|
||||
ledgerClient: LedgerClient,
|
||||
staticTime: Option[StaticTime],
|
||||
time: TimeProviderWithType,
|
||||
)
|
||||
case class Connected(ledgerClient: Try[ConnectionResult])
|
||||
|
||||
case class StateConnected(
|
||||
ledgerClient: LedgerClient,
|
||||
// A key in `parties` could be: a Party's displayName, a party's name, a User's id, or a name specified in our config
|
||||
parties: Map[String, PartyState],
|
||||
staticTime: Option[StaticTime],
|
||||
time: TimeProviderWithType,
|
||||
)
|
||||
case class StateInitial(parties: Map[String, PartyState])
|
||||
case class StateFailed(error: Throwable)
|
||||
}
|
||||
|
||||
/** Store implementation that accesses the platform API to fetch data and execute commands. */
|
||||
@SuppressWarnings(Array("org.wartremover.warts.Any"))
|
||||
class PlatformStore(
|
||||
platformHost: String,
|
||||
platformPort: Int,
|
||||
tlsConfig: Option[TlsConfiguration],
|
||||
token: Option[String],
|
||||
timeProviderType: TimeProviderType,
|
||||
applicationInfo: ApplicationInfo,
|
||||
ledgerMaxInbound: Int,
|
||||
enableUserManagement: Boolean,
|
||||
) extends Actor
|
||||
with ActorLogging
|
||||
with Stash {
|
||||
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// Global immutable state - mutable state is stored in parameters of the receive methods
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
private val system = context.system
|
||||
|
||||
implicit val s: Scheduler = system.scheduler
|
||||
implicit val ec: ExecutionContext = system.dispatcher
|
||||
implicit val materializer: Materializer = Materializer(system)
|
||||
implicit val esf: ExecutionSequencerFactory =
|
||||
new PekkoExecutionSequencerPool("esf-" + this.getClass.getSimpleName)(system)
|
||||
|
||||
private val applicationId =
|
||||
Ref.LedgerString.assertFromString(
|
||||
applicationInfo.id + "#" + new Random(System.currentTimeMillis()).nextLong().toHexString
|
||||
)
|
||||
|
||||
private[this] def userFacingLogger = LoggerFactory.getLogger("user-facing-logs")
|
||||
|
||||
import PlatformStore._
|
||||
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
override def preStart(): Unit = {
|
||||
connect()
|
||||
}
|
||||
|
||||
override def postStop(): Unit = {
|
||||
esf.close()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// Messages
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
override def receive: Receive = connecting(StateInitial(Map.empty[String, PartyState]))
|
||||
|
||||
def connecting(state: StateInitial): Receive = {
|
||||
case Connected(Success(value)) =>
|
||||
context.become(
|
||||
connected(StateConnected(value.ledgerClient, state.parties, value.staticTime, value.time))
|
||||
)
|
||||
unstashAll()
|
||||
|
||||
case Connected(Failure(e)) =>
|
||||
// Connection failed even after several retries - not sure how to recover from this
|
||||
val message =
|
||||
s"Permanently failed to connect to the ledger at $platformHost:$platformPort. " +
|
||||
"Please fix any issues and restart this application."
|
||||
userFacingLogger.error(message)
|
||||
context.become(failed(StateFailed(e)))
|
||||
|
||||
case GetApplicationStateInfo =>
|
||||
sender() ! ApplicationStateConnecting(
|
||||
platformHost,
|
||||
platformPort,
|
||||
tlsConfig.exists(_.enabled),
|
||||
applicationId,
|
||||
)
|
||||
|
||||
case _ =>
|
||||
stash()
|
||||
}
|
||||
|
||||
def connected(state: StateConnected): Receive = {
|
||||
case UpdateUsersOrParties =>
|
||||
// If user management is enabled for navigator (the default),
|
||||
// and the ledger supports it, only display users on the login screen.
|
||||
// Otherwise, we fall back to legacy behavior of logging in as a party.
|
||||
val hasUserManagement =
|
||||
if (!enableUserManagement) Future.successful(false)
|
||||
else
|
||||
state.ledgerClient.versionClient
|
||||
.getApiFeatures(state.ledgerClient.ledgerId)
|
||||
.map { features =>
|
||||
val hasUserManagement = features.contains(Feature.UserManagement)
|
||||
if (!hasUserManagement) {
|
||||
log.warning("User management is enabled but ledger does not expose this feature.")
|
||||
}
|
||||
hasUserManagement
|
||||
}
|
||||
|
||||
// TODO https://github.com/digital-asset/daml/issues/12663 participant user management: Emulating no-pagination
|
||||
def listAllUsers(client: LedgerClient) =
|
||||
client.userManagementClient
|
||||
.listUsers(token = None /* set at startup */, pageToken = "", pageSize = 10000)
|
||||
.map { case (users, _) =>
|
||||
users.toList
|
||||
}
|
||||
|
||||
hasUserManagement
|
||||
.flatMap {
|
||||
case true =>
|
||||
listAllUsers(state.ledgerClient)
|
||||
.map(details => UpdatedUsers(details): Any)
|
||||
// ^^^^^
|
||||
// make wartremover happy about the lub of UpdatedUsers and UpdatedParties
|
||||
case false =>
|
||||
state.ledgerClient.partyManagementClient
|
||||
.listKnownParties()
|
||||
.map(details => UpdatedParties(details): Any)
|
||||
}
|
||||
.pipeTo(self)
|
||||
|
||||
()
|
||||
|
||||
case UpdatedUsers(users) =>
|
||||
// Note: you cannot log in as a user without a primary party
|
||||
val usersWithPrimaryParties = users.flatMap { user =>
|
||||
user.primaryParty.map(p => (user.id, p))
|
||||
}
|
||||
|
||||
users.filter(_.primaryParty.isEmpty) match {
|
||||
case usersWithoutPrimaryParty if !usersWithoutPrimaryParty.isEmpty =>
|
||||
log.warning(
|
||||
s"Users without primary party (counted ${usersWithoutPrimaryParty.length})" +
|
||||
s" cannot be used for login ${usersWithoutPrimaryParty.take(10).map(_.id.toString).mkString("(e.g., ", ", ", ")")}."
|
||||
)
|
||||
}
|
||||
|
||||
usersWithPrimaryParties.foreach { case (userId, party) =>
|
||||
self ! Subscribe(userId, ApiTypes.Party(party))
|
||||
}
|
||||
|
||||
case UpdatedParties(details) =>
|
||||
details.foreach { partyDetails =>
|
||||
if (partyDetails.isLocal) {
|
||||
val displayName = partyDetails.displayName.getOrElse(partyDetails.party)
|
||||
self ! Subscribe(displayName, ApiTypes.Party(partyDetails.party))
|
||||
} else {
|
||||
log.debug(s"Ignoring non-local party ${partyDetails.party}")
|
||||
}
|
||||
}
|
||||
|
||||
case Subscribe(displayName, name, userRole, useDatabase) =>
|
||||
if (!state.parties.contains(displayName)) {
|
||||
val partyState =
|
||||
new PartyState(name, userRole, useDatabase) // do this allocation only once per party
|
||||
log.info(s"Starting actor for party ${partyState.name} (display name $displayName)")
|
||||
|
||||
// start party actor if needed (since users subscribe to their primary party,
|
||||
// we may subscribe to the same party under different display names, but we should only create one actor per party)
|
||||
val partyActorName = partyState.actorName
|
||||
if (context.child(partyActorName).isEmpty)
|
||||
context.actorOf(
|
||||
PlatformSubscriber.props(state.ledgerClient, partyState, applicationId, token),
|
||||
partyActorName,
|
||||
)
|
||||
|
||||
val updatedParties = state.parties + (displayName -> partyState)
|
||||
context.become(connected(state.copy(parties = updatedParties)))
|
||||
} else {
|
||||
log.debug(s"Actor for party $name (display name $displayName) is already running")
|
||||
}
|
||||
|
||||
case CreateContract(party, templateId, value) =>
|
||||
createContract(state.time.time.getCurrentTime, party, templateId, value, sender())
|
||||
|
||||
case ExerciseChoice(party, contractId, interfaceId, choiceId, value) =>
|
||||
exerciseChoice(
|
||||
state.time.time.getCurrentTime,
|
||||
party,
|
||||
contractId,
|
||||
interfaceId,
|
||||
choiceId,
|
||||
value,
|
||||
sender(),
|
||||
)
|
||||
|
||||
case GetParties(search) =>
|
||||
val lowerCaseSearch = search.toLowerCase
|
||||
val result = state.parties.values.view.collect {
|
||||
case party if party.name.unwrap.toLowerCase.contains(lowerCaseSearch) => party.name
|
||||
}
|
||||
sender() ! PartyList(result.toList)
|
||||
|
||||
case ReportCurrentTime =>
|
||||
sender() ! Success(state.time)
|
||||
|
||||
case AdvanceTime(to) =>
|
||||
advanceTime(state.staticTime, to, sender())
|
||||
|
||||
case ResetConnection =>
|
||||
// Wait for all children to stop, then initiate new connection
|
||||
implicit val actorTimeout: Timeout = Timeout(60, TimeUnit.SECONDS)
|
||||
context.children.foreach(child => child ? ResetConnection)
|
||||
context.become(connecting(StateInitial(state.parties)))
|
||||
connect()
|
||||
|
||||
case GetApplicationStateInfo =>
|
||||
implicit val actorTimeout: Timeout = Timeout(5, TimeUnit.SECONDS)
|
||||
|
||||
// Store the original sender (sender is mutable)
|
||||
val snd = sender()
|
||||
|
||||
// context.child must be invoked from actor thread
|
||||
val userIdToActorRef = state.parties.view
|
||||
.mapValues(partyState => context.child(partyState.actorName))
|
||||
.toList // TODO can we keep this a map and make traverse work?
|
||||
|
||||
Future
|
||||
.traverse(userIdToActorRef) { case (userId, actorRef) =>
|
||||
// let Future deal with empty option actorRef
|
||||
Future { actorRef.get }
|
||||
.flatMap(_ ? GetPartyActorInfo)
|
||||
.mapTo[PartyActorInfo]
|
||||
.map(info => PartyActorRunning(info): PartyActorResponse)
|
||||
.recover { case _ => PartyActorUnresponsive }
|
||||
.map((userId, _))
|
||||
}
|
||||
.map(_.toMap)
|
||||
.recover { case error =>
|
||||
log.error(error.getMessage)
|
||||
Map.empty[String, PartyActorResponse]
|
||||
}
|
||||
.foreach { userIdToPartyActorResponse =>
|
||||
snd ! ApplicationStateConnected(
|
||||
platformHost,
|
||||
platformPort,
|
||||
tlsConfig.exists(_.enabled),
|
||||
applicationId,
|
||||
state.ledgerClient.ledgerId.unwrap,
|
||||
state.time,
|
||||
userIdToPartyActorResponse,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Permanently failed state
|
||||
def failed(state: StateFailed): Receive = {
|
||||
case GetApplicationStateInfo =>
|
||||
sender() ! ApplicationStateFailed(
|
||||
platformHost,
|
||||
platformPort,
|
||||
tlsConfig.exists(_.enabled),
|
||||
applicationId,
|
||||
state.error,
|
||||
)
|
||||
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
private def sslContext: Option[SslContext] =
|
||||
tlsConfig.flatMap { c =>
|
||||
if (c.enabled)
|
||||
Some(
|
||||
GrpcSslContexts
|
||||
.forClient()
|
||||
.trustManager(c.trustCollectionFile.orNull)
|
||||
.keyManager(c.certChainFile.orNull, c.privateKeyFile.orNull)
|
||||
.build
|
||||
)
|
||||
else None
|
||||
}
|
||||
|
||||
private def connect(): Unit = {
|
||||
val retryMaxAttempts = 10
|
||||
val retryDelay = 5.seconds
|
||||
val maxCommandsInFlight = 10
|
||||
val maxParallelSubmissions = 10
|
||||
|
||||
val clientConfig = LedgerClientConfiguration(
|
||||
applicationId,
|
||||
LedgerIdRequirement.none,
|
||||
CommandClientConfiguration(
|
||||
maxCommandsInFlight,
|
||||
maxParallelSubmissions,
|
||||
Duration.ofSeconds(30),
|
||||
),
|
||||
token,
|
||||
)
|
||||
|
||||
val channelConfig = LedgerClientChannelConfiguration(sslContext)
|
||||
|
||||
val result =
|
||||
RetryHelper.retry(retryMaxAttempts, retryDelay)(RetryHelper.failFastOnPermissionDenied)(
|
||||
tryConnect(clientConfig, channelConfig)
|
||||
)
|
||||
|
||||
result onComplete {
|
||||
case Failure(error) =>
|
||||
log.error(
|
||||
"Failed to connect to platform at '{}:{}': {}",
|
||||
platformHost,
|
||||
platformPort,
|
||||
error.getMessage,
|
||||
)
|
||||
self ! PlatformStore.Connected(Failure(error))
|
||||
case Success(connection) =>
|
||||
log.info("Connected to platform at '{}:{}'", platformHost, platformPort)
|
||||
self ! PlatformStore.Connected(Success(connection))
|
||||
}
|
||||
}
|
||||
|
||||
private def tryConnect(
|
||||
clientConfig: LedgerClientConfiguration,
|
||||
channelConfig: LedgerClientChannelConfiguration,
|
||||
): Future[ConnectionResult] = {
|
||||
|
||||
if (channelConfig.sslContext.isDefined) {
|
||||
log.info("Connecting to {}:{}, using TLS", platformHost, platformPort)
|
||||
} else {
|
||||
log.info("Connecting to {}:{}, using a plaintext connection", platformHost, platformPort)
|
||||
}
|
||||
|
||||
for {
|
||||
ledgerClient <- LedgerClient.singleHost(
|
||||
platformHost,
|
||||
platformPort,
|
||||
clientConfig,
|
||||
channelConfig.copy(maxInboundMessageSize = ledgerMaxInbound),
|
||||
)
|
||||
staticTime <- getStaticTime(ledgerClient.channel, ledgerClient.ledgerId.unwrap)
|
||||
time <- getTimeProvider(staticTime)
|
||||
} yield ConnectionResult(ledgerClient, staticTime, time)
|
||||
}
|
||||
|
||||
private def getStaticTime(channel: Channel, ledgerId: String): Future[Option[StaticTime]] = {
|
||||
// Note: StaticTime is a TimeProvider that is automatically updated by push events from the ledger.
|
||||
Future
|
||||
.fromTry(Try(TimeServiceGrpc.stub(channel)))
|
||||
.flatMap(tp => StaticTime.updatedVia(tp, ledgerId, token))
|
||||
.map(staticTime => {
|
||||
log.info(s"Time service is available, platform time is ${staticTime.getCurrentTime}")
|
||||
Some(staticTime)
|
||||
})
|
||||
.recover({
|
||||
// If the time service is not implemented, then the ledger uses UTC time.
|
||||
case GrpcException.UNIMPLEMENTED() =>
|
||||
log.info("Time service is not implemented")
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
private def getTimeProvider(ledgerTime: Option[StaticTime]): Future[TimeProviderWithType] = {
|
||||
TimeProviderFactory(timeProviderType, ledgerTime)
|
||||
.fold[Future[TimeProviderWithType]]({
|
||||
log.error("Unable to initialize the time provider")
|
||||
Future.failed(StoreException("Unable to initialize the time provider"))
|
||||
})(t => {
|
||||
log.debug(s"Time provider initialized: type=${t.`type`}, time=${t.time.getCurrentTime}")
|
||||
Future.successful(t)
|
||||
})
|
||||
}
|
||||
|
||||
private def advanceTime(staticTime: Option[StaticTime], to: Instant, sender: ActorRef): Unit = {
|
||||
staticTime.fold[Unit](
|
||||
sender ! Failure(StoreException("staticTime not available"))
|
||||
)(t => {
|
||||
log.info("Advancing time from {} to {}.", t.getCurrentTime, to)
|
||||
t.setTime(to)
|
||||
.map(_ => TimeProviderWithType(t, TimeProviderType.Static))
|
||||
.recoverWith(apiFailure)
|
||||
.andThen(returnToSender[TimeProviderWithType](sender))
|
||||
()
|
||||
})
|
||||
}
|
||||
|
||||
private def createContract(
|
||||
platformTime: Instant,
|
||||
party: PartyState,
|
||||
templateId: TemplateStringId,
|
||||
value: ApiRecord,
|
||||
sender: ActorRef,
|
||||
): Unit = {
|
||||
val commandId = commandIdGenerator.generateRandom
|
||||
val workflowId = workflowIdGenerator.generateRandom
|
||||
val index = commandIndex.incrementAndGet()
|
||||
|
||||
parseTemplateOpaqueIdentifier(templateId).fold({
|
||||
val msg = s"Create contract command not sent, '$templateId' is not a valid Daml-LF identifier"
|
||||
log.warning(msg)
|
||||
sender ! Failure(StoreException(msg))
|
||||
})(id => {
|
||||
val command = CreateCommand(commandId, index, workflowId, platformTime, id, value)
|
||||
submitCommand(party, command, sender)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private def exerciseChoice(
|
||||
platformTime: Instant,
|
||||
party: PartyState,
|
||||
contractId: ApiTypes.ContractId,
|
||||
interfaceId: Option[InterfaceStringId],
|
||||
choice: ApiTypes.Choice,
|
||||
value: ApiValue,
|
||||
sender: ActorRef,
|
||||
): Unit = {
|
||||
val commandId = commandIdGenerator.generateRandom
|
||||
val workflowId = workflowIdGenerator.generateRandom
|
||||
val index = commandIndex.incrementAndGet()
|
||||
// Note: the ledger API does not need the template ID to submit an exercise command.
|
||||
// However, Navigator needs it to serialize/deserialize the choice argument, so we look up the template here.
|
||||
party.ledger
|
||||
.contract(contractId, party.packageRegistry)
|
||||
.fold({
|
||||
val msg = s"Exercise contract command not sent, contract $contractId not found"
|
||||
log.warning(msg)
|
||||
sender ! Failure(StoreException(msg))
|
||||
})(contract => {
|
||||
val command =
|
||||
ExerciseCommand(
|
||||
commandId,
|
||||
index,
|
||||
workflowId,
|
||||
platformTime,
|
||||
contractId,
|
||||
contract.template.id,
|
||||
interfaceId.flatMap(parseInterfaceOpaqueIdentifier),
|
||||
choice,
|
||||
value,
|
||||
)
|
||||
|
||||
submitCommand(party, command, sender)
|
||||
})
|
||||
}
|
||||
|
||||
private def submitCommand(partyState: PartyState, command: Command, sender: ActorRef): Unit = {
|
||||
// Each party has its own command completion stream.
|
||||
// Forward the request to the party actor, so that it can be tracked.
|
||||
context
|
||||
.child(partyState.actorName)
|
||||
.foreach(child => child ! PlatformSubscriber.SubmitCommand(command, sender))
|
||||
}
|
||||
|
||||
private val idGeneratorSeed = System.currentTimeMillis()
|
||||
private val workflowIdGenerator: IdGenerator[ApiTypes.WorkflowIdTag] = new IdGenerator(
|
||||
idGeneratorSeed
|
||||
)
|
||||
private val commandIdGenerator: IdGenerator[ApiTypes.CommandIdTag] = new IdGenerator(
|
||||
idGeneratorSeed + 1
|
||||
)
|
||||
private val commandIndex = new AtomicLong(0)
|
||||
|
||||
private def returnToSender[T](sender: ActorRef): PartialFunction[Try[T], Unit] = {
|
||||
case Success(e) =>
|
||||
log.debug(s"Sending Success($e) to $sender")
|
||||
sender ! Success(e)
|
||||
case Failure(f) =>
|
||||
log.debug(s"Sending Failure($f) to $sender")
|
||||
sender ! Failure(f)
|
||||
}
|
||||
|
||||
private def apiFailure[T]: PartialFunction[Throwable, Future[T]] = { case exception: Exception =>
|
||||
log.error("Unable to perform API operation: {}", exception.getMessage)
|
||||
Future.failed(StoreException(exception.getMessage))
|
||||
}
|
||||
|
||||
}
|
@ -1,383 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.store.platform
|
||||
|
||||
import org.apache.pekko.NotUsed
|
||||
import org.apache.pekko.actor.{Actor, ActorLogging, ActorRef, Props, Stash}
|
||||
import org.apache.pekko.stream._
|
||||
import org.apache.pekko.stream.scaladsl.{Sink, Source, SourceQueueWithComplete}
|
||||
import com.daml.ledger.api.v1
|
||||
import com.daml.ledger.client.LedgerClient
|
||||
import com.daml.ledger.client.services.commands.CommandSubmission
|
||||
import com.daml.ledger.client.services.commands.tracker.CompletionResponse
|
||||
import com.daml.lf.archive.ArchivePayloadParser
|
||||
import com.daml.lf.data.{Ref => DamlLfRef}
|
||||
import com.daml.lf.typesig.reader.{Errors, SignatureReader}
|
||||
import com.daml.navigator.model._
|
||||
import com.daml.navigator.model.converter.TypeNotFoundError
|
||||
import com.daml.navigator.store.Store._
|
||||
import com.daml.util.Ctx
|
||||
import com.google.rpc.code
|
||||
import scalaz.Tag
|
||||
import scalaz.syntax.tag._
|
||||
|
||||
import scala.annotation.nowarn
|
||||
import scala.concurrent.Future
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
object PlatformSubscriber {
|
||||
// Actor messages
|
||||
case object ConnectionReset
|
||||
case class Started(commandTracker: TrackCommandsSource)
|
||||
case class SubmitCommand(command: Command, sender: ActorRef)
|
||||
|
||||
// Actor state
|
||||
case class StateRunning(commandTracker: TrackCommandsSource)
|
||||
|
||||
type TrackCommandsSource = SourceQueueWithComplete[Ctx[Command, CommandSubmission]]
|
||||
|
||||
def props(
|
||||
ledgerClient: LedgerClient,
|
||||
party: PartyState,
|
||||
applicationId: DamlLfRef.LedgerString,
|
||||
token: Option[String],
|
||||
) =
|
||||
Props(classOf[PlatformSubscriber], ledgerClient, party, applicationId, token)
|
||||
}
|
||||
|
||||
/** Actor subscribing to platform event stream of a single DA party. */
|
||||
@SuppressWarnings(Array("org.wartremover.warts.Any"))
|
||||
class PlatformSubscriber(
|
||||
ledgerClient: LedgerClient,
|
||||
party: PartyState,
|
||||
applicationId: DamlLfRef.LedgerString,
|
||||
token: Option[String],
|
||||
) extends Actor
|
||||
with ActorLogging
|
||||
with Stash {
|
||||
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// Global immutable state - mutable state is stored in parameters of the receive methods
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
private val system = context.system
|
||||
private val killSwitch = KillSwitches.shared("platform-subscriber")
|
||||
|
||||
import system.dispatcher
|
||||
|
||||
implicit val materializer: Materializer = Materializer(system)
|
||||
import PlatformSubscriber._
|
||||
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
override def preStart(): Unit = {
|
||||
log.debug("Starting actor for '{}'", party.name)
|
||||
|
||||
val started = for {
|
||||
_ <- fetchPackages(ledgerClient)
|
||||
tracker <- startTrackingCommands()
|
||||
_ <- startStreamingTransactions()
|
||||
} yield {
|
||||
Started(tracker)
|
||||
}
|
||||
|
||||
started onComplete {
|
||||
case Success(value) =>
|
||||
log.debug("Started actor for '{}'", party.name)
|
||||
self ! value
|
||||
case Failure(error) =>
|
||||
// Failed to start up, giving up
|
||||
log.error(
|
||||
"Failed to start actor for party '{}': {}. Please fix any issues and restart this application.",
|
||||
party.name,
|
||||
error,
|
||||
)
|
||||
context.become(failed(error))
|
||||
}
|
||||
}
|
||||
|
||||
override def postStop(): Unit = {
|
||||
log.debug("Stopped actor for '{}'", party.name)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// Messages
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
override def receive: Receive = initial
|
||||
|
||||
def initial: Receive = {
|
||||
case Started(tracker) =>
|
||||
context.become(running(StateRunning(tracker)))
|
||||
unstashAll()
|
||||
|
||||
case GetPartyActorInfo =>
|
||||
sender() ! PartyActorStarting(party)
|
||||
|
||||
case _ =>
|
||||
stash()
|
||||
}
|
||||
|
||||
def running(state: StateRunning): Receive = {
|
||||
case ResetConnection =>
|
||||
log.debug("Resetting connection for '{}'", party.name)
|
||||
killSwitch.shutdown()
|
||||
context.stop(self)
|
||||
sender() ! ConnectionReset
|
||||
|
||||
case SubmitCommand(command, commandSender) =>
|
||||
// Submit command and reply to
|
||||
submitCommand(ledgerClient, state.commandTracker, party, command, commandSender)
|
||||
|
||||
case GetPartyActorInfo =>
|
||||
sender() ! PartyActorStarted(party)
|
||||
}
|
||||
|
||||
// Permanently failed state
|
||||
def failed(error: Throwable): Receive = {
|
||||
case GetApplicationStateInfo =>
|
||||
sender() ! PartyActorFailed(party, error)
|
||||
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
|
||||
/** This has two side effects:
|
||||
* - each converted transaction is added to PartyState.ledger
|
||||
* - if packages need to be re-fetched, PartyState.packageRegistry is updated
|
||||
*/
|
||||
private def processTransaction[Tx](
|
||||
id: String,
|
||||
transaction: Tx,
|
||||
reader: (Tx, converter.LedgerApiV1.Context) => Either[converter.ConversionError, Transaction],
|
||||
): Future[NotUsed] = {
|
||||
def go(retryMissingTemplate: Boolean): Future[NotUsed] = {
|
||||
val ttx =
|
||||
reader(transaction, converter.LedgerApiV1.Context(party.name, party.packageRegistry))
|
||||
ttx match {
|
||||
case Right(tx) =>
|
||||
party.addLatestTransaction(tx)
|
||||
Future.successful(NotUsed)
|
||||
case Left(e: TypeNotFoundError) =>
|
||||
if (retryMissingTemplate) {
|
||||
log.info(
|
||||
"Template '{}' not found while processing transaction '{}', retrying after re-fetching packages.",
|
||||
e.id,
|
||||
id,
|
||||
)
|
||||
fetchPackages(ledgerClient)
|
||||
.flatMap(_ => go(false))
|
||||
} else {
|
||||
Future.failed(e)
|
||||
}
|
||||
case Left(e) =>
|
||||
Future.failed(e)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
go(true).recoverWith { case e: Throwable =>
|
||||
log.error(
|
||||
"Error processing transaction {}: {}. Its effects will not be visible.",
|
||||
e.getMessage,
|
||||
id,
|
||||
)
|
||||
Future.failed(e)
|
||||
}
|
||||
} catch {
|
||||
case e: Throwable =>
|
||||
log.error(
|
||||
"Error processing transaction {}: {}. Its effects will not be visible.",
|
||||
e.getMessage,
|
||||
id,
|
||||
)
|
||||
Future.failed(e)
|
||||
}
|
||||
}
|
||||
|
||||
private def startStreamingTransactions(): Future[Unit] = {
|
||||
val ledgerBegin = v1.ledger_offset.LedgerOffset(
|
||||
v1.ledger_offset.LedgerOffset.Value
|
||||
.Boundary(v1.ledger_offset.LedgerOffset.LedgerBoundary.LEDGER_BEGIN)
|
||||
)
|
||||
val transactionFilter = v1.transaction_filter.TransactionFilter(
|
||||
Map(Tag.unwrap(party.name) -> v1.transaction_filter.Filters(None))
|
||||
)
|
||||
|
||||
// Create a source (transactions stream from ledger)
|
||||
val treeSource: Source[NotUsed, NotUsed] =
|
||||
ledgerClient.transactionClient
|
||||
.getTransactionTrees(ledgerBegin, None, transactionFilter, token = token)
|
||||
.mapAsync(1)(tx =>
|
||||
processTransaction(tx.transactionId, tx, converter.LedgerApiV1.readTransactionTree)
|
||||
)
|
||||
|
||||
// Run the stream.
|
||||
// The stream runs independently of the actor. Add a kill switch to terminate the stream when actor stops.
|
||||
// Note: processTransaction changes the PartyState as a side effect. The stream output can be ignored.
|
||||
treeSource
|
||||
.via(killSwitch.flow)
|
||||
.runWith(Sink.ignore)
|
||||
|
||||
// This stream starts immediately
|
||||
Future.unit
|
||||
}
|
||||
|
||||
@nowarn("msg=method dropNew in object OverflowStrategy is deprecated")
|
||||
private def startTrackingCommands()
|
||||
: Future[SourceQueueWithComplete[Ctx[Command, CommandSubmission]]] = {
|
||||
for {
|
||||
commandTracker <- ledgerClient.commandClient
|
||||
.trackCommands[Command](List(Tag.unwrap(party.name)), token)
|
||||
} yield
|
||||
// Note: this uses a buffer of 1000 commands awaiting handling by the command client.
|
||||
// The command client itself can process (config.maxParallelSubmissions) commands in parallel.
|
||||
// In the highly unlikely case both buffers are full, the command submission will fail immediately.
|
||||
Source
|
||||
.queue[Ctx[Command, CommandSubmission]](1000, OverflowStrategy.dropNew)
|
||||
.via(commandTracker)
|
||||
.map(result => {
|
||||
val commandId = result.context.id
|
||||
result.value match {
|
||||
case Right(_) =>
|
||||
// Command completed with success.
|
||||
// Don't update anything, state will be updated from the transaction stream instead.
|
||||
// This is because the completion stream does not contain information to correlate command id with transaction id.
|
||||
log.info("Command '{}' completed successfully.", commandId)
|
||||
case Left(error) =>
|
||||
error match {
|
||||
case notOk: CompletionResponse.NotOkResponse =>
|
||||
// Command completed with an error
|
||||
val status = CommandStatusError(
|
||||
code.Code.fromValue(notOk.grpcStatus.code).toString(),
|
||||
notOk.grpcStatus.message,
|
||||
)
|
||||
party.addCommandStatus(commandId, status)
|
||||
log.info("Command '{}' completed with status '{}'.", commandId, status)
|
||||
case CompletionResponse.TimeoutResponse(_) =>
|
||||
val status = CommandStatusError(code.Code.ABORTED.toString(), "Timeout")
|
||||
party.addCommandStatus(commandId, status)
|
||||
log.info("Command '{}' completed with status '{}'.", commandId, status)
|
||||
case CompletionResponse.NoStatusInResponse(_, _) =>
|
||||
party.addCommandStatus(commandId, CommandStatusUnknown())
|
||||
log.error(
|
||||
"Command tracking failed. Status unknown for command '{}': {}.",
|
||||
commandId,
|
||||
error,
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
.to(Sink.ignore)
|
||||
.run()
|
||||
}
|
||||
|
||||
/*
|
||||
Note about packages:
|
||||
- There may be packages uploaded at a later time
|
||||
- All parties see the same list of packages
|
||||
- The packages associated to package ids will never change
|
||||
- The package set should probably only grow, but it's not codified anywhere
|
||||
|
||||
Whenever the client sees a template that it does not recognize in the transaction stream,
|
||||
it can re-fetch the packages from the server to get the metadata it needs to make sense of them.
|
||||
*/
|
||||
private def fetchPackages(ledgerClient: LedgerClient): Future[Unit] = {
|
||||
ledgerClient.packageClient
|
||||
.listPackages(token)
|
||||
.flatMap(response => {
|
||||
Future.traverse(response.packageIds)(id => {
|
||||
party.packageRegistry.pack(DamlLfRef.PackageId.assertFromString(id)) match {
|
||||
case Some(_) =>
|
||||
Future.successful(None)
|
||||
case None =>
|
||||
ledgerClient.packageClient.getPackage(id, token).map(Some(_))
|
||||
}
|
||||
})
|
||||
})
|
||||
.flatMap(responses0 => {
|
||||
val interfaces = responses0
|
||||
.collect { case Some(resp) => resp }
|
||||
.map(decodePackage)
|
||||
.toList
|
||||
party.addPackages(interfaces)
|
||||
log.info(
|
||||
"Successfully loaded packages {}",
|
||||
interfaces.map(_.packageId).mkString("[", ", ", "]"),
|
||||
)
|
||||
Future.unit
|
||||
})
|
||||
.recoverWith(apiFailureF)
|
||||
}
|
||||
|
||||
private def decodePackage(res: v1.package_service.GetPackageResponse) = {
|
||||
val payload = ArchivePayloadParser.assertFromByteString(res.archivePayload)
|
||||
val (errors, out) =
|
||||
SignatureReader.readPackageSignature(DamlLfRef.PackageId.assertFromString(res.hash), payload)
|
||||
if (!errors.equals(Errors.zeroErrors)) {
|
||||
log.error("Errors loading package {}: {}", res.hash, errors.toString)
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
private def submitCommand(
|
||||
ledgerClient: LedgerClient,
|
||||
commandTracker: TrackCommandsSource,
|
||||
party: PartyState,
|
||||
command: Command,
|
||||
sender: ActorRef,
|
||||
): Unit = {
|
||||
// Convert to ledger API command
|
||||
converter.LedgerApiV1
|
||||
.writeCommands(party, command, ledgerClient.ledgerId.unwrap, applicationId)
|
||||
.fold[Unit](
|
||||
error => {
|
||||
// Failed to convert command. Most likely, the argument is incomplete.
|
||||
sender ! Failure(error)
|
||||
},
|
||||
commands => {
|
||||
import org.apache.pekko.stream.{QueueOfferResult => QOR}
|
||||
|
||||
// Store the command
|
||||
party.addCommand(command)
|
||||
|
||||
// Send command to ledger
|
||||
commandTracker
|
||||
.offer(Ctx(command, CommandSubmission(commands)))
|
||||
.andThen {
|
||||
case Success(QOR.Dropped) =>
|
||||
party.addCommandStatus(
|
||||
command.id,
|
||||
CommandStatusError("INTERNAL", "Command submission failed: buffer full"),
|
||||
)
|
||||
case Success(QOR.QueueClosed) =>
|
||||
party.addCommandStatus(
|
||||
command.id,
|
||||
CommandStatusError("INTERNAL", "Command submission failed: queue closed"),
|
||||
)
|
||||
case Success(QOR.Failure(e)) =>
|
||||
party.addCommandStatus(
|
||||
command.id,
|
||||
CommandStatusError("INTERNAL", s"Command submission failed: $e"),
|
||||
)
|
||||
case Failure(e) =>
|
||||
party.addCommandStatus(
|
||||
command.id,
|
||||
CommandStatusError("INTERNAL", s"Command submission failed: $e"),
|
||||
)
|
||||
}
|
||||
|
||||
// Immediately return the command ID
|
||||
sender ! Success(command.id)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private def apiFailureF[T]: PartialFunction[Throwable, Future[T]] = { case exception: Exception =>
|
||||
log.error("Unable to perform API operation: {}", exception.getMessage)
|
||||
Future.failed(StoreException(exception.getMessage))
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.time
|
||||
|
||||
import java.time.Duration
|
||||
|
||||
import com.daml.api.util.TimeProvider
|
||||
import com.daml.ledger.client.services.testing.time.StaticTime
|
||||
|
||||
case class TimeProviderWithType(time: TimeProvider, `type`: TimeProviderType)
|
||||
|
||||
object TimeProviderFactory {
|
||||
|
||||
def apply(
|
||||
timeProviderType: TimeProviderType,
|
||||
ledgerTime: Option[StaticTime],
|
||||
): Option[TimeProviderWithType] =
|
||||
timeProviderType match {
|
||||
case TimeProviderType.Auto =>
|
||||
ledgerTime.fold(
|
||||
Some(TimeProviderWithType(TimeProvider.UTC, TimeProviderType.WallClock))
|
||||
)(t => Some(TimeProviderWithType(t, TimeProviderType.Static)))
|
||||
case TimeProviderType.WallClock =>
|
||||
Some(TimeProviderWithType(TimeProvider.UTC, TimeProviderType.WallClock))
|
||||
case TimeProviderType.Static =>
|
||||
ledgerTime.map(t => TimeProviderWithType(t, TimeProviderType.Static))
|
||||
case TimeProviderType.Simulated =>
|
||||
ledgerTime.map(lt => {
|
||||
val utc: TimeProvider = TimeProvider.UTC
|
||||
val diff = Duration.between(lt.getCurrentTime, utc.getCurrentTime)
|
||||
TimeProviderWithType(
|
||||
TimeProvider.MappedTimeProvider(utc, i => i minus diff),
|
||||
TimeProviderType.Simulated,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.time
|
||||
|
||||
import scopt.Read
|
||||
|
||||
sealed abstract class TimeProviderType extends Product with Serializable {
|
||||
def name: String
|
||||
}
|
||||
object TimeProviderType {
|
||||
|
||||
case object Auto extends TimeProviderType {
|
||||
val name = "auto"
|
||||
}
|
||||
case object Static extends TimeProviderType {
|
||||
val name = "static"
|
||||
}
|
||||
case object Simulated extends TimeProviderType {
|
||||
val name = "simulated"
|
||||
}
|
||||
case object WallClock extends TimeProviderType {
|
||||
val name = "wallclock"
|
||||
}
|
||||
|
||||
def unapply(s: String): Option[TimeProviderType] =
|
||||
if (s.compareToIgnoreCase(Auto.name) == 0) Some(Auto)
|
||||
else if (s.compareToIgnoreCase(Static.name) == 0) Some(Static)
|
||||
else if (s.compareToIgnoreCase(Simulated.name) == 0) Some(Simulated)
|
||||
else if (s.compareToIgnoreCase(WallClock.name) == 0) Some(WallClock)
|
||||
else None
|
||||
|
||||
def write(tpt: TimeProviderType): String = tpt.name
|
||||
|
||||
def acceptedValues: List[String] =
|
||||
List(Auto, Static, Simulated, WallClock).map(_.name)
|
||||
|
||||
implicit val read: Read[TimeProviderType] = new Read[TimeProviderType] {
|
||||
override def arity: Int = 1
|
||||
|
||||
override def reads: String => TimeProviderType = { s =>
|
||||
unapply(s).getOrElse(
|
||||
throw new IllegalArgumentException(
|
||||
s"$s is not a valid time provider. The following values are accepted: ${acceptedValues
|
||||
.mkString(", ")}."
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.util
|
||||
|
||||
import java.lang.Math.floor
|
||||
|
||||
import org.apache.pekko.actor.Scheduler
|
||||
import org.apache.pekko.pattern.after
|
||||
import com.daml.grpc.GrpcException
|
||||
import com.typesafe.scalalogging.LazyLogging
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
/** Configuration values for initial service binding retrial
|
||||
*/
|
||||
trait IRetryConfig {
|
||||
|
||||
/** @return The interval between retries.
|
||||
*/
|
||||
def intervalMs: Long
|
||||
def interval: FiniteDuration = intervalMs.millis
|
||||
|
||||
/** @return The total timeout we allow for the operation to succeed.
|
||||
*/
|
||||
def timeoutMs: Long
|
||||
def timeout: FiniteDuration = timeoutMs.millis
|
||||
}
|
||||
|
||||
object RetryHelper extends LazyLogging {
|
||||
|
||||
/** Return '''true''' if you want to re-try a statement that caused the specified exception.
|
||||
*/
|
||||
type RetryStrategy = PartialFunction[Throwable, Boolean]
|
||||
|
||||
/** Always retries if exception is `NonFatal`.
|
||||
*/
|
||||
val always: RetryStrategy = { case NonFatal(_) =>
|
||||
true
|
||||
}
|
||||
|
||||
val failFastOnPermissionDenied: RetryStrategy = {
|
||||
case GrpcException.PERMISSION_DENIED() => false
|
||||
case NonFatal(_) => true
|
||||
}
|
||||
|
||||
def retry[T](
|
||||
retryConfig: Option[(Scheduler, IRetryConfig)]
|
||||
)(retryStrategy: RetryStrategy)(f: => Future[T])(implicit ec: ExecutionContext): Future[T] = {
|
||||
retryConfig match {
|
||||
case None =>
|
||||
f
|
||||
case Some(rc) =>
|
||||
implicit val scheduler: Scheduler = rc._1
|
||||
retry(Option(rc._2))(retryStrategy)(f)
|
||||
}
|
||||
}
|
||||
|
||||
def retry[T](retryConfig: Option[IRetryConfig])(
|
||||
retryStrategy: RetryStrategy
|
||||
)(f: => Future[T])(implicit ec: ExecutionContext, s: Scheduler): Future[T] = {
|
||||
retryConfig match {
|
||||
case None =>
|
||||
f
|
||||
case Some(rc) =>
|
||||
val maxAttempts = floor(rc.timeout / rc.interval).toInt
|
||||
retry(maxAttempts, rc.interval)(retryStrategy)(f)
|
||||
}
|
||||
}
|
||||
|
||||
def retry[T](maxAttempts: Int, delay: FiniteDuration)(
|
||||
retryStrategy: RetryStrategy
|
||||
)(f: => Future[T])(implicit ec: ExecutionContext, s: Scheduler): Future[T] = {
|
||||
|
||||
def shouldRetry(n: Int, e: Throwable): Boolean =
|
||||
n > 0 && retryStrategy.applyOrElse(e, (_: Throwable) => false)
|
||||
|
||||
val remainingAttempts = maxAttempts - 1 // the next line will trigger a future evaluation
|
||||
|
||||
f.recoverWith {
|
||||
case NonFatal(e) if shouldRetry(remainingAttempts, e) =>
|
||||
logWarning(remainingAttempts, e)
|
||||
after(delay, s)(retry(remainingAttempts, delay)(retryStrategy)(f))
|
||||
}
|
||||
}
|
||||
|
||||
private def logWarning(remainingAttempts: Int, e: Throwable): Unit = {
|
||||
logger.warn(
|
||||
s"Retrying after failure. Attempts remaining: $remainingAttempts. Error: ${e.getMessage}"
|
||||
)
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss} %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
</configuration>
|
@ -1,236 +0,0 @@
|
||||
type Choice {
|
||||
name: String!
|
||||
parameter: DamlLfType!
|
||||
consuming: Boolean!
|
||||
inheritedInterface: String
|
||||
}
|
||||
|
||||
interface Command {
|
||||
id: ID!
|
||||
workflowId: ID!
|
||||
index: Int!
|
||||
platformTime: Time!
|
||||
status: CommandStatus!
|
||||
}
|
||||
|
||||
|
||||
type CommandEdge {
|
||||
node: Command!
|
||||
cursor: String!
|
||||
}
|
||||
|
||||
scalar CommandId
|
||||
|
||||
type CommandPagination {
|
||||
beforeCount: Int!
|
||||
totalCount: Int!
|
||||
sortings: [Sorting!]
|
||||
edges: [CommandEdge!]!
|
||||
}
|
||||
|
||||
interface CommandStatus {
|
||||
completed: Boolean!
|
||||
}
|
||||
|
||||
type CommandStatusWaiting implements CommandStatus
|
||||
|
||||
type CommandStatusSuccess implements CommandStatus {
|
||||
transaction: Transaction!
|
||||
}
|
||||
|
||||
type CommandStatusError implements CommandStatus {
|
||||
code: String!
|
||||
details: String!
|
||||
}
|
||||
|
||||
type CommandStatusUnknown implements CommandStatus
|
||||
|
||||
type Contract implements Node {
|
||||
id: ID!
|
||||
template: Template!
|
||||
createEvent: CreatedEvent!
|
||||
archiveEvent: ExercisedEvent
|
||||
exerciseEvents: [ExercisedEvent!]!
|
||||
argument: DamlLfValueRecord!
|
||||
agreementText: String
|
||||
signatories: [String!]!
|
||||
observers: [String!]!
|
||||
key: DamlLfValue
|
||||
}
|
||||
|
||||
type ContractEdge {
|
||||
node: Contract!
|
||||
cursor: String!
|
||||
}
|
||||
|
||||
type ContractPagination {
|
||||
beforeCount: Int!
|
||||
totalCount: Int!
|
||||
sortings: [Sorting!]
|
||||
edges: [ContractEdge!]!
|
||||
}
|
||||
|
||||
type CreateCommand implements Node & Command {
|
||||
template: Template
|
||||
templateId: String!
|
||||
argument: DamlLfValueRecord!
|
||||
}
|
||||
|
||||
type CreatedEvent implements Node & Event {
|
||||
id: ID!
|
||||
parent: Event
|
||||
transaction: Transaction!
|
||||
witnessParties: [Party!]!
|
||||
workflowId: String!
|
||||
contract: Contract!
|
||||
argument: DamlLfValueRecord!
|
||||
}
|
||||
|
||||
scalar DamlLfDataType
|
||||
|
||||
type DamlLfDefDataType implements Node & DamlLfNode {
|
||||
id: ID!
|
||||
package: String!
|
||||
module: String!
|
||||
name: String!
|
||||
dataType: DamlLfDataType!
|
||||
typeVars: [String!]!
|
||||
dependencies(depth: Int): [DamlLfDefDataType!]!
|
||||
}
|
||||
|
||||
interface DamlLfNode {
|
||||
id: ID!
|
||||
package: String!
|
||||
module: String!
|
||||
name: String!
|
||||
}
|
||||
|
||||
scalar DamlLfType
|
||||
|
||||
scalar DamlLfValue
|
||||
|
||||
scalar DamlLfValueRecord
|
||||
|
||||
enum Direction {
|
||||
ASCENDING
|
||||
DESCENDING
|
||||
}
|
||||
|
||||
interface Event {
|
||||
id: ID!
|
||||
parent: Event
|
||||
transaction: Transaction!
|
||||
witnessParties: [Party!]!
|
||||
workflowId: String!
|
||||
}
|
||||
|
||||
type ExerciseCommand implements Node & Command {
|
||||
contract: Contract
|
||||
contractId: String!
|
||||
interfaceId: String
|
||||
choice: String!
|
||||
argument: DamlLfValue!
|
||||
}
|
||||
|
||||
type ExercisedEvent implements Node & Event {
|
||||
id: ID!
|
||||
parent: Event
|
||||
transaction: Transaction!
|
||||
witnessParties: [Party!]!
|
||||
workflowId: String!
|
||||
contract: Contract!
|
||||
choice: String!
|
||||
argument: DamlLfValue!
|
||||
actingParties: [Party!]!
|
||||
consuming: Boolean!
|
||||
children: [Event!]!
|
||||
}
|
||||
|
||||
input FilterCriterion {
|
||||
field: String!
|
||||
value: String!
|
||||
}
|
||||
|
||||
scalar LedgerOffset
|
||||
|
||||
type LedgerTime {
|
||||
id: ID!
|
||||
time: Time!
|
||||
type: TimeType!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
advanceTime(time: Time!): LedgerTime!
|
||||
create(templateId: ID!, argument: DamlLfValue): CommandId!
|
||||
exercise(contractId: ID!, interfaceId: ID, choiceId: ID!, argument: DamlLfValue): CommandId!
|
||||
}
|
||||
|
||||
interface Node {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
scalar Party
|
||||
|
||||
type Query {
|
||||
parties(search: String): [Party!]!
|
||||
ledgerTime: LedgerTime!
|
||||
latestTransaction: Transaction
|
||||
node(typename: String!, id: ID!): Node
|
||||
nodes(typename: String!, ids: [ID!]!): [Node!]!
|
||||
template(topLevelDecl: String!): [Template!]!
|
||||
commandStatus(id: ID!): CommandStatus
|
||||
templates(search: String, filter: [FilterCriterion!], count: Int, start: String, sort: [SortCriterion!]): TemplatePagination!
|
||||
contracts(search: String, filter: [FilterCriterion!], includeArchived: Boolean, count: Int, start: String, sort: [SortCriterion!]): ContractPagination!
|
||||
commands(search: String, filter: [FilterCriterion!], count: Int, start: String, sort: [SortCriterion!]): CommandPagination!
|
||||
}
|
||||
|
||||
input SortCriterion {
|
||||
field: String!
|
||||
direction: Direction!
|
||||
}
|
||||
|
||||
type Sorting {
|
||||
field: String!
|
||||
direction: Direction!
|
||||
}
|
||||
|
||||
type Template implements Node & DamlLfNode {
|
||||
id: ID!
|
||||
package: String!
|
||||
module: String!
|
||||
name: String!
|
||||
topLevelDecl: String!
|
||||
parameter: DamlLfType!
|
||||
parameterDef: DamlLfDefDataType!
|
||||
choices: [Choice!]!
|
||||
contracts(search: String, filter: [FilterCriterion!], includeArchived: Boolean, count: Int, start: String, sort: [SortCriterion!]): ContractPagination!
|
||||
implementedInterfaces: [String!]!
|
||||
}
|
||||
|
||||
type TemplateEdge {
|
||||
node: Template!
|
||||
cursor: String!
|
||||
}
|
||||
|
||||
type TemplatePagination {
|
||||
beforeCount: Int!
|
||||
totalCount: Int!
|
||||
sortings: [Sorting!]
|
||||
edges: [TemplateEdge!]!
|
||||
}
|
||||
|
||||
scalar Time
|
||||
|
||||
enum TimeType {
|
||||
static
|
||||
wallclock
|
||||
simulated
|
||||
}
|
||||
|
||||
type Transaction implements Node {
|
||||
id: ID!
|
||||
offset: LedgerOffset!
|
||||
effectiveAt: Time!
|
||||
commandId: CommandId
|
||||
events: [Event!]!
|
||||
}
|
@ -1,362 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator
|
||||
|
||||
import com.daml.lf.data.{
|
||||
Decimal => LfDecimal,
|
||||
FrontStack,
|
||||
ImmArray,
|
||||
SortedLookupList,
|
||||
Ref => DamlLfRef,
|
||||
}
|
||||
import com.daml.navigator.model._
|
||||
import com.daml.lf.{typesig => DamlLfIface}
|
||||
import com.daml.lf.value.{Value => V}
|
||||
import com.daml.lf.value.json.ApiValueImplicits._
|
||||
|
||||
import scala.language.implicitConversions
|
||||
|
||||
case class DamlConstants()
|
||||
|
||||
/** Daml related constants usable in tests
|
||||
*/
|
||||
case object DamlConstants {
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Daml-LF: Ids
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
val packageId0 = DamlLfRef.PackageId.assertFromString("hash")
|
||||
|
||||
def defRef(name: String): DamlLfIdentifier = DamlLfIdentifier(
|
||||
packageId0,
|
||||
DamlLfQualifiedName(
|
||||
DamlLfDottedName.assertFromString("module"),
|
||||
DamlLfDottedName.assertFromString(name),
|
||||
),
|
||||
)
|
||||
val ref0: DamlLfIdentifier = defRef("T0")
|
||||
val ref1: DamlLfIdentifier = defRef("T1")
|
||||
val ref2: DamlLfIdentifier = defRef("T2")
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Daml-LF: simple types
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
val simpleTextT = DamlLfTypePrim(DamlLfPrimType.Text, DamlLfImmArraySeq())
|
||||
val simpleInt64T = DamlLfTypePrim(DamlLfPrimType.Int64, DamlLfImmArraySeq())
|
||||
val simpleDecimalT = DamlLfTypeNumeric(LfDecimal.scale)
|
||||
val simpleUnitT = DamlLfTypePrim(DamlLfPrimType.Unit, DamlLfImmArraySeq())
|
||||
val simpleDateT = DamlLfTypePrim(DamlLfPrimType.Date, DamlLfImmArraySeq())
|
||||
val simpleTimestampT = DamlLfTypePrim(DamlLfPrimType.Timestamp, DamlLfImmArraySeq())
|
||||
val simpleBoolT = DamlLfTypePrim(DamlLfPrimType.Bool, DamlLfImmArraySeq())
|
||||
val simplePartyT = DamlLfTypePrim(DamlLfPrimType.Party, DamlLfImmArraySeq())
|
||||
val simpleContractIdT = DamlLfTypePrim(DamlLfPrimType.ContractId, DamlLfImmArraySeq())
|
||||
def simpleOptionalT(typ: DamlLfIface.Type) =
|
||||
DamlLfTypePrim(DamlLfPrimType.Optional, DamlLfImmArraySeq(typ))
|
||||
def simpleListT(typ: DamlLfIface.Type) =
|
||||
DamlLfTypePrim(DamlLfPrimType.List, DamlLfImmArraySeq(typ))
|
||||
def simpleTextMapT(typ: DamlLfIface.Type) =
|
||||
DamlLfTypePrim(DamlLfPrimType.TextMap, DamlLfImmArraySeq(typ))
|
||||
|
||||
val simpleTextV = V.ValueText("foo")
|
||||
val simpleInt64V = V.ValueInt64(100)
|
||||
val simpleDecimalV = V.ValueNumeric(LfDecimal assertFromString "100")
|
||||
val simpleUnitV = V.ValueUnit
|
||||
val simpleDateV = V.ValueDate.fromIso8601("2019-01-28")
|
||||
val simpleTimestampV = V.ValueTimestamp.fromIso8601("2019-01-28T12:44:33.22Z")
|
||||
val simpleOptionalV = V.ValueOptional(Some(V.ValueText("foo")))
|
||||
val simpleTextMapV = V.ValueTextMap(
|
||||
SortedLookupList(Map("1" -> V.ValueInt64(1), "2" -> V.ValueInt64(2), "3" -> V.ValueInt64(3)))
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Daml-LF: empty record
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
val emptyRecordId: DamlLfIdentifier = defRef("EmptyRecord")
|
||||
val emptyRecordGD = DamlLfRecord(DamlLfImmArraySeq())
|
||||
val emptyRecordGC = DamlLfDefDataType(DamlLfImmArraySeq(), emptyRecordGD)
|
||||
val emptyRecordTC = DamlLfTypeCon(DamlLfTypeConName(emptyRecordId), DamlLfImmArraySeq())
|
||||
val emptyRecordT = emptyRecordTC.instantiate(emptyRecordGC).asInstanceOf[DamlLfRecord]
|
||||
val emptyRecordV = V.ValueRecord(Some(emptyRecordId), ImmArray.Empty)
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Daml-LF: simple record (data SimpleRecord a b = {fA: a, fB: b})
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
val simpleRecordId: DamlLfIdentifier = defRef("SimpleRecord")
|
||||
val simpleRecordGD = DamlLfRecord(
|
||||
DamlLfImmArraySeq(
|
||||
name("fA") -> DamlLfTypeVar("a"),
|
||||
name("fB") -> DamlLfTypeVar("b"),
|
||||
)
|
||||
)
|
||||
val simpleRecordGC = DamlLfDefDataType(DamlLfImmArraySeq("a", "b"), simpleRecordGD)
|
||||
val simpleRecordTC = DamlLfTypeCon(
|
||||
DamlLfTypeConName(simpleRecordId),
|
||||
DamlLfImmArraySeq(simpleTextT, simpleInt64T),
|
||||
)
|
||||
val simpleRecordT = simpleRecordTC.instantiate(simpleRecordGC).asInstanceOf[DamlLfRecord]
|
||||
def simpleRecordV =
|
||||
V.ValueRecord(
|
||||
Some(simpleRecordId),
|
||||
ImmArray(
|
||||
(Some(name("fA")), V.ValueText("foo")),
|
||||
(Some(name("fB")), V.ValueInt64(100)),
|
||||
),
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Daml-LF: simple variant (data DamlLfVariant a b = fA a | fB b)
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
val simpleVariantId: DamlLfIdentifier = defRef("SimpleVariant")
|
||||
val simpleVariantGD = DamlLfVariant(
|
||||
DamlLfImmArraySeq(
|
||||
name("fA") -> DamlLfTypeVar("a"),
|
||||
name("fB") -> DamlLfTypeVar("b"),
|
||||
)
|
||||
)
|
||||
val simpleVariantGC = DamlLfDefDataType(DamlLfImmArraySeq("a", "b"), simpleVariantGD)
|
||||
val simpleVariantTC = DamlLfTypeCon(
|
||||
DamlLfTypeConName(simpleVariantId),
|
||||
DamlLfImmArraySeq(simpleTextT, simpleInt64T),
|
||||
)
|
||||
val simpleVariantT = simpleVariantTC.instantiate(simpleVariantGC).asInstanceOf[DamlLfVariant]
|
||||
def simpleVariantV = V.ValueVariant(Some(simpleVariantId), "fA", V.ValueText("foo"))
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Daml-LF: recursive type (data Tree = Leaf a | Node {left: Tree a, right: Tree a})
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
val treeNodeId: DamlLfIdentifier = defRef("TreeNode")
|
||||
val treeId: DamlLfIdentifier = defRef("Tree")
|
||||
|
||||
val treeNodeGD = DamlLfRecord(
|
||||
DamlLfImmArraySeq(
|
||||
name("left") -> DamlLfTypeCon(
|
||||
DamlLfTypeConName(treeId),
|
||||
DamlLfImmArraySeq(DamlLfTypeVar("a")),
|
||||
),
|
||||
name("right") -> DamlLfTypeCon(
|
||||
DamlLfTypeConName(treeId),
|
||||
DamlLfImmArraySeq(DamlLfTypeVar("a")),
|
||||
),
|
||||
)
|
||||
)
|
||||
val treeNodeGC = DamlLfDefDataType(DamlLfImmArraySeq("a"), treeNodeGD)
|
||||
val treeNodeTC = DamlLfTypeCon(
|
||||
DamlLfTypeConName(treeNodeId),
|
||||
DamlLfImmArraySeq(simpleTextT),
|
||||
)
|
||||
val treeNodeT = treeNodeTC.instantiate(treeNodeGC).asInstanceOf[DamlLfRecord]
|
||||
|
||||
val treeGD = DamlLfVariant(
|
||||
DamlLfImmArraySeq(
|
||||
name("Leaf") -> DamlLfTypeVar("a"),
|
||||
name("Node") -> DamlLfTypeCon(
|
||||
DamlLfTypeConName(treeNodeId),
|
||||
DamlLfImmArraySeq(DamlLfTypeVar("a")),
|
||||
),
|
||||
)
|
||||
)
|
||||
val treeGC = DamlLfDefDataType(DamlLfImmArraySeq("a"), treeGD)
|
||||
val treeTC = DamlLfTypeCon(
|
||||
DamlLfTypeConName(treeId),
|
||||
DamlLfImmArraySeq(simpleTextT),
|
||||
)
|
||||
|
||||
val treeLeftV = V.ValueVariant(
|
||||
Some(treeId),
|
||||
"Node",
|
||||
V.ValueRecord(
|
||||
Some(treeNodeId),
|
||||
ImmArray(
|
||||
Some(name("left")) -> V.ValueVariant(Some(treeId), "Leaf", V.ValueText("LL")),
|
||||
Some(name("right")) -> V.ValueVariant(Some(treeId), "Leaf", V.ValueText("LR")),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val treeRightV = V.ValueVariant(
|
||||
Some(treeId),
|
||||
"Node",
|
||||
V.ValueRecord(
|
||||
Some(treeNodeId),
|
||||
ImmArray(
|
||||
Some(name("left")) -> V.ValueVariant(Some(treeId), "Leaf", V.ValueText("RL")),
|
||||
Some(name("right")) -> V.ValueVariant(Some(treeId), "Leaf", V.ValueText("RR")),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val treeV = V.ValueVariant(
|
||||
Some(treeId),
|
||||
"Node",
|
||||
V.ValueRecord(
|
||||
Some(treeNodeId),
|
||||
ImmArray(Some(name("left")) -> treeLeftV, Some(name("right")) -> treeRightV),
|
||||
),
|
||||
)
|
||||
|
||||
val colorGD = DamlLfEnum(DamlLfImmArraySeq(name("Red"), name("Green"), name("Blue")))
|
||||
val colorGC = DamlLfDefDataType(DamlLfImmArraySeq.empty, colorGD)
|
||||
val colorId: DamlLfIdentifier = defRef("Color")
|
||||
val redTC = DamlLfTypeCon(
|
||||
DamlLfTypeConName(colorId),
|
||||
DamlLfImmArraySeq.empty,
|
||||
)
|
||||
val redV = V.ValueEnum(Some(colorId), "Red")
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Daml-LF: complex record containing all Daml types
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
val complexRecordId: DamlLfIdentifier = defRef("ComplexRecord")
|
||||
val complexRecordGD = DamlLfRecord(
|
||||
DamlLfImmArraySeq(
|
||||
name("fText") -> simpleTextT,
|
||||
name("fBool") -> simpleBoolT,
|
||||
name("fDecimal") -> simpleDecimalT,
|
||||
name("fUnit") -> simpleUnitT,
|
||||
name("fInt64") -> simpleInt64T,
|
||||
name("fParty") -> simplePartyT,
|
||||
name("fContractId") -> simpleContractIdT,
|
||||
name("fListOfText") -> simpleListT(simpleTextT),
|
||||
name("fListOfUnit") -> simpleListT(simpleUnitT),
|
||||
name("fDate") -> simpleDateT,
|
||||
name("fTimestamp") -> simpleTimestampT,
|
||||
name("fOptionalText") -> simpleOptionalT(simpleTextT),
|
||||
name("fOptionalUnit") -> simpleOptionalT(simpleUnitT),
|
||||
name("fOptOptText") -> simpleOptionalT(simpleOptionalT(simpleTextT)),
|
||||
name("fMap") -> simpleTextMapT(simpleInt64T),
|
||||
name("fVariant") -> simpleVariantTC,
|
||||
name("fRecord") -> simpleRecordTC,
|
||||
)
|
||||
)
|
||||
val complexRecordGC = DamlLfDefDataType(DamlLfImmArraySeq(), complexRecordGD)
|
||||
val complexRecordTC = DamlLfTypeCon(DamlLfTypeConName(complexRecordId), DamlLfImmArraySeq())
|
||||
val complexRecordT = complexRecordTC.instantiate(complexRecordGC).asInstanceOf[DamlLfRecord]
|
||||
val complexRecordV = V.ValueRecord(
|
||||
Some(complexRecordId),
|
||||
ImmArray(
|
||||
("fText", simpleTextV),
|
||||
("fBool", V.ValueTrue),
|
||||
("fDecimal", simpleDecimalV),
|
||||
("fUnit", V.ValueUnit),
|
||||
("fInt64", simpleInt64V),
|
||||
("fParty", V.ValueParty(DamlLfRef.Party assertFromString "BANK1")),
|
||||
("fContractId", V.ValueContractId(V.ContractId.assertFromString("00" + "00" * 32 + "c0"))),
|
||||
("fListOfText", V.ValueList(FrontStack(V.ValueText("foo"), V.ValueText("bar")))),
|
||||
("fListOfUnit", V.ValueList(FrontStack(V.ValueUnit, V.ValueUnit))),
|
||||
("fDate", simpleDateV),
|
||||
("fTimestamp", simpleTimestampV),
|
||||
("fOptionalText", V.ValueNone),
|
||||
("fOptionalUnit", V.ValueOptional(Some(V.ValueUnit))),
|
||||
("fOptOptText", V.ValueOptional(Some(V.ValueOptional(Some(V.ValueText("foo")))))),
|
||||
(
|
||||
"fMap",
|
||||
V.ValueTextMap(
|
||||
SortedLookupList(
|
||||
Map("1" -> V.ValueInt64(1), "2" -> V.ValueInt64(2), "3" -> V.ValueInt64(3))
|
||||
)
|
||||
),
|
||||
),
|
||||
("fVariant", simpleVariantV),
|
||||
("fRecord", simpleRecordV),
|
||||
).map { case (k, v) => (Some(name(k)), v) },
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// Daml-LF: package mockup
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
val allTypes: Map[DamlLfIdentifier, DamlLfDefDataType] = Map(
|
||||
emptyRecordId -> emptyRecordGC,
|
||||
simpleRecordId -> simpleRecordGC,
|
||||
simpleVariantId -> simpleVariantGC,
|
||||
complexRecordId -> complexRecordGC,
|
||||
treeId -> treeGC,
|
||||
treeNodeId -> treeNodeGC,
|
||||
colorId -> colorGC,
|
||||
)
|
||||
|
||||
// Note: these templates may not be valid Daml templates
|
||||
val simpleRecordTemplateId: DamlLfIdentifier = defRef("SimpleRecordTemplate")
|
||||
private val ChoiceUnit = DamlLfRef.Name.assertFromString("unit")
|
||||
private val choiceText = DamlLfRef.Name.assertFromString("text")
|
||||
private val choiceNonconsuming = DamlLfRef.Name.assertFromString("nonconsuming")
|
||||
private val ChoiceReplace = DamlLfRef.Name.assertFromString("replace")
|
||||
|
||||
val simpleRecordTemplate = DamlLfIface.PackageSignature.TypeDecl.Template(
|
||||
simpleRecordT,
|
||||
DamlLfIface.DefTemplate(
|
||||
DamlLfIface.TemplateChoices.Resolved fromDirect Map(
|
||||
ChoiceUnit -> DamlLfIface.TemplateChoice(simpleUnitT, false, simpleUnitT),
|
||||
choiceText -> DamlLfIface.TemplateChoice(simpleTextT, false, simpleUnitT),
|
||||
choiceNonconsuming -> DamlLfIface.TemplateChoice(simpleUnitT, true, simpleUnitT),
|
||||
ChoiceReplace -> DamlLfIface.TemplateChoice(simpleRecordTC, false, simpleUnitT),
|
||||
),
|
||||
None,
|
||||
Seq.empty,
|
||||
),
|
||||
)
|
||||
val complexRecordTemplate = DamlLfIface.PackageSignature.TypeDecl.Template(
|
||||
complexRecordT,
|
||||
DamlLfIface.DefTemplate(
|
||||
DamlLfIface.TemplateChoices.Resolved fromDirect Map(
|
||||
ChoiceUnit -> DamlLfIface.TemplateChoice(simpleUnitT, false, simpleUnitT),
|
||||
choiceText -> DamlLfIface.TemplateChoice(simpleTextT, false, simpleUnitT),
|
||||
choiceNonconsuming -> DamlLfIface.TemplateChoice(simpleUnitT, true, simpleUnitT),
|
||||
ChoiceReplace -> DamlLfIface.TemplateChoice(complexRecordTC, false, simpleUnitT),
|
||||
),
|
||||
None,
|
||||
Seq.empty,
|
||||
),
|
||||
)
|
||||
val treeNodeTemplate = DamlLfIface.PackageSignature.TypeDecl.Template(
|
||||
treeNodeT,
|
||||
DamlLfIface.DefTemplate(
|
||||
DamlLfIface.TemplateChoices.Resolved fromDirect Map(
|
||||
ChoiceUnit -> DamlLfIface.TemplateChoice(simpleUnitT, false, simpleUnitT),
|
||||
choiceText -> DamlLfIface.TemplateChoice(simpleTextT, false, simpleUnitT),
|
||||
choiceNonconsuming -> DamlLfIface.TemplateChoice(simpleUnitT, true, simpleUnitT),
|
||||
ChoiceReplace -> DamlLfIface.TemplateChoice(treeNodeTC, false, simpleUnitT),
|
||||
),
|
||||
None,
|
||||
Seq.empty,
|
||||
),
|
||||
)
|
||||
|
||||
def complexGenMapT(keyTyp: DamlLfIface.Type, valueTyp: DamlLfIface.Type) =
|
||||
DamlLfTypePrim(DamlLfPrimType.GenMap, DamlLfImmArraySeq(keyTyp, valueTyp))
|
||||
|
||||
val complexGenMapV = V.ValueGenMap(
|
||||
ImmArray(
|
||||
treeV -> V.ValueInt64(1),
|
||||
treeLeftV -> V.ValueInt64(2),
|
||||
treeRightV -> V.ValueInt64(3),
|
||||
)
|
||||
)
|
||||
|
||||
val iface = DamlLfIface.PackageSignature(
|
||||
packageId0,
|
||||
None,
|
||||
Map(
|
||||
emptyRecordId.qualifiedName -> DamlLfIface.PackageSignature.TypeDecl.Normal(emptyRecordGC),
|
||||
simpleRecordId.qualifiedName -> DamlLfIface.PackageSignature.TypeDecl.Normal(simpleRecordGC),
|
||||
simpleVariantId.qualifiedName -> DamlLfIface.PackageSignature.TypeDecl
|
||||
.Normal(simpleVariantGC),
|
||||
treeId.qualifiedName -> DamlLfIface.PackageSignature.TypeDecl.Normal(treeGC),
|
||||
treeNodeId.qualifiedName -> DamlLfIface.PackageSignature.TypeDecl.Normal(treeNodeGC),
|
||||
simpleRecordTemplateId.qualifiedName -> simpleRecordTemplate,
|
||||
complexRecordId.qualifiedName -> complexRecordTemplate,
|
||||
treeNodeId.qualifiedName -> treeNodeTemplate,
|
||||
),
|
||||
Map.empty,
|
||||
)
|
||||
|
||||
private[navigator] implicit def name(s: String): DamlLfRef.Name =
|
||||
DamlLfRef.Name.assertFromString(s)
|
||||
|
||||
@throws[IllegalArgumentException]
|
||||
private[navigator] def record(fields: (String, ApiValue)*): ApiRecord =
|
||||
V.ValueRecord(
|
||||
None,
|
||||
fields.iterator.map { case (label, value) => Some(name(label)) -> value }.to(ImmArray),
|
||||
)
|
||||
|
||||
}
|
@ -1,221 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator
|
||||
|
||||
import org.apache.pekko.actor.ActorSystem
|
||||
import org.apache.pekko.http.scaladsl.Http
|
||||
import org.apache.pekko.http.scaladsl.model.{HttpRequest, StatusCodes, Uri}
|
||||
import org.apache.pekko.http.scaladsl.server.Route
|
||||
import org.apache.pekko.http.scaladsl.settings.ServerSettings
|
||||
import org.apache.pekko.util.ByteString
|
||||
import com.daml.buildinfo.BuildInfo
|
||||
import com.daml.integrationtest.CantonFixture
|
||||
import com.daml.ledger.api.domain
|
||||
import com.daml.ledger.client.LedgerClient
|
||||
import com.daml.lf.data.Ref
|
||||
import com.daml.lf.data.Ref.UserId
|
||||
import com.daml.navigator.config.{Arguments, Config}
|
||||
import org.scalatest._
|
||||
import org.scalatest.freespec.AsyncFreeSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import com.daml.timer.RetryStrategy
|
||||
import com.google.protobuf.field_mask.FieldMask
|
||||
|
||||
import java.util.UUID
|
||||
import scala.concurrent.{Await, Future}
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class IntegrationTest extends AsyncFreeSpec with CantonFixture with Matchers {
|
||||
self: Suite =>
|
||||
|
||||
private def withNavigator[A](
|
||||
userMgmt: Boolean
|
||||
)(testFn: Uri => LedgerClient => Future[A]): Future[A] = {
|
||||
import scala.jdk.FutureConverters
|
||||
val args = Arguments(
|
||||
port = 0,
|
||||
participantPort = ports.head.value,
|
||||
enableUserManagement = userMgmt,
|
||||
)
|
||||
val sys = ActorSystem(s"navigator-${UUID.randomUUID().toString}")
|
||||
val backend = new UIBackend {
|
||||
override def customEndpoints: Set[CustomEndpoint[_]] = Set()
|
||||
override def customRoutes: List[Route] = Nil
|
||||
override def applicationInfo: ApplicationInfo = ApplicationInfo(
|
||||
id = s"Test-Navigator-${UUID.randomUUID().toString}",
|
||||
name = "Test-Navigator",
|
||||
version = BuildInfo.Version,
|
||||
)
|
||||
}
|
||||
|
||||
val (graphQL, info, _, getAppState, partyRefresh) = backend.setup(args, Config())(sys)
|
||||
val bindingF = Http()
|
||||
.newServerAt("localhost", 0)
|
||||
.withSettings(ServerSettings(system).withTransparentHeadRequests(true))
|
||||
.bind(backend.getRoute(system, args, graphQL, info, getAppState))
|
||||
|
||||
// TODO https://github.com/digital-asset/daml/issues/12663 participant user management: Emulating no-pagination
|
||||
def listAllUsers(client: LedgerClient) =
|
||||
client.userManagementClient.listUsers(pageToken = "", pageSize = 10000).map {
|
||||
case (users, _) => users.toList
|
||||
}
|
||||
|
||||
// Don't close the LedgerClient on termination, because all it does is close the channel,
|
||||
// which then causes a subsequent test to fail when creating a LedgerClient using a closed channel
|
||||
// ("io.grpc.StatusRuntimeException: UNAVAILABLE: Channel shutdown invoked")
|
||||
sys.registerOnTermination {
|
||||
partyRefresh.foreach(_.cancel())
|
||||
Await.ready(bindingF.flatMap(_.terminate(30.seconds)), 30.seconds)
|
||||
}
|
||||
import FutureConverters._
|
||||
for {
|
||||
binding <- bindingF
|
||||
client <- defaultLedgerClient()
|
||||
uri = Uri.from(
|
||||
scheme = "http",
|
||||
host = binding.localAddress.getHostName,
|
||||
port = binding.localAddress.getPort,
|
||||
)
|
||||
a <- testFn(uri)(client)
|
||||
_ <- sys.terminate()
|
||||
_ <- Await.ready(sys.getWhenTerminated.asScala, 30.seconds)
|
||||
_ = logger.info(s"Terminated actor system ${sys.name}")
|
||||
// Reset sandbox enough to avoid users leaking between tests: delete all users except admin
|
||||
users <- listAllUsers(client)
|
||||
_ <- Future.traverse(users)(user =>
|
||||
if (user.id != "participant_admin") client.userManagementClient.deleteUser(user.id)
|
||||
else Future.unit
|
||||
)
|
||||
_ = logger.info("Removed all users from ledger as part of cleanup.")
|
||||
} yield a
|
||||
}
|
||||
|
||||
private def okSessionBody(expectedBody: String, useRegex: Boolean = false)(implicit
|
||||
uri: Uri
|
||||
): Future[Assertion] = {
|
||||
RetryStrategy.constant(20, 1.second) { case (_, _) =>
|
||||
for {
|
||||
resp <- Http().singleRequest(
|
||||
HttpRequest(uri = uri.withPath(Uri.Path("/api/session/")))
|
||||
)
|
||||
respBody <- resp.entity.dataBytes
|
||||
.runFold(ByteString.empty)((b, a) => b ++ a)
|
||||
.map(_.utf8String)
|
||||
_ = resp.status shouldBe StatusCodes.OK
|
||||
} yield
|
||||
if (useRegex)
|
||||
(respBody should fullyMatch regex expectedBody.r)
|
||||
else
|
||||
(respBody shouldBe expectedBody)
|
||||
}
|
||||
}
|
||||
|
||||
private def allocateParty(partyName: String)(implicit client: LedgerClient) = {
|
||||
client.partyManagementClient
|
||||
.allocateParty(hint = None, displayName = Some(partyName))
|
||||
}
|
||||
|
||||
private def createUser(userName: String, primaryParty: Ref.Party)(implicit
|
||||
client: LedgerClient
|
||||
): Future[domain.User] = {
|
||||
client.userManagementClient
|
||||
.createUser(
|
||||
domain.User(UserId.assertFromString(userName), Some(primaryParty))
|
||||
)
|
||||
}
|
||||
|
||||
private def updateUser(userName: String, primaryParty: Ref.Party)(implicit
|
||||
client: LedgerClient
|
||||
): Future[domain.User] = {
|
||||
client.userManagementClient
|
||||
.updateUser(
|
||||
domain.User(UserId.assertFromString(userName), Some(primaryParty)),
|
||||
Some(FieldMask(Seq("primary_party"))),
|
||||
)
|
||||
}
|
||||
|
||||
private def createUser(userName: String)(implicit client: LedgerClient): Future[domain.User] = {
|
||||
client.userManagementClient
|
||||
.createUser(
|
||||
domain.User(UserId.assertFromString(userName), None)
|
||||
)
|
||||
}
|
||||
|
||||
"Navigator (parties)" - {
|
||||
"picks up newly allocated parties" in withNavigator(userMgmt = false) {
|
||||
implicit uri => implicit client =>
|
||||
for {
|
||||
_ <- okSessionBody(
|
||||
"""\{"method":\{"type":"select","users":\["participant0::[a-f0-9]+"\]\},"type":"sign-in"\}""",
|
||||
true,
|
||||
)
|
||||
_ <- allocateParty("display-name")
|
||||
_ <- okSessionBody(
|
||||
"""\{"method":\{"type":"select","users":\["participant0::[a-f0-9]+","display-name"\]\},"type":"sign-in"\}""",
|
||||
true,
|
||||
)
|
||||
} yield succeed
|
||||
}
|
||||
}
|
||||
|
||||
"Navigator (basic user)" - {
|
||||
"picks up newly created users (1 user)" in withNavigator(userMgmt = true) {
|
||||
implicit uri => implicit client =>
|
||||
for {
|
||||
_ <- okSessionBody("""{"method":{"type":"select","users":[]},"type":"sign-in"}""")
|
||||
partyDetails <- allocateParty("primary-party")
|
||||
_ <- createUser("user-name", partyDetails.party)
|
||||
_ <- okSessionBody(
|
||||
"""{"method":{"type":"select","users":["user-name"]},"type":"sign-in"}"""
|
||||
)
|
||||
} yield succeed
|
||||
}
|
||||
}
|
||||
|
||||
"Navigator (users)" - {
|
||||
"picks up newly created users (2 users, 1 primary party)" in withNavigator(userMgmt = true) {
|
||||
implicit uri => implicit client =>
|
||||
for {
|
||||
_ <- okSessionBody("""{"method":{"type":"select","users":[]},"type":"sign-in"}""")
|
||||
partyDetails <- allocateParty("primary-party")
|
||||
_ <- createUser("user-name-1", partyDetails.party)
|
||||
_ <- createUser("user-name-2", partyDetails.party)
|
||||
_ <- okSessionBody(
|
||||
"""{"method":{"type":"select","users":["user-name-1","user-name-2"]},"type":"sign-in"}"""
|
||||
)
|
||||
} yield succeed
|
||||
}
|
||||
|
||||
"picks up newly created users (2 users, 2 primary parties)" in withNavigator(userMgmt = true) {
|
||||
implicit uri => implicit client =>
|
||||
for {
|
||||
_ <- okSessionBody("""{"method":{"type":"select","users":[]},"type":"sign-in"}""")
|
||||
partyDetails <- allocateParty("primary-party")
|
||||
_ <- createUser("user-name-1", partyDetails.party)
|
||||
partyDetails2 <- allocateParty("primary-party2")
|
||||
_ <- createUser("user-name-2", partyDetails2.party)
|
||||
_ <- okSessionBody(
|
||||
"""{"method":{"type":"select","users":["user-name-1","user-name-2"]},"type":"sign-in"}"""
|
||||
)
|
||||
_ <- updateUser("user-name-2", partyDetails.party)
|
||||
_ <- okSessionBody(
|
||||
"""{"method":{"type":"select","users":["user-name-1","user-name-2"]},"type":"sign-in"}"""
|
||||
)
|
||||
} yield succeed
|
||||
}
|
||||
|
||||
"can only log in if user has a primary party (1 user, 0 primary parties)" in withNavigator(
|
||||
userMgmt = true
|
||||
) { implicit uri => implicit client =>
|
||||
for {
|
||||
_ <- okSessionBody("""{"method":{"type":"select","users":[]},"type":"sign-in"}""")
|
||||
_ <- createUser("user-name-1")
|
||||
_ <- okSessionBody(
|
||||
"""{"method":{"type":"select","users":[]},"type":"sign-in"}"""
|
||||
)
|
||||
} yield succeed
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,197 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator
|
||||
|
||||
import org.apache.pekko.actor.ActorSystem
|
||||
import org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
|
||||
import org.apache.pekko.http.scaladsl.model.headers.{Cookie, `Set-Cookie`}
|
||||
import org.apache.pekko.http.scaladsl.server.Route
|
||||
import org.apache.pekko.http.scaladsl.testkit.ScalatestRouteTest
|
||||
import org.apache.pekko.http.scaladsl.unmarshalling.Unmarshal
|
||||
import com.daml.api.util.TimeProvider.UTC
|
||||
import com.daml.ledger.api.refinements.ApiTypes
|
||||
import com.daml.navigator.SessionJsonProtocol._
|
||||
import com.daml.navigator.config.Arguments
|
||||
import com.daml.navigator.model.PartyState
|
||||
import com.daml.navigator.store.Store.{
|
||||
ApplicationStateConnected,
|
||||
ApplicationStateConnecting,
|
||||
ApplicationStateFailed,
|
||||
ApplicationStateInfo,
|
||||
PartyActorRunning,
|
||||
PartyActorStarted,
|
||||
}
|
||||
import com.daml.navigator.time.TimeProviderType.Static
|
||||
import com.daml.navigator.time.TimeProviderWithType
|
||||
import com.typesafe.scalalogging.LazyLogging
|
||||
import org.scalatest.OptionValues
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import scalaz.syntax.tag._
|
||||
import spray.json._
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.util.Success
|
||||
|
||||
class ServerTest
|
||||
extends AnyFlatSpec
|
||||
with Matchers
|
||||
with ScalatestRouteTest
|
||||
with LazyLogging
|
||||
with OptionValues {
|
||||
|
||||
val userId = "userId"
|
||||
val role = "role"
|
||||
val party = ApiTypes.Party("party")
|
||||
val partyState = new PartyState(party, Some(role), useDatabase = false)
|
||||
val user = User(userId, partyState, Some(role), true)
|
||||
|
||||
val userJson = JsObject(
|
||||
"id" -> JsString(userId),
|
||||
"role" -> JsString(role),
|
||||
"party" -> JsString(party.unwrap),
|
||||
"canAdvanceTime" -> JsBoolean(true),
|
||||
)
|
||||
|
||||
val sessionJson = JsObject(
|
||||
"type" -> JsString("session"),
|
||||
"user" -> userJson,
|
||||
)
|
||||
|
||||
case object TestInfoHandler extends InfoHandler {
|
||||
override def getInfo: Future[JsValue] = Future.successful(JsString("test"))
|
||||
}
|
||||
|
||||
private def route(state: ApplicationStateInfo): Route =
|
||||
NavigatorBackend.getRoute(
|
||||
system = ActorSystem("da-ui-backend-test"),
|
||||
arguments = Arguments.default,
|
||||
graphQL = DefaultGraphQLHandler(Set.empty, None),
|
||||
info = TestInfoHandler,
|
||||
getAppState = () => Future.successful(state),
|
||||
)
|
||||
|
||||
private[this] val connected =
|
||||
route(
|
||||
ApplicationStateConnected(
|
||||
"localhost",
|
||||
6865,
|
||||
true,
|
||||
"n/a",
|
||||
"0",
|
||||
TimeProviderWithType(UTC, Static),
|
||||
Map(userId -> PartyActorRunning(PartyActorStarted(partyState))),
|
||||
)
|
||||
)
|
||||
|
||||
private[this] val unauthorized =
|
||||
route(
|
||||
ApplicationStateFailed(
|
||||
"localhost",
|
||||
6865,
|
||||
true,
|
||||
"n/a",
|
||||
io.grpc.Status.PERMISSION_DENIED.asException,
|
||||
)
|
||||
)
|
||||
|
||||
private[this] val failed =
|
||||
route(
|
||||
ApplicationStateFailed(
|
||||
"localhost",
|
||||
6865,
|
||||
true,
|
||||
"n/a",
|
||||
io.grpc.Status.INVALID_ARGUMENT.asException,
|
||||
)
|
||||
)
|
||||
|
||||
private[this] val connecting =
|
||||
route(
|
||||
ApplicationStateConnecting(
|
||||
"localhost",
|
||||
6865,
|
||||
true,
|
||||
"n/a",
|
||||
)
|
||||
)
|
||||
|
||||
def sessionCookie(): String = {
|
||||
val cookies = headers.collect { case `Set-Cookie`(x) if x.name == "session-id" => x }
|
||||
cookies should have size 1
|
||||
cookies.head.value
|
||||
}
|
||||
|
||||
def withCleanSessions[T](f: => T): T = {
|
||||
Session.clean()
|
||||
f
|
||||
}
|
||||
|
||||
"SelectMode GET /api/session/" should "respond SignIn with method SignInSelect with the available users" in withCleanSessions {
|
||||
Get("/api/session/") ~> connected ~> check {
|
||||
responseAs[SignIn] shouldEqual SignIn(method = SignInSelect(userIds = Set(userId)))
|
||||
}
|
||||
}
|
||||
|
||||
it should "respond with the Session when already signed-in" in withCleanSessions {
|
||||
val sessionId = "session-id-value"
|
||||
Session.open(sessionId, userId, Some(role), user.party)
|
||||
Get("/api/session/") ~> Cookie("session-id" -> sessionId) ~> connected ~> check {
|
||||
Unmarshal(response.entity).to[String].value.map(_.map(_.parseJson)) shouldEqual Some(
|
||||
Success((sessionJson))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
"SelectMode POST /api/session/" should "allow to SignIn with an existing user" in withCleanSessions {
|
||||
Post("/api/session/", LoginRequest(userId)) ~> connected ~> check {
|
||||
Unmarshal(response.entity).to[String].value.map(_.map(_.parseJson)) shouldEqual Some(
|
||||
Success((sessionJson))
|
||||
)
|
||||
val sessionId = sessionCookie()
|
||||
Session.current(sessionId).value shouldEqual Session(user)
|
||||
}
|
||||
}
|
||||
|
||||
it should "forbid to SignIn with a non existing user" in withCleanSessions {
|
||||
Post("/api/session/", LoginRequest(userId + " ")) ~> connected ~> check {
|
||||
responseAs[SignIn] shouldEqual SignIn(
|
||||
method = SignInSelect(userIds = Set(userId)),
|
||||
Some(InvalidCredentials),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
it should "forbid to SignIn with when unauthorized and report the error" in withCleanSessions {
|
||||
Post("/api/session/", LoginRequest(userId)) ~> unauthorized ~> check {
|
||||
responseAs[SignIn] shouldEqual SignIn(
|
||||
method = SignInSelect(userIds = Set()),
|
||||
Some(InvalidCredentials),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
it should "forbid to SignIn with when it's impossible to connect to the ledger" in withCleanSessions {
|
||||
Post("/api/session/", LoginRequest(userId)) ~> failed ~> check {
|
||||
responseAs[SignIn] shouldEqual SignIn(method = SignInSelect(userIds = Set()), Some(Unknown))
|
||||
}
|
||||
}
|
||||
|
||||
it should "forbid to SignIn with when still connecting to a ledger" in withCleanSessions {
|
||||
Post("/api/session/", LoginRequest(userId)) ~> connecting ~> check {
|
||||
responseAs[SignIn] shouldEqual SignIn(
|
||||
method = SignInSelect(userIds = Set()),
|
||||
Some(NotConnected),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
"SelectMode DELETE /api/session/" should "delete a given Session when signed-in" in withCleanSessions {
|
||||
val sessionId = "session-id-value-2"
|
||||
Session.open(sessionId, userId, Some(role), user.party)
|
||||
Delete("/api/session/") ~> Cookie("session-id", sessionId) ~> connected ~> check {
|
||||
Session.current(sessionId) shouldBe None
|
||||
}
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator
|
||||
|
||||
import com.daml.navigator.model.PartyState
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import SessionJsonProtocol.userWriter
|
||||
import com.daml.ledger.api.refinements.ApiTypes
|
||||
import spray.json.{JsBoolean, JsObject, JsString}
|
||||
|
||||
class SessionJsonProtocolTest extends AnyFlatSpec with Matchers {
|
||||
|
||||
val userClassName = User.getClass.getSimpleName
|
||||
val party = ApiTypes.Party("party")
|
||||
|
||||
behavior of s"JsonCodec[$userClassName]"
|
||||
|
||||
it should s"encode $userClassName without role" in {
|
||||
val user =
|
||||
User(id = "id", party = new PartyState(party, None, false), canAdvanceTime = true)
|
||||
val userJson = JsObject(
|
||||
"id" -> JsString("id"),
|
||||
"party" -> JsString("party"),
|
||||
"canAdvanceTime" -> JsBoolean(true),
|
||||
)
|
||||
userWriter.write(user) shouldEqual userJson
|
||||
}
|
||||
|
||||
it should s"encode $userClassName with role" in {
|
||||
val user = User(
|
||||
id = "id",
|
||||
party = new PartyState(party, Some("role"), false),
|
||||
role = Some("role"),
|
||||
canAdvanceTime = false,
|
||||
)
|
||||
val userJson = JsObject(
|
||||
"id" -> JsString("id"),
|
||||
"role" -> JsString("role"),
|
||||
"party" -> JsString("party"),
|
||||
"canAdvanceTime" -> JsBoolean(false),
|
||||
)
|
||||
userWriter.write(user) shouldEqual userJson
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.config
|
||||
|
||||
import com.daml.ledger.api.refinements.ApiTypes.Party
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.{Files, Path, Paths}
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.freespec.AnyFreeSpec
|
||||
|
||||
class RowSpec extends AnyFreeSpec with Matchers {
|
||||
private val defaultConfigText = "users { Alice { party = alice }, Bob { party = bob } }"
|
||||
private val defaultConfig = Config(
|
||||
Map(
|
||||
"Alice" -> UserConfig(Party("alice"), None, false),
|
||||
"Bob" -> UserConfig(Party("bob"), None, false),
|
||||
)
|
||||
)
|
||||
|
||||
private def withConfig[A](f: Path => A): A = {
|
||||
val path = Files.createTempFile("navigator", ".conf")
|
||||
try {
|
||||
Files.write(path, defaultConfigText.getBytes(StandardCharsets.UTF_8))
|
||||
f(path)
|
||||
} finally {
|
||||
Files.delete(path)
|
||||
}
|
||||
}
|
||||
"Config" - {
|
||||
"load" - {
|
||||
"loads explicit config" in {
|
||||
withConfig { conf =>
|
||||
Config.load(ExplicitConfig(conf), false) shouldBe Right(defaultConfig)
|
||||
}
|
||||
}
|
||||
"fails if explicit config does not exist" in {
|
||||
Config.load(ExplicitConfig(Paths.get("nonexistentgarbage")), false) shouldBe Left(
|
||||
ConfigNotFound("File nonexistentgarbage not found")
|
||||
)
|
||||
}
|
||||
"loads default config if none is specified" in {
|
||||
withConfig { conf =>
|
||||
Config.load(DefaultConfig(conf), false) shouldBe Right(defaultConfig)
|
||||
}
|
||||
}
|
||||
"loads an empty config if default config does not exist" in {
|
||||
Config.load(DefaultConfig(Paths.get("nonexistentgarbage")), false) shouldBe Right(Config())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,166 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.data
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
import com.daml.ledger.api.refinements.ApiTypes
|
||||
import com.daml.navigator.model.{
|
||||
ChoiceExercised,
|
||||
CommandStatusError,
|
||||
CommandStatusSuccess,
|
||||
CommandStatusUnknown,
|
||||
CommandStatusWaiting,
|
||||
ContractCreated,
|
||||
CreateCommand,
|
||||
ExerciseCommand,
|
||||
PackageRegistry,
|
||||
Transaction,
|
||||
}
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
|
||||
import scala.util.Success
|
||||
|
||||
class RowSpec extends AnyWordSpec with Matchers {
|
||||
import com.daml.navigator.{DamlConstants => C}
|
||||
|
||||
private val registry: PackageRegistry = PackageRegistry().withPackages(List(C.iface))
|
||||
|
||||
"CommandRow" when {
|
||||
"converting CreateCommand" should {
|
||||
val value = CreateCommand(
|
||||
ApiTypes.CommandId("c01"),
|
||||
1L,
|
||||
ApiTypes.WorkflowId("w01"),
|
||||
Instant.EPOCH,
|
||||
C.complexRecordId,
|
||||
C.complexRecordV,
|
||||
)
|
||||
|
||||
"not change the value" in {
|
||||
CommandRow.fromCommand(value).toCommand(registry) shouldBe Success(value)
|
||||
}
|
||||
}
|
||||
|
||||
"converting ExerciseCommand" should {
|
||||
val value = ExerciseCommand(
|
||||
ApiTypes.CommandId("c01"),
|
||||
1L,
|
||||
ApiTypes.WorkflowId("w01"),
|
||||
Instant.EPOCH,
|
||||
ApiTypes.ContractId("#0:0"),
|
||||
C.complexRecordId,
|
||||
Some(C.complexRecordId),
|
||||
ApiTypes.Choice("text"),
|
||||
C.simpleTextV,
|
||||
)
|
||||
|
||||
"not change the value" in {
|
||||
CommandRow.fromCommand(value).toCommand(registry) shouldBe Success(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"CommandStatusRow" when {
|
||||
"converting CommandStatusWaiting" should {
|
||||
val id = ApiTypes.CommandId("c01")
|
||||
val value = CommandStatusWaiting()
|
||||
|
||||
"not change the value" in {
|
||||
CommandStatusRow
|
||||
.fromCommandStatus(id, value)
|
||||
.toCommandStatus(_ => Success(None)) shouldBe Success(value)
|
||||
}
|
||||
}
|
||||
|
||||
"converting CommandStatusError" should {
|
||||
val id = ApiTypes.CommandId("c01")
|
||||
val value = CommandStatusError("code", "message")
|
||||
|
||||
"not change the value" in {
|
||||
CommandStatusRow
|
||||
.fromCommandStatus(id, value)
|
||||
.toCommandStatus(_ => Success(None)) shouldBe Success(value)
|
||||
}
|
||||
}
|
||||
|
||||
"converting CommandStatusSuccess" should {
|
||||
val id = ApiTypes.CommandId("c01")
|
||||
val tx = Transaction(
|
||||
ApiTypes.TransactionId("t01"),
|
||||
Some(ApiTypes.CommandId("c01")),
|
||||
Instant.EPOCH,
|
||||
"1",
|
||||
List.empty,
|
||||
)
|
||||
val value = CommandStatusSuccess(tx)
|
||||
|
||||
"not change the value" in {
|
||||
CommandStatusRow
|
||||
.fromCommandStatus(id, value)
|
||||
.toCommandStatus(_ => Success(Some(tx))) shouldBe Success(value)
|
||||
}
|
||||
}
|
||||
|
||||
"converting CommandStatusUnknown" should {
|
||||
val id = ApiTypes.CommandId("c01")
|
||||
val value = CommandStatusUnknown()
|
||||
|
||||
"not change the value" in {
|
||||
CommandStatusRow
|
||||
.fromCommandStatus(id, value)
|
||||
.toCommandStatus(_ => Success(None)) shouldBe Success(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"EventRow" when {
|
||||
|
||||
val alice = ApiTypes.Party("Alice")
|
||||
val bob = ApiTypes.Party("Bob")
|
||||
val charlie = ApiTypes.Party("Charlie")
|
||||
|
||||
"converting ContractCreated" should {
|
||||
val value = ContractCreated(
|
||||
ApiTypes.EventId("e01"),
|
||||
Some(ApiTypes.EventId("e00")),
|
||||
ApiTypes.TransactionId("t01"),
|
||||
List(ApiTypes.Party("p01")),
|
||||
ApiTypes.WorkflowId("w01"),
|
||||
ApiTypes.ContractId("c01"),
|
||||
C.complexRecordId,
|
||||
C.complexRecordV,
|
||||
Some("agreement"),
|
||||
List(alice),
|
||||
List(bob, charlie),
|
||||
None,
|
||||
)
|
||||
|
||||
"not change the value" in {
|
||||
EventRow.fromEvent(value).toEvent(registry) shouldBe Success(value)
|
||||
}
|
||||
}
|
||||
|
||||
"converting ChoiceExercised" should {
|
||||
val value = ChoiceExercised(
|
||||
ApiTypes.EventId("e01"),
|
||||
Some(ApiTypes.EventId("e00")),
|
||||
ApiTypes.TransactionId("t01"),
|
||||
List(ApiTypes.Party("p01")),
|
||||
ApiTypes.WorkflowId("w01"),
|
||||
ApiTypes.ContractId("c01"),
|
||||
C.complexRecordId,
|
||||
ApiTypes.Choice("text"),
|
||||
C.simpleTextV,
|
||||
List(ApiTypes.Party("p01")),
|
||||
true,
|
||||
)
|
||||
|
||||
"not change the value" in {
|
||||
EventRow.fromEvent(value).toEvent(registry) shouldBe Success(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.dotnot
|
||||
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class DotNotSpec extends AnyFlatSpec with Matchers {
|
||||
|
||||
behavior of "DotNot"
|
||||
|
||||
it should "register an action on leaf values" in {
|
||||
val tree =
|
||||
root[String, Int, Unit]("foobar")
|
||||
.onLeaf("foo")
|
||||
.onAnyValue
|
||||
.const(1)
|
||||
|
||||
tree.onTree.nameMatcherToActions should have size 1
|
||||
tree.valueMatcherToActions should have size 1
|
||||
tree.run("", PropertyCursor.fromString("foo"), "", ()) shouldEqual Right(1)
|
||||
}
|
||||
|
||||
it should "register multiple actions on leaf values" in {
|
||||
val tree =
|
||||
root[String, Int, Unit]("foobar")
|
||||
.onLeaf("foo")
|
||||
.onValue("*")
|
||||
.const(1)
|
||||
.onValue("2")
|
||||
.const(2)
|
||||
.onAnyValue
|
||||
.const(3)
|
||||
|
||||
val cursor = PropertyCursor.fromString("foo")
|
||||
tree.onTree.nameMatcherToActions should have size 1
|
||||
tree.valueMatcherToActions should have size 3
|
||||
tree.run("", cursor, "*", ()) shouldEqual Right(1)
|
||||
tree.run("", cursor, "2", ()) shouldEqual Right(2)
|
||||
tree.run("", cursor, "", ()) shouldEqual Right(3)
|
||||
val wrongCursor = PropertyCursor.fromString("bar")
|
||||
tree.run("", wrongCursor, "", ()) shouldBe a[Left[_, _]]
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.graphql
|
||||
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
import sangria.parser.QueryParser
|
||||
import sangria.schema.SchemaChange.DescriptionChange
|
||||
import sangria.schema.Schema
|
||||
|
||||
import scala.io.Source
|
||||
|
||||
class GraphQLSchemaSpec extends AnyWordSpec with Matchers {
|
||||
"The rendered schema" should {
|
||||
"match the expected schema definition" in {
|
||||
val idl =
|
||||
Source.fromInputStream(getClass.getResourceAsStream("/schema.graphql"), "UTF-8").mkString
|
||||
val schema = Schema.buildFromAst(QueryParser.parse(idl).get)
|
||||
|
||||
// Compare schemata but ignore description changes.
|
||||
val changes = schema
|
||||
.compare(new GraphQLSchema(Set()).QuerySchema)
|
||||
.filter(!_.isInstanceOf[DescriptionChange])
|
||||
|
||||
if (changes.nonEmpty) {
|
||||
fail(
|
||||
s"Schema definition does not match:\n- ${changes.map(_.description).mkString("\n- ")}\n"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.json
|
||||
|
||||
import com.daml.navigator.model
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
|
||||
import scala.util.{Success, Try}
|
||||
|
||||
class ApiCodecCompressedSpec extends AnyWordSpec with Matchers {
|
||||
import com.daml.navigator.{DamlConstants => C}
|
||||
|
||||
/** Serializes the API value to JSON, then parses it back to an API value */
|
||||
private def serializeAndParse(
|
||||
value: model.ApiValue,
|
||||
typ: model.DamlLfType,
|
||||
): Try[model.ApiValue] = {
|
||||
import com.daml.lf.value.json.ApiCodecCompressed
|
||||
import ApiCodecCompressed.JsonImplicits._
|
||||
import spray.json._
|
||||
|
||||
for {
|
||||
serialized <- Try(value.toJson.prettyPrint)
|
||||
json <- Try(serialized.parseJson)
|
||||
parsed <- Try(ApiCodecCompressed.jsValueToApiValue(json, typ, C.allTypes.get _))
|
||||
} yield parsed
|
||||
}
|
||||
|
||||
"API verbose JSON codec" when {
|
||||
|
||||
"serializing and parsing a value" should {
|
||||
"work for SimpleRecord" in {
|
||||
serializeAndParse(C.simpleRecordV, C.simpleRecordTC) shouldBe Success(C.simpleRecordV)
|
||||
}
|
||||
"work for SimpleVariant" in {
|
||||
serializeAndParse(C.simpleVariantV, C.simpleVariantTC) shouldBe Success(C.simpleVariantV)
|
||||
}
|
||||
"work for ComplexRecord" in {
|
||||
serializeAndParse(C.complexRecordV, C.complexRecordTC) shouldBe Success(C.complexRecordV)
|
||||
}
|
||||
"work for Tree" in {
|
||||
serializeAndParse(C.treeV, C.treeTC) shouldBe Success(C.treeV)
|
||||
}
|
||||
"work for Enum" in {
|
||||
serializeAndParse(C.redV, C.redTC) shouldBe Success(C.redV)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.json
|
||||
|
||||
import com.daml.navigator.model
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
|
||||
import scala.util.{Success, Try}
|
||||
|
||||
class ApiCodecVerboseSpec extends AnyWordSpec with Matchers {
|
||||
import com.daml.navigator.{DamlConstants => C}
|
||||
|
||||
/** Serializes the API value to JSON, then parses it back to an API value */
|
||||
private def serializeAndParse(value: model.ApiValue): Try[model.ApiValue] = {
|
||||
import com.daml.navigator.json.ApiCodecVerbose.JsonImplicits._
|
||||
import spray.json._
|
||||
|
||||
for {
|
||||
serialized <- Try(value.toJson.prettyPrint)
|
||||
json <- Try(serialized.parseJson)
|
||||
parsed <- Try(json.convertTo[model.ApiValue])
|
||||
} yield parsed
|
||||
}
|
||||
|
||||
"API verbose JSON codec" when {
|
||||
|
||||
"serializing and parsing a value" should {
|
||||
|
||||
"work for Text" in {
|
||||
serializeAndParse(C.simpleTextV) shouldBe Success(C.simpleTextV)
|
||||
}
|
||||
"work for Int64" in {
|
||||
serializeAndParse(C.simpleInt64V) shouldBe Success(C.simpleInt64V)
|
||||
}
|
||||
"work for Decimal" in {
|
||||
serializeAndParse(C.simpleDecimalV) shouldBe Success(C.simpleDecimalV)
|
||||
}
|
||||
"work for Unit" in {
|
||||
serializeAndParse(C.simpleUnitV) shouldBe Success(C.simpleUnitV)
|
||||
}
|
||||
"work for Date" in {
|
||||
serializeAndParse(C.simpleDateV) shouldBe Success(C.simpleDateV)
|
||||
}
|
||||
"work for Timestamp" in {
|
||||
serializeAndParse(C.simpleTimestampV) shouldBe Success(C.simpleTimestampV)
|
||||
}
|
||||
"work for Optional" in {
|
||||
serializeAndParse(C.simpleOptionalV) shouldBe Success(C.simpleOptionalV)
|
||||
}
|
||||
"work for EmptyRecord" in {
|
||||
serializeAndParse(C.emptyRecordV) shouldBe Success(C.emptyRecordV)
|
||||
}
|
||||
"work for SimpleRecord" in {
|
||||
serializeAndParse(C.simpleRecordV) shouldBe Success(C.simpleRecordV)
|
||||
}
|
||||
"work for SimpleVariant" in {
|
||||
serializeAndParse(C.simpleVariantV) shouldBe Success(C.simpleVariantV)
|
||||
}
|
||||
"work for ComplexRecord" in {
|
||||
serializeAndParse(C.complexRecordV) shouldBe Success(C.complexRecordV)
|
||||
}
|
||||
"work for Tree" in {
|
||||
serializeAndParse(C.treeV) shouldBe Success(C.treeV)
|
||||
}
|
||||
"work for TextMap" in {
|
||||
serializeAndParse(C.simpleTextMapV) shouldBe Success(C.simpleTextMapV)
|
||||
}
|
||||
"work for GenMap" in {
|
||||
serializeAndParse(C.complexGenMapV) shouldBe Success(C.complexGenMapV)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.json
|
||||
|
||||
import com.daml.navigator.json.DamlLfCodec.JsonImplicits._
|
||||
import com.daml.navigator.model
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
|
||||
import scala.util.{Success, Try}
|
||||
|
||||
class DamlLfCodecSpec extends AnyWordSpec with Matchers {
|
||||
import com.daml.navigator.{DamlConstants => C}
|
||||
|
||||
/** Serializes the API value to JSON, then parses it back to an API value */
|
||||
private def serializeAndParse[T](value: T)(implicit fmt: spray.json.JsonFormat[T]): Try[T] = {
|
||||
import spray.json._
|
||||
|
||||
for {
|
||||
serialized <- Try(value.toJson.prettyPrint)
|
||||
json <- Try(serialized.parseJson)
|
||||
parsed <- Try(json.convertTo[T])
|
||||
} yield parsed
|
||||
}
|
||||
|
||||
"Daml-LF JSON codec" when {
|
||||
|
||||
"serializing and parsing a Daml-LF object" should {
|
||||
|
||||
"work for DamlLFIdentifier" in {
|
||||
serializeAndParse(C.ref0) shouldBe Success(C.ref0)
|
||||
}
|
||||
"work for DamlLfTypePrim(Text)" in {
|
||||
serializeAndParse[model.DamlLfType](C.simpleTextT) shouldBe Success(C.simpleTextT)
|
||||
}
|
||||
"work for DamlLfTypeCon(SimpleRecord)" in {
|
||||
serializeAndParse[model.DamlLfType](C.simpleRecordTC) shouldBe Success(C.simpleRecordTC)
|
||||
}
|
||||
"work for DamlLfTypeCon(Tree)" in {
|
||||
serializeAndParse[model.DamlLfType](C.treeTC) shouldBe Success(C.treeTC)
|
||||
}
|
||||
"work for DamlLfDefDataType(SimpleRecord)" in {
|
||||
serializeAndParse[model.DamlLfDefDataType](C.simpleRecordGC) shouldBe Success(
|
||||
C.simpleRecordGC
|
||||
)
|
||||
}
|
||||
"work for DamlLfDefDataType(Tree)" in {
|
||||
serializeAndParse[model.DamlLfDefDataType](C.treeGC) shouldBe Success(C.treeGC)
|
||||
}
|
||||
"work for DamlLfDefDataType(ComplexRecord)" in {
|
||||
serializeAndParse[model.DamlLfDefDataType](C.complexRecordGC) shouldBe Success(
|
||||
C.complexRecordGC
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.navigator.model
|
||||
|
||||
import java.time.{Instant, LocalDate}
|
||||
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
|
||||
import com.daml.lf.data.{Time => LfTime}
|
||||
import com.daml.lf.value.Value.{ValueDate, ValueTimestamp}
|
||||
import com.daml.lf.value.json.ApiValueImplicits._
|
||||
|
||||
class ApiValueSpec extends AnyWordSpec with Matchers {
|
||||
|
||||
"Api values" when {
|
||||
"converting Date from ISO and back" should {
|
||||
val isoDate = "2019-01-28"
|
||||
val result = ValueDate.fromIso8601(isoDate).toIso8601
|
||||
|
||||
"not change the value" in {
|
||||
result shouldBe isoDate
|
||||
}
|
||||
}
|
||||
|
||||
"converting Date to ISO and back" should {
|
||||
val date = ValueDate(LfTime.Date assertFromDaysSinceEpoch 10000)
|
||||
val result = ValueDate.fromIso8601(date.toIso8601)
|
||||
|
||||
"not change the value" in {
|
||||
result shouldBe date
|
||||
}
|
||||
}
|
||||
|
||||
"converting Date from LocalDate and back" should {
|
||||
val localDate = LocalDate.of(2019, 1, 28)
|
||||
val result = ValueDate.fromLocalDate(localDate).toLocalDate
|
||||
|
||||
"not change the value" in {
|
||||
result shouldBe localDate
|
||||
}
|
||||
}
|
||||
|
||||
"converting Timestamp from ISO and back" should {
|
||||
// Timestamp has microsecond resolution
|
||||
val isoDateTime = "2019-01-28T12:44:33.123456Z"
|
||||
val result = ValueTimestamp.fromIso8601(isoDateTime).toIso8601
|
||||
|
||||
"not change the value" in {
|
||||
result shouldBe isoDateTime
|
||||
}
|
||||
}
|
||||
|
||||
"converting Timestamp to ISO and back" should {
|
||||
val timestamp = ValueTimestamp(LfTime.Timestamp assertFromLong 123456789123456L)
|
||||
val result = ValueTimestamp.fromIso8601(timestamp.toIso8601)
|
||||
|
||||
"not change the value" in {
|
||||
result shouldBe timestamp
|
||||
}
|
||||
}
|
||||
|
||||
"converting Timestamp from Instant and back" should {
|
||||
// Timestamp has microsecond resolution
|
||||
val instant = Instant.ofEpochSecond(86400L * 365L * 30L, 123456L * 1000L)
|
||||
val result = ValueTimestamp.fromInstant(instant).toInstant
|
||||
|
||||
"not change the value" in {
|
||||
result shouldBe instant
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user