in query argument, rename %templates to templateIds, and nest query under 'query' field (#4082)

* in query argument, rename %templates to templateIds, and nest query under 'query' field

CHANGELOG_BEGIN
- [JSON API - Experimental] In 'search' endpoint arguments, %templates is now templateIds.
  Additionally, all contract query fields must occur under 'query'.
  See `issue #3450 <https://github.com/digital-asset/daml/issues/3450>`__.
CHANGELOG_END

* fix other old query format usages
This commit is contained in:
Stephen Compall 2020-01-17 14:34:27 -05:00 committed by GitHub
parent fa8a92f772
commit 0520fdfa84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 51 additions and 33 deletions

View File

@ -180,8 +180,8 @@ application/json body, formatted according to the :doc:`search-query-language`:
.. code-block:: json .. code-block:: json
{ {
"%templates": ["Iou:Iou"], "templateIds": ["Iou:Iou"],
"amount": 999.99 "query": {"amount": 999.99}
} }
Empty Response Empty Response
@ -265,7 +265,7 @@ Two subprotocols must be passed, as described in `Choosing a party
application/json body must be sent first, formatted according to the application/json body must be sent first, formatted according to the
:doc:`search-query-language`:: :doc:`search-query-language`::
{"%templates": ["Iou:Iou"]} {"templateIds": ["Iou:Iou"]}
output a series of JSON documents, each ``argument`` formatted according output a series of JSON documents, each ``argument`` formatted according
to :doc:`lf-value-specification`:: to :doc:`lf-value-specification`::

View File

@ -6,8 +6,8 @@
The body of ``POST /contracts/search`` looks like so:: The body of ``POST /contracts/search`` looks like so::
{"%templates": [...template IDs...], {"templateIds": [...template IDs...],
...other query elements...} "query": {...query elements...}}
The elements of that query are defined here. The elements of that query are defined here.
@ -108,10 +108,10 @@ For these reasons, as with LF value input via JSON, queries written in
JSON are also always interpreted with respect to some specified LF types JSON are also always interpreted with respect to some specified LF types
(e.g. template IDs). For example:: (e.g. template IDs). For example::
{"%templates": [{"moduleName": "Foo", "entityName": "A"}, {"templateIds": [{"moduleName": "Foo", "entityName": "A"},
{"moduleName": "Foo", "entityName": "B"}, {"moduleName": "Foo", "entityName": "B"},
{"moduleName": "Foo", "entityName": "C"}], {"moduleName": "Foo", "entityName": "C"}],
"foo": "bar"} "query": {"foo": "bar"}}
will treat ``"foo"`` as a field equality query for A and B, and will treat ``"foo"`` as a field equality query for A and B, and
(supposing templates' associated data types were permitted to be (supposing templates' associated data types were permitted to be

View File

@ -127,7 +127,7 @@ class Ledger {
* for a description of the query language. * for a description of the query language.
*/ */
async query<T extends object, K>(template: Template<T, K>, query: Query<T>): Promise<CreateEvent<T, K>[]> { async query<T extends object, K>(template: Template<T, K>, query: Query<T>): Promise<CreateEvent<T, K>[]> {
const payload = {"%templates": [template.templateId], ...query}; const payload = {templateIds: [template.templateId], query};
const json = await this.submit('contracts/search', payload); const json = await this.submit('contracts/search', payload);
return jtv.Result.withException(jtv.array(decodeCreateEvent(template)).run(json)); return jtv.Result.withException(jtv.array(decodeCreateEvent(template)).run(json));
} }

View File

@ -196,17 +196,28 @@ object JsonProtocol extends DefaultJsonProtocol {
implicit val ArchivedContractFormat: RootJsonFormat[domain.ArchivedContract] = implicit val ArchivedContractFormat: RootJsonFormat[domain.ArchivedContract] =
jsonFormat2(domain.ArchivedContract.apply) jsonFormat2(domain.ArchivedContract.apply)
private val templatesKey = "%templates" /** Derived from autogenerated with 3 extra features:
* 1. template IDs are required
* 2. query key may be absent
* 3. special error if you appear to have queried outside 'query'
*/
implicit val GetActiveContractsRequestFormat: RootJsonReader[domain.GetActiveContractsRequest] = { implicit val GetActiveContractsRequestFormat: RootJsonReader[domain.GetActiveContractsRequest] = {
case JsObject(fields) => case class GACR(
val templates = (fields get templatesKey) templateIds: Set[domain.TemplateId.OptionalPkg],
.map(_.convertTo[Set[domain.TemplateId.OptionalPkg]]) query: Option[Map[String, JsValue]])
.filter(_.nonEmpty) val validKeys = Set("templateIds", "query")
.getOrElse( implicit val primitive: JsonReader[GACR] = jsonFormat2(GACR.apply)
deserializationError("/contracts/search requires at least one item in '%templates'")) jsv =>
domain.GetActiveContractsRequest(templateIds = templates, query = fields - templatesKey) {
case _ => deserializationError("/contracts/search must receive an object") val GACR(tids, q) = jsv.convertTo[GACR]
val extras = jsv.asJsObject.fields.keySet diff validKeys
if (tids.isEmpty)
deserializationError("search requires at least one item in 'templateIds'")
else if (extras.nonEmpty)
deserializationError(
s"unsupported query fields ${extras}; likely should be within 'query' subobject")
domain.GetActiveContractsRequest(tids, q getOrElse Map.empty)
}
} }
implicit val CommandMetaFormat: RootJsonFormat[domain.CommandMeta] = jsonFormat3( implicit val CommandMetaFormat: RootJsonFormat[domain.CommandMeta] = jsonFormat3(

View File

@ -143,7 +143,7 @@ abstract class AbstractHttpServiceIntegrationTest
"contracts/search POST with empty query" in withHttpService { (uri, encoder, _) => "contracts/search POST with empty query" in withHttpService { (uri, encoder, _) =>
searchWithQuery( searchWithQuery(
searchDataSet, searchDataSet,
jsObject("""{"%templates": ["Iou:Iou"]}"""), jsObject("""{"templateIds": ["Iou:Iou"]}"""),
uri, uri,
encoder encoder
).map { acl: List[domain.ActiveContract[JsValue]] => ).map { acl: List[domain.ActiveContract[JsValue]] =>
@ -154,7 +154,7 @@ abstract class AbstractHttpServiceIntegrationTest
"contracts/search with query, one field" in withHttpService { (uri, encoder, _) => "contracts/search with query, one field" in withHttpService { (uri, encoder, _) =>
searchWithQuery( searchWithQuery(
searchDataSet, searchDataSet,
jsObject("""{"%templates": ["Iou:Iou"], "currency": "EUR"}"""), jsObject("""{"templateIds": ["Iou:Iou"], "query": {"currency": "EUR"}}"""),
uri, uri,
encoder encoder
).map { acl: List[domain.ActiveContract[JsValue]] => ).map { acl: List[domain.ActiveContract[JsValue]] =>
@ -165,7 +165,8 @@ abstract class AbstractHttpServiceIntegrationTest
"contracts/search returns unknown Template IDs as warnings" in withHttpService { (uri, _, _) => "contracts/search returns unknown Template IDs as warnings" in withHttpService { (uri, _, _) =>
val query = val query =
jsObject("""{"%templates": ["Iou:Iou", "UnknownModule:UnknownEntity"], "currency": "EUR"}""") jsObject(
"""{"templateIds": ["Iou:Iou", "UnknownModule:UnknownEntity"], "query": {"currency": "EUR"}}""")
postJsonRequest(uri.withPath(Uri.Path("/contracts/search")), query) postJsonRequest(uri.withPath(Uri.Path("/contracts/search")), query)
.map { .map {
@ -186,8 +187,10 @@ abstract class AbstractHttpServiceIntegrationTest
rs: List[(StatusCode, JsValue)] => rs: List[(StatusCode, JsValue)] =>
rs.map(_._1) shouldBe List.fill(searchDataSet.size)(StatusCodes.OK) rs.map(_._1) shouldBe List.fill(searchDataSet.size)(StatusCodes.OK)
val queryAmountAsString = jsObject("""{"%templates": ["Iou:Iou"], "amount": "111.11"}""") def queryAmountAs(s: String) =
val queryAmountAsNumber = jsObject("""{"%templates": ["Iou:Iou"], "amount": 111.11}""") jsObject(s"""{"templateIds": ["Iou:Iou"], "query": {"amount": $s}}""")
val queryAmountAsString = queryAmountAs("\"111.11\"")
val queryAmountAsNumber = queryAmountAs("111.11")
List( List(
postJsonRequest(uri.withPath(Uri.Path("/contracts/search")), queryAmountAsString), postJsonRequest(uri.withPath(Uri.Path("/contracts/search")), queryAmountAsString),
@ -212,7 +215,8 @@ abstract class AbstractHttpServiceIntegrationTest
"contracts/search with query, two fields" in withHttpService { (uri, encoder, _) => "contracts/search with query, two fields" in withHttpService { (uri, encoder, _) =>
searchWithQuery( searchWithQuery(
searchDataSet, searchDataSet,
jsObject("""{"%templates": ["Iou:Iou"], "currency": "EUR", "amount": "111.11"}"""), jsObject(
"""{"templateIds": ["Iou:Iou"], "query": {"currency": "EUR", "amount": "111.11"}}"""),
uri, uri,
encoder).map { acl: List[domain.ActiveContract[JsValue]] => encoder).map { acl: List[domain.ActiveContract[JsValue]] =>
acl.size shouldBe 1 acl.size shouldBe 1
@ -224,7 +228,8 @@ abstract class AbstractHttpServiceIntegrationTest
"contracts/search with query, no results" in withHttpService { (uri, encoder, _) => "contracts/search with query, no results" in withHttpService { (uri, encoder, _) =>
searchWithQuery( searchWithQuery(
searchDataSet, searchDataSet,
jsObject("""{"%templates": ["Iou:Iou"], "currency": "RUB", "amount": "666.66"}"""), jsObject(
"""{"templateIds": ["Iou:Iou"], "query": {"currency": "RUB", "amount": "666.66"}}"""),
uri, uri,
encoder encoder
).map { acl: List[domain.ActiveContract[JsValue]] => ).map { acl: List[domain.ActiveContract[JsValue]] =>
@ -653,9 +658,11 @@ abstract class AbstractHttpServiceIntegrationTest
val contractId: ContractId = getContractId(getResult(output)) val contractId: ContractId = getContractId(getResult(output))
val query = jsObject(s"""{ val query = jsObject(s"""{
"%templates": ["$packageId:Account:Account"], "templateIds": ["$packageId:Account:Account"],
"query": {
"number" : "abc123", "number" : "abc123",
"status" : {"tag": "Enabled", "value": "${nowStr: String}"} "status" : {"tag": "Enabled", "value": "${nowStr: String}"}
}
}""") }""")
postJsonRequest(uri.withPath(Uri.Path("/contracts/search")), query).map { postJsonRequest(uri.withPath(Uri.Path("/contracts/search")), query).map {

View File

@ -35,7 +35,7 @@ class HttpServiceWithPostgresIntTest
"contracts/search persists all active contracts" in withHttpService { (uri, encoder, _) => "contracts/search persists all active contracts" in withHttpService { (uri, encoder, _) =>
searchWithQuery( searchWithQuery(
searchDataSet, searchDataSet,
jsObject("""{"%templates": ["Iou:Iou"], "currency": "EUR"}"""), jsObject("""{"templateIds": ["Iou:Iou"], "query": {"currency": "EUR"}}"""),
uri, uri,
encoder encoder
).flatMap { searchResult: List[domain.ActiveContract[JsValue]] => ).flatMap { searchResult: List[domain.ActiveContract[JsValue]] =>

View File

@ -83,7 +83,7 @@ class WebsocketServiceIntegrationTest
subprotocol = validSubprotocol)) subprotocol = validSubprotocol))
clientMsg <- Source clientMsg <- Source
.single(TextMessage("""{"%templates": ["Iou:Iou"]}""")) .single(TextMessage("""{"templateIds": ["Iou:Iou"]}"""))
.via(webSocketFlow) .via(webSocketFlow)
.runWith(collectResultsAsRawString) .runWith(collectResultsAsRawString)
} yield } yield
@ -148,7 +148,7 @@ class WebsocketServiceIntegrationTest
subprotocol = validSubprotocol)) subprotocol = validSubprotocol))
val query = val query =
TextMessage.Strict("""{"%templates": ["Iou:Iou"]}""") TextMessage.Strict("""{"templateIds": ["Iou:Iou"]}""")
val parseResp: Flow[Message, JsValue, NotUsed] = val parseResp: Flow[Message, JsValue, NotUsed] =
Flow[Message] Flow[Message]