koopjs / FeatureServer

An open source Geoservices Implementation (deprecated)
https://geoservices.github.io
Other
101 stars 32 forks source link

New fields generation files and tests. #227

Closed rgwozdz closed 2 years ago

rgwozdz commented 2 years ago

This PR is second in a series that refactors the way FeatureServer generates the fields property in the responses for requests to FeatureServer/:layer/query and FeatureServer/:layer/.

Key background

The fields property provides metadata for all of the properties/attributes on each feature. For example:

https://services2.arcgis.com/zNjnZafDYCAJAbN0/ArcGIS/rest/services/Street_ROW_Trees/FeatureServer/2/query?resultRecordCount=1&outFields=*&where=1%3D1&f=pjson

{
    "objectIdFieldName": "OBJECTID",
    "uniqueIdField": {
        "name": "OBJECTID",
        "isSystemMaintained": true
    },
    "globalIdFieldName": "",
    "geometryType": "esriGeometryPoint",
    "spatialReference": {
        "wkid": 102100,
        "latestWkid": 3857
    },
    "fields": [
        {
            "name": "OBJECTID",
            "type": "esriFieldTypeOID",
            "alias": "OBJECTID",
            "sqlType": "sqlTypeOther",
            "domain": null,
            "defaultValue": null
        },
        {
            "name": "Common_Name",
            "type": "esriFieldTypeString",
            "alias": "Common_Name",
            "sqlType": "sqlTypeOther",
            "length": 75,
            "domain": null,
            "defaultValue": null
        },
        {
            "name": "Genus",
            "type": "esriFieldTypeString",
            "alias": "Genus",
            "sqlType": "sqlTypeOther",
            "length": 50,
            "domain": null,
            "defaultValue": null
        },
        {
            "name": "Species",
            "type": "esriFieldTypeString",
            "alias": "Species",
            "sqlType": "sqlTypeOther",
            "length": 30,
            "domain": null,
            "defaultValue": null
        },
        {
            "name": "House_Number",
            "type": "esriFieldTypeInteger",
            "alias": "House_Number",
            "sqlType": "sqlTypeOther",
            "domain": null,
            "defaultValue": null
        },
        {
            "name": "Street_Direction",
            "type": "esriFieldTypeString",
            "alias": "Street_Direction",
            "sqlType": "sqlTypeOther",
            "length": 2,
            "domain": null,
            "defaultValue": null
        },
        {
            "name": "Street_Name",
            "type": "esriFieldTypeString",
            "alias": "Street_Name",
            "sqlType": "sqlTypeOther",
            "length": 50,
            "domain": null,
            "defaultValue": null
        },
        {
            "name": "Street_Type",
            "type": "esriFieldTypeString",
            "alias": "Street_Type",
            "sqlType": "sqlTypeOther",
            "length": 4,
            "domain": null,
            "defaultValue": null
        },
        {
            "name": "Street_Suffix",
            "type": "esriFieldTypeString",
            "alias": "Street_Suffix",
            "sqlType": "sqlTypeOther",
            "length": 5,
            "domain": null,
            "defaultValue": null
        },
        {
            "name": "Trunk_Diameter",
            "type": "esriFieldTypeDouble",
            "alias": "Trunk_Diameter",
            "sqlType": "sqlTypeOther",
            "domain": null,
            "defaultValue": null
        },
        {
            "name": "Longitude",
            "type": "esriFieldTypeDouble",
            "alias": "Longitude",
            "sqlType": "sqlTypeOther",
            "domain": null,
            "defaultValue": null
        },
        {
            "name": "Latitude",
            "type": "esriFieldTypeDouble",
            "alias": "Latitude",
            "sqlType": "sqlTypeOther",
            "domain": null,
            "defaultValue": null
        }
    ],
    "exceededTransferLimit": true,
    "features": [
        {
            "attributes": {
                "OBJECTID": 1,
                "Common_Name": "AMERICAN SWEETGUM",
                "Genus": "LIQUIDAMBAR",
                "Species": "STYRACIFLUA",
                "House_Number": 3167,
                "Street_Direction": null,
                "Street_Name": "ALAMEDA",
                "Street_Type": "ST",
                "Street_Suffix": null,
                "Trunk_Diameter": 10,
                "Longitude": -118.085134478522,
                "Latitude": 34.153484325806
            },
            "geometry": {
                "x": -13145177.040404357,
                "y": 4049429.8874464519
            }
        }
    ]
}

Each property in the feature's attributes is described in the fields array.

As noted above, the layer-info route also returns a fields array. It's almost identical to what is returned in the query endpoint, except that each field includes a nullable and editable boolean field:

https://services2.arcgis.com/zNjnZafDYCAJAbN0/ArcGIS/rest/services/Street_ROW_Trees/FeatureServer/2?f=pjson

"fields" : [
    {
      "name" : "OBJECTID", 
      "type" : "esriFieldTypeOID", 
      "alias" : "OBJECTID", 
      "sqlType" : "sqlTypeOther", 
      "nullable" : false, 
      "editable" : false, 
      "domain" : null, 
      "defaultValue" : null
    }, 
    {
      "name" : "Common_Name", 
      "type" : "esriFieldTypeString", 
      "alias" : "Common_Name", 
      "sqlType" : "sqlTypeOther", 
      "length" : 75, 
      "nullable" : true, 
      "editable" : true, 
      "domain" : null, 
      "defaultValue" : null
    }, 
...
...
]

The query endpoint contains one known variation, and this occurs when statistical aggregations have been requested (i.e., outStatistics parameter):

statistics query

{
  "objectIdFieldName" : "OBJECTID", 
  "uniqueIdField" : 
  {
    "name" : "OBJECTID", 
    "isSystemMaintained" : true
  }, 
  "globalIdFieldName" : "", 
  "geometryType" : "esriGeometryPoint", 
  "spatialReference" : {
    "wkid" : 102100, 
    "latestWkid" : 3857
  }, 
  "fields" : [
    {
      "name" : "TOTAL_STUD_SUM", 
      "type" : "esriFieldTypeDouble", 
      "alias" : "TOTAL_STUD_SUM", 
      "sqlType" : "sqlTypeFloat", 
      "domain" : null, 
      "defaultValue" : null
    }, 
    {
      "name" : "ZIP_CODE_COUNT", 
      "type" : "esriFieldTypeDouble", 
      "alias" : "ZIP_CODE_COUNT", 
      "sqlType" : "sqlTypeFloat", 
      "domain" : null, 
      "defaultValue" : null
    }
  ], 
  "features" : [
    {
      "attributes" : {
        "TOTAL_STUD_SUM" : 897872.999999995, 
        "ZIP_CODE_COUNT" : 263.5
      }
    }
  ]
}

Note here that ALL statistics fields with the exception of dates are tagged as type esriFieldTypeDouble regardless of their source field's type. So even if you do a SUM of an integer type, the resulting statistics field is type double. Any groupBy fields are typed as their source field's type.

PR content

This PR contains only the new code used for fields-array generation for the three cases noted above: query, layer-info, query-statistics. You will see that there are two ways field objects can be generated - either from a field definition that is attached the metadata of the provider or by inspecting the data type of a sample value from the feature set.

This PR does not contain the implementation of the new field generation code in the response handler. That will come in a follow-up PR - this is an effort to decrease the scope of each individual PR.