A data model is made up of a set of entities. Such entities in Typetta are defined in GraphQL language, following the principles and syntax included in the official specification on graphql.org.
The basic definition of an entity is therefore that of a GraphQL type with a list of fields:
type User {
id: ID!
firstName: String
lastName: String
}
Note that, according to GraphQL syntax, each field can be annotated as required or optional by adding or not a !
operator after the type name.
Without further specification, an entity such as the previous one will only produce the related TypeScript type. No data access components will be generated as it does not appear to be an entity directly stored on a data source.
For an entity to be connected to its data structure on a data source and to allow the related CRUD operations, it must be explicitly annotated with GraphQL directives. There are currently two database drivers available in Typetta, one for SQL databases and one for MongoDB, and two related directives @sql
and @mongodb
. Each stored entity must also be annotated with @entity
.
Once one of these annotations has been added, the entity is coupled to its data structure: to the table on SQL or to the collection on MongoDB.
To specify to the system that an entity is linked to a MongoDB collection, it must be defined as follows:
type User @entity @mongodb {
id: ID!
firstName: String
lastName: String
}
@mongodb
directive also accepts two optional params:
source
: represents the MongoDB database where the collection this entity is linked to resides. It is a logical label whose configuration will be set on the EntityManager. The default is default
.
collection
: represents the MongoDB collection name; the default is the pluralised camel case entity name (so in the example above it would be users
).
Here is a complete example:
type User @entity @mongodb(source: "secondary-database", collection: "_users") {
id: ID!
firstName: String
lastName: String
}
To specify to the system that an entity is linked to a SQL table, it must be defined as follows:
type User @entity @sql {
id: ID!
firstName: String
lastName: String
}
@sql
directive also accepts two optional params:
source
: represents the SQL database where the table this entity is linked to resides. It is a logical label whose configuration will be set on the EntityManager. The default is default
.
table
: represents the SQL table name; the default is the pluralised camel case entity name (so, in the example above, it would be users
).
Here is a complete example:
type User @entity @sql(source: "secondary-database", table: "_users") {
id: ID!
firstName: String
lastName: String
}
Each stored entity needs a unique identifier. Any entity field can be annotated as @id
as long as it is of a scalar type and not another entity type. There is no correlation between the scalar GraphQL ID and the entity identifier.
To define the entity identifier you need to add the @id
directive as in the following example:
type User @entity @mongodb {
id: ID! @id
firstName: String
lastName: String
}
This directive also receives an optional from
parameter, which can take the following values:
db
: when the id is auto-generated by the DB, be it a SQL auto-increment integer or a MongoDB ObjectID, or whatever the database supports.type User @entity @mongodb {
id: String! @id(from: "db") @alias(value: "_id")
name: String!
}
user
: when the id is manually generated by the user, it will therefore be a mandatory field of every insert operation.
generator
: when the id is auto-generated by Typetta with configurable logic at EntityManager or single DAO level. An ID generator can be specified for each scalar and it will then be invoked for all fields of that specific scalar annotated with the @id
directive.
A simple example of generator
policy can be the following where all entity identifiers are of the ID type and must be managed as UUIDs auto-generated by the system. To achieve this, the EntityManager can then be configured as follows:
import { v4 as uuidv4 } from 'uuid'
const entityManager = new EntityManager({
scalars: {
ID: {
generator: () => uuidv4()
}
}
});
If you want different behaviour for a single DAO, you can create an override like the following:
import { v4 as uuidv4 } from 'uuid'
const entityManager = new EntityManager({
scalars: {
ID: {
generator: () => uuidv4()
}
}
overrides: {
user: {
scalars: {
ID: {
generator: () => 'user_' + uuidv4()
}
}
}
}
});
The GraphQL specification allows you to define enumerations with the following syntax:
enum UserType {
ADMINISTRATOR
CUSTOMER
}
An enumeration can be used just like a scalar to define the fields of an entity, as in the following example:
type User {
id: ID!
firstName: String
lastName: String
type: UserType!
}
Typetta supports enumerations both at TypeScript and database level. They are serialised to string data type by default on both SQL and MongoDB.
An embedded entity is an entity not directly stored in a dedicated SQL table or MongoDB collection, but only embedded within another entity. Embedded entities are a typical concept of document databases, which, however, can also be partially supported by SQL databases, as described below.
In Typetta, each entity can have one or more fields which are not of scalar type with reference to embedded entities. These referred entities cannot be annotated as @sql
or @mongodb
. Here is a simple example:
type Address {
street: String
city: String
district: String
zipcode: String
country: String
}
type User @entity @mongodb {
id: ID! @id
firstName: String
lastName: String
address: Address
}
Typetta offers the most advanced support for embedded entities on MongoDB, which are translated into embedded documents, thus giving the possibility to select, filter and sort their fields. On SQL databases, these entities are instead flattened on multiple columns of the root table, also offering the possibility to select, filter and sort in this instance. However, embedded entity arrays are not supported on SQL due to database limits.
Typetta offers the possibility to define default values for scalar fields of your entities using the @default
directive:
type User @entity @mongodb {
id: ID! @id
firstName: String
lastName: String
creationDate: Date @default(from: "generator")
live: Boolean @default(from: "middleware")
}
There are two options you can choose from to define the default value:
@id
directive, if a @default(from="generator")
is specified, the value is generated according to the scalar by a function that must be configured when creating the EntityManager
.const entityManager = new EntityManager({
scalars: {
Date: {
generator: () => new Date()
}
}
});
@default(from="middleware")
is specified instead, the value is generated by a middleware that is supposed to be configured on the EntityManager
.const entityManager = new EntityManager({
overrides: {
user: {
middlewares: [
defaultValueMiddleware('live', () => true),
]
}
}
});
By using a middleware you can create a default value that depends on EntityManager
metadata, where you can store, for example, authorisation information about the caller.
Each field of a stored entity has a direct correspondence with the linked SQL column or the linked key of the MongoDB document and this correspondence is given by the name of the field itself. If you want to decouple the name of the entity of the data model from the database structure, you can use the @alias
directive as follows:
type User @entity @mongodb {
id: ID! @id
firstName: String @alias(value: "name")
lastName: String @alias(value: "surname")
address: Address
}
In the above example, the generated TypeScript type will have the fields firstName: string
and lastName: string
, while the MongoDB documents will have two keys: name
and surname
.
You can define fields in the data model that have no correspondence in the SQL table or in the MongoDB collection. These fields are then reflected in the TypeScript data type but are neither serialised nor deserialised in the database.
To define an excluded field you can use the @exclude
directive as follows:
type User @entity @mongodb {
id: ID! @id
firstName: String
lastName: String
excludedField: String @exclude
}
Typetta generates a schema object for each entity of the defined data model. That schema can be sometimes useful to build powerful middlewares that need a reflective view on the data model. Given the following entity definition:
type User @entity @mongodb {
id: ID! @id
firstName: String
lastName: String
dateOfBirth: Date!
}
Typetta automatically generates this schema:
{
id: {
scalar: 'ID',
required: true
},
firstName: {
scalar: 'String',
},
lastName: {
scalar: 'String',
},
dateOfBirth: {
scalar: 'Date',
required: true
}
}
Schema generation can be extended with any directive out of Typetta domains. Every unknown directives is added to the schema under directives
field. This is particularly useful to implement middlewares with different behaviours based on domain model concepts.
Following an example of a custom metadata on a dateOfBirth
field:
type User @entity @mongodb {
id: ID! @id
firstName: String
lastName: String
dateOfBirth: Date! @addDays(value: 1)
}
And the schema resulting from it:
{
id: {
scalar: 'ID',
required: true
},
firstName: {
scalar: 'String',
},
lastName: {
scalar: 'String',
},
dateOfBirth: {
scalar: 'Date',
required: true,
directives: {
addDays: { value: 1 }
}
}
}
Now you can easily implement a middleware that, reading that schema, increment all the fields containing the addDays
metadata by n
days before insertion.
Here’s another example of a field with a custom directive that has more than one key.
type SomeType @entity @mongodb {
id: ID! @id(from: "db") @alias(value: "_id")
someField: Date @metadata(value: true, values: [ "one", 2, "three", false ], obj: { f: 1.1 }) @addDays(value: 2)
}
And the schema resulting from it:
{
id: {
type: 'scalar',
scalar: 'ID',
isId: true,
generationStrategy: 'db',
required: true,
alias: '_id',
},
someField: {
type: 'scalar',
scalar: 'Date',
directives: {
metadata: {
value: true,
values: ['one', 2, 'three', false],
obj: { f: 1.1 }
},
addDays: { value: 2 }
},
},
}