In Petro.ai 5 we've consolidated data oriented requests into a single endpoint. Previously getting data required making a request to /api/<type> which resulted in a lot of endpoints and inconsistency in behaviors. Now all you need in order to interact with data is target the /api/data endpoint.

# Quick Reference

Petro.ai uses a single endpoint to deliver all the data types to the API user.

Querying

POST /api/data/query

{
  "type": "Petron",
  "query": {
    "id": "123"
  }
}

Inserting

POST /api/data

{
  "type": "Petron",
  "data": [
    {
      "name": "My new Petron"
    }
  ]
}

Updating

PATCH /api/data

{
  "type": "Petron",
  "query": {
    "name": "My new Petron"
  },
  "body": {
    "name": "My updated Petron"
  }
}

Replacing

PUT /api/data

{
  "type": "Petron",
  "data": [
    {
      "id": "1234",
      "name": "This Petron's values were replaced"
    },
    {
      "name": "This is a new Petron"
    }
  ]
}

Deleting Data

DELETE /api/data

{
  "type": "Petron",
  "query": {
    "name": "My new Petron"
  }
}

# Query Data

Querying data is done using the /api/data/query endpoint. It leverages the body of an HTTP request to provide a richer querying experience than route parameters would provide. A simple id query looks like

// POST /api/data/query
{
  "type": "Petron",
  "query": {
    "id": "1234"
  }
}

To query for everything of a specific type, that is done by making a request that looks like:

{
  "type": "Petron",
  "query": { }
}

Leaving the query object empty indicates that the user wants to find everything.

TIP

The fields in the query object match to the schema of the provided type. You can use the /api/data/schema endpoint to retrieve an example schema for reference.

Users have the option to provide more advanced query operators by using an object syntax instead of providing a direct value. For example, query above can be rewritten as:

{
  "type": "Petron",
  "query": {
    "id": {
      "eq": "1234"
    }
  }
}

Where the id object is defining the query operators to use.

Another convenience shorthand is for querying for many items in a single field. For example, a user may want to find Petron's with ids ["1234", "5678"]. In order to do that you would simply change your query to look like:

// POST /api/data/query
{
  "type": "Petron",
  "query": {
    "id": ["1234", "5678"]
  }
}

# Query Options

The query endpoint also accepts an object of options that can be used to augment the return.

Using query options is done by adding an options object to the request body. An example request with options looks like:

{
  "type": "Petron",
  "query": { },
  "options": {
    "limit": 10,
  }
}

There are a few fields that users will likely want to use which are skip and limit.

name type description
limit int Limit the number of documents returned by the API
skip int Skip x number of documents and return the next y from the API
sortBy string Sort by a field on the provided type
sortOrder (-1, 0, 1) Sort by descending, none, or ascending respectively

# Query Response

The response objects on successful requests are the same throughout the application. A sample successful response for querying looks something like:

{
    "data": [
        {
            "name": "A Petron",
            "description": null,
            "notes": null,
            "ownerId": null,
            "unitsId": null,
            "timeZoneId": "UTC",
            "dockedApps": [],
            "id": "5ef3c4ca53191718ec68026e",
            "createdAt": "2020-06-24T21:25:30.725Z",
        }
    ],
    "cursor": null,
    "empty": false,
    "count": 1,
    "total": 1,
    "activityId": null,
    "hasCursor": false,
    "success": true,
    "message": "",
    "errors": [],
    "issues": [],
    "hasErrors": false,
    "insertedIds": [],
    "idsNotFound": [],
}

If a request is unsuccesful the success field on the response will be set to false and there may be additional information in the message field or errors field depending on the severity of the error.

The total indicates how many items were matched with the provided query. total and count will only differ whenever the limit or skip values are provided in the options. count is simply a quick property for getting the length of the data array.


# Querying Multiple Fields

Querying multiple fields are treated as SQL and statements. For example

{
  "type": "Petron",
  "query": {
    "createdAt": {
      "gt": "1/1/1900"
    },
    "updatedAt": {
      "lt": "12/31/2199"
    }
  }
}

Will query for all Petron's that were created after 1/1/1900 but before 12/31/2199.

Currently or statements aren't supported through the API.

# Multiple Conditions

To support ands on the same field, the query API supports having multiple operators per field

{
  "query": {
     "createdAt": {
         "gt": "2020-01-01",
         "lt": "2020-12-31"
     }
  }
}

The above example looks for data that was created between 2020-01-01 and 2020-12-31

# Query Operations

Currently the supported query operations are:

  • Eq - Query for data that match a value
  • Ne - Query for data that don't match a value
  • In - Query for data that match elements in an array
  • Lt - Filter data with a value less than a provided value
  • Lte - Filter data with a value less than or equal to a provided value
  • Gt - Filter data with a value greater than a provided value
  • Gte - Filter data with a value greater than or equal to a provided value
  • Regex - Search for data that match on a provided regular expression
  • Bounds - Search for data within a bounding box
  • Polygon - Search for data within a polygon

# Using Query Operators

To specify a query operator in the query block use the following.

{
  "query": {
     "createdAt": {
         "gt": "2020-01-01"
     }
  }
}

# Eq Operator

eq filter to data that only has the field equal to the provided value

{
  "query": {
    "id": {
      "eq": "1234"
    }
  }
}

eq can also be simplified to

{
  "query": {
    "id": "1234"
  }
}

# Ne Operator

ne filters data where the set field is not the provided value

{
  "query": {
    "id": {
      "ne": "1234"
    }
  }
}

# In Operator

The in operator allows a user to query data that have elements "in" the respective list. The in operator can be used in two different ways Explicitly:

{
  "query": {
    "id": {
      "in": ["1234", "5678"]
    }
  }
}

or implicitly:

{
  "query": {
    "id": ["1234", "5678"]
  }
}

The implicit form is really a convenient way to build queries without having to maintain the operator object. Under the hood, the shorthand form gets converted into the operator form.

# Lt/Lte Operator

lt simply filters documents that are less than the specified value

{
  "query": {
    "numberField": {
      "lt":  1500
    }
  }
}

lte will include documents that include the field as well as documents that are less than it.

# Gt/Gte Operator

gt filters documents that are greater than the specified value

{
  "query": {
    "numberField": {
      "gt": 1500
    }
  }
}

gte will include documents that include the field as well as documents that are greater than it.

# RegEx Operator

regex allows a user to define a regular expression to match string fields on. The regex operator expects a string value that can be a plain string or any valid regex string.

// Sample POST to /api/data/query
{
   "type": "Well",
   "query": {
       "name": {
           // No flags
           "regex": "TOM.*"
       },
       "basinName": {
           // With flags
           "regex": "/tom.*/i"
       }
   }
}

# Bounds Operator

A bound operator simply accepts an array containing coordinates to generate a bounding box. The array should contain 4 elements in the sequence of

[
  0,    //<lower-left-long>,
  0,    //<lower-left-lat>,
  10,   //<lower-right-long>,
  10    //<lower-right-lat>
]

# Polygon Operator

The polygon operator is the most complicated. It expects a set of valid GEO Json polygon coordinates in order to find documents that match within it.

WARNING

Querying on a field that isn't a geo point will result in returning all values. Typically fields that support polygon queries will be marked as location or you can use the /api/data/schema endpoint to find fields that are labeled as GeoPoint

The polygon operator works solely off of the coordinates of a GEO Json Geometry object. A polygon object in GEO Json looks something like:

{
  "type": "Polygon",
  "coordinates": [
    // exterior of the polygon
    [
      [ -95.36026, 29.7634972 ],
      [ -95.36003, 29.7633604 ],
      [ -95.35996, 29.7634489 ],
      [ -95.36019, 29.7635886 ],
      [ -95.36026, 29.7634972 ]
    ],
    // ... holes in the polygon or other polygons
  ]
}

With that, a basic query in Petro.ai looks something like

{
  "query": {
    "location": {
      "polygon": [
        [
          [ -95.36026, 29.7634972 ],
          [ -95.36003, 29.7633604 ],
          [ -95.35996, 29.7634489 ],
          [ -95.36019, 29.7635886 ],
          [ -95.36026, 29.7634972 ]
        ]
      ]
    }
  }
}

An example query with a complex polygon (including a hole) looks like

{
  "query": {
    "location": {
      "polygon": [
        // polygon exterior
        [
            [ 2.0, 9.0 ],
            [ -0.33, 7.0 ],
            [ 0.7, 1.8 ],
            [ 5.0, 0.15 ],
            [ 9.0, 2.0 ],
            [ 3.14, 4.0 ],
            [ 2.0, 7.0 ],
            [ 8.0, 3.0 ],
            [ 9.0, 8.0 ],
            [ 9.0, 10.0 ],
            [ 2.0, 9.0 ]
        ],
        // hole 0
        [
            [ 5.0, 8.0 ],
            [ 5.0, 7.0 ],
            [ 6.0, 7.5 ],
            [ 5.0, 8.0 ]
        ]
    ]
    }
  }
}

TIP

To get more familiar with GEO Json this tool and the incredibly brief intro are useful starting points.

# Inserting Data

To insert data to the endpoint, make a POST request to /api/data with a body in the following form.

// POST /api/data
{
  "type": "Petron",
  "data": [
    {
      "name": "A new petron!"
    }
  ]
}

# Insert Response

Whenever data is inserted successfully, a response that looks something like:

{
    "data": [],
    "cursor": null,
    "empty": true,
    "count": 1,
    "total": 1,
    "activityId": null,
    "hasCursor": false,
    "success": true,
    "message": "",
    "errors": [],
    "issues": [],
    "hasErrors": false,
    "insertedIds": [
      "5ef3c4ca53191718ec68026e"
    ],
    "idsNotFound": [],
}

will be returned. The insertedIds field is an unordered list of the document ids that were used to create your data in the database.

count and total represent how many documents were inserted. These values shouldn't differ when making an insert request.

# Unique Key Collisions

Some collections have unique indexes that they use to enforce application logic. When inserting data, this can pop up every now and then. If this happens the user will see a message in the response that looks something like:

{
  // ...
  "message": "An error occurred while inserting into core.Links:A bulk write operation resulted in one or more errors.\r\n  E11000 duplicate key error collection: petro-alex-dos.core.Links index: PAI_LinkId_1.0.0 dup key: { : \"5e7a7fe810e71e258c4e733c\", : \"f5daff3f-aa9a-4e51-b7a4-5b02bfd2ff57\", : null }. An item you were inserting into core.Links already has a document",
  "errors": [
      {
          "message": "An item you were inserting into core.Links already has a document"
      }
  ],
}

This may be alarming, but it just means that one of the documents you inserted has a unique key collision.

WARNING

One thing to keep in mind, is that if this error is encountered it will look like it was unsuccessful in inserting the other provided documents. However, the remaining documents were inserted okay. This is a bug and will be addressed in an upcoming release.

# Replacing Data

While replacing data has the same syntax as inserting, the differences reside in the behavior. Documents provided in the data array that already have an id will get replaced, the rest would be inserted.

// PUT /api/data
{
  "type": "petron",
  "data": [
    {
        "name" : "petron-to-insert"
    },
    {
        "id": "5f5a744a97cfa50e6ca35b1b",
        "name" : "petron-to-replace"
    }
  ]
}

In the above example, the petron-to-insert will get inserted into the database. Whereas the petron-to-replace will have its values replaced because it has the id field set.

# Replace Response

The response that the above request makes looks like:

{
    "data": [],
    "cursor": null,
    "empty": true,
    "count": 2,
    "total": 2,
    "activityId": null,
    "hasCursor": false,
    "success": true,
    "message": "",
    "errors": [],
    "issues": [],
    "hasErrors": false,
    "insertedIds": [
        "5f5a786f97cfa50e6ca35b23"
    ],
    "updatedIds": [],
    "deletedIds": [],
    "idsNotFound": [],
    "referencesRemoved": [],
    "referencesUpdated": [],
    "referencesInserted": []
}

You'll notice that the insertedIds has a single element and that the count field is 2. This is to indicate that the replace was successful and the insert was also successful.

# Updating Data

Updates have been improved since the 4.3.2 release. To define the fields, you'll need to provide them as key-value pairs in the body option. For example, updating a document with type="doc" with id=2 and a field named name you would POST something like this:

// PATCH /api/data
{
   "type": "doc",
   "query": {
       "id": 2
   },
   "body": {
       "name": "Updated Name!"
   }
}

# Update Response

Update responses are mainly focused on the count field. It simply indicates how many documents that the provided query matched on and were updated

{
    "data": [],
    "cursor": null,
    "empty": true,
    "count": 1,
    "total": 1,
    "activityId": null,
    "hasCursor": false,
    "success": true,
    "message": "",
    "errors": [],
    "issues": [],
    "hasErrors": false,
    "insertedIds": [],
    "updatedIds": [],
    "deletedIds": [],
    "idsNotFound": [],
}

TIP

The updatedIds array is a deprecated field. The way updates work now can no longer offer the actual ids of what was affected by a query.

# Deleting Data

Deletes are functionally similar to queries except. The primary difference is that a delete does not accept empty queries. It will return an OK but the result object will have a message indicating that you tried something illegal. Example for deleting a specific document:

// DELETE /api/data
{
   "type": "Petron",
   "query": {
      "id": 123
   }
}

# Delete Response

Delete responses are similarly focused on the count field much like the updated response is.

{
    "data": [],
    "cursor": null,
    "empty": true,
    "count": 1,
    "total": 1,
    "activityId": null,
    "hasCursor": false,
    "success": true,
    "message": "",
    "errors": [],
    "issues": [],
    "hasErrors": false,
    "insertedIds": [],
    "updatedIds": [],
    "deletedIds": [],
    "idsNotFound": [],
}

TIP

The deletedIds array is a deprecated field. The way deletes work now can no longer offer the actual ids of what was affected by a query.

# Utility Endpoints

These endpoints don't really provide application value however, they can be useful when trying to debug or figure out how to structure a request.

# Types

The /api/data route provides a couple of utility endpoints. The first being the types endpoint. This endpoint is a GET only endpoint. It returns a list of strings of the available data types in the platform. These values tie directly with the type field in all of the data requests that can be made.

Making this request is pretty straight forward in terms of API calls

GET /api/data/types

Sample Response

{
    "data": [
        "Petron",
        "Link",
        "Comment",
        "UnitsDefinition"
    ],
    ...
}

# Schemas

The /api/data/schema endpoint is useful for getting the structure of an object.

WARNING

Currently this endpoint only returns top level fields of the object.