Data Modeling | Vendia

Data Modeling

Vendia automatically converts your JSON Schema-based data model into a production-grade, distributed application composed of serverless public cloud resources, complete with GraphQL APIs.

Much as a conventional database turns a CREATE_TABLE definition into a single, centralized database, Vendia turns your JSON Schema-based data model into a distributed, decentralized database…and all the platform services needed to make that data available and useful in the cloud.

This guide explains how JSON schema types are converted into the GraphQL types that each workspace in your project can access, update, and subscribe to.

Defining “Entities”

Each key/value pair in the top-level"properties" object of your JSON schema will be converted into a Vendia “Entity”. Here’s an example of a minimal JSON schema with a single entity called “Product”:

{

"$schema": "http://json-schema.org/draft-07/schema#",

"$id": "http://vendia.com/schemas/sample.schema.json",

"title": "Acme Project Schema",

"description": "Defines data model for Acme's Product Project",

"type": "object",

"properties": {

"Product": {   <------------------------------ This is an entity!

"description": "Acme Product Line",

"type": "array",

"items": {

"type": "object",

"properties": {

"name": {   <--------------------------- This is a field on the Product entity

"type" : "string"

}

}

}

}

}

}

Entities are special in Vendia. Entities receive their own GraphQL APIs for CRUD operations (e.g. addProduct, getProduct, listProductItems) and entities of type "array" can be indexed to enable efficient data retrieval ( More on indexes below).

Only immediate children of your JSON schema’s top-level "properties" object will be converted to Entities. In the example above, the Product entity is an "array" type with items of type "object" - the object definition, in turn, has a "name" field.

Entity fields can contain complex data types (e.g. "object", "array") in addition to scalar data types (e.g. "string", "number", "integer"), but arrays of data nested within entities will not have their own APIs and cannot be indexed. Data models that require large amounts of total storage or very high cardinality for nested arrays should be rewritten to make these appear as top-level Entity arrays in order to expose them to indexing and sharding in the database.

About Scalar Types

Strings, numbers, booleans… We often refer to these fundamental data types as “scalars”. Your project’s data model might be organized into any number of entities and these entities might consist of complex structures of nested arrays and objects, but ultimately you’re going to need scalar types to store meaningful data.

Vendia supports any scalar type that can be defined in JSON schema.

Some notes on the particulars:

Project JSON Schema Example

The following is a sample data model for a product catalog and orders, written as JSON schema

Sample Project Schema

{

"$schema": "http://json-schema.org/draft-07/schema#",

"$id": "http://vendia.com/schemas/sample.schema.json",

"title": "Acme Project Schema",

"description": "Defines data model for Acme's Product Project",

"type": "object",

"properties": {

"ContactInfo": {

"type": "object",

"description": "Global setting that records general purpose contact info for the chain as a whole",

"properties": {

"addressLine1": {

"type": "string"

},

"addressLine2": {

"type": "string"

},

"city": {

"type": "string"

},

"state": {

"type": "string"

},

"zipCode": {

"type": "string"

}

}

},

"Participant": {

"description": "Blockchain participant names",

"type": "array",

"items": {

"type": "string"

},

"minItems": 1,

"uniqueItems": true

},

"Order": {

"description": "3b11 replenishment order",

"type": "array",

"items": {

"type": "object",

"properties": {

"orderId": {

"description": "The unique identifier for an order",

"type": "string"

},

"owner": {

"description": "Role who initially authorized order",

"type": "string"

},

"creationTimestamp": {

"description": "Timestamp when order was initially created",

"type": "string"

},

"orderContent": {

"description": "Product IDs in this order",

"type": "array",

"items": {

"type": "string"

},

"minItems": 1,

"uniqueItems": true

}

},

"required": ["orderId", "owner", "creationTimestamp", "orderContent"]

},

"minItems": 0,

"uniqueItems": true

},

"ShipmentMessage": {

"description": "Update to shipping status of an order",

"type": "array",

"items": {

"type": "object",

"properties": {

"orderId": {

"description": "The unique identifier for all messages related to this shipment",

"type": "string"

},

"carrier": {

"description": "The carrier handling the shipment",

"type": "string"

},

"timestamp": {

"description": "The arrival time",

"type": "string"

},

"fromAddress": {

"description": "Origin for this shipment update",

"type": "object",

"properties": {

"isInitial": {

"type": "boolean"

},

"contact": {

"type": "string"

},

"streetAddress": {

"type": "string"

},

"city": {

"type": "string"

},

"postalCode": {

"type": "string"

},

"country": {

"type": "string"

}

},

"required": ["streetAddress", "city", "country"]

},

"toAddress": {

"description": "Destination for this shipment update",

"type": "object",

"properties": {

"isFinal": {

"type": "boolean"

},

"contact": {

"type": "string"

},

"streetAddress": {

"type": "string"

},

"city": {

"type": "string"

},

"postalCode": {

"type": "string"

},

"country": {

"type": "string"

}

},

"required": ["streetAddress", "city", "country"]

}

},

"required": [

"orderId",

"carrier",

"timestamp",

"fromAddress",

"toAddress"

]

},

"minItems": 0,

"uniqueItems": true

},

"Product": {

"description": "Acme Product Line",

"type": "array",

"items": {

"type": "object",

"properties": {

"productId": {

"description": "The unique identifier for a product",

"type": "integer"

},

"productName": {

"description": "Name of the product",

"type": "string"

},

"price": {

"description": "The price of the product",

"type": "number",

"exclusiveMinimum": 0

},

"tags": {

"description": "Tags for the product",

"type": "array",

"items": {

"type": "string"

},

"minItems": 1,

"uniqueItems": true

},

"dimensions": {

"type": "object",

"properties": {

"length": {

"type": "number"

},

"width": {

"type": "number"

},

"height": {

"type": "number"

}

},

"required": ["length", "width"]

},

"sales": {

"description": "Sales for the product",

"type": "array",

"items": {

"type": "object",

"properties": {

"start": {

"type": "string"

},

"end": {

"type": "string"

},

"discountPercent": {

"type": "number"

}

},

"required": ["start", "end"]

}

}

},

"required": ["productId", "productName", "price"]

},

"minItems": 1,

"uniqueItems": true

}

},

"required": ["ContactInfo", "Participant", "Product"]

}

Vendia will convert the above schema into a GraphQL representation similar to the one below. (Note that this is representative only; details of this translation may vary based on your registration, settings, etc.)

Representative Generated GraphQL Schema

input ModelBooleanInput {

ne: Boolean

eq: Boolean

}

input ModelFloatInput {

ne: Float

eq: Float

le: Float

lt: Float

ge: Float

gt: Float

between: [Float]
}

input ModelIDInput {

ne: ID

eq: ID
}

input ModelIntInput {

ne: Int

eq: Int

le: Int

lt: Int

ge: Int

gt: Int

between: [Int]
}

enum ModelSortDirection {

ASC

DESC
}

input ModelStringInput {

ne: String

eq: String

le: String

lt: String

ge: String

gt: String

contains: String

notContains: String

between: [String]

beginsWith: String
}

type Transaction {

tx_id: String!

tx_version: String!

submission_time: String!

node_owner: String!
}

type Transaction_Result {

error: String

result: Transaction
}

input ContactInfoConditionInput {

addressLine1: ModelStringInput

addressLine2: ModelStringInput

city: ModelStringInput

state: ModelStringInput

zipCode: ModelStringInput

and: [ContactInfoConditionInput]

or: [ContactInfoConditionInput]

not: ContactInfoConditionInput
}

input ContactInfoFilterInput {

addressLine1: ModelStringInput

addressLine2: ModelStringInput

city: ModelStringInput

state: ModelStringInput

zipCode: ModelStringInput

and: [ContactInfoFilterInput]

or: [ContactInfoFilterInput]

not: ContactInfoFilterInput
}

type ContactInfo {

addressLine1: String

addressLine2: String

city: String

state: String

zipCode: String
}

type ContactInfo_Result {

error: String

result: ContactInfo
}

input ContactInfoInput {

addressLine1: String

addressLine2: String

city: String

state: String

zipCode: String
}

// The rest of the GraphQL schema continues here...

## Adding Attributes via Schema Designer

Vendia’s Schema Designer provides an intuitive interface for adding attributes to your data model entities. The “Add Attribute” workflow guides you through creating different types of attributes with various data types and configurations.

### Basic Attribute Creation

When adding a new attribute to an entity, you can specify:

- **Name**: The attribute identifier (e.g., `roomTypeCode`, `roomTypevendor`, `issuePrice`)
- **Description**: Optional documentation for the attribute
- **Data Type**: Choose from various types including:

- Number (whole numbers like 1, 2, 3)
  - Text
  - Number (decimal numbers like 1.1, 2.12, 3.123)

### Attribute Configuration Options

The Schema Designer supports several attribute configuration options:

#### Required Attributes

Mark attributes as required to ensure they must be provided when creating or updating entities. Required attributes help maintain data integrity and ensure critical fields are always populated.

#### Unique Attributes

Set attributes as unique to prevent duplicate values across all instances of an entity. This is useful for identifier fields like product codes or email addresses.

#### Indexed Attributes

Enable indexing on attributes to support efficient querying and filtering in GraphQL operations. Indexed attributes can be used with various filter operators and sorting capabilities.

### Advanced Attribute Types

#### Computed Attributes

Computed attributes allow you to create derived values based on calculations or transformations of other attributes. These attributes are automatically calculated and maintained by the system.

**Configuration Options:**

- **Lookup**: Create references to other entities or lookup tables
- **Calculation**: Perform mathematical operations on existing attributes

#### Lookup Attributes

Lookup attributes create relationships between entities by referencing data from other tables or entities within your **project**.

**Lookup Configuration:**

- **Lookup table name**: Specify the target entity to reference
- **Associated attributes**: Define which attributes to link between entities

#### Calculation Attributes

Calculation attributes perform mathematical operations to derive new values from existing data.

**Calculation Configuration:**

- **Attribute**: Select the source attribute for calculations
- **Operator**: Choose mathematical operations (sum, average, count, etc.)
- **Calculate this attribute**: Define the specific calculation logic

### Best Practices for Attribute Design

1. **Use descriptive names**: Choose clear, meaningful names for your attributes that reflect their purpose
2. **Set appropriate data types**: Select the most specific data type that fits your data to ensure proper validation
3. **Consider indexing**: Add indexes to attributes that will be frequently queried or filtered
4. **Document your attributes**: Use the description field to explain the purpose and expected values
5. **Plan for relationships**: Use lookup attributes to create meaningful connections between entities

### Data Type Considerations

When selecting data types for your attributes:

- **Text**: Use for string values, names, descriptions, and non-numeric identifiers
- **Number (whole)**: Use for integers, counts, and whole number values
- **Number (decimal)**: Use for prices, measurements, percentages, and precise calculations
- **Boolean**: Use for true/false flags and binary states
- **Date/Time**: Use for timestamps, dates, and time-based values

## Runtime Validation of GraphQL Mutations

Your **project**’s JSON schema can be used to express data restrictions that extend beyond basic types. Strings, for example, support constraints such as:

- “minLength”
- “maxLength”
- “pattern” (regular expressions)
- “format” ( [supports predefined values such as “date-time”](https://json-schema.org/understanding-json-schema/reference/string.html#built-in-formats))

**These constraints will be used to validate incoming GraphQL mutations and violations will return descriptive error messages.**

For example, here is a sample of JSON schema defining a `"Shipment"` entity with a `"created"` field that must adhere to a `"date-time"` format”:

"Shipment": {

"description": "Shipment information",

"type": "array",

"items": {

"type": "object",

"properties": {

"created": {

"type": "string",

"format": "date-time" <---- Values for this field must adhere to "date-time" format!

},


The following GraphQL mutation attempts to add a new shipment object with a malformed value for the `"created"` field:

mutation addShipment {

add_Shipment(input: { created: "yesterday" }) {

result {

_id

created

}

}

}


The following GraphQL error will be returned describing the violation:

'yesterday' is not a 'date-time'

Failed validating 'format' in schema['properties']['Shipment']['items']['properties']['created']:

{'format': 'date-time',

'type': 'string'}

On instance['Shipment'][0]['created']:

'yesterday'


[You can learn more about the string constraints mentioned above here](https://json-schema.org/understanding-json-schema/reference/string.html) \- Vendia currently supports all constraints available in **JSON schema draft 7**.

## Indexes

Indexes can be defined in the JSON schema to support efficient queries on arbitrary attributes via the GraphQL `filter` argument. Indexes can also be used to sort the results of list queries via the GraphQL `order` argument.

Indexes can only be added to entities of type `"array"` and are restricted to top-level scalar fields (e.g. “string”, “number”, “integer”) on these entities.

The top-level directive `"x-vendia-indexes"` is used to define indexes on attributes. For example, to support an efficient list query of Orders by owner, an index can be defined in the schema referencing the `owner` property of the existing `Order` type.

{

"$schema": "http://json-schema.org/draft-07/schema#",

"$id": "http://vendia.com/schemas/sample.schema.json",

"title": "Acme Project Schema",

"description": "Defines data model for Acme's Product Project",

"x-vendia-indexes": {

"OrderOwnerIndex": [

{

"type": "Order",

"property": "owner"

}

]

}

...

}


### Filtering on an Indexed Field

Using the index defined above we can now list Orders, filtering on the `owner` property, and make use of our new index:

list_OrderItems(filter: {owner: {eq: "bob@acme.com"}}) {

nextToken

_OrderItems {

_id

owner

orderContent

}

}


Indexes can be used with the following GraphQL filter operators:

- `eq`
- `gt`
- `lt`
- `le`
- `ge`
- `between`
- `beginsWith`

The remaining filter operators can be used, but your list query will no longer take advantage of the index.

### Sorting List Results by an Indexed Field

In addition to filtering, list query results can now be sorted by the `owner` property in ascending or descending order:

list_OrderItems(order: { owner: DESC }) {

nextToken

_OrderItems {

_id

owner

orderContent

}

}


Note that the `filter` and `order` arguments can be used in the same query with the following restrictions:

1. Only one index can be used per query - if `order` and `filter` make use of two different indexes, the index used for `order` will take precedence.
2. If `order` and `filter` are both used with the _same_ index, then `filter` _must_ be restricted to the [supported operators listed above](https://docs.vendia.com/platform/operational/modeling/#filtering-on-an-indexed-field).

## Vendia-Specific JSON Schema Restrictions

Vendia endeavors to support the widest possible range of data models allowable in standard JSON schema. That said, translating JSON schema into strongly-typed GraphQL APIs requires us to enforce some minor restrictions as GraphQL itself is simply more strict about what can and cannot be supported.

While you may never bump into the following restrictions, they are listed here for clarity and transparency. If your JSON schema happens to violate any of the following rules, you will receive an error message explaining the problem and can update your schema accordingly.

1. “Empty” objects aren’t allowed in GraphQL schema. All `object` types must have `"properties`. In turn, `properties` must contain at least one property definition.
2. Similarly, all `array` types must contain an `items` definition.
3. Any fields marked as required via the `required` array property _must_ be defined on the corresponding `object` definition.
4. JSON schema uses `additionalProperties` to determine whether an `object` type can include additional properties not defined explicitly in your JSON schema. The value of this property will _always_ be set to `false` implicitly by Vendia. GraphQL is strongly-typed and does not allow storing/retrieving arbitrary additional fields on objects.
5. JSON schema “combining” functionality (e.g. `allOf`, `anyOf`, `oneOf`, `not`) is not supported at this time.
6. JSON schema definitions must use the `definitions` keyword rather than `$defs` (consistent with JSON schema draft 7).

## Limits

A **project** is limited to a total of **12 indexes**. Indexes can be defined at **project** registration time or added later via schema evolution. Only one index change is allowed per schema evolution.