Propagating language to labels and facts.

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

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

import module namespace entities = "http://28.io/modules/xbrl/entities";
import module namespace components = "http://28.io/modules/xbrl/components";
import module namespace reports = "http://28.io/modules/xbrl/reports";
import module namespace labels = "http://28.io/modules/xbrl/labels";
import module namespace config = "http://apps.28.io/config";

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

import module namespace mw = "http://28.io/modules/xbrl/mongo-wrapper";

declare option rest:response "first-item";

declare function local:concepts-for-archives-and-labels($aids as string*, $labels as string) as object*
{
   mw:run-cmd-deterministic(
      {
        "text" : "concepts",
        "filter" : { "Archive" : { "$in" : [ $aids ] } },
        "search" : $labels,
        "limit" : 100,
        "score" : { "$meta" : "textScore" },
        "sort" : { score: { "$meta" : "textScore" } }
      }).results[].obj
};

(: 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 := "ALL";
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriod   as string*  external := "ALL";
declare  %rest:case-insensitive %rest:distinct  variable $filingKind     as string* external := ();
declare  %rest:case-insensitive %rest:distinct  variable $aid            as string*  external;
declare  %rest:case-insensitive %rest:distinct  variable $disclosure     as string*  external;
declare  %rest:case-insensitive %rest:distinct  variable $section        as string*  external;
declare  %rest:case-insensitive %rest:distinct  variable $role           as string*  external;
declare  %rest:case-insensitive %rest:distinct  variable $eid            as string*  external;
declare  %rest:case-insensitive %rest:distinct  variable $label          as string*  external;
declare  %rest:case-insensitive                 variable $report         as string?  external;
declare  %rest:case-insensitive %rest:distinct  variable $name           as string*  external;
declare  %rest:case-insensitive                 variable $onlyNames      as boolean? external := false;
declare  %rest:case-insensitive                 variable $onlyTextBlocks as boolean? external := ();
declare  %rest:case-insensitive                 variable $abstract       as boolean? external := ();
declare  %rest:case-insensitive                 variable $kind           as string*  external := ();
declare  %rest:case-insensitive                 variable $count          as boolean external := false;
declare  %rest:case-insensitive                 variable $language           as string  external := "en-US";

(: Post-processing :)
let $format as string? := api:preprocess-format($format, $request-uri)
let $fiscalYear as integer* := api:preprocess-fiscal-years($fiscalYear)
let $fiscalPeriod as string* := api:preprocess-fiscal-periods($fiscalPeriod)
let $tag as string* := api:preprocess-tags($tag)
let $kind as string* := api:preprocess-concept-kind($kind)
let $section := distinct-values(($section, $role))

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,
  $tag,
  $ticker,
  $sic,
  (),
  (),
  (),
  ())

let $entities-not-found as boolean :=
  exists(($eid, $cik, $tag, $ticker, $sic)) and empty($entities)

let $archives as object* := multiplexer:filings(
  $profile-name,
  $entities,
  $fiscalPeriod,
  $fiscalYear,
  $filingKind,
  $aid)

let $archives-not-found as boolean :=
  exists(($entities, $fiscalPeriod, $fiscalYear, $filingKind, $aid)) and empty($archives)

let $entities as object* :=
    ($entities[entities:eid($$) = $archives.Entity],
    let $not-found := $archives.Entity[not entities:eid($entities) = $$]
    where exists($not-found)
    return entities:entities($not-found))

let $report as object? := reports:reports($report)

let $concepts as object* :=
    multiplexer:concepts(
      $profile-name,
      $archives,
      $name,
      $disclosure,
      $section,
      $label[$profile-name ne "sec"],
      $label[$profile-name eq "sec"],
      $report,
      $onlyNames)[
    (empty($onlyTextBlocks) or $$.IsTextBlock eq $onlyTextBlocks) and
    (empty($abstract) or $$.IsAbstract eq $abstract) and
    (empty($kind) or $$.Kind = $kind)
  ]

let $result :=
  let $all-aids as string* := distinct-values($concepts.Archive)
  let $roles as string* := distinct-values($concepts.Role)
  let $components := components:components-for-archives-and-roles($all-aids, $roles)
  return {
    ReportElements : [
      switch(true)
      case $count return ()
      case $onlyNames return distinct-values($concepts.Name)
      default return
        for $concept in $concepts
        group by $archive := $concept.Archive,  $role := $concept.Role
        let $component as object := $components[$$.Archive eq $archive and $$.Role eq $role]
        return
              let $archive-object as object := $archives[$$._id eq $archive]
              let $entity as object := $entities[entities:eid($$) = $archive-object.Entity]
              let $metadata := {
                  Archive : $archive,
                  Section : $component.Label,
                  SectionURI : $component.Role
              }
              let $metadata-sec := {
                  CIK : entities:eid($entity),
                  EntityRegistrantName : $entity.Profiles.SEC.CompanyName,
                  FiscalYear : $archive-object.Profiles.SEC.Fiscal.DocumentFiscalYearFocus,
                  FiscalPeriod : $archive-object.Profiles.SEC.Fiscal.DocumentFiscalPeriodFocus
              }[$profile-name eq "sec"]
              for $concept in $concept
                let $original-name := ($concept.Origin, $concept.Name)[1]
                return {|
                    {
                      Label: values(labels:labels(
                        $concept.Name,
                        $labels:STANDARD_LABEL_ROLE,
                        $language,
                        $concept,
                        (),
                        ()
                      ))
                    },
                    project($concept, ("Name", "Origin")),
                    {
                      Labels: backend:url("labels", {|
                        {
                          token: $token,
                          concept: $original-name,
                          aid: $archive,
                          section: $role,
                          format: $format,
                          profile-name: $profile-name,
                          language: $language
                        }
                      |}),
                      Facts:
                        if($concept.Kind eq "Concept")
                        then backend:url("facts", {|
                          {
                            token: $token,
                            "xbrl:Concept": $original-name,
                            aid: $archive,
                            format: $format,
                            profile-name: $profile-name,
                            language: $language
                          },
                          {
                            fiscalYear: "ALL",
                            fiscalPeriod: "ALL",
                            fiscalPeriodType: "ALL"
                          }[$profile-name = ("sec", "japan")]
                        |})
                        else "None"
                    },
                    trim($concept, ("Origin", "_id", "Archive", "Role", "Name",  "Label", "Labels", "IsAbstract", "SubstitutionGroup")),
                    $metadata,
                    $metadata-sec
                |}
        ]
    }
let $comment := {
    NumConcepts: count($result.ReportElements[]),
    TotalNumConcepts: session:num-concepts()
}
let $serializers := {
    to-xml : function($res as object) as node() {
        <ReportElements>{
          if ($onlyNames)
          then
            <ReportElement>{
              for $c in $res.ReportElements[]
              return <Name>{$c}</Name>
            }</ReportElement>
          else api:json-to-xml($res.ReportElements[], "ReportElement")
        }</ReportElements>
    },
    to-csv : function($res as object) as string {
      if ($onlyNames)
      then
          string-join(("Name", $res.ReportElements[]), "
")
      else
          api:json-to-csv($res.ReportElements[])
    }
}
let $results := api:serialize($result, $comment, $serializers, $format, "report-elements")
return switch(true)
       case $entities-not-found return api:not-found("entity")
       case $archives-not-found return api:not-found("archive")
       default return api:check-and-return-results($token, $results, $format)

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

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

import module namespace entities = "http://28.io/modules/xbrl/entities";
import module namespace components = "http://28.io/modules/xbrl/components";
import module namespace reports = "http://28.io/modules/xbrl/reports";
import module namespace labels = "http://28.io/modules/xbrl/labels";
import module namespace config = "http://apps.28.io/config";

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

import module namespace mw = "http://28.io/modules/xbrl/mongo-wrapper";

declare option rest:response "first-item";

declare function local:concepts-for-archives-and-labels($aids as string*, $labels as string) as object*
{
   mw:run-cmd-deterministic(
      {
        "text" : "concepts",
        "filter" : { "Archive" : { "$in" : [ $aids ] } },
        "search" : $labels,
        "limit" : 100,
        "score" : { "$meta" : "textScore" },
        "sort" : { score: { "$meta" : "textScore" } }
      }).results[].obj
};

(: 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 := "ALL";
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriod   as string*  external := "ALL";
declare  %rest:case-insensitive %rest:distinct  variable $filingKind     as string* external := ();
declare  %rest:case-insensitive %rest:distinct  variable $aid            as string*  external;
declare  %rest:case-insensitive %rest:distinct  variable $disclosure     as string*  external;
declare  %rest:case-insensitive %rest:distinct  variable $section        as string*  external;
declare  %rest:case-insensitive %rest:distinct  variable $role           as string*  external;
declare  %rest:case-insensitive %rest:distinct  variable $eid            as string*  external;
declare  %rest:case-insensitive %rest:distinct  variable $label          as string*  external;
declare  %rest:case-insensitive                 variable $report         as string?  external;
declare  %rest:case-insensitive %rest:distinct  variable $name           as string*  external;
declare  %rest:case-insensitive                 variable $onlyNames      as boolean? external := false;
declare  %rest:case-insensitive                 variable $onlyTextBlocks as boolean? external := ();
declare  %rest:case-insensitive                 variable $abstract       as boolean? external := ();
declare  %rest:case-insensitive                 variable $kind           as string*  external := ();
declare  %rest:case-insensitive                 variable $count          as boolean external := false;
declare  %rest:case-insensitive                 variable $language           as string  external := "en-US";

(: Post-processing :)
let $format as string? := api:preprocess-format($format, $request-uri)
let $fiscalYear as integer* := api:preprocess-fiscal-years($fiscalYear)
let $fiscalPeriod as string* := api:preprocess-fiscal-periods($fiscalPeriod)
let $tag as string* := api:preprocess-tags($tag)
let $kind as string* := api:preprocess-concept-kind($kind)
let $section := distinct-values(($section, $role))

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,
  $tag,
  $ticker,
  $sic,
  (),
  (),
  (),
  ())

let $entities-not-found as boolean :=
  exists(($eid, $cik, $tag, $ticker, $sic)) and empty($entities)

let $archives as object* := multiplexer:filings(
  $profile-name,
  $entities,
  $fiscalPeriod,
  $fiscalYear,
  $filingKind,
  $aid)

let $archives-not-found as boolean :=
  exists(($entities, $fiscalPeriod, $fiscalYear, $filingKind, $aid)) and empty($archives)

let $entities as object* :=
    ($entities[entities:eid($$) = $archives.Entity],
    let $not-found := $archives.Entity[not entities:eid($entities) = $$]
    where exists($not-found)
    return entities:entities($not-found))

let $report as object? := reports:reports($report)

let $concepts as object* :=
    multiplexer:concepts(
      $profile-name,
      $archives,
      $name,
      $disclosure,
      $section,
      $label[$profile-name ne "sec"],
      $label[$profile-name eq "sec"],
      $report,
      $onlyNames)[
    (empty($onlyTextBlocks) or $$.IsTextBlock eq $onlyTextBlocks) and
    (empty($abstract) or $$.IsAbstract eq $abstract) and
    (empty($kind) or $$.Kind = $kind)
  ]

let $result :=
  let $all-aids as string* := distinct-values($concepts.Archive)
  let $roles as string* := distinct-values($concepts.Role)
  let $components := components:components-for-archives-and-roles($all-aids, $roles)
  return {
    ReportElements : [
      switch(true)
      case $count return ()
      case $onlyNames return distinct-values($concepts.Name)
      default return
        for $concept in $concepts
        group by $archive := $concept.Archive,  $role := $concept.Role
        let $component as object := $components[$$.Archive eq $archive and $$.Role eq $role]
        return
              let $archive-object as object := $archives[$$._id eq $archive]
              let $entity as object := $entities[entities:eid($$) = $archive-object.Entity]
              let $metadata := {
                  Archive : $archive,
                  Section : $component.Label,
                  SectionURI : $component.Role
              }
              let $metadata-sec := {
                  CIK : entities:eid($entity),
                  EntityRegistrantName : $entity.Profiles.SEC.CompanyName,
                  FiscalYear : $archive-object.Profiles.SEC.Fiscal.DocumentFiscalYearFocus,
                  FiscalPeriod : $archive-object.Profiles.SEC.Fiscal.DocumentFiscalPeriodFocus
              }[$profile-name eq "sec"]
              for $concept in $concept
                let $original-name := ($concept.Origin, $concept.Name)[1]
                return {|
                    {
                      Label: values(labels:labels(
                        $concept.Name,
                        $labels:STANDARD_LABEL_ROLE,
                        $language,
                        $concept,
                        (),
                        ()
                      ))
                    },
                    project($concept, ("Name", "Origin")),
                    {
                      Labels: backend:url("labels", {|
                        {
                          token: $token,
                          concept: $original-name,
                          aid: $archive,
                          section: $role,
                          format: $format,
                          profile-name: $profile-name
                        }
                      |}),
                      Facts:
                        if($concept.Kind eq "Concept")
                        then backend:url("facts", {|
                          {
                            token: $token,
                            "xbrl:Concept": $original-name,
                            aid: $archive,
                            format: $format,
                            profile-name: $profile-name
                          },
                          {
                            fiscalYear: "ALL",
                            fiscalPeriod: "ALL",
                            fiscalPeriodType: "ALL"
                          }[$profile-name = ("sec", "japan")]
                        |})
                        else "None"
                    },
                    trim($concept, ("Origin", "_id", "Archive", "Role", "Name",  "Label", "Labels", "IsAbstract", "SubstitutionGroup")),
                    $metadata,
                    $metadata-sec
                |}
        ]
    }
let $comment := {
    NumConcepts: count($result.ReportElements[]),
    TotalNumConcepts: session:num-concepts()
}
let $serializers := {
    to-xml : function($res as object) as node() {
        <ReportElements>{
          if ($onlyNames)
          then
            <ReportElement>{
              for $c in $res.ReportElements[]
              return <Name>{$c}</Name>
            }</ReportElement>
          else api:json-to-xml($res.ReportElements[], "ReportElement")
        }</ReportElements>
    },
    to-csv : function($res as object) as string {
      if ($onlyNames)
      then
          string-join(("Name", $res.ReportElements[]), "
")
      else
          api:json-to-csv($res.ReportElements[])
    }
}
let $results := api:serialize($result, $comment, $serializers, $format, "report-elements")
return switch(true)
       case $entities-not-found return api:not-found("entity")
       case $archives-not-found return api:not-found("archive")
       default return api:check-and-return-results($token, $results, $format)


More simplifications.

More simplifications.

#!/usr/bin/env jq
# This is a collection of filters for `jq`, intended to manipulate JSON objects
# into JSON representing a website index, for further processing in a template.

# Convert "truthy" value to actual boolean.
def bool:
    [. == (null,false,0,{},[])] | any | not
;

# Generate this object and all its children.
def all_children:
    ., recurse(.contents[]?)
;

# To remove an object, we first mark it for removal, then actually remove it
# later. It would be nicer if we could just do `all_children ... |= empty`,
# which works in some cases but not all. I suppose it has to do with changing
# objects as we are iterating over them.
def mark:
    .remove = true
;

# Remove all objects marked for removal.
def remove_marked:
    if .remove == true
    then empty
    else (.contents // empty) |= map(remove_marked)
    end
;

# Wrap an object in other objects so that the original object exists at a
# particular "path". Like `{} | setpath(["a","b"], …)`, but instead of making
# objects like `{"a":{"b":…}}`, this makes objects like `{"name":"a",
# "contents":[{"name":"b",…}]}`.
def tree($path):
    if ($path | length) <= 1
    then .name = $path[0]
    else { "name": $path[0], "contents": [ tree($path[1:]) ] }
    end
;

# Merge an array of page objects into a single page object.
def merge:
    map(to_entries) | add | group_by(.key) | map(
        { "key": (.[0].key)
        , "value": (
            if .[0].key == "contents"
            then [ .[].value ] | add | group_by(.name) | map(merge)
            elif [ .[].value == .[0].value ] | all
            then .[0].value
            else error("can't merge incompatible values")
            end
        ) }
    ) | from_entries
;

# Add paths to each object, that is, the names of the ancestors.
def directory:
    def f($d): .directory = $d | .name as $n | .contents[]? |= f($d+[$n]);
    f([])
;

# Every leaf node gets a basename, eg name without extension.
def basename:
    (all_children | select(has("contents") | not)) |= (
        .basename = (.name | sub(".([A-z]*)$";""))
    )
;

# Any Markdown page gets a link to its HTML page.
def link:
    (all_children | select(has("directory") and has("basename") and (.name | test(".(md|markdown)$";"i")))) |= (
        .link = ((.directory + [.basename + ".html"]) | join("/") | ltrimstr("./"))
    )
;

# Front matter is the page to be associated with the enclosing directory rather
# than itself.
def frontmatter:
    (all_children | select(has("contents"))) |= (
        (.frontmatter = [.contents[] | select(.meta.frontmatter | bool)][0]) |
        (.contents = .contents - [.frontmatter])
    )
;

# Drafts can be excluded from being uploaded and included in the table of
# contents. To not count as a draft, a document should be explicitly marked as
# "publish" in the metadata.
def explicit_publish:
    def publish: has("contents") or .external or .meta.publish | bool;
    (( all_children | select(publish | not)) |= mark) | remove_marked
;

# Sort content first according to sort order in metadata.
def sort_content:
    (all_children | select(has("contents")) | .contents) |= (
        sort_by(.meta.sort // .meta.title // .name)
    )
;

# Add a note that tells us whether this has only children who have no more
# subchildren.
def annotate_leaves:
    (all_children | select(has("contents"))) |= (
        .only_leaves = (.contents | all(has("contents") | not))
    )
;

# Combines the given stream of JSON objects by merging them, and performs the
# given operations to turn it into a proper index.
def index:
    merge
    | directory
    | basename
    | link
    | explicit_publish
    | frontmatter
    | sort_content
    | annotate_leaves
;

# Get publishable targets from an index, as an object containing the names of
# the relevant Makefile recipes.
def targets($dest):
    [ all_children
        | ., (.frontmatter // empty)
        | select(has("directory") and has("basename") and (has("external") | not))
        | .directory as $dir
        | {"target": "pdf-targets", "name": (.basename + ".pdf")}
        , {"target": "html-targets", "name": (.basename + ".html")}
        , {"target": "external-targets", "name": .targets?[]}
        | .name |= ($dir + [.] | join("/") | ltrimstr("./") | ($dest + "/" + .) )
    ]
    | group_by(.target)
    | map({(.[0].target): map(.name)})
    | add
;

#!/usr/bin/env jq
# This is a collection of filters for `jq`, intended to manipulate JSON objects
# into JSON representing a website index, for further processing in a template.

# Convert "truthy" value to actual boolean.
def bool:
    [. == (null,false,0,{},[])] | any | not
;

# Generate this object and all its children.
def all_children:
    ., recurse(.contents[]?)
;

# To remove an object, we first mark it for removal, then actually remove it
# later. It would be nicer if we could just do `all_children ... |= empty`,
# which works in some cases but not all. I suppose it has to do with changing
# objects as we are iterating over them.
def mark:
    .marked_for_removal = true
;

# Remove all objects marked for removal.
def remove_marked:
    if .marked_for_removal? == true
    then empty
    else (.contents? // empty) |= map(remove_marked)
    end
;

# Wrap an object in other objects so that the original object exists at a
# particular "path". Like `{} | setpath(["a","b"], …)`, but instead of making
# objects like `{"a":{"b":…}}`, this makes objects like `{"name":"a",
# "contents":[{"name":"b",…}]}`.
def tree($path):
    if ($path | length) <= 1
    then .name = $path[0]
    else { "name": $path[0], "contents": [ tree($path[1:]) ] }
    end
;

# Merge an array of page objects into a single page object.
def merge:
    map(to_entries) | add | group_by(.key) | map(
        { "key": (.[0].key)
        , "value": (
            if .[0].key == "contents"
            then [ .[].value ] | add | group_by(.name) | map(merge)
            elif [ .[].value == .[0].value ] | all
            then .[0].value
            else error("can't merge incompatible values")
            end
        ) }
    ) | from_entries
;

# Add paths to each object, that is, the names of the ancestors.
def directory:
    def add_directories_aux($history):
        ($history + [.name]) as $present |
        .directory = $history |
        (.contents[]? |= add_directories_aux($present))
    ;
    add_directories_aux([])
;

# Every leaf node gets a basename, eg name without extension.
def basename:
    (all_children | select(has("contents") | not)) |= (
        .basename = (.name | sub(".([A-z]*)$";""))
    )
;

# Any Markdown page gets a link to its HTML page.
def link:
    (all_children | select(has("directory") and has("basename") and (.name | test(".(md|markdown)$";"i")))) |= (
        .link = ((.directory + [.basename + ".html"]) | join("/") | ltrimstr("./"))
    )
;

# Front matter is the page to be associated with the enclosing directory rather
# than itself.
def frontmatter:
    (all_children | select(has("contents"))) |= (
        (.frontmatter = [.contents[] | select(.meta.frontmatter | bool)][0]) |
        (.contents = .contents - [.frontmatter])
    )
;

# Drafts can be excluded from being uploaded and included in the table of
# contents. To not count as a draft, a document should be explicitly marked as
# "publish" in the metadata.
def explicit_publish:
    def publish: has("contents") or .external or .meta.publish | bool;
    (( all_children | select(publish | not)) |= mark) | remove_marked
;

# Sort content first according to sort order in metadata.
def sort_content:
    (all_children | select(has("contents")) | .contents) |= (
        sort_by(.meta.sort // .meta.title // .name)
    )
;

# Add a note that tells us whether this has only children who have no more
# subchildren.
def annotate_leaves:
    (all_children | select(has("contents"))) |= (
        .only_leaves = (.contents | all(has("contents") | not))
    )
;

# Combines the given stream of JSON objects by merging them, and performs the
# given operations to turn it into a proper index.
def index:
    merge
    | directory
    | basename
    | link
    | explicit_publish
    | frontmatter
    | sort_content
    | annotate_leaves
;

# Get publishable targets from an index, as an object containing the names of
# the relevant Makefile recipes.
def targets($dest):
    [ all_children
        | ., (.frontmatter // empty)
        | select(has("directory") and has("basename") and (has("external") | not))
        | .directory as $dir
        | {"target": "pdf-targets", "name": (.basename + ".pdf")}
        , {"target": "html-targets", "name": (.basename + ".html")}
        , {"target": "external-targets", "name": .targets?[]}
        | .name |= ($dir + [.] | join("/") | ltrimstr("./") | ($dest + "/" + .) )
    ]
    | group_by(.target)
    | map({(.[0].target): map(.name)})
    | add
;


:green_heart: Fix build.

:green_heart: Fix build.

import module namespace test = "http://apps.28.io/test";

test:check-all-success({
    cocacola: test:invoke-and-assert-deep-equal(
      "spreadsheet-for-component",
      {
        ticker:"ko",
        fiscalYear: "2013",
        fiscalPeriod: "FY",
        merge: "false"
      },
      function($b as item*) as item* { count(flatten($b.TableSet[].TableCells.Facts).Value) },
      1
    ),
    tickerrole: test:invoke-and-assert-deep-equal(
      "spreadsheet-for-component",
      {
        ticker:"ko",
        networkIdentifier:"http://www.thecocacolacompany.com/role/ConsolidatedStatementsOfIncome",
        fiscalYear: "2013",
        fiscalPeriod: "FY"
      },
      function($b as item*) as item* { count(flatten($b.TableSet[].TableCells.Facts).Value) },
      60
    ),
    tickerconcept: test:invoke-and-assert-deep-equal(
      "spreadsheet-for-component",
      {
        ticker:"ko",
        fiscalYear:"2013",
        concept:"us-gaap:Assets",
        fiscalPeriod: "FY"
      },
      function($b as item*) as item* { count(flatten($b.TableSet[].TableCells.Facts).Value) },
      76
    ),
    tickerfyfprole: test:invoke-and-assert-deep-equal(
      "spreadsheet-for-component",
      {
        ticker:"ko",
        fiscalYear:"2012",
        fiscalPeriod:"Q1",
        networkIdentifier:"http://www.thecoca-colacompany.com/role/CondensedConsolidatedBalanceSheets"
      },
      function($b as item*) as item* { count(flatten($b.TableSet[].TableCells.Facts).Value) },
      70
    ),
    generic: test:invoke-and-assert-deep-equal(
          "spreadsheet-for-component",
          {
        profile-name: "generic",
        aid: "0000021344-14-000008",
        role:"http://www.thecocacolacompany.com/role/ConsolidatedBalanceSheets"
      },
      function($b as item*) as item* { count(flatten($b.TableSet[].TableCells.Facts).Value) },
      76
    ),
    merge: test:invoke-and-assert-deep-equal(
          "spreadsheet-for-component",
          {
            ticker: "ko",
            fiscalYear: [ "2012", "2013" ],
            fiscalPeriod: "FY",
            disclosure: "BalanceSheet"
          },
          function($b as item*) as item* { count(flatten($b.TableSet[].TableCells.Facts).Value) },
          153
        ),
    html: test:invoke-and-assert-status(
          "spreadsheet-for-component",
          {
            ticker:"ko",
            fiscalYear:"2012",
            fiscalPeriod:"Q1",
            networkIdentifier:"http://www.thecoca-colacompany.com/role/CondensedConsolidatedBalanceSheets",
            format: "html"
          },
          302
    ),
    override: test:invoke-and-assert-deep-equal(
          "spreadsheet-for-component",
          {
            ticker: "ko",
            fiscalYear: [ "2012", "2013" ],
            fiscalPeriod: "FY",
            disclosure: "BalanceSheet",
            override: "true"
          },
          function($b as item*) as item* { count(flatten($b.TableSet[].TableCells.Facts).Value) },
          72
        ),
        one-table: test:invoke-and-assert-deep-equal(
          "spreadsheet-for-component",
          {
              "aid" : "0001214659-13-001731",
              "role" : "http://www.brin.com/role/SupplementalCashFlowInformationDetails",
              "hypercube" : "us-gaap:ScheduleOfShareBasedCompensationArrangementsByShareBasedPaymentAwardTable"
          },
          function($res as item*) as item* { count(flatten($res.TableSet[].TableCells.Facts).Value) },
          6
        ),
    qualcomm: test:invoke-and-assert-deep-equal(
      "spreadsheet-for-component",
      {
          "cik" : "804328",
          "fiscalPeriod" : "FY",
          "fiscalYear" : [ "2013", "2014" ],
          "fiscalPeriodType" : [ "instant", "YTD" ],
          "disclosure" : "IncomeStatement",
          "override" : "true"
      },
      function($res as item*) as item* { count(flatten($res.TableSet[].TableCells.Facts).Value) },
      52
    )
})

import module namespace test = "http://apps.28.io/test";

test:check-all-success({
    cocacola: test:invoke-and-assert-deep-equal(
      "spreadsheet-for-component",
      {
        ticker:"ko",
        fiscalYear: "2013",
        fiscalPeriod: "FY",
        merge: "false"
      },
      function($b as item*) as item* { count(flatten($b.TableSet[].TableCells.Facts).Value) },
      1
    ),
    tickerrole: test:invoke-and-assert-deep-equal(
      "spreadsheet-for-component",
      {
        ticker:"ko",
        networkIdentifier:"http://www.thecocacolacompany.com/role/ConsolidatedStatementsOfIncome",
        fiscalYear: "2013",
        fiscalPeriod: "FY"
      },
      function($b as item*) as item* { count(flatten($b.TableSet[].TableCells.Facts).Value) },
      60
    ),
    tickerconcept: test:invoke-and-assert-deep-equal(
      "spreadsheet-for-component",
      {
        ticker:"ko",
        fiscalYear:"2013",
        concept:"us-gaap:Assets",
        fiscalPeriod: "FY"
      },
      function($b as item*) as item* { count(flatten($b.TableSet[].TableCells.Facts).Value) },
      76
    ),
    tickerfyfprole: test:invoke-and-assert-deep-equal(
      "spreadsheet-for-component",
      {
        ticker:"ko",
        fiscalYear:"2012",
        fiscalPeriod:"Q1",
        networkIdentifier:"http://www.thecoca-colacompany.com/role/CondensedConsolidatedBalanceSheets"
      },
      function($b as item*) as item* { count(flatten($b.TableSet[].TableCells.Facts).Value) },
      70
    ),
    generic: test:invoke-and-assert-deep-equal(
          "spreadsheet-for-component",
          {
        profile-name: "generic",
        aid: "0000021344-14-000008",
        role:"http://www.thecocacolacompany.com/role/ConsolidatedBalanceSheets"
      },
      function($b as item*) as item* { count(flatten($b.TableSet[].TableCells.Facts).Value) },
      76
    ),
    merge: test:invoke-and-assert-deep-equal(
          "spreadsheet-for-component",
          {
            ticker: "ko",
            fiscalYear: [ "2012", "2013" ],
            fiscalPeriod: "FY",
            disclosure: "BalanceSheet"
          },
          function($b as item*) as item* { count(flatten($b.TableSet[].TableCells.Facts).Value) },
          153
        ),
    html: test:invoke-and-assert-status(
          "spreadsheet-for-component",
          {
            ticker:"ko",
            fiscalYear:"2012",
            fiscalPeriod:"Q1",
            networkIdentifier:"http://www.thecoca-colacompany.com/role/CondensedConsolidatedBalanceSheets",
            format: "html"
          },
          302
    ),
    override: test:invoke-and-assert-deep-equal(
          "spreadsheet-for-component",
          {
            ticker: "ko",
            fiscalYear: [ "2012", "2013" ],
            fiscalPeriod: "FY",
            disclosure: "BalanceSheet",
            override: "true"
          },
          function($b as item*) as item* { count(flatten($b.TableSet[].TableCells.Facts).Value) },
          72
        ),
        one-table: test:invoke-and-assert-deep-equal(
          "spreadsheet-for-component",
          {
              "aid" : "0001214659-13-001731",
              "role" : "http://www.brin.com/role/SupplementalCashFlowInformationDetails",
              "hypercube" : "us-gaap:ScheduleOfShareBasedCompensationArrangementsByShareBasedPaymentAwardTable"
          },
          function($res as item*) as item* { count(flatten($res.TableSet[].TableCells.Facts).Value) },
          6
        ),
    qualcomm: test:invoke-and-assert-deep-equal(
      "spreadsheet-for-component",
      {
          "cik" : "804328",
          "fiscalPeriod" : "FY",
          "fiscalYear" : "2013",
          "fiscalYear" : "2014",
          "fiscalPeriodType" : "instant",
          "fiscalPeriodType" : "YTD",
          "disclosure" : "IncomeStatement",
          "override" : "true"
      },
      function($res as item*) as item* { count(flatten($res.TableSet[].TableCells.Facts).Value) },
      6
    )
})


Revert ":green_heart: Fix test."

Revert ":green_heart: Fix test."

This reverts commit b67c7d191ff5b176049eb0f4d3c22a711ce9a2f0.

jsoniq version "1.0";

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

import module namespace session    = "http://apps.28.io/session";
import module namespace sec-fiscal =
    "http://28.io/modules/xbrl/profiles/sec/fiscal/core";
import module namespace csv = "http://zorba.io/modules/json-csv";
import module namespace request = "http://www.28msec.com/modules/http-request";

declare function api:json-to-csv($objects as object*) as string
{
  let $flattened as object* := api:flatten-json-object($objects, ())
  return string-join(
    csv:serialize($flattened, { field-names: [ keys($flattened) ], serialize-null-as : "" }),
    "")
  };

declare function api:json-to-xml-value($key as string, $value as item) as element()
{
  typeswitch($value)
  case array return
    let $key-with-s := if(substring($key, string-length($key), 1) eq "s")
                       then $key
                       else $key || "s"
    let $key-no-s := if(substring($key, string-length($key), 1) eq "s")
                       then substring($key, 1, string-length($key) - 1)
                       else $key
    return element {$key-with-s} { for $v in $value[] return element { $key-no-s } { $v } }
  default return element { $key } { $value }
};

declare function api:json-to-xml($objects as object*, $root-name as string) as element()*
{
  for $flattened-object as object in $objects
  return element { $root-name } {

    let $flattened-object := api:flatten-json-object(trim($flattened-object, "Profiles"), { KeepArray: true })
    for $key in keys($flattened-object)
    return api:json-to-xml-value($key, $flattened-object.$key),

    for $profile in keys($flattened-object.Profiles)
    return <Profile name="{$profile}">{
      let $flattened-object := api:flatten-json-object($flattened-object.Profiles.$profile, { KeepArray: true })
      for $key in keys($flattened-object)
      return api:json-to-xml-value($key, $flattened-object.$key)
    }</Profile>

  }
};

declare %private function api:flatten-json-object(
  $items as item*,
  $options as object?) as item*
{
  for $item in $items
  return typeswitch($item)
         case atomic return $item
         case array return let $atomic-items := flatten($item)[$$ instance of atomic]
                           return if ($options.KeepArray)
                                 then [ $atomic-items ]
                                  else string-join($atomic-items, ",\r")
         case object return {|
             for $key in keys($item)
             return typeswitch($item.$key)
                    case object return api:flatten-json-object($item.$key, $options)
                    default return { $key: api:flatten-json-object($item.$key, $options) }
         |}
         default return ()
};

declare function api:validate-regexp($name as string, $value as string?, $regexp as string)
as ()
{
  if (exists($value))
  then
    if (matches($value, "^" || $regexp || "$"))
    then ();
    else fn:error(xs:QName("api:bad-parameter"), "Provided parameter " || $name || " with value " || $value || " does not match reg. expression " || $regexp || ".");
  else ();
};

declare function api:validate-enum($name as string, $value as string?, $enum as string*)
as ()
{
  if (exists($value))
  then
    if ($value = $enum)
    then ();
    else fn:error(xs:QName("api:bad-parameter"), "Provided parameter " || $name || " with value " || $value || " is not one of these: " || string-join($enum,", ") || ".");
  else ();
};

declare %private function api:sgpl($count as integer, $singular as string, $plural as string)
as string
{
    if ($count eq 1)
    then $count || " " || $singular
    else $count || " " || $plural
};

declare function api:passed($timestamp as xs:dateTime?)
as string
{
    if (empty($timestamp) or $timestamp eq null)
    then "Never"
    else
        let $now := current-dateTime()
        let $difference := $now - $timestamp
        let $years := years-from-duration($difference)
        let $month := months-from-duration($difference)
        let $days := days-from-duration($difference)
        let $hours := hours-from-duration($difference)
        let $minutes := minutes-from-duration($difference)
        return
            if ($years gt 0) then api:sgpl($years,"year ago","years ago")
            else if ($month gt 0) then api:sgpl($month,"month ago","month ago")
            else if ($days gt 0) then api:sgpl($days, "day ago", "days ago")
            else if ($hours gt 0) then api:sgpl($hours, "hour ago", "hours ago")
            else if ($minutes gt 0) then api:sgpl($minutes, "minute ago", "minutes ago")
            else if ($difference div xs:dayTimeDuration("PT1S") lt 0) then "future"
            else "Just Now"
};

declare function api:success() as object
{
  { "success" : true }
};

declare function api:success($data as object()) as object
{
  {|
     {"success" : true },
     $data
  |}
};

declare function api:check-and-return-results(
    $token as string?,
    $results as item*,
    $format as string?
) as item*
{
    switch(session:has-access($token, "data_sec"))
    case $session:ACCESS-ALLOWED return
        $results
    case $session:ACCESS-DENIED return {
          { status: 403 },
          session:error("access denied", $format)
       }
    case $session:ACCESS-AUTH-REQUIRED return {
          { status: 401 },
          session:error("authentication required or session expired", $format)
       }
    default return error()
};

declare function api:normalize-facts(
    $facts as object*) as object*
{
  trim($facts, ("_id", "Type"))
};

declare function api:serialize(
    $result as json-item,
    $comment as object,
    $serializers as object,
    $format as string?,
    $file-name as string) as item*
{
    switch ($format)
    case "xml" return {
        {
            serialization: { method: "xml", "omit-xml-declaration" : false, indent : true }
        },
        session:comment("xml", $comment),
        $serializers.to-xml($result)
    }
    case "text" case "csv" return {
        {
            "content-type": "text/csv",
            headers: { "Content-Disposition": "attachment; filename=" || $file-name || ".csv" }
        },
        $serializers.to-csv($result)
    }
    case "html" return {
        let $csv as string := $serializers.to-csv($result)
        return
        {
            { "content-type": "text/html" },
            api:csv-to-html($csv)
        }
    }
    case "excel" return {
        {
            "content-type": "application/vnd.ms-excel",
            headers: { "Content-Disposition": "attachment; filename=" || $file-name || ".csv" }
        },
        $serializers.to-csv($result)
    }
    default return {
        { serialization: { method: "json", indent : true } },
        {|
            session:comment("json", $comment),
            $result
        |}
    }
};

declare function api:csv-to-html(
    $csv as string) as item*
{
    let $csv as string* := $csv
    let $rows :=
      let $newline-tokens :=
        (: We go through all quotes. Outside of quotes, we mark newlines. Inside
           quotes, we don't interpret newlines :)
        for tumbling window $token as string* in tokenize($csv, "\"")
        start when true
        end $u at $j next $v
            (: If we've seen an even number of quotes so far, and the current
               token is not empty, then the current token (outside of quotes)
               is completed.
               If it's empty, then it means that the last closing quote was
               actually an escaped quote "" and we cannot close the window yet:)
            when ($j mod 2 eq 1 and $u ne "")
            (: If we've seen an odd number of quotes so far, and the next
               token is not empty, then the current token (inside quotes)
               is completed.
               If it's empty, then it means that the upcoming closing quote is
               actually an escaped quote "" and we cannot close the window yet:)
            or ($j mod 2 eq 0 and $v ne "")
        count $k
        return if($k mod 2 eq 1)
               (: We interpret \n as a csv newline for odd windows :)
               then $token ! replace($$, "\n", "______28newline______")
               (: We don't interpret anything for even windows, but paste
                  tokens back together (this is the "" escaping) :)
               else "\"" || string-join($token, "\"") || "\""
      return tokenize(string-join($newline-tokens), "______28newline______")
    let $header := $rows[1]
    let $body := subsequence($rows, 2)
    let $display-cell := function($row as string?, $cell-tag-name as string) {
      for $cell in
        (: We go through all quotes. Outside of quotes, we mark commas. Inside
           quotes, we don't interpret commas :)
        let $comma-tokens :=
          for tumbling window $token as string* in tokenize($row, "\"")
          start when true
          end $u at $j next $v
            (: If we've seen an even number of quotes so far, and the current
               token is not empty, then the current token (outside of quotes)
               is completed.
               If it's empty, then it means that the last closing quote was
               actually an escaped quote "" and we cannot close the window yet:)
              when ($j mod 2 eq 1 and $u ne "")
            (: If we've seen an odd number of quotes so far, and the next
               token is not empty, then the current token (inside quotes)
               is completed.
               If it's empty, then it means that the upcoming closing quote is
               actually an escaped quote "" and we cannot close the window yet:)
              or ($j mod 2 eq 0 and $v ne "")
          count $k
          return if($k mod 2 eq 1)
               (: We interpret the comma as a csv comma for odd windows :)
                 then $token ! replace($$, ",", "______28comma______")
               (: We don't interpret anything for even windows, but paste
                  tokens back together (this is the "" escaping) :)
                 else string-join($token, "\"")
        return tokenize(string-join($comma-tokens), "______28comma______")
      return switch(true)
             case contains($cell, "http://") and
                  (contains($cell, request:server-name()) or
                   contains($cell, "rendering.secxbrl.info"))
             return element { $cell-tag-name } { Link }
             default return element { $cell-tag-name } {tokenize($cell, "\r") ! ($$,
)}
    }
    return

        <title>Cell Store REST API</title>

{$display-cell($header, "th")}

          {
            for $row as string in $body
            return
{$display-cell($row, "td")}

          }

};

declare function api:preprocess-fiscal-years($fiscal-years as string*) as integer*
{
  distinct-values(
    for $fy in $fiscal-years ! upper-case($$)
    return switch($fy)
           case "LATEST" return $sec-fiscal:LATEST_FISCAL_YEAR
           case "ALL" return $sec-fiscal:ALL_FISCAL_YEARS
           default return if($fy castable as integer) then integer($fy) else ()
  )
};

declare function api:preprocess-fiscal-periods($fiscal-periods as string*) as string*
{
  distinct-values(
    for $fp in $fiscal-periods ! upper-case($$)
    return switch($fp)
           case "Q1"
           case "Q2"
           case "Q3"
           case "FY"
             return $fp
           case "Q4"
             return "FY"
           case "ALL"
             return $sec-fiscal:ALL_FISCAL_PERIODS
           default
             return error(xs:QName("local:INVALID-PERIOD"),
               $fp || ": fiscalPeriod values must be one or more of Q1, Q2, Q3, Q4, FY, ALL")
  )
};

declare function api:preprocess-fiscal-period-types($fiscal-period-types as string*) as string*
{
  distinct-values(
    for $fpt in $fiscal-period-types
    return switch($fpt)
           case "instant"
           case "QTD"
           case "YTD"
             return $fpt
           case "ALL"
             return $sec-fiscal:ALL_FISCAL_PERIOD_TYPES
           default
             return error(xs:QName("local:INVALID-PERIOD-TYPE"),
               $fpt || ": fiscalPeriodType values must be one or more of instant, YTD, QTD")
  )
};

declare function api:preprocess-format($format as string?, $request-uri as string) as string?
{
  let $request-path := tokenize($request-uri, "\\?")[1]
  return lower-case(($format, substring-after($request-path, ".jq."))[1])
};

declare function api:preprocess-tags($tags as string*) as string*
{
  distinct-values($tags ! upper-case($$))
};

declare function api:preprocess-concept-kind($kind as string*) as string*
{
  distinct-values(
    for $kind in $kind
    return switch(lower-case($kind))
           case "hypercube" return "Hypercube"
           case "dimension" return "Dimension"
           case "member" return "Member"
           case "lineitems" return "LineItems"
           case "abstract" return "Abstract"
           case "concept" return "Concept"
           default return ()
  )
};

declare function api:preprocess-boolean($name as string, $value as string)
as boolean
{
  if ($value eq "")
  then true
  else
  {
      api:validate-enum($name, lower-case($value), ("true", "false"));
      boolean($value)
  }
};

declare function api:not-found($name as string) as item*
{
  { status: 404, content-type: "application/json" },
  session:error("No such " || $name || " could be found.", "json")
};

jsoniq version "1.0";

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

import module namespace session    = "http://apps.28.io/session";
import module namespace sec-fiscal =
    "http://28.io/modules/xbrl/profiles/sec/fiscal/core";
import module namespace csv = "http://zorba.io/modules/json-csv";
import module namespace request = "http://www.28msec.com/modules/http-request";

declare function api:json-to-csv($objects as object*) as string
{
  let $flattened as object* := api:flatten-json-object($objects, ())
  return string-join(
    csv:serialize($flattened, { field-names: [ keys($flattened) ], serialize-null-as : "" }),
    "")
  };

declare function api:json-to-xml-value($key as string, $value as item) as element()
{
  typeswitch($value)
  case array return
    let $key-with-s := if(substring($key, string-length($key), 1) eq "s")
                       then $key
                       else $key || "s"
    let $key-no-s := if(substring($key, string-length($key), 1) eq "s")
                       then substring($key, 1, string-length($key) - 1)
                       else $key
    return element {$key-with-s} { for $v in $value[] return element { $key-no-s } { $v } }
  default return element { $key } { $value }
};

declare function api:json-to-xml($objects as object*, $root-name as string) as element()*
{
  for $flattened-object as object in $objects
  return element { $root-name } {

    let $flattened-object := api:flatten-json-object(trim($flattened-object, "Profiles"), { KeepArray: true })
    for $key in keys($flattened-object)
    return api:json-to-xml-value($key, $flattened-object.$key),

    for $profile in keys($flattened-object.Profiles)
    return <Profile name="{$profile}">{
      let $flattened-object := api:flatten-json-object($flattened-object.Profiles.$profile, { KeepArray: true })
      for $key in keys($flattened-object)[$$ ne "Name"]
      return api:json-to-xml-value($key, $flattened-object.$key)
    }</Profile>

  }
};

declare %private function api:flatten-json-object(
  $items as item*,
  $options as object?) as item*
{
  for $item in $items
  return typeswitch($item)
         case atomic return $item
         case array return let $atomic-items := flatten($item)[$$ instance of atomic]
                           return if ($options.KeepArray)
                                 then [ $atomic-items ]
                                  else string-join($atomic-items, ",\r")
         case object return {|
             for $key in keys($item)
             return typeswitch($item.$key)
                    case object return api:flatten-json-object($item.$key, $options)
                    default return { $key: api:flatten-json-object($item.$key, $options) }
         |}
         default return ()
};

declare function api:validate-regexp($name as string, $value as string?, $regexp as string)
as ()
{
  if (exists($value))
  then
    if (matches($value, "^" || $regexp || "$"))
    then ();
    else fn:error(xs:QName("api:bad-parameter"), "Provided parameter " || $name || " with value " || $value || " does not match reg. expression " || $regexp || ".");
  else ();
};

declare function api:validate-enum($name as string, $value as string?, $enum as string*)
as ()
{
  if (exists($value))
  then
    if ($value = $enum)
    then ();
    else fn:error(xs:QName("api:bad-parameter"), "Provided parameter " || $name || " with value " || $value || " is not one of these: " || string-join($enum,", ") || ".");
  else ();
};

declare %private function api:sgpl($count as integer, $singular as string, $plural as string)
as string
{
    if ($count eq 1)
    then $count || " " || $singular
    else $count || " " || $plural
};

declare function api:passed($timestamp as xs:dateTime?)
as string
{
    if (empty($timestamp) or $timestamp eq null)
    then "Never"
    else
        let $now := current-dateTime()
        let $difference := $now - $timestamp
        let $years := years-from-duration($difference)
        let $month := months-from-duration($difference)
        let $days := days-from-duration($difference)
        let $hours := hours-from-duration($difference)
        let $minutes := minutes-from-duration($difference)
        return
            if ($years gt 0) then api:sgpl($years,"year ago","years ago")
            else if ($month gt 0) then api:sgpl($month,"month ago","month ago")
            else if ($days gt 0) then api:sgpl($days, "day ago", "days ago")
            else if ($hours gt 0) then api:sgpl($hours, "hour ago", "hours ago")
            else if ($minutes gt 0) then api:sgpl($minutes, "minute ago", "minutes ago")
            else if ($difference div xs:dayTimeDuration("PT1S") lt 0) then "future"
            else "Just Now"
};

declare function api:success() as object
{
  { "success" : true }
};

declare function api:success($data as object()) as object
{
  {|
     {"success" : true },
     $data
  |}
};

declare function api:check-and-return-results(
    $token as string?,
    $results as item*,
    $format as string?
) as item*
{
    switch(session:has-access($token, "data_sec"))
    case $session:ACCESS-ALLOWED return
        $results
    case $session:ACCESS-DENIED return {
          { status: 403 },
          session:error("access denied", $format)
       }
    case $session:ACCESS-AUTH-REQUIRED return {
          { status: 401 },
          session:error("authentication required or session expired", $format)
       }
    default return error()
};

declare function api:normalize-facts(
    $facts as object*) as object*
{
  trim($facts, ("_id", "Type"))
};

declare function api:serialize(
    $result as json-item,
    $comment as object,
    $serializers as object,
    $format as string?,
    $file-name as string) as item*
{
    switch ($format)
    case "xml" return {
        {
            serialization: { method: "xml", "omit-xml-declaration" : false, indent : true }
        },
        session:comment("xml", $comment),
        $serializers.to-xml($result)
    }
    case "text" case "csv" return {
        {
            "content-type": "text/csv",
            headers: { "Content-Disposition": "attachment; filename=" || $file-name || ".csv" }
        },
        $serializers.to-csv($result)
    }
    case "html" return {
        let $csv as string := $serializers.to-csv($result)
        return
        {
            { "content-type": "text/html" },
            api:csv-to-html($csv)
        }
    }
    case "excel" return {
        {
            "content-type": "application/vnd.ms-excel",
            headers: { "Content-Disposition": "attachment; filename=" || $file-name || ".csv" }
        },
        $serializers.to-csv($result)
    }
    default return {
        { serialization: { method: "json", indent : true } },
        {|
            session:comment("json", $comment),
            $result
        |}
    }
};

declare function api:csv-to-html(
    $csv as string) as item*
{
    let $csv as string* := $csv
    let $rows :=
      let $newline-tokens :=
        (: We go through all quotes. Outside of quotes, we mark newlines. Inside
           quotes, we don't interpret newlines :)
        for tumbling window $token as string* in tokenize($csv, "\"")
        start when true
        end $u at $j next $v
            (: If we've seen an even number of quotes so far, and the current
               token is not empty, then the current token (outside of quotes)
               is completed.
               If it's empty, then it means that the last closing quote was
               actually an escaped quote "" and we cannot close the window yet:)
            when ($j mod 2 eq 1 and $u ne "")
            (: If we've seen an odd number of quotes so far, and the next
               token is not empty, then the current token (inside quotes)
               is completed.
               If it's empty, then it means that the upcoming closing quote is
               actually an escaped quote "" and we cannot close the window yet:)
            or ($j mod 2 eq 0 and $v ne "")
        count $k
        return if($k mod 2 eq 1)
               (: We interpret \n as a csv newline for odd windows :)
               then $token ! replace($$, "\n", "______28newline______")
               (: We don't interpret anything for even windows, but paste
                  tokens back together (this is the "" escaping) :)
               else "\"" || string-join($token, "\"") || "\""
      return tokenize(string-join($newline-tokens), "______28newline______")
    let $header := $rows[1]
    let $body := subsequence($rows, 2)
    let $display-cell := function($row as string?, $cell-tag-name as string) {
      for $cell in
        (: We go through all quotes. Outside of quotes, we mark commas. Inside
           quotes, we don't interpret commas :)
        let $comma-tokens :=
          for tumbling window $token as string* in tokenize($row, "\"")
          start when true
          end $u at $j next $v
            (: If we've seen an even number of quotes so far, and the current
               token is not empty, then the current token (outside of quotes)
               is completed.
               If it's empty, then it means that the last closing quote was
               actually an escaped quote "" and we cannot close the window yet:)
              when ($j mod 2 eq 1 and $u ne "")
            (: If we've seen an odd number of quotes so far, and the next
               token is not empty, then the current token (inside quotes)
               is completed.
               If it's empty, then it means that the upcoming closing quote is
               actually an escaped quote "" and we cannot close the window yet:)
              or ($j mod 2 eq 0 and $v ne "")
          count $k
          return if($k mod 2 eq 1)
               (: We interpret the comma as a csv comma for odd windows :)
                 then $token ! replace($$, ",", "______28comma______")
               (: We don't interpret anything for even windows, but paste
                  tokens back together (this is the "" escaping) :)
                 else string-join($token, "\"")
        return tokenize(string-join($comma-tokens), "______28comma______")
      return switch(true)
             case contains($cell, "http://") and
                  (contains($cell, request:server-name()) or
                   contains($cell, "rendering.secxbrl.info"))
             return element { $cell-tag-name } { Link }
             default return element { $cell-tag-name } {tokenize($cell, "\r") ! ($$,
)}
    }
    return

        <title>Cell Store REST API</title>

{$display-cell($header, "th")}

          {
            for $row as string in $body
            return
{$display-cell($row, "td")}

          }

};

declare function api:preprocess-fiscal-years($fiscal-years as string*) as integer*
{
  distinct-values(
    for $fy in $fiscal-years ! upper-case($$)
    return switch($fy)
           case "LATEST" return $sec-fiscal:LATEST_FISCAL_YEAR
           case "ALL" return $sec-fiscal:ALL_FISCAL_YEARS
           default return if($fy castable as integer) then integer($fy) else ()
  )
};

declare function api:preprocess-fiscal-periods($fiscal-periods as string*) as string*
{
  distinct-values(
    for $fp in $fiscal-periods ! upper-case($$)
    return switch($fp)
           case "Q1"
           case "Q2"
           case "Q3"
           case "FY"
             return $fp
           case "Q4"
             return "FY"
           case "ALL"
             return $sec-fiscal:ALL_FISCAL_PERIODS
           default
             return error(xs:QName("local:INVALID-PERIOD"),
               $fp || ": fiscalPeriod values must be one or more of Q1, Q2, Q3, Q4, FY, ALL")
  )
};

declare function api:preprocess-fiscal-period-types($fiscal-period-types as string*) as string*
{
  distinct-values(
    for $fpt in $fiscal-period-types
    return switch($fpt)
           case "instant"
           case "QTD"
           case "YTD"
             return $fpt
           case "ALL"
             return $sec-fiscal:ALL_FISCAL_PERIOD_TYPES
           default
             return error(xs:QName("local:INVALID-PERIOD-TYPE"),
               $fpt || ": fiscalPeriodType values must be one or more of instant, YTD, QTD")
  )
};

declare function api:preprocess-format($format as string?, $request-uri as string) as string?
{
  let $request-path := tokenize($request-uri, "\\?")[1]
  return lower-case(($format, substring-after($request-path, ".jq."))[1])
};

declare function api:preprocess-tags($tags as string*) as string*
{
  distinct-values($tags ! upper-case($$))
};

declare function api:preprocess-concept-kind($kind as string*) as string*
{
  distinct-values(
    for $kind in $kind
    return switch(lower-case($kind))
           case "hypercube" return "Hypercube"
           case "dimension" return "Dimension"
           case "member" return "Member"
           case "lineitems" return "LineItems"
           case "abstract" return "Abstract"
           case "concept" return "Concept"
           default return ()
  )
};

declare function api:preprocess-boolean($name as string, $value as string)
as boolean
{
  if ($value eq "")
  then true
  else
  {
      api:validate-enum($name, lower-case($value), ("true", "false"));
      boolean($value)
  }
};

declare function api:not-found($name as string) as item*
{
  { status: 404, content-type: "application/json" },
  session:error("No such " || $name || " could be found.", "json")
};