Update facttable-for-report.jq

Update facttable-for-report.jq

import module namespace response = "http://www.28msec.com/modules/http-response";
import module namespace request = "http://www.28msec.com/modules/http-request";
import module namespace schema = "http://zorba.io/modules/schema";
import module namespace test = "http://apps.28.io/test";
import module namespace http-client = "http://zorba.io/modules/http-client";

declare function local:expected-OtherOperatingIncomeExpenses-ATnT-2013() as object {
    {
        "Aspects": {
            "xbrl28:Archive": "0000732717-14-000010",
            "xbrl:Concept": "fac:OtherOperatingIncomeExpenses",
            "xbrl:Entity": "http://www.sec.gov/CIK 0000732717",
            "xbrl:Period": "2013-01-01/2013-12-31",
            "sec:FiscalPeriod": "FY",
            "sec:FiscalPeriodType": "YTD",
            "sec:FiscalYear": 2013,
            "sec:Accepted": "20140221162554",
            "dei:LegalEntityAxis": "sec:DefaultLegalEntity"
        },
        "KeyAspects": [
            "xbrl:Concept",
            "xbrl:Entity",
            "xbrl:Period",
            "xbrl:Unit",
            "sec:Accepted"
        ],
        "Type": "NumericValue",
        "Value": -145082000000,
        "Decimals": -6,
        "Concept": {
            "Balance": "CREDIT",
            "PeriodType": "duration",
            "DataType": "xbrli:monetaryItemType"
        },
        "AuditTrails": [
            {
                "Id": "bf2d1587-491c-4492-b587-ddd00d762f2d",
                "Type": "xbrl28:formula",
                "Label": "Other Operating Income (Expenses)",
                "Message": "fac:OtherOperatingIncomeExpenses[-145,082,000,000 USD] = fac:OperatingIncomeLoss[30,479,000,000 USD] - fac:GrossProfit[77,288,000,000 USD] - fac:OperatingExpenses[98,273,000,000 USD]",
                "Data": {
                    "OutputConcept": "fac:OtherOperatingIncomeExpenses"
                }
            }
        ],
        "xbrl28:Type": "xbrl28:formula",
        "EntityRegistrantName": "AT&T INC.",
        "Unit": "iso4217:USD"
    }
};

declare function local:compare-audit-trails($expected as array?, $actual as array?) as object* {
    for $exp as object at $pos in $expected[]
    let $act as object? := $actual[][$pos]
    return
        if (exists($exp.Data.ValidatedFacts))
        then
            let $expM := copy $e := $exp modify ( delete json $e.Data.ValidatedFacts ) return $e
            let $actM := copy $a := $act modify ( delete json $a.Data.ValidatedFacts ) return $a
            return  (
                if(deep-equal($expM, $actM))
                then ()
                else
                    {
                        type: "unexpected-audittrails-value",
                        expected: $expM,
                        actual: $actM
                    },
                if(count($exp.Data.ValidatedFacts[]) eq count($act.Data.ValidatedFacts[]))
                then ()
                else
                    {
                        type: "number-of-validated-facts-differs",
                        expected: $exp,
                        actual: $act
                    }
            )
        else
            if(deep-equal($exp, $act))
            then ()
            else
                {
                    type: "unexpected-audittrails-value",
                    expected: $exp,
                    actual: $act
                }
};

declare function local:diff-facts($fact-expected as object, $fact-actual as object?) as object* {
    if(empty($fact-actual))
    then
        {
            type: "missing-fact-in-result"
        }
    else
        (
            for $key in distinct-values((keys($fact-expected), keys($fact-actual)))
            where not($key = ("Aspects", "KeyAspects", "Profiles", "AuditTrails"))
            let $exp-value := $fact-expected.$key
            let $act-value := $fact-actual.$key
            return
                if(deep-equal($exp-value, $act-value) or
                   (schema:schema-type($exp-value) eq schema:schema-type($act-value) and schema:schema-type($exp-value) eq xs:QName("xs:decimal") and fn:format-number(xs:decimal($exp-value),"#,##0.0000000000000") eq fn:format-number(xs:decimal($act-value),"#,##0.0000000000000"))
                )
                then ()
                else
                    {
                        type: "unexpected-fact-field-value",
                        field: $key,
                        expected: $exp-value,
                        actual: $act-value
                    },
            (: compare aspects :)
            for $key in distinct-values((keys($fact-expected.Aspects), keys($fact-actual.Aspects)))
            let $exp-value := $fact-expected.Aspects.$key
            let $act-value := $fact-actual.Aspects.$key
            return
                if(deep-equal($exp-value, $act-value))
                then ()
                else
                    {
                        type: "unexpected-aspect-value",
                        aspect: $key,
                        expected: $exp-value,
                        actual: $act-value
                    },
            (: compare keyaspects :)
            let $exp-value := $fact-expected.KeyAspects
            let $act-value := $fact-actual.KeyAspects
            return
                if(deep-equal($exp-value, $act-value))
                then ()
                else
                    {
                        type: "unexpected-keyaspects-value",
                        expected: $exp-value,
                        actual: $act-value
                    },
            (: compare Profiles :)
            let $exp-value := $fact-expected.Profiles
            let $act-value := $fact-actual.Profiles
            return
                if(deep-equal($exp-value, $act-value))
                then ()
                else
                    {
                        type: "unexpected-profiles-value",
                        expected: $exp-value,
                        actual: $act-value
                    },
            (: compare Audittrails :)
            let $exp-value := $fact-expected.AuditTrails
            let $act-value := $fact-actual.AuditTrails
            return
                local:compare-audit-trails($exp-value, $act-value)
        )
};

declare function local:compare-fact-tables($fact-table-expected as object, $fact-table-actual as object) as object*{
    let $actual-facts as object+ := $fact-table-actual.FactTable[]
    let $expected-facts as object+ := $fact-table-expected.FactTable[]
    let $errors :=
        (
            for $exp in $expected-facts
            let $act := $actual-facts[$$.Aspects."xbrl:Concept" eq $exp.Aspects."xbrl:Concept"
                                      and $$.Aspects."xbrl:Entity" eq $exp.Aspects."xbrl:Entity"
                                      and $$.Aspects."xbrl:Period" eq $exp.Aspects."xbrl:Period"
                                      and $$.Aspects."xbrl28:Archive" eq $exp.Aspects."xbrl28:Archive"]
            let $errors as object* := local:diff-facts($exp, $act)
            return
                if(empty($errors))
                then ()
                else {
                    expectedFact: $exp,
                    actualFact: $act,
                    errors: [$errors]
                },

            for $act in $actual-facts
            let $exp := $expected-facts[$$.Aspects."xbrl:Concept" eq $act.Aspects."xbrl:Concept"
                                      and $$.Aspects."xbrl:Entity" eq $act.Aspects."xbrl:Entity"
                                      and $$.Aspects."xbrl:Period" eq $act.Aspects."xbrl:Period"
                                      and $$.Aspects."xbrl28:Archive" eq $act.Aspects."xbrl28:Archive"]
            where empty($exp)
            return
                {
                    unexpectedFact: $act
                }
        )
    return
        if(empty($errors))
        then ()
        else (
                {
                    expectedNumberOfFacts: count($expected-facts),
                    actualNumberOfFacts: count($actual-facts)
                },
                $errors
            )
};

declare %an:nondeterministic function local:test-facttable($expected as integer, $params as object) as item
{
    let $endpoint := "facttable-for-report"
    let $request := test:invoke($endpoint, $params)
    let $actual as integer := count($request[2].FactTable[])
    let $status as integer := $request[1]
    return test:assert-eq($expected, $actual, $status, test:url($endpoint, $params))
};

declare %an:nondeterministic function local:test-facttable-fact($concept as string, $expected as object, $params as object) as item
{
    let $endpoint := "facttable-for-report"
    let $request := test:invoke($endpoint, $params)
    let $facts as object* := $request[2].FactTable[]
    let $actual := $facts[$$.Aspects."xbrl:Concept" eq $concept]
    let $diff :=
      for $f in $actual return local:diff-facts($expected, $f)
    return if (empty($diff)) then true else { url: test:url($endpoint, $params), factDiffErrors: [ $diff ], expectedFact: $expected, actualFact: $actual }
};

declare %an:nondeterministic function local:test-report-does-not-exist($params as object) as item
{
    let $endpoint := "facttable-for-report"
    let $request := test:invoke($endpoint, $params)
    let $status as integer := $request[1]
    return if ($status eq 404) then true else { url: test:url($endpoint, $params), unexpectedResponse: $request[2] }
};

declare %an:sequential function local:check($o as object) as object
{
    if (not(every $k in (keys($o) ! $o.$$) satisfies ($k instance of boolean and $k)))
    then {
            response:status-code(500);
            $o
    } else
            $o
};

declare %an:nondeterministic function local:test-values() as item*
{
    let $endpoint := "facttable-for-report"
    let $params := {ticker:"ko",fiscalYear:"2013",fiscalPeriod:"FY",fiscalPeriodType: ["instant", "YTD", "QTD"],report:"FundamentalAccountingConcepts"}
    let $request := test:invoke($endpoint, $params)
    let $actual as object := $request[2]
    let $expected := parse-json(
        http-client:get("http://" || request:server-name() || ":" || request:server-port() ||
                        "/test/facttable-for-report-expected1.jq").body.content)
    let $diff := local:compare-fact-tables($expected, $actual)
    return if (empty($diff)) then true else { url: test:url($endpoint, $params), factTableDiff: [ ({ params : $params },$diff) ], expectedFactTable: $expected, actualFactTable: $actual }
};

let $dow30 := test:is-dow30()
local:check({
    cocacola: local:test-facttable(95, {
        report:"FundamentalAccountingConcepts",
        ticker:"ko",
        fiscalYear:"2013",
        fiscalPeriod:"Q1"}),
    aid: local:test-facttable(if($dow30) then 0 else 95, {
        aid:"0001193125-14-157120",
        report:"FundamentalAccountingConcepts"}),
    aid2: local:test-facttable(96, {
        aid:"0000732717-14-000022",
        report:"FundamentalAccountingConcepts"}),
    tickerrole: local:test-facttable(95, {
        report:"FundamentalAccountingConcepts",
        ticker:"ko",
        fiscalYear:"2012",
        fiscalPeriod:"Q1"}),
    tickerfyfprole: local:test-facttable(246, {
        report:"FundamentalAccountingConcepts",
        ticker:["ko","wmt"],
        fiscalYear:"2013",
        fiscalPeriod:"FY",
        fiscalPeriodType:["instant", "QTD"]
    }),
    allvalues : local:test-values(),
    otheroperatingincometest: local:test-facttable-fact("fac:OtherOperatingIncomeExpenses",
                                                        local:expected-OtherOperatingIncomeExpenses-ATnT-2013(),
                                                        {
        report:"FundamentalAccountingConcepts",
        ticker:"t",
        fiscalYear:"2013",
        fiscalPeriod:["FY"]}),
    reportDoesntExist: local:test-report-does-not-exist({
        report:"report-not-found",
        ticker:"MSFT",
        fiscalYear:"ALL",
        fiscalPeriod:"FY"})
})

import module namespace response = "http://www.28msec.com/modules/http-response";
import module namespace request = "http://www.28msec.com/modules/http-request";
import module namespace schema = "http://zorba.io/modules/schema";
import module namespace test = "http://apps.28.io/test";
import module namespace http-client = "http://zorba.io/modules/http-client";

declare function local:expected-OtherOperatingIncomeExpenses-ATnT-2013() as object {
    {
        "Aspects": {
            "xbrl28:Archive": "0000732717-14-000010",
            "xbrl:Concept": "fac:OtherOperatingIncomeExpenses",
            "xbrl:Entity": "http://www.sec.gov/CIK 0000732717",
            "xbrl:Period": "2013-01-01/2013-12-31",
            "sec:FiscalPeriod": "FY",
            "sec:FiscalPeriodType": "YTD",
            "sec:FiscalYear": 2013,
            "sec:Accepted": "20140221162554",
            "dei:LegalEntityAxis": "sec:DefaultLegalEntity"
        },
        "KeyAspects": [
            "xbrl:Concept",
            "xbrl:Entity",
            "xbrl:Period",
            "xbrl:Unit",
            "sec:Accepted"
        ],
        "Type": "NumericValue",
        "Value": -145082000000,
        "Decimals": -6,
        "Concept": {
            "Balance": "CREDIT",
            "PeriodType": "duration",
            "DataType": "xbrli:monetaryItemType"
        },
        "AuditTrails": [
            {
                "Id": "bf2d1587-491c-4492-b587-ddd00d762f2d",
                "Type": "xbrl28:formula",
                "Label": "Other Operating Income (Expenses)",
                "Message": "fac:OtherOperatingIncomeExpenses[-145,082,000,000 USD] = fac:OperatingIncomeLoss[30,479,000,000 USD] - fac:GrossProfit[77,288,000,000 USD] - fac:OperatingExpenses[98,273,000,000 USD]",
                "Data": {
                    "OutputConcept": "fac:OtherOperatingIncomeExpenses"
                }
            }
        ],
        "xbrl28:Type": "xbrl28:formula",
        "EntityRegistrantName": "AT&T INC.",
        "Unit": "iso4217:USD"
    }
};

declare function local:compare-audit-trails($expected as array?, $actual as array?) as object* {
    for $exp as object at $pos in $expected[]
    let $act as object? := $actual[][$pos]
    return
        if (exists($exp.Data.ValidatedFacts))
        then
            let $expM := copy $e := $exp modify ( delete json $e.Data.ValidatedFacts ) return $e
            let $actM := copy $a := $act modify ( delete json $a.Data.ValidatedFacts ) return $a
            return  (
                if(deep-equal($expM, $actM))
                then ()
                else
                    {
                        type: "unexpected-audittrails-value",
                        expected: $expM,
                        actual: $actM
                    },
                if(count($exp.Data.ValidatedFacts[]) eq count($act.Data.ValidatedFacts[]))
                then ()
                else
                    {
                        type: "number-of-validated-facts-differs",
                        expected: $exp,
                        actual: $act
                    }
            )
        else
            if(deep-equal($exp, $act))
            then ()
            else
                {
                    type: "unexpected-audittrails-value",
                    expected: $exp,
                    actual: $act
                }
};

declare function local:diff-facts($fact-expected as object, $fact-actual as object?) as object* {
    if(empty($fact-actual))
    then
        {
            type: "missing-fact-in-result"
        }
    else
        (
            for $key in distinct-values((keys($fact-expected), keys($fact-actual)))
            where not($key = ("Aspects", "KeyAspects", "Profiles", "AuditTrails"))
            let $exp-value := $fact-expected.$key
            let $act-value := $fact-actual.$key
            return
                if(deep-equal($exp-value, $act-value) or
                   (schema:schema-type($exp-value) eq schema:schema-type($act-value) and schema:schema-type($exp-value) eq xs:QName("xs:decimal") and fn:format-number(xs:decimal($exp-value),"#,##0.0000000000000") eq fn:format-number(xs:decimal($act-value),"#,##0.0000000000000"))
                )
                then ()
                else
                    {
                        type: "unexpected-fact-field-value",
                        field: $key,
                        expected: $exp-value,
                        actual: $act-value
                    },
            (: compare aspects :)
            for $key in distinct-values((keys($fact-expected.Aspects), keys($fact-actual.Aspects)))
            let $exp-value := $fact-expected.Aspects.$key
            let $act-value := $fact-actual.Aspects.$key
            return
                if(deep-equal($exp-value, $act-value))
                then ()
                else
                    {
                        type: "unexpected-aspect-value",
                        aspect: $key,
                        expected: $exp-value,
                        actual: $act-value
                    },
            (: compare keyaspects :)
            let $exp-value := $fact-expected.KeyAspects
            let $act-value := $fact-actual.KeyAspects
            return
                if(deep-equal($exp-value, $act-value))
                then ()
                else
                    {
                        type: "unexpected-keyaspects-value",
                        expected: $exp-value,
                        actual: $act-value
                    },
            (: compare Profiles :)
            let $exp-value := $fact-expected.Profiles
            let $act-value := $fact-actual.Profiles
            return
                if(deep-equal($exp-value, $act-value))
                then ()
                else
                    {
                        type: "unexpected-profiles-value",
                        expected: $exp-value,
                        actual: $act-value
                    },
            (: compare Audittrails :)
            let $exp-value := $fact-expected.AuditTrails
            let $act-value := $fact-actual.AuditTrails
            return
                local:compare-audit-trails($exp-value, $act-value)
        )
};

declare function local:compare-fact-tables($fact-table-expected as object, $fact-table-actual as object) as object*{
    let $actual-facts as object+ := $fact-table-actual.FactTable[]
    let $expected-facts as object+ := $fact-table-expected.FactTable[]
    let $errors :=
        (
            for $exp in $expected-facts
            let $act := $actual-facts[$$.Aspects."xbrl:Concept" eq $exp.Aspects."xbrl:Concept"
                                      and $$.Aspects."xbrl:Entity" eq $exp.Aspects."xbrl:Entity"
                                      and $$.Aspects."xbrl:Period" eq $exp.Aspects."xbrl:Period"
                                      and $$.Aspects."xbrl28:Archive" eq $exp.Aspects."xbrl28:Archive"]
            let $errors as object* := local:diff-facts($exp, $act)
            return
                if(empty($errors))
                then ()
                else {
                    expectedFact: $exp,
                    actualFact: $act,
                    errors: [$errors]
                },

            for $act in $actual-facts
            let $exp := $expected-facts[$$.Aspects."xbrl:Concept" eq $act.Aspects."xbrl:Concept"
                                      and $$.Aspects."xbrl:Entity" eq $act.Aspects."xbrl:Entity"
                                      and $$.Aspects."xbrl:Period" eq $act.Aspects."xbrl:Period"
                                      and $$.Aspects."xbrl28:Archive" eq $act.Aspects."xbrl28:Archive"]
            where empty($exp)
            return
                {
                    unexpectedFact: $act
                }
        )
    return
        if(empty($errors))
        then ()
        else (
                {
                    expectedNumberOfFacts: count($expected-facts),
                    actualNumberOfFacts: count($actual-facts)
                },
                $errors
            )
};

declare %an:nondeterministic function local:test-facttable($expected as integer, $params as object) as item
{
    let $endpoint := "facttable-for-report"
    let $request := test:invoke($endpoint, $params)
    let $actual as integer := count($request[2].FactTable[])
    let $status as integer := $request[1]
    return test:assert-eq($expected, $actual, $status, test:url($endpoint, $params))
};

declare %an:nondeterministic function local:test-facttable-fact($concept as string, $expected as object, $params as object) as item
{
    let $endpoint := "facttable-for-report"
    let $request := test:invoke($endpoint, $params)
    let $facts as object* := $request[2].FactTable[]
    let $actual := $facts[$$.Aspects."xbrl:Concept" eq $concept]
    let $diff :=
      for $f in $actual return local:diff-facts($expected, $f)
    return if (empty($diff)) then true else { url: test:url($endpoint, $params), factDiffErrors: [ $diff ], expectedFact: $expected, actualFact: $actual }
};

declare %an:nondeterministic function local:test-report-does-not-exist($params as object) as item
{
    let $endpoint := "facttable-for-report"
    let $request := test:invoke($endpoint, $params)
    let $status as integer := $request[1]
    return if ($status eq 404) then true else { url: test:url($endpoint, $params), unexpectedResponse: $request[2] }
};

declare %an:sequential function local:check($o as object) as object
{
    if (not(every $k in (keys($o) ! $o.$$) satisfies ($k instance of boolean and $k)))
    then {
            response:status-code(500);
            $o
    } else
            $o
};

declare %an:nondeterministic function local:test-values() as item*
{
    let $endpoint := "facttable-for-report"
    let $params := {ticker:"ko",fiscalYear:"2013",fiscalPeriod:"FY",fiscalPeriodType: ["instant", "YTD", "QTD"],report:"FundamentalAccountingConcepts"}
    let $request := test:invoke($endpoint, $params)
    let $actual as object := $request[2]
    let $expected := parse-json(
        http-client:get("http://" || request:server-name() || ":" || request:server-port() ||
                        "/test/facttable-for-report-expected1.jq").body.content)
    let $diff := local:compare-fact-tables($expected, $actual)
    return if (empty($diff)) then true else { url: test:url($endpoint, $params), factTableDiff: [ ({ params : $params },$diff) ], expectedFactTable: $expected, actualFactTable: $actual }
};

local:check({
    cocacola: local:test-facttable(95, {
        report:"FundamentalAccountingConcepts",
        ticker:"ko",
        fiscalYear:"2013",
        fiscalPeriod:"Q1"}),
    aid: local:test-facttable(95, {
        aid:"0001193125-14-157120",
        report:"FundamentalAccountingConcepts"}),
    aid2: local:test-facttable(96, {
        aid:"0000732717-14-000022",
        report:"FundamentalAccountingConcepts"}),
    tickerrole: local:test-facttable(95, {
        report:"FundamentalAccountingConcepts",
        ticker:"ko",
        fiscalYear:"2012",
        fiscalPeriod:"Q1"}),
    tickerfyfprole: local:test-facttable(246, {
        report:"FundamentalAccountingConcepts",
        ticker:["ko","wmt"],
        fiscalYear:"2013",
        fiscalPeriod:"FY",
        fiscalPeriodType:["instant", "QTD"]
    }),
    allvalues : local:test-values(),
    otheroperatingincometest: local:test-facttable-fact("fac:OtherOperatingIncomeExpenses",
                                                        local:expected-OtherOperatingIncomeExpenses-ATnT-2013(),
                                                        {
        report:"FundamentalAccountingConcepts",
        ticker:"t",
        fiscalYear:"2013",
        fiscalPeriod:["FY"]}),
    reportDoesntExist: local:test-report-does-not-exist({
        report:"report-not-found",
        ticker:"MSFT",
        fiscalYear:"ALL",
        fiscalPeriod:"FY"})
})


:lipstick: Remove unused variable.

:lipstick: Remove unused variable.

import module namespace config = "http://apps.28.io/config";
import module namespace api = "http://apps.28.io/api";
import module namespace session = "http://apps.28.io/session";

import module namespace conversion = "http://28.io/modules/xbrl/conversion";
import module namespace hypercubes = "http://28.io/modules/xbrl/hypercubes";
import module namespace reports = "http://28.io/modules/xbrl/reports";
import module namespace concepts = "http://28.io/modules/xbrl/concepts";
import module namespace facts = "http://28.io/modules/xbrl/facts";
import module namespace labels = "http://28.io/modules/xbrl/labels";
import module namespace rules = "http://28.io/modules/xbrl/rules";
import module namespace components = "http://28.io/modules/xbrl/components";
import module namespace entities = "http://28.io/modules/xbrl/entities";
import module namespace archives = "http://28.io/modules/xbrl/archives";

import module namespace sec = "http://28.io/modules/xbrl/profiles/sec/core";
import module namespace multiplexer = "http://28.io/modules/xbrl/profiles/multiplexer";

import module namespace request = "http://www.28msec.com/modules/http-request";

declare variable $local:additional-concepts as object* := (
  {
    Name: "sec:DefaultLegalEntity",
    Labels : [ {
        Role : "http://www.xbrl.org/2003/role/label",
        Language : "en-us",
        Value : "Default Legal Entity [Member]"
    } ]
  },
  {
    Name: "xbrl:NonNumeric",
    Labels : [ {
        Role : "http://www.xbrl.org/2003/role/label",
        Language : "en-us",
        Value : "Non Numeric"
    } ]
  }
);

declare function local:param-values(
    $name as string,
    $entities as object*) as string*
{
    switch(true)
     case $name eq "xbrl:Concept"
        return (request:param-values("concept"), request:param-values("xbrl:Concept"))

    (: sec profile :)
     case $name eq "sec:FiscalYear" and $profile-name eq "sec"
        return ($fiscalYear, request:param-values("sec:FiscalYear"))[$$ ne "LATEST"]
     case $name eq "sec:FiscalYear::type" and $profile-name eq "sec"
        return "integer"
     case $name eq "sec:FiscalPeriod" and $profile-name eq "sec"
        return ($fiscalPeriod, request:param-values("sec:FiscalPeriod"))
     case $name eq "sec:FiscalPeriod::type" and $profile-name eq "sec"
        return "string"
     case $name eq "sec:FiscalPeriodType" and $profile-name eq "sec"
        return ($fiscalPeriodType, request:param-values("sec:FiscalPeriodType"))
     case $name eq "sec:FiscalPeriodType::type" and $profile-name eq "sec"
        return "string"
     case $name eq "dei:LegalEntityAxis" and $profile-name eq "sec"
        return
         if(empty((request:param-values("sec:LegalEntityAxis"), request:param-values("sec:LegalEntityAxis::default"))))
         then "sec:DefaultLegalEntity"
         else request:param-values("sec:LegalEntityAxis")
     case $name eq "dei:LegalEntityAxis::default" return
         if(empty((request:param-values("sec:LegalEntityAxis"), request:param-values("sec:LegalEntityAxis::default"))))
         then "sec:DefaultLegalEntity"
         else request:param-values("sec:LegalEntityAxis::default")

     case $name eq "xbrl:Entity" and $profile-name = ("sec", "japan")
         return (
                if(empty(($cik,$tag,$ticker,$sic)) or exists($entities))
                then entities:eid($entities)
                else "dummy",
                request:param-values("xbrl:Entity")
            )
     case $name eq "xbrl28:Archive" and $profile-name = ("sec", "japan") return (
            let $prefix as string :=
                switch($profile-name)
                case "sec" return "sec"
                case "japan" return "fsa"
                default return (: not reachable :) ()
            let $fiscalYears :=
                let $years := request:param-values( $prefix || ":FiscalYear")
                return if ($years) then $years else $fiscalYear
            let $fiscalPeriods := local:param-values($prefix || ":FiscalPeriod", $entities)
            return
                if($fiscalYears = "LATEST")
                then archives:aid(multiplexer:latest-filings($profile-name, $entities, $fiscalPeriods))
                else (),
            $aid,
            request:param-values("xbrl28:Archive")
        )

     (: japan profile :)
     case $name eq "fsa:FiscalYear" and $profile-name eq "japan"
     return ($fiscalYear, request:param-values("fsa:FiscalYear"))[$$ ne "LATEST"]
     case $name eq "fsa:FiscalYear::type" and $profile-name eq "japan"
     return "integer"
     case $name eq "fsa:FiscalPeriod" and $profile-name eq "japan"
     return ($fiscalPeriod, request:param-values("fsa:FiscalPeriod"))
     case $name eq "fsa:FiscalPeriod::type" and $profile-name eq "japan"
     return "string"
     case $name eq "fsa:FiscalPeriodType" and $profile-name eq "japan"
     return ($fiscalPeriodType, request:param-values("fsa:FiscalPeriodType"))
     case $name eq "fsa:FiscalPeriodType::type" and $profile-name eq "japan"
     return "string"

     default return request:param-values($name)
};

declare function local:param-names() as string*
{
    let $names := request:param-names()
    return distinct-values((
        (: generic xbrl :)
        $names[contains($$, ":")],
        "xbrl:Entity"[$names = ("eid")],
        "xbrl28:Archive"[$profile-name = ("sec", "japan")],
        "xbrl:Concept"[$names = "concept"],

        (: sec profile :)
        "sec:Accepted"[$profile-name eq "sec"],
        "sec:FiscalPeriod"[$profile-name eq "sec"],
        "sec:FiscalPeriodType"[$profile-name eq "sec"],
        "sec:FiscalYear"[$profile-name eq "sec"],
        "xbrl:Entity"[$profile-name eq "sec" and $names = ("cik", "tag", "ticker", "sic")],
        "dei:LegalEntityAxis"[$profile-name eq "sec"],
        "dei:LegalEntityAxis::default"[$profile-name eq "sec"],

        (: japan profile :)
        "fsa:Submitted"[$profile-name eq "japan"],
        "fsa:FiscalPeriod"[$profile-name eq "japan"],
        "fsa:FiscalPeriodType"[$profile-name eq "japan"],
        "fsa:FiscalYear"[$profile-name eq "japan"]
    ))
};

declare function local:cast-sequence($values as atomic*, $type as string) as atomic*
{
  for $value in $values
  return
      switch ($type)
      case "integer" return $value cast as integer
      case "string" return $value cast as string
      default return error(xs:QName("local:unsupported-type"), $type || ": unsupported type")
};

declare function local:hypercube($entities as object*) as object
{
    let $hypercube-spec :=
    {|
        for $parameter in local:param-names()
        group by $dimension-name :=
            switch(true)
            case ends-with(lower-case($parameter), "::default")
                return substring-before($parameter, "::default")
            case ends-with(lower-case($parameter), ":default")
                return substring-before($parameter, ":default")
            case ends-with(lower-case($parameter), "::type")
                return substring-before($parameter, "::type")
            default
                return $parameter
        let $all as boolean :=
            (local:param-values($dimension-name, $entities) ! upper-case($$)) = "ALL"
        let $type as string? :=
            (local:param-values($dimension-name || "::type", $entities),
             local:param-values($dimension-name || ":type", $entities))[1]

        let $values := local:param-values($dimension-name, $entities)
        let $typed-values := if (exists($type)) then local:cast-sequence($values[$$ ne "ALL"], $type) else $values

        let $has-default := ($parameter = $dimension-name || "::default") or ($parameter = $dimension-name || ":default")
        let $default-value := (local:param-values($dimension-name || "::default", $entities),
                               local:param-values($dimension-name || ":default", $entities))[1]
        let $typed-default-value := if (exists($type)) then local:cast-sequence($default-value, $type) else $default-value

        return
        {
            $dimension-name : {|
                { "Type" : $type }[exists($type)],
                if(not($all))
                then { "Domain" : [ $typed-values ] }[exists($typed-values)]
                else (),
                { "Default" : $typed-default-value }[$has-default],
                { "Default" : null }[(not $has-default) and $all and exists($type)]
            |}
        }
    |}
    return hypercubes:user-defined-hypercube($hypercube-spec)
};

(: Query parameters :)
declare  %rest:case-insensitive                 variable $token             as string? external;
declare  %rest:case-insensitive                 variable $profile-name      as string  external := $config:profile-name;
declare  %rest:env                              variable $request-uri       as string  external;
declare  %rest:case-insensitive                 variable $format            as string? external;
declare  %rest:case-insensitive %rest:distinct  variable $cik               as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $edinetcode        as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $tag               as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $ticker            as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $sic               as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $fiscalYear        as string* external := "LATEST";
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriod      as string* external := "FY";
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriodType  as string* external := ("instant", "YTD");
declare  %rest:case-insensitive %rest:distinct  variable $eid               as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $aid               as string* external;
declare  %rest:case-insensitive                 variable $map               as string? external;
declare  %rest:case-insensitive                 variable $rule              as string? external;
declare  %rest:case-insensitive                 variable $report            as string? external;
declare  %rest:case-insensitive                 variable $validate          as boolean external := false;
declare  %rest:case-insensitive                 variable $labels            as boolean external := false;
declare  %rest:case-insensitive                 variable $additional-rules  as string? external;
declare  %rest:case-insensitive                 variable $debug             as boolean external := false;
declare  %rest:case-insensitive                 variable $language          as string  external := "en-US";

session:audit-call($token);

(: Post-processing :)
let $format as string? := api:preprocess-format($format, $request-uri)
let $tag as string* := api:preprocess-tags($tag)

let $cik as string* :=
    switch($profile-name)
    case "sec" return $cik
    case "japan" return $edinetcode
    default return ()

(: Entity resolution :)
let $entities := multiplexer:entities(
  $profile-name,
  $eid,
  $cik,
  api:preprocess-tags($tag),
  $ticker,
  $sic,
  $aid)

let $report as object? := reports:reports($report)
let $map as item* :=
    if(exists($report))
    then reports:concept-map($report)
    else $map
let $rule as item* :=
    (
        if(exists($report))
        then reports:rules($report)
        else rules:rules($rule),
        if(exists($additional-rules))
        then rules:rules($additional-rules)
        else ()
    )

let $hypercube := local:hypercube($entities)

let $facts :=
    let $options := {|
      {
        Hypercube : $hypercube,
        Validate: $validate
      },
      { "ConceptMaps" : $map }[exists($map)],
      { "Rules" : [ $rule ] }[exists($rule)],
      { "Concepts" : $report.Concepts }[exists($report) and exists($report.Concepts)]
    |}
    return if($profile-name eq "sec")
    then sec:facts-for($options)
    else facts:facts-for($options)

let $facts :=
  if(not $labels)
  then $facts
  else
    let $archives as string* := distinct-values($facts.Aspects."xbrl28:Archive")
    let $concept-names as string* := (
      distinct-values((keys($facts.Aspects), values($facts.Aspects)[$$ instance of string]))
    )
    let $concepts as object* :=
      (
          concepts:concepts($concept-names, $archives, $concepts:ANY_COMPONENT_LINK_ROLE),
          $report.Concepts[][$$.Name = $concept-names],
          if($profile-name eq "sec") then $local:additional-concepts else ()
      )
    let $language as string := ( $language, $report.$components:DEFAULT-LANGUAGE , $labels:AMERICAN_ENGLISH )[1]
    let $nonFetchedEntities as string* := request:param-values("xbrl:Entity")[not $$ = entities:eid($entities)]
    let $entities as object* := ($entities, entities:entities($nonFetchedEntities))
    for $fact as object in $facts
    return
    {|
      $fact,
      let $concept-labels as object? := labels:labels-for-facts(
        $fact,
        $labels:STANDARD_LABEL_ROLE,
        $language,
        $concepts,
        $entities,
        ()
      )
      return { Labels : $concept-labels }
    |}

let $facts :=
  if($profile-name eq "sec")
  then api:normalize-facts($facts)
  else $facts

let $result := {
    NetworkIdentifier : "http://bizql.io/facts",
    TableName : "xbrl:Facts",
    FactTable : [ $facts ]
}
let $comment :=
{
    NumFacts: count($facts),
    TotalNumFacts: session:num-facts(),
    TotalNumArchives: session:num-archives(),
    TotalNumEntities: session:num-entities()
}
let $comment :=
    if($debug)
    then {|
        $comment,
        { DebugInfo: { Hypercube: $hypercube } }
    |}
    else $comment
let $serializers := {
    to-xml : function($res as object) as node()* {
           <FactTable NetworkIdentifier="http://bizql.io/facts"
            TableName="xbrl:Facts">{
                conversion:facts-to-xml($res.FactTable[], { Caller: "Report"})
        }</FactTable>
    },
    to-csv : function($res as object) as string {
        (conversion:facts-to-csv($res.FactTable[], { Caller: "Report" }), "")[1]
    }
}

let $results := api:serialize($result, $comment, $serializers, $format, "facts")
return api:check-and-return-results($token, $results, $format)

import module namespace config = "http://apps.28.io/config";
import module namespace api = "http://apps.28.io/api";
import module namespace session = "http://apps.28.io/session";

import module namespace conversion = "http://28.io/modules/xbrl/conversion";
import module namespace hypercubes = "http://28.io/modules/xbrl/hypercubes";
import module namespace reports = "http://28.io/modules/xbrl/reports";
import module namespace concepts = "http://28.io/modules/xbrl/concepts";
import module namespace facts = "http://28.io/modules/xbrl/facts";
import module namespace labels = "http://28.io/modules/xbrl/labels";
import module namespace rules = "http://28.io/modules/xbrl/rules";
import module namespace components = "http://28.io/modules/xbrl/components";
import module namespace entities = "http://28.io/modules/xbrl/entities";
import module namespace archives = "http://28.io/modules/xbrl/archives";

import module namespace sec = "http://28.io/modules/xbrl/profiles/sec/core";
import module namespace multiplexer = "http://28.io/modules/xbrl/profiles/multiplexer";

import module namespace request = "http://www.28msec.com/modules/http-request";

declare variable $local:additional-concepts as object* := (
  {
    Name: "sec:DefaultLegalEntity",
    Labels : [ {
        Role : "http://www.xbrl.org/2003/role/label",
        Language : "en-us",
        Value : "Default Legal Entity [Member]"
    } ]
  },
  {
    Name: "xbrl:NonNumeric",
    Labels : [ {
        Role : "http://www.xbrl.org/2003/role/label",
        Language : "en-us",
        Value : "Non Numeric"
    } ]
  }
);

declare function local:param-values(
    $name as string,
    $entities as object*) as string*
{
    switch(true)
     case $name eq "xbrl:Concept"
        return (request:param-values("concept"), request:param-values("xbrl:Concept"))

    (: sec profile :)
     case $name eq "sec:FiscalYear" and $profile-name eq "sec"
        return ($fiscalYear, request:param-values("sec:FiscalYear"))[$$ ne "LATEST"]
     case $name eq "sec:FiscalYear::type" and $profile-name eq "sec"
        return "integer"
     case $name eq "sec:FiscalPeriod" and $profile-name eq "sec"
        return ($fiscalPeriod, request:param-values("sec:FiscalPeriod"))
     case $name eq "sec:FiscalPeriod::type" and $profile-name eq "sec"
        return "string"
     case $name eq "sec:FiscalPeriodType" and $profile-name eq "sec"
        return ($fiscalPeriodType, request:param-values("sec:FiscalPeriodType"))
     case $name eq "sec:FiscalPeriodType::type" and $profile-name eq "sec"
        return "string"
     case $name eq "dei:LegalEntityAxis" and $profile-name eq "sec"
        return
         if(empty((request:param-values("sec:LegalEntityAxis"), request:param-values("sec:LegalEntityAxis::default"))))
         then "sec:DefaultLegalEntity"
         else request:param-values("sec:LegalEntityAxis")
     case $name eq "dei:LegalEntityAxis::default" return
         if(empty((request:param-values("sec:LegalEntityAxis"), request:param-values("sec:LegalEntityAxis::default"))))
         then "sec:DefaultLegalEntity"
         else request:param-values("sec:LegalEntityAxis::default")

     case $name eq "xbrl:Entity" and $profile-name = ("sec", "japan")
         return (
                if(empty(($cik,$tag,$ticker,$sic)) or exists($entities))
                then entities:eid($entities)
                else "dummy",
                request:param-values("xbrl:Entity")
            )
     case $name eq "xbrl28:Archive" and $profile-name = ("sec", "japan") return (
            let $prefix as string :=
                switch($profile-name)
                case "sec" return "sec"
                case "japan" return "fsa"
                default return (: not reachable :) ()
            let $fiscalYears :=
                let $years := request:param-values( $prefix || ":FiscalYear")
                return if ($years) then $years else $fiscalYear
            let $fiscalPeriods := local:param-values($prefix || ":FiscalPeriod", $entities)
            return
                if($fiscalYears = "LATEST")
                then archives:aid(multiplexer:latest-filings($profile-name, $entities, $fiscalPeriods))
                else (),
            $aid,
            request:param-values("xbrl28:Archive")
        )

     (: japan profile :)
     case $name eq "fsa:FiscalYear" and $profile-name eq "japan"
     return ($fiscalYear, request:param-values("fsa:FiscalYear"))[$$ ne "LATEST"]
     case $name eq "fsa:FiscalYear::type" and $profile-name eq "japan"
     return "integer"
     case $name eq "fsa:FiscalPeriod" and $profile-name eq "japan"
     return ($fiscalPeriod, request:param-values("fsa:FiscalPeriod"))
     case $name eq "fsa:FiscalPeriod::type" and $profile-name eq "japan"
     return "string"
     case $name eq "fsa:FiscalPeriodType" and $profile-name eq "japan"
     return ($fiscalPeriodType, request:param-values("fsa:FiscalPeriodType"))
     case $name eq "fsa:FiscalPeriodType::type" and $profile-name eq "japan"
     return "string"

     default return request:param-values($name)
};

declare function local:param-names() as string*
{
    let $names := request:param-names()
    return distinct-values((
        (: generic xbrl :)
        $names[contains($$, ":")],
        "xbrl:Entity"[$names = ("eid")],
        "xbrl28:Archive"[$profile-name = ("sec", "japan")],
        "xbrl:Concept"[$names = "concept"],

        (: sec profile :)
        "sec:Accepted"[$profile-name eq "sec"],
        "sec:FiscalPeriod"[$profile-name eq "sec"],
        "sec:FiscalPeriodType"[$profile-name eq "sec"],
        "sec:FiscalYear"[$profile-name eq "sec"],
        "xbrl:Entity"[$profile-name eq "sec" and $names = ("cik", "tag", "ticker", "sic")],
        "dei:LegalEntityAxis"[$profile-name eq "sec"],
        "dei:LegalEntityAxis::default"[$profile-name eq "sec"],

        (: japan profile :)
        "fsa:Submitted"[$profile-name eq "japan"],
        "fsa:FiscalPeriod"[$profile-name eq "japan"],
        "fsa:FiscalPeriodType"[$profile-name eq "japan"],
        "fsa:FiscalYear"[$profile-name eq "japan"]
    ))
};

declare function local:cast-sequence($values as atomic*, $type as string) as atomic*
{
  for $value in $values
  return
      switch ($type)
      case "integer" return $value cast as integer
      case "string" return $value cast as string
      default return error(xs:QName("local:unsupported-type"), $type || ": unsupported type")
};

declare function local:hypercube($entities as object*) as object
{
    let $hypercube-spec :=
    {|
        for $parameter in local:param-names()
        group by $dimension-name :=
            switch(true)
            case ends-with(lower-case($parameter), "::default")
                return substring-before($parameter, "::default")
            case ends-with(lower-case($parameter), ":default")
                return substring-before($parameter, ":default")
            case ends-with(lower-case($parameter), "::type")
                return substring-before($parameter, "::type")
            default
                return $parameter
        let $all as boolean :=
            (local:param-values($dimension-name, $entities) ! upper-case($$)) = "ALL"
        let $type as string? :=
            (local:param-values($dimension-name || "::type", $entities),
             local:param-values($dimension-name || ":type", $entities))[1]

        let $values := local:param-values($dimension-name, $entities)
        let $typed-values := if (exists($type)) then local:cast-sequence($values[$$ ne "ALL"], $type) else $values

        let $has-default := ($parameter = $dimension-name || "::default") or ($parameter = $dimension-name || ":default")
        let $default-value := (local:param-values($dimension-name || "::default", $entities),
                               local:param-values($dimension-name || ":default", $entities))[1]
        let $typed-default-value := if (exists($type)) then local:cast-sequence($default-value, $type) else $default-value

        return
        {
            $dimension-name : {|
                { "Type" : $type }[exists($type)],
                if(not($all))
                then { "Domain" : [ $typed-values ] }[exists($typed-values)]
                else (),
                { "Default" : $typed-default-value }[$has-default],
                { "Default" : null }[(not $has-default) and $all and exists($type)]
            |}
        }
    |}
    return hypercubes:user-defined-hypercube($hypercube-spec)
};

(: Query parameters :)
declare  %rest:case-insensitive                 variable $token             as string? external;
declare  %rest:case-insensitive                 variable $profile-name      as string  external := $config:profile-name;
declare  %rest:env                              variable $request-uri       as string  external;
declare  %rest:case-insensitive                 variable $format            as string? external;
declare  %rest:case-insensitive %rest:distinct  variable $cik               as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $edinetcode        as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $tag               as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $ticker            as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $sic               as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $fiscalYear        as string* external := "LATEST";
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriod      as string* external := "FY";
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriodType  as string* external := ("instant", "YTD");
declare  %rest:case-insensitive %rest:distinct  variable $eid               as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $aid               as string* external;
declare  %rest:case-insensitive                 variable $map               as string? external;
declare  %rest:case-insensitive                 variable $rule              as string? external;
declare  %rest:case-insensitive                 variable $report            as string? external;
declare  %rest:case-insensitive                 variable $validate          as boolean external := false;
declare  %rest:case-insensitive                 variable $labels            as boolean external := false;
declare  %rest:case-insensitive                 variable $additional-rules  as string? external;
declare  %rest:case-insensitive                 variable $debug             as boolean external := false;
declare  %rest:case-insensitive                 variable $language          as string  external := "en-US";

session:audit-call($token);

(: Post-processing :)
let $format as string? := api:preprocess-format($format, $request-uri)
let $tag as string* := api:preprocess-tags($tag)

let $cik as string* :=
    switch($profile-name)
    case "sec" return $cik
    case "japan" return $edinetcode
    default return ()

(: Entity resolution :)
let $entities := multiplexer:entities(
  $profile-name,
  $eid,
  $cik,
  api:preprocess-tags($tag),
  $ticker,
  $sic,
  $aid)

let $report as object? := reports:reports($report)
let $map as item* :=
    if(exists($report))
    then reports:concept-map($report)
    else $map
let $rule as item* :=
    (
        if(exists($report))
        then reports:rules($report)
        else rules:rules($rule),
        if(exists($additional-rules))
        then rules:rules($additional-rules)
        else ()
    )

let $hypercube := local:hypercube($entities)

let $facts :=
    let $options := {|
      {
        Hypercube : $hypercube,
        Validate: $validate
      },
      { "ConceptMaps" : $map }[exists($map)],
      { "Rules" : [ $rule ] }[exists($rule)],
      { "Concepts" : $report.Concepts }[exists($report) and exists($report.Concepts)]
    |}
    return if($profile-name eq "sec")
    then sec:facts-for($options)
    else facts:facts-for($options)

let $facts :=
  if(not $labels)
  then $facts
  else
    let $archives as string* := distinct-values($facts.Aspects."xbrl28:Archive")
    let $concept-names as string* := (
      distinct-values((keys($facts.Aspects), values($facts.Aspects)[$$ instance of string]))
    )
    let $concepts as object* :=
      (
          concepts:concepts($concept-names, $archives, $concepts:ANY_COMPONENT_LINK_ROLE),
          $report.Concepts[][$$.Name = $concept-names],
          if($profile-name eq "sec") then $local:additional-concepts else ()
      )
    let $language as string := ( $language, $report.$components:DEFAULT-LANGUAGE , $labels:AMERICAN_ENGLISH )[1]
    let $nonFetchedEntities as string* := request:param-values("xbrl:Entity")[not $$ = entities:eid($entities)]
    let $entities as object* := ($entities, entities:entities($nonFetchedEntities))
    for $fact as object in $facts
    let $entityName as string :=
        switch(true)
        case $profile-name eq "sec" return
            $entities[entities:eid($$) = $fact.Aspects."xbrl:Entity"].Profiles.SEC.CompanyName
        case $profile-name eq "japan" return
            $entities[entities:eid($$) = $fact.Aspects."xbrl:Entity"].Profiles.FSA.SubmitterName
        default return $fact.Aspects."xbrl:Entity"
    return
    {|
      $fact,
      let $concept-labels as object? := labels:labels-for-facts(
        $fact,
        $labels:STANDARD_LABEL_ROLE,
        $language,
        $concepts,
        $entities,
        ()
      )
      return { Labels : $concept-labels }
    |}

let $facts :=
  if($profile-name eq "sec")
  then api:normalize-facts($facts)
  else $facts

let $result := {
    NetworkIdentifier : "http://bizql.io/facts",
    TableName : "xbrl:Facts",
    FactTable : [ $facts ]
}
let $comment :=
{
    NumFacts: count($facts),
    TotalNumFacts: session:num-facts(),
    TotalNumArchives: session:num-archives(),
    TotalNumEntities: session:num-entities()
}
let $comment :=
    if($debug)
    then {|
        $comment,
        { DebugInfo: { Hypercube: $hypercube } }
    |}
    else $comment
let $serializers := {
    to-xml : function($res as object) as node()* {
           <FactTable NetworkIdentifier="http://bizql.io/facts"
            TableName="xbrl:Facts">{
                conversion:facts-to-xml($res.FactTable[], { Caller: "Report"})
        }</FactTable>
    },
    to-csv : function($res as object) as string {
        (conversion:facts-to-csv($res.FactTable[], { Caller: "Report" }), "")[1]
    }
}

let $results := api:serialize($result, $comment, $serializers, $format, "facts")
return api:check-and-return-results($token, $results, $format)


Makes generated SQL more readable

Makes generated SQL more readable

###############################################################################
## jq helper functions for SQL generation
###############################################################################

include "util";

# a handy way to quote SQL identifiers
# See: http://www.postgresql.org/docs/current/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
def asSqlIdent: "\"\( tostring  # anything can be turned into a SQL identifier if they are inside double quotes
                    | gsub("\""; "\"\"") # even double quote themselves (in fact, PostgreSQL says \0 cannot be included)
                    )\"";

# a handy way to turn things into SQL string literals (as it is hard to write single quote within single quote in this Bash script)
def asSqlLiteral:
    # map null to NULL
    if type == "null" then "NULL"
    # shouldn't turn primitive data types into string literals
    elif type | in({number:1, boolean:1}) then tostring
    # surround strings with single quotes with appropriate escaping
    # and turn any array or object into JSON format
    else "'\(tostring | gsub("'"; "''"))'"
    end;

# a handy way to generate a string from an array with prefix/suffix and delimiters
# where an empty string is desired for empty input
# to simplify the following SQL SELECT query generation
def mapJoinString(prefix; eachItem; delimiter; suffix):
    "\(prefix)\(map(eachItem) | join(delimiter))\(suffix)";
def mapJoinOrEmptyString(prefix; eachItem; delimiter; suffix):
    if type == null then null
    elif length == 0 then ""
    else
        if type == "array" then . else [.] end |
        mapJoinString(prefix; eachItem; delimiter; suffix)
    end;
# some short hands
def mapJoinString(prefix; eachItem; delimiter)        : mapJoinString(prefix; eachItem; delimiter; "");
def mapJoinString(prefix; eachItem)                   : mapJoinString(prefix; eachItem; "");
def mapJoinString(prefix)                             : mapJoinString(prefix; .);
def mapJoinOrEmptyString(prefix; eachItem; delimiter) : mapJoinOrEmptyString(prefix; eachItem; delimiter; "");
def mapJoinOrEmptyString(prefix; eachItem)            : mapJoinOrEmptyString(prefix; eachItem; "");
def mapJoinOrEmptyString(prefix)                      : mapJoinOrEmptyString(prefix; .);

# compile SQL expression: table.column or generic expression
def asSqlExpr:
    if has("expr")                       then .expr | tostring  # TODO support more structured expressions, e.g., binary operators and asSqlCondition
    elif has("table") and has("column") then "\(.table | asSqlIdent).\(.column | asSqlIdent)"
    elif has("column")                   then .column | asSqlIdent
    else error("Neither keys .expr or .column found for SQL expression in \(tostring)")
    end;
# compile `expression AS alias` in the field list of SELECT clauses
def asSqlExprAlias:
    (asSqlExpr) + (.alias | mapJoinOrEmptyString(" AS "; asSqlIdent));
# compile `table alias` mainly for the FROM or JOIN clauses
def asSqlTableAlias(asSql):
    if has("table") then
        (.table | asSqlIdent) +
        (.alias | mapJoinOrEmptyString(" "; asSqlIdent))
    elif has("sql") then
        "(\(.sql | asSql)) " +
        (.alias // error(".alias must be set for subquery \(.sql | tostring)") | asSqlIdent)
    else error("Neither keys .table or .sql found for FROM clause in \(tostring)")
    end;
def asSqlJoinTypeTableAlias(asSql):
    # five types of joins in ANSI SQL standard: https://en.wikipedia.org/wiki/Join_(SQL)
    (.LEFT_OUTER  | mapJoinOrEmptyString("LEFT OUTER JOIN "  ; asSqlTableAlias(asSql))) //
    (.RIGHT_OUTER | mapJoinOrEmptyString("RIGHT OUTER JOIN " ; asSqlTableAlias(asSql))) //
    (.FULL_OUTER  | mapJoinOrEmptyString("FULL OUTER JOIN "  ; asSqlTableAlias(asSql))) //
    (.INNER       | mapJoinOrEmptyString("INNER JOIN "       ; asSqlTableAlias(asSql))) //
    (.CROSS       | mapJoinOrEmptyString("CROSS JOIN "       ; asSqlTableAlias(asSql))) //
    error("Join table must be specified under one of these keys: LEFT_OUTER, RIGHT_OUTER, FULL_OUTER, INNER, CROSS, but found: \(tostring)");
# compile SQL conditional expressions for WHILE, HAVING, and JOIN ON clauses
# TODO maybe these comparisons should be folded into asSqlExpr
def asSqlCondition:
    def asSqlBinaryComparison(op): nullOr(
        if type == "array" and length == 2 then
            "\(.[0] | asSqlExpr) \(op) \(.[1] | asSqlExpr)"
        else error("Comparison '\(op)' expects exactly two expressions as an array, but found: \(tostring)")
        end);
    (.eq | asSqlBinaryComparison("=" )) //
    (.gt | asSqlBinaryComparison(">" )) //
    (.ge | asSqlBinaryComparison(">=")) //
    (.le | asSqlBinaryComparison("<=")) //
    (.lt | asSqlBinaryComparison("<" )) //
    (.isNull   | nullOr("\(asSqlExpr) IS NULL")) //
    (.isntNull | nullOr("\(asSqlExpr) IS NOT NULL")) //
    (.and | nullOr(    mapJoinString(""; asSqlCondition; " AND "))   ) //
    (.or  | nullOr("(\(mapJoinString(""; asSqlCondition;  " OR ")))")) //
    error("Unrecognized SQL condition \(tostring)");
# a more structured way to generate a SQL (Structured! Query Language) SELECT query than assembling strings
# which turns an object in a particular format into SQL, taking care of many escaping issues
def asSql:
    [ (.SELECT  |mapJoinOrEmptyString("SELECT "   ; asSqlExprAlias                                      ; "\n     , "))
    , (.FROM    |mapJoinOrEmptyString("FROM "     ; asSqlTableAlias(asSql)                              ; "\n   , "))
    , (.JOIN    |mapJoinOrEmptyString(""; "\(asSqlJoinTypeTableAlias(asSql))\n  ON \(.ON | asSqlCondition)"; "\n" ))
    , (.WHERE   |mapJoinOrEmptyString("WHERE "    ; asSqlCondition                                      ; "\n  AND " ))
    , (.GROUP_BY|mapJoinOrEmptyString("GROUP BY " ; asSqlExpr                                           ; "\n    , "))
    , (.HAVING  |mapJoinOrEmptyString("HAVING "   ; asSqlCondition                                      ; "\n   AND " ))
    , (.ORDER_BY|mapJoinOrEmptyString("ORDER BY " ; "\(.expr | asSqlExpr) \(.order // "ASC")"           ; "\n    , "))
    ] | join("\n") | trimWhitespace;

## finally, a test case
#if
#    { SELECT:
#        [ { column: "id" }
#        , { expr: "CASE WHEN isfixed THEN 1 ELSE 0 END" }
#        , { expr: "COALESCE(initvalue, 0)" }
#        ]
#    , FROM:
#        [ { table:"dd_weights_foo" }
#        ]
#    } | asSql | debug |
#false then . else . end |
###############################################################################

###############################################################################
## jq helper functions for SQL generation
###############################################################################

include "util";

# a handy way to quote SQL identifiers
# See: http://www.postgresql.org/docs/current/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
def asSqlIdent: "\"\( tostring  # anything can be turned into a SQL identifier if they are inside double quotes
                    | gsub("\""; "\"\"") # even double quote themselves (in fact, PostgreSQL says \0 cannot be included)
                    )\"";

# a handy way to turn things into SQL string literals (as it is hard to write single quote within single quote in this Bash script)
def asSqlLiteral:
    # map null to NULL
    if type == "null" then "NULL"
    # shouldn't turn primitive data types into string literals
    elif type | in({number:1, boolean:1}) then tostring
    # surround strings with single quotes with appropriate escaping
    # and turn any array or object into JSON format
    else "'\(tostring | gsub("'"; "''"))'"
    end;

# a handy way to generate a string from an array with prefix/suffix and delimiters
# where an empty string is desired for empty input
# to simplify the following SQL SELECT query generation
def mapJoinString(prefix; eachItem; delimiter; suffix):
    "\(prefix)\(map(eachItem) | join(delimiter))\(suffix)";
def mapJoinOrEmptyString(prefix; eachItem; delimiter; suffix):
    if type == null then null
    elif length == 0 then ""
    else
        if type == "array" then . else [.] end |
        mapJoinString(prefix; eachItem; delimiter; suffix)
    end;
# some short hands
def mapJoinString(prefix; eachItem; delimiter)        : mapJoinString(prefix; eachItem; delimiter; "");
def mapJoinString(prefix; eachItem)                   : mapJoinString(prefix; eachItem; "");
def mapJoinString(prefix)                             : mapJoinString(prefix; .);
def mapJoinOrEmptyString(prefix; eachItem; delimiter) : mapJoinOrEmptyString(prefix; eachItem; delimiter; "");
def mapJoinOrEmptyString(prefix; eachItem)            : mapJoinOrEmptyString(prefix; eachItem; "");
def mapJoinOrEmptyString(prefix)                      : mapJoinOrEmptyString(prefix; .);

# compile SQL expression: table.column or generic expression
def asSqlExpr:
    if has("expr")                       then .expr | tostring  # TODO support more structured expressions, e.g., binary operators and asSqlCondition
    elif has("table") and has("column") then "\(.table | asSqlIdent).\(.column | asSqlIdent)"
    elif has("column")                   then .column | asSqlIdent
    else error("Neither keys .expr or .column found for SQL expression in \(tostring)")
    end;
# compile `expression AS alias` in the field list of SELECT clauses
def asSqlExprAlias:
    (asSqlExpr) + (.alias | mapJoinOrEmptyString(" AS "; asSqlIdent));
# compile `table alias` mainly for the FROM or JOIN clauses
def asSqlTableAlias(asSql):
    if has("table") then
        (.table | asSqlIdent) +
        (.alias | mapJoinOrEmptyString(" "; asSqlIdent))
    elif has("sql") then
        "(\(.sql | asSql)) " +
        (.alias // error(".alias must be set for subquery \(.sql | tostring)") | asSqlIdent)
    else error("Neither keys .table or .sql found for FROM clause in \(tostring)")
    end;
def asSqlJoinTypeTableAlias(asSql):
    # five types of joins in ANSI SQL standard: https://en.wikipedia.org/wiki/Join_(SQL)
    (.LEFT_OUTER  | mapJoinOrEmptyString("LEFT OUTER JOIN "  ; asSqlTableAlias(asSql))) //
    (.RIGHT_OUTER | mapJoinOrEmptyString("RIGHT OUTER JOIN " ; asSqlTableAlias(asSql))) //
    (.FULL_OUTER  | mapJoinOrEmptyString("FULL OUTER JOIN "  ; asSqlTableAlias(asSql))) //
    (.INNER       | mapJoinOrEmptyString("INNER JOIN "       ; asSqlTableAlias(asSql))) //
    (.CROSS       | mapJoinOrEmptyString("CROSS JOIN "       ; asSqlTableAlias(asSql))) //
    error("Join table must be specified under one of these keys: LEFT_OUTER, RIGHT_OUTER, FULL_OUTER, INNER, CROSS, but found: \(tostring)");
# compile SQL conditional expressions for WHILE, HAVING, and JOIN ON clauses
# TODO maybe these comparisons should be folded into asSqlExpr
def asSqlCondition:
    def asSqlBinaryComparison(op): nullOr(
        if type == "array" and length == 2 then
            "\(.[0] | asSqlExpr) \(op) \(.[1] | asSqlExpr)"
        else error("Comparison '\(op)' expects exactly two expressions as an array, but found: \(tostring)")
        end);
    (.eq | asSqlBinaryComparison("=" )) //
    (.gt | asSqlBinaryComparison(">" )) //
    (.ge | asSqlBinaryComparison(">=")) //
    (.le | asSqlBinaryComparison("<=")) //
    (.lt | asSqlBinaryComparison("<" )) //
    (.isNull   | nullOr("\(asSqlExpr) IS NULL")) //
    (.isntNull | nullOr("\(asSqlExpr) IS NOT NULL")) //
    (.and | nullOr(    mapJoinString(""; asSqlCondition; " AND "))   ) //
    (.or  | nullOr("(\(mapJoinString(""; asSqlCondition;  " OR ")))")) //
    error("Unrecognized SQL condition \(tostring)");
# a more structured way to generate a SQL (Structured! Query Language) SELECT query than assembling strings
# which turns an object in a particular format into SQL, taking care of many escaping issues
def asSql:
    [ (.SELECT  |mapJoinOrEmptyString("SELECT "   ; asSqlExprAlias                                      ; "\n     , "))
    , (.FROM    |mapJoinOrEmptyString("FROM "     ; asSqlTableAlias(asSql)                              ; ", "))
    , (.JOIN    |mapJoinOrEmptyString(""; "\(asSqlJoinTypeTableAlias(asSql)) ON \(.ON | asSqlCondition)"; " " ))
    , (.WHERE   |mapJoinOrEmptyString("WHERE "    ; asSqlCondition                                      ; " AND " ))
    , (.GROUP_BY|mapJoinOrEmptyString("GROUP BY " ; asSqlExpr                                           ; ", "    ))
    , (.HAVING  |mapJoinOrEmptyString("HAVING "   ; asSqlCondition                                      ; " AND " ))
    , (.ORDER_BY|mapJoinOrEmptyString("ORDER BY " ; "\(.expr | asSqlExpr) \(.order // "ASC")"           ; ", "    ))
    ] | join("\n") | trimWhitespace;

## finally, a test case
#if
#    { SELECT:
#        [ { column: "id" }
#        , { expr: "CASE WHEN isfixed THEN 1 ELSE 0 END" }
#        , { expr: "COALESCE(initvalue, 0)" }
#        ]
#    , FROM:
#        [ { table:"dd_weights_foo" }
#        ]
#    } | asSql | debug |
#false then . else . end |
###############################################################################


:bug: allow different protocols than https.

jsoniq version "1.0";
(:
 : Copyright 2012 28msec Inc.
 :)

(:~
 :
This module provides a driver to access
 : ElasticSearch.

 :
 : @author Dennis Knochenwefel
 :)
module namespace es = "http://28.io/modules/elasticsearch";

import module namespace credentials =
    "http://www.28msec.com/modules/credentials";
import module namespace http = "http://zorba.io/modules/http-client";

declare namespace ver = "http://zorba.io/options/versioning";

declare option ver:module-version "1.0";

declare %private variable $es:CREDENTIALS-CATEGORY as xs:string :=
    "ElasticSearch";
declare %private variable $es:RETRY-OPTIONS :=
  {
    retry :
    {
      "delay" : [1000, 2000, 4000],
      "on-connection-error" : true(),
      "on-statuses" : [ 500, 503 ]
    }
  };

declare function es:connection($credentials-name as xs:string?)
as object?
{
  let $credentials as object? := credentials:credentials(
      $es:CREDENTIALS-CATEGORY,
      $credentials-name)
  where exists($credentials)
  return
      let $urls as string+ :=
        for $url in $credentials.urls[]
        return $url || ("/"[not ends-with($url, "/")])
      let $protocol as string := ($credentials.protocol, "https")[1]
      let $user as string? := $credentials.user
      let $password as string? := $credentials.pass
      let $authentication as string? :=
        (
          encode-for-uri($user) || ":" || encode-for-uri($password) || "@"
        )[exists($user) and exists($password)]
      return
        {
          "urls": [
            for $url in $urls
            return $protocol || "://" || $authentication || $url
          ]
        }
};

declare %private function es:url(
  $connection as object,
  $paths as string*
) as string
{
  es:url($connection, $paths, ())
};

declare %private function es:url(
  $connection as object,
  $paths as string*,
  $parameters as string*
) as string
{
  let $server as string := $connection.urls[][2]
  let $parameters as string* :=
    for $parameter in $parameters
    return encode-for-uri($parameter)
  let $path as string := string-join($paths, "/")
  let $query as string := string-join($parameters, "&")
  return
    $server || string-join(($path, $query), "?")
};

declare %private function es:request(
  $method as string,
  $url as string,
  $body as item?
) as object
{
  {|
    {
      "method": $method,
      "href": $url,
      "options": $es:RETRY-OPTIONS
    },
    switch(true)

    case empty($body) return ()

    case $body instance of string
    return
      {
        "body":
        {
          "media-type": "text/plain",
          "content": $body
        }
      }

    case $body instance of object
    case $body instance of array
    return
      {
        "body":
        {
          "media-type": "application/json",
          "content": serialize($body)
        }
      }

    default return
      error(xs:QName("es:UNKNOWN_BODY_TYPE"),
            "cannot create request from body content",
            $body)
  |}
};

declare %an:nondeterministic function es:exists(
  $connection as object,
  $index as string
) as boolean
{
  es:exists($connection, $index, (), ())
};

declare %an:nondeterministic function es:exists(
  $connection as object,
  $index as string,
  $type as string?
) as boolean
{
  es:exists($connection, $index, $type, ())
};

declare %an:nondeterministic function es:exists(
  $connection as object,
  $index as string,
  $type as string?,
  $id as string?
) as boolean
{
  let $url as string := es:url($connection, ($index, $type, $id))
  let $request as object := es:request("HEAD", $url, ())
  let $response as object := http:send-nondeterministic-request($request)
  return
    switch ($response.status)
    case 200
    case 204 return true
    case 404 return false
    default return error(xs:QName("es:SERVER_ERROR"),
                         $response.body.content,
                         $response)
};

declare %an:sequential function es:create(
  $connection as object,
  $index as string
) as object
{
  es:create($connection, $index, ())
};

declare %an:sequential function es:create(
  $connection as object,
  $index as string,
  $mapping as object?
) as object
{
  let $method as string :=
    if(empty($mapping))
    then "POST"
    else "PUT"
  let $url as string := es:url($connection, ($index))
  let $request as object := es:request($method, $url, $mapping)
  let $response as object := http:send-request($request)
  return
    if($response.status eq 200)
    then parse-json($response.body.content)
    else error(xs:QName("es:SERVER_ERROR"), $response.body.content, $response)
};

declare %an:sequential function es:create(
  $connection as object,
  $index as string,
  $type as string,
  $mapping as object
) as object?
{
  let $url as string := es:url($connection, ($index, "_mapping", $type))
  let $request as object := es:request("PUT", $url, $mapping)
  let $response as object := http:send-request($request)
  return
    if($response.status eq 200)
    then parse-json($response.body.content)
    else error(xs:QName("es:SERVER_ERROR"), $response.body.content, $response)
};

declare %an:sequential function es:delete(
  $connection as object,
  $index as string
) as object?
{
  es:delete($connection, $index, (),())
};

declare %an:sequential function es:delete(
  $connection as object,
  $index as string,
  $type as string?
) as object?
{
  es:delete($connection, $index, $type, ())
};

declare %an:sequential function es:delete(
  $connection as object,
  $index as string,
  $type as string?,
  $id as string?
) as object?
{
  let $url as string := es:url($connection, ($index, $type, $id))
  let $request as object := es:request("DELETE", $url, ())
  let $response as object := http:send-request($request)
  return
    if($response.status eq 200)
    then parse-json($response.body.content)
    else error(xs:QName("es:SERVER_ERROR"), $response.body.content, $response)
};

declare %an:nondeterministic function es:get(
  $connection as object,
  $index as string,
  $type as string,
  $id as string
) as object?
{
  let $url as string := es:url($connection, ($index, $type, $id))
  let $response as object := http:get($url)
  return
    switch ($response.status)
    case 200 return parse-json($response.body.content)
    case 404 return ()
    default return error(xs:QName("es:CONNECTION_ERROR"),
                         $response.body.content,
                         $response)
};

declare %an:sequential function es:insert(
  $connection as object,
  $index as string,
  $type as string,
  $doc as object
) as object?
{
  let $id as string? := $doc._id
  return es:insert($connection, $index, $type, $id, $doc)
};

declare %an:sequential function es:insert(
  $connection as object,
  $index as string,
  $type as string,
  $id as string?,
  $doc as object
) as object?
{
  let $method as string := if(empty($id)) then "POST" else "PUT"
  let $url as string := es:url($connection, ($index, $type, $id))
  let $request as object := es:request($method, $url, $doc)
  let $response as object := http:send-request($request)
  return
    switch ($response.status)
    case 200
    case 201 return parse-json($response.body.content)
    default return error(xs:QName("es:CONNECTION_ERROR"),
                         $response.body.content,
                         $response)
};

declare %an:nondeterministic function es:list(
  $connection as object,
  $index as string,
  $type as string
) as object?
{
  let $url as string := es:url($connection, ($index, $type, "_search"))
  let $request as object := es:request("GET", $url, ())
  let $response as object := http:send-nondeterministic-request($request)
  return
    if($response.status eq 200)
    then parse-json($response.body.content)
    else error(xs:QName("es:CONNECTION_ERROR"),
                        $response.body.content,
                        $response)
};

declare %an:nondeterministic function es:search(
  $connection as object,
  $index as string,
  $type as string,
  $query as object
) as object?
{
  let $url as string := es:url($connection, ($index, $type, "_search"))
  let $request as object := es:request("GET", $url, $query)
  let $response as object := http:send-nondeterministic-request($request)
  return
    if($response.status eq 200)
    then parse-json($response.body.content)
    else error(xs:QName("es:CONNECTION_ERROR"),
                        $response.body.content,
                        $response)
};

jsoniq version "1.0";
(:
 : Copyright 2012 28msec Inc.
 :)

(:~
 :
This module provides a driver to access
 : ElasticSearch.

 :
 : @author Dennis Knochenwefel
 :)
module namespace es = "http://28.io/modules/elasticsearch";

import module namespace credentials =
    "http://www.28msec.com/modules/credentials";
import module namespace http = "http://zorba.io/modules/http-client";

declare namespace ver = "http://zorba.io/options/versioning";

declare option ver:module-version "1.0";

declare %private variable $es:CREDENTIALS-CATEGORY as xs:string :=
    "ElasticSearch";
declare %private variable $es:RETRY-OPTIONS :=
  {
    retry :
    {
      "delay" : [1000, 2000, 4000],
      "on-connection-error" : true(),
      "on-statuses" : [ 500, 503 ]
    }
  };

declare function es:connection($credentials-name as xs:string?)
as object?
{
  let $credentials as object? := credentials:credentials(
      $es:CREDENTIALS-CATEGORY,
      $credentials-name)
  where exists($credentials)
  return
      let $urls as string+ :=
        for $url in $credentials.urls[]
        return $url || ("/"[not ends-with($url, "/")])
      let $user as string? := $credentials.user
      let $password as string? := $credentials.pass
      let $authentication as string? :=
        (
          encode-for-uri($user) || ":" || encode-for-uri($password) || "@"
        )[exists($user) and exists($password)]
      return
        {
          "urls": [
            for $url in $urls
            return "https://" || $authentication || $url
          ]
        }
};

declare %private function es:url(
  $connection as object,
  $paths as string*
) as string
{
  es:url($connection, $paths, ())
};

declare %private function es:url(
  $connection as object,
  $paths as string*,
  $parameters as string*
) as string
{
  let $server as string := $connection.urls[][2]
  let $parameters as string* :=
    for $parameter in $parameters
    return encode-for-uri($parameter)
  let $path as string := string-join($paths, "/")
  let $query as string := string-join($parameters, "&")
  return
    $server || string-join(($path, $query), "?")
};

declare %private function es:request(
  $method as string,
  $url as string,
  $body as item?
) as object
{
  {|
    {
      "method": $method,
      "href": $url,
      "options": $es:RETRY-OPTIONS
    },
    switch(true)

    case empty($body) return ()

    case $body instance of string
    return
      {
        "body":
        {
          "media-type": "text/plain",
          "content": $body
        }
      }

    case $body instance of object
    case $body instance of array
    return
      {
        "body":
        {
          "media-type": "application/json",
          "content": serialize($body)
        }
      }

    default return
      error(xs:QName("es:UNKNOWN_BODY_TYPE"),
            "cannot create request from body content",
            $body)
  |}
};

declare %an:nondeterministic function es:exists(
  $connection as object,
  $index as string
) as boolean
{
  es:exists($connection, $index, (), ())
};

declare %an:nondeterministic function es:exists(
  $connection as object,
  $index as string,
  $type as string?
) as boolean
{
  es:exists($connection, $index, $type, ())
};

declare %an:nondeterministic function es:exists(
  $connection as object,
  $index as string,
  $type as string?,
  $id as string?
) as boolean
{
  let $url as string := es:url($connection, ($index, $type, $id))
  let $request as object := es:request("HEAD", $url, ())
  let $response as object := http:send-nondeterministic-request($request)
  return
    switch ($response.status)
    case 200
    case 204 return true
    case 404 return false
    default return error(xs:QName("es:SERVER_ERROR"),
                         $response.body.content,
                         $response)
};

declare %an:sequential function es:create(
  $connection as object,
  $index as string
) as object
{
  es:create($connection, $index, ())
};

declare %an:sequential function es:create(
  $connection as object,
  $index as string,
  $mapping as object?
) as object
{
  let $method as string :=
    if(empty($mapping))
    then "POST"
    else "PUT"
  let $url as string := es:url($connection, ($index))
  let $request as object := es:request($method, $url, $mapping)
  let $response as object := http:send-request($request)
  return
    if($response.status eq 200)
    then parse-json($response.body.content)
    else error(xs:QName("es:SERVER_ERROR"), $response.body.content, $response)
};

declare %an:sequential function es:create(
  $connection as object,
  $index as string,
  $type as string,
  $mapping as object
) as object?
{
  let $url as string := es:url($connection, ($index, "_mapping", $type))
  let $request as object := es:request("PUT", $url, $mapping)
  let $response as object := http:send-request($request)
  return
    if($response.status eq 200)
    then parse-json($response.body.content)
    else error(xs:QName("es:SERVER_ERROR"), $response.body.content, $response)
};

declare %an:sequential function es:delete(
  $connection as object,
  $index as string
) as object?
{
  es:delete($connection, $index, (),())
};

declare %an:sequential function es:delete(
  $connection as object,
  $index as string,
  $type as string?
) as object?
{
  es:delete($connection, $index, $type, ())
};

declare %an:sequential function es:delete(
  $connection as object,
  $index as string,
  $type as string?,
  $id as string?
) as object?
{
  let $url as string := es:url($connection, ($index, $type, $id))
  let $request as object := es:request("DELETE", $url, ())
  let $response as object := http:send-request($request)
  return
    if($response.status eq 200)
    then parse-json($response.body.content)
    else error(xs:QName("es:SERVER_ERROR"), $response.body.content, $response)
};

declare %an:nondeterministic function es:get(
  $connection as object,
  $index as string,
  $type as string,
  $id as string
) as object?
{
  let $url as string := es:url($connection, ($index, $type, $id))
  let $response as object := http:get($url)
  return
    switch ($response.status)
    case 200 return parse-json($response.body.content)
    case 404 return ()
    default return error(xs:QName("es:CONNECTION_ERROR"),
                         $response.body.content,
                         $response)
};

declare %an:sequential function es:insert(
  $connection as object,
  $index as string,
  $type as string,
  $doc as object
) as object?
{
  let $id as string? := $doc._id
  return es:insert($connection, $index, $type, $id, $doc)
};

declare %an:sequential function es:insert(
  $connection as object,
  $index as string,
  $type as string,
  $id as string?,
  $doc as object
) as object?
{
  let $method as string := if(empty($id)) then "POST" else "PUT"
  let $url as string := es:url($connection, ($index, $type, $id))
  let $request as object := es:request($method, $url, $doc)
  let $response as object := http:send-request($request)
  return
    switch ($response.status)
    case 200
    case 201 return parse-json($response.body.content)
    default return error(xs:QName("es:CONNECTION_ERROR"),
                         $response.body.content,
                         $response)
};

declare %an:nondeterministic function es:list(
  $connection as object,
  $index as string,
  $type as string
) as object?
{
  let $url as string := es:url($connection, ($index, $type, "_search"))
  let $request as object := es:request("GET", $url, ())
  let $response as object := http:send-nondeterministic-request($request)
  return
    if($response.status eq 200)
    then parse-json($response.body.content)
    else error(xs:QName("es:CONNECTION_ERROR"),
                        $response.body.content,
                        $response)
};

declare %an:nondeterministic function es:search(
  $connection as object,
  $index as string,
  $type as string,
  $query as object
) as object?
{
  let $url as string := es:url($connection, ($index, $type, "_search"))
  let $request as object := es:request("GET", $url, $query)
  let $response as object := http:send-nondeterministic-request($request)
  return
    if($response.status eq 200)
    then parse-json($response.body.content)
    else error(xs:QName("es:CONNECTION_ERROR"),
                        $response.body.content,
                        $response)
};