loopback-datasource-juggler/docs/definition-language.md

489 lines
16 KiB
Markdown
Raw Permalink Normal View History

2013-08-27 18:07:13 +00:00
# LoopBack Definition Language Guide
2013-08-16 17:53:33 +00:00
LoopBack Definition Language (LDL) is simple DSL to define data models in
JavaScript or plain JSON. With LoopBack, we often start with a model definition
which describes the structure and types of data. The model establishes common
knowledge of data in LoopBack.
2013-08-16 17:53:33 +00:00
2013-08-19 21:00:33 +00:00
## Describing a simple model
2013-08-16 17:53:33 +00:00
Let's start with a simple example in plain JSON.
{
"id": "number",
"firstName": "string",
"lastName": "string"
}
The model simply defines a `user` model that consists of three properties:
* id - The user id. It's a number.
* firstName - The first name. It's a string.
* lastName - The last name. It's a string.
Each key in the JSON object defines a property in our model which will be cast
to its associated type. The simplest form of a property definition is
`propertyName: type`. The key is the name of the property and the value is the
type of the property. We'll cover more advanced form later in this guide.
2013-08-16 17:53:33 +00:00
LDL supports a list of built-in types, including the basic types from JSON:
* String
* Number
* Boolean
* Array
* Object
**Note**: The type name is case-insensitive, i.e., either "Number" or "number"
can be used.
2013-08-16 17:53:33 +00:00
The same model can also be described in JavaScript code:
var UserDefinition = {
id: Number,
firstName: String,
lastName: String
}
As we can see, the JavaScript version is less verbose as it doesn't require
quotes for property names. The types are described using JavaScript constructors,
for example, `Number` for `"Number"`. String literals are also supported.
2013-08-16 17:53:33 +00:00
Now we have the definition of a model, how do we use it in LoopBack Node.js
code? It's easy, LoopBack will build a JavaScript constructor (or class) for you.
2013-08-16 17:53:33 +00:00
## Creating a model constructor
LDL compiles the model definition into a JavaScript constructor using
`ModelBuilder.define` APIs. ModelBuilder is the basic factory to create model
constructors.
2013-08-16 17:53:33 +00:00
ModelBuilder.define() method takes the following arguments:
- name: The model name
- properties: An object of property definitions
- options: An object of options, optional
2013-08-16 19:13:25 +00:00
Here is an example,
2013-08-16 17:53:33 +00:00
var ModelBuilder = require('loopback-datasource-juggler').ModelBuilder;
// Create an instance of the ModelBuilder
var modelBuilder = new ModelBuilder();
// Describe the user model
var UserDefinition = {
id: Number,
firstName: String,
lastName: String
}
// Compile the user model definition into a JavaScript constructor
var User = modelBuilder.define('User', UserDefinition);
// Create a new instance of User
var user = new User({id: 1, firstName: 'John', lastName: 'Smith'});
console.log(user.id); // 1
console.log(user.firstName); // 'John'
console.log(user.lastName); // 'Smith'
2013-08-16 19:13:25 +00:00
2013-08-16 17:53:33 +00:00
That's it. Now you have a User constructor representing the user model.
At this point, the constructor only has a set of accessors to model properties.
No behaviors have been introduced yet.
2013-08-16 17:53:33 +00:00
2013-08-19 21:00:33 +00:00
## Adding logic to a model
Models describe the shape of data. To leverage the data, we'll add logic to the
model for various purposes, such as:
2013-08-19 21:00:33 +00:00
- Interact with the data store for CRUD
- Add behavior around a model instance
- Add service operations using the model as the context
2013-08-16 17:53:33 +00:00
There are a few ways to add methods to a model constructor:
2013-08-23 18:46:56 +00:00
### Create the model constructor from a data source
2013-08-19 21:00:33 +00:00
A LoopBack data source injects methods on the model.
2013-08-16 17:53:33 +00:00
2013-08-19 21:00:33 +00:00
var DataSource = require('loopback-datasource-juggler').DataSource;
var ds = new DataSource('memory');
2013-08-16 17:53:33 +00:00
2013-08-19 21:00:33 +00:00
// Compile the user model definition into a JavaScript constructor
var User = ds.define('User', UserDefinition);
2013-08-16 17:53:33 +00:00
2013-08-19 21:00:33 +00:00
// Create a new instance of User
User.create({id: 1, firstName: 'John', lastName: 'Smith'}, function(err, user) {
console.log(user); // The newly created user instance
User.findById(1, function(err, user) {
console.log(user); // The user instance for id 1
user.firstName = 'John1'; // Change the property
user.save(function(err, user) {
console.log(user); // The modified user instance for id 1
});
};
});
2013-08-16 17:53:33 +00:00
2013-08-23 18:46:56 +00:00
### Attach the model to a data source
2013-08-16 17:53:33 +00:00
A plain model constructor created from `ModelBuilder` can be attached a `DataSource`.
2013-08-19 21:00:33 +00:00
var DataSource = require('loopback-datasource-juggler').DataSource;
var ds = new DataSource('memory');
2013-08-16 17:53:33 +00:00
2013-08-19 21:00:33 +00:00
User.attachTo(ds); // The CRUD methods will be mixed into the User constructor
2013-08-16 17:53:33 +00:00
2013-08-23 18:46:56 +00:00
### Manually add methods to the model constructor
Static methods can be added by declaring a function as a member of the model
constructor. Within a class method, other class methods can be called using the
model as usual.
2013-08-16 17:53:33 +00:00
2013-08-19 21:00:33 +00:00
2013-08-16 17:53:33 +00:00
// Define a static method
2013-08-20 18:58:04 +00:00
User.findByLastName = function(lastName, cb) {
User.find({where: {lastName: lastName}, cb);
2013-08-16 17:53:33 +00:00
};
2013-08-20 18:58:04 +00:00
User.findByLastName('Smith', function(err, users) {
console.log(users); // Print an array of user instances
});
Instance methods can be added to the prototype. Within instance methods, the
model instance itself can be referenced with this keyword.
2013-08-20 18:58:04 +00:00
2013-08-16 17:53:33 +00:00
// Define a prototype method
User.prototype.getFullName = function () {
return this.firstName + ' ' + this.lastName;
};
var user = new User({id: 1, firstName: 'John', lastName: 'Smith'});
console.log(user.getFullName()); // 'John Smith'
2013-08-19 21:00:33 +00:00
## Exploring advanced LDL features
As we mentioned before, a complete model definition is an object with three
properties:
2013-08-19 21:00:33 +00:00
- name: The model name
- options: An object of options, optional
- properties: An object of property definitions
### Model level options
There are a set of options to control the model definition.
- strict:
2013-09-16 17:59:12 +00:00
- true: Only properties defined in the model are accepted. Use this
mode if you want to make sure only predefined properties are accepted. Relational databases only support this setting.
2013-09-16 17:59:12 +00:00
- false: The model will be an open model. All properties are accepted,
including the ones that not predefined with the model. This mode is useful
if the mobile application just wants to store free form JSON data to
a schema-less database such as MongoDB. For relational databases, the value will be converted back to true.
2013-09-16 17:59:12 +00:00
- undefined: Default to false unless the data source is backed by a
relational database such as Oracle or MySQL.
2013-08-16 17:53:33 +00:00
2013-08-19 21:00:33 +00:00
- idInjection:
- true: An `id` property will be added to the model automatically
- false: No `id` property will be added to the model
2013-10-24 16:00:11 +00:00
- plural: The plural form of the model name. If not present, it will be derived from the model name following English
conventions.
2013-08-19 21:00:33 +00:00
- Data source specific mappings
The model can be decorated with connector-specific options to customize the
mapping between the model and the connector. For example, we can define the
corresponding schema/table names for Oracle as follows:
2013-08-19 21:00:33 +00:00
{
"name": "Location",
"options": {
"idInjection": false,
"oracle": {
"schema": "BLACKPOOL",
"table": "LOCATION"
}
},
...
}
### Property definitions
A model consists of a list of properties. The basic example use
`propertyName: type` to describe a property.
2013-08-16 17:53:33 +00:00
Properties can have options in addition to the type. LDL uses a JSON object to
describe such properties, for example:
2013-08-16 17:53:33 +00:00
"id": {"type": "number", "id": true, "doc": "User ID"}
"firstName": {"type": "string", "required": true, "oracle": {"column": "FIRST_NAME", "type": "VARCHAR(32)"}}
2013-08-20 18:58:04 +00:00
**Note** `"id": "number"` is a short form of `"id": {"type": "number"}`.
2013-08-16 17:53:33 +00:00
2013-08-19 21:00:33 +00:00
#### Data types
2013-08-20 18:58:04 +00:00
LDL supports the following data types.
2013-08-16 19:13:25 +00:00
2013-08-16 17:53:33 +00:00
- String/Text
- Number
- Date
- Boolean
- Buffer/Binary
- Array
- Any/Object/JSON
- GeoPoint
2013-08-19 21:00:33 +00:00
##### Array types
2013-08-16 19:13:25 +00:00
LDL supports array types as follows:
2013-08-19 21:00:33 +00:00
- `{emails: [String]}`
- `{"emails": ["String"]}`
- `{emails: [{type: String, length: 64}]}`
2013-08-16 19:13:25 +00:00
2013-08-19 21:00:33 +00:00
##### Object types
A model often has properties that consist of other properties. For example, the
user model can have an `address` property
2013-08-16 19:13:25 +00:00
that in turn has properties such as `street`, `city`, `state`, and `zipCode`.
LDL allows inline declaration of such properties, for example,
var UserModel = {
firstName: String,
lastName: String,
address: {
street: String,
city: String,
state: String,
zipCode: String
},
...
}
The value of the address is the definition of the `address` type, which can be
also considered as an anonymous model.
2013-08-16 19:13:25 +00:00
If you intend to reuse the address model, we can define it independently and
reference it in the user model. For example,
2013-08-16 19:13:25 +00:00
var AddressModel = {
street: String,
city: String,
state: String,
zipCode: String
};
var Address = ds.define('Address', AddressModel);
var UserModel = {
firstName: String,
lastName: String,
address: 'Address', // or address: Address
...
}
var User = ds.define('User', UserModel);
**Note**: The user model has to reference the Address constructor or the model
name - `'Address'`.
2013-08-16 19:13:25 +00:00
#### ID(s) for a model
A model representing data to be persisted in a database usually has one or more
properties as an id to uniquely identify the model instance. For example, the
`user` model can have user ids.
2013-08-16 19:13:25 +00:00
By default, if no id properties are defined and the `idInjection` of the model
options is false, LDL will automatically add an id property to the model as follows:
2013-08-20 18:58:04 +00:00
id: {type: Number, generated: true, id: true}
To explicitly specify a property as `id`, LDL provides an `id` property for the
option. The value can be true, false, or a number.
2013-08-20 18:58:04 +00:00
- true: It's an id
- false or any falsey values: It's not an id (default)
- a positive number, such as 1 or 2: It's the index of the composite id
2013-08-16 17:53:33 +00:00
LDL supports the definition of a composite id that has more than one properties.
For example,
2013-08-16 17:53:33 +00:00
var InventoryDefinition =
{
productId: {type: String, id: 1},
locationId: {type: String, id: 2},
qty: Number
}
The composite id is (productId, locationId) for an inventory model.
2013-09-16 17:59:12 +00:00
**Note: Composite ids are NOT supported as query parameters in REST APIs yet.**
2013-08-16 19:13:25 +00:00
#### Property documentation
2013-08-16 17:53:33 +00:00
* doc: Documentation of the property
2013-08-16 19:13:25 +00:00
#### Constraints
2013-08-20 18:58:04 +00:00
Constraints are modeled as options too, for example:
2013-08-16 17:53:33 +00:00
2013-08-20 18:58:04 +00:00
* default: The default value of the property
2013-08-16 17:53:33 +00:00
* required: Indicate if the property is required
* pattern: A regular expression pattern that a string should match
* min/max: The minimal and maximal value
* length: The maximal length of a string
2013-08-16 19:13:25 +00:00
#### Conversion and formatting
2013-08-16 17:53:33 +00:00
Format conversions can also be declared as options, for example:
* trim: Trim the string
* lowercase: Convert the string to be lowercase
* uppercase: Convert the string to be uppercase
* format: Format a Date
2013-08-16 19:13:25 +00:00
#### Mapping
Data source specific mappings can be added to the property options, for example,
to map a property to be a column in Oracle database table, you can use the
following syntax:
2013-08-16 17:53:33 +00:00
"oracle": {"column": "FIRST_NAME", "type": "VARCHAR", "length": 32}
### Relations between models
#### belongsTo
A `belongsTo` relation sets up a one-to-one connection with another model, such
that each instance of the declaring model "belongs to" one instance of the other
model. For example, if your application includes customers and orders, and each order
can be placed by exactly one customer.
![belongsTo](belongs-to.png "belongsTo")
var Order = ds.createModel('Order', {
customerId: Number,
orderDate: Date
});
var Customer = ds.createModel('Customer', {
name: String
});
Order.belongsTo(Customer);
The code above basically says Order has a reference called `customer` to User using
the `customerId` property of Order as the foreign key. Now we can access the customer
in one of the following styles:
order.customer(callback); // Get the customer for the order
order.customer(); // Get the customer for the order synchronously
order.customer(customer); // Set the customer for the order
2013-08-20 18:58:04 +00:00
#### hasMany
A `hasMany` relation builds a one-to-many connection with another model. You'll
often find this relation on the "other side" of a `belongsTo` relation. This
relation indicates that each instance of the model has zero or more instances
of another model. For example, in an application containing customers and orders, a
customer has zero or more orders.
2013-08-20 18:58:04 +00:00
![hasMany](has-many.png "hasMany")
2013-08-20 18:58:04 +00:00
var Order = ds.createModel('Order', {
customerId: Number,
orderDate: Date
});
2013-08-20 18:58:04 +00:00
var Customer = ds.createModel('Customer', {
name: String
});
2013-08-20 18:58:04 +00:00
Customer.hasMany(Order, {as: 'orders', foreignKey: 'customerId'});
2013-08-20 18:58:04 +00:00
Scope methods created on the base model by hasMany allows to build, create and
query instances of other class. For example,
2013-08-20 18:58:04 +00:00
customer.orders(filter, callback); // Find orders for the customer
customer.orders.build(data); // Build a new order
customer.orders.create(data, callback); // Create a new order for the customer
customer.orders.destroyAll(callback); // Remove all orders for the customer
customer.orders.findById(orderId, callback); // Find an order by id
customer.orders.destroy(orderId, callback); // Delete and order by id
2013-08-20 18:58:04 +00:00
#### hasMany through
2013-08-20 18:58:04 +00:00
A `hasMany through` relation is often used to set up a many-to-many connection with another model. This relation
indicates that the declaring model can be matched with zero or more instances of another model by proceeding through
a third model. For example, consider a medical practice where patients make appointments to see physicians. The
relevant association declarations could look like this:
2013-08-16 17:53:33 +00:00
![hasManyThrough](has-many-through.png "hasManyThrough")
2013-08-16 17:53:33 +00:00
var Physician = ds.createModel('Physician', {name: String});
var Patient = ds.createModel('Patient', {name: String});
var Appointment = ds.createModel('Appointment', {
physicianId: Number,
patientId: Number,
appointmentDate: Date
});
Appointment.belongsTo(Patient);
Appointment.belongsTo(Physician);
2013-08-20 18:58:04 +00:00
Physician.hasMany(Patient, {through: Appointment});
Patient.hasMany(Physician, {through: Appointment});
Now the Physician model has a virtual property called `patients`:
physician.patients(filter, callback); // Find patients for the physician
physician.patients.build(data); // Build a new patient
physician.patients.create(data, callback); // Create a new patient for the physician
physician.patients.destroyAll(callback); // Remove all patients for the physician
physician.patients.add(patient, callback); // Add an patient to the physician
physician.patients.remove(patient, callback); // Remove an patient from the physician
physician.patients.findById(patientId, callback); // Find an patient by id
2013-08-20 18:58:04 +00:00
#### hasAndBelongsToMany
A `hasAndBelongsToMany` relation creates a direct many-to-many connection with
another model, with no intervening model. For example, if your application
includes users and groups, with each group having many users and each user
appearing in many groups, you could declare the models this way,
2013-08-20 18:58:04 +00:00
![hasAndBelongsToMany](has-and-belongs-to-many.png "hasAndBelongsToMany")
2013-08-20 18:58:04 +00:00
User.hasAndBelongsToMany('groups', {model: Group, foreignKey: 'groupId'});
user.groups(callback); // get groups of the user
user.groups.create(data, callback); // create a new group and connect it with the user
user.groups.add(group, callback); // connect an existing group with the user
user.groups.remove(group, callback); // remove the user from the group
2013-08-16 17:53:33 +00:00
### Extend from a base model
LDL allows a new model to extend from an existing model. For example, Customer
can extend from User as follows. The Customer model will inherit properties and
methods from the User model.
2013-08-16 17:53:33 +00:00
2013-08-20 18:58:04 +00:00
var Customer = User.extend('customer', {
2013-08-23 18:46:56 +00:00
accountId: String,
vip: Boolean
2013-08-20 18:58:04 +00:00
});
### Mix in model definitions
Some models share the common set of properties and logic around. LDL allows a
model to mix in one or more other models. For example,
2013-08-16 17:53:33 +00:00
2013-08-23 18:46:56 +00:00
var TimeStamp = modelBuilder.define('TimeStamp', {created: Date, modified: Date});
2013-08-20 18:58:04 +00:00
var Group = modelBuilder.define('Group', {groups: [String]});
2013-09-16 17:59:12 +00:00
User.mixin(Group, TimeStamp);