1 | # LoopBack Definition Language Guide
|
2 |
|
3 | LoopBack Definition Language (LDL) is simple DSL to define data models in
|
4 | JavaScript or plain JSON. With LoopBack, we often start with a model definition
|
5 | which describes the structure and types of data. The model establishes common
|
6 | knowledge of data in LoopBack.
|
7 |
|
8 | ## Describing a simple model
|
9 |
|
10 | Let's start with a simple example in plain JSON.
|
11 |
|
12 | {
|
13 | "id": "number",
|
14 | "firstName": "string",
|
15 | "lastName": "string"
|
16 | }
|
17 |
|
18 | The model simply defines a `user` model that consists of three properties:
|
19 |
|
20 | * id - The user id. It's a number.
|
21 | * firstName - The first name. It's a string.
|
22 | * lastName - The last name. It's a string.
|
23 |
|
24 | Each key in the JSON object defines a property in our model which will be cast
|
25 | to its associated type. The simplest form of a property definition is
|
26 | `propertyName: type`. The key is the name of the property and the value is the
|
27 | type of the property. We'll cover more advanced form later in this guide.
|
28 |
|
29 | LDL supports a list of built-in types, including the basic types from JSON:
|
30 |
|
31 | * String
|
32 | * Number
|
33 | * Boolean
|
34 | * Array
|
35 | * Object
|
36 |
|
37 | **Note**: The type name is case-insensitive, i.e., either "Number" or "number"
|
38 | can be used.
|
39 |
|
40 | The same model can also be described in JavaScript code:
|
41 |
|
42 | var UserDefinition = {
|
43 | id: Number,
|
44 | firstName: String,
|
45 | lastName: String
|
46 | }
|
47 |
|
48 | As we can see, the JavaScript version is less verbose as it doesn't require
|
49 | quotes for property names. The types are described using JavaScript constructors,
|
50 | for example, `Number` for `"Number"`. String literals are also supported.
|
51 |
|
52 | Now we have the definition of a model, how do we use it in LoopBack Node.js
|
53 | code? It's easy, LoopBack will build a JavaScript constructor (or class) for you.
|
54 |
|
55 | ## Creating a model constructor
|
56 |
|
57 | LDL compiles the model definition into a JavaScript constructor using
|
58 | `ModelBuilder.define` APIs. ModelBuilder is the basic factory to create model
|
59 | constructors.
|
60 |
|
61 | ModelBuilder.define() method takes the following arguments:
|
62 |
|
63 | - name: The model name
|
64 | - properties: An object of property definitions
|
65 | - options: An object of options, optional
|
66 |
|
67 | Here is an example,
|
68 |
|
69 | var ModelBuilder = require('loopback-datasource-juggler').ModelBuilder;
|
70 |
|
71 | // Create an instance of the ModelBuilder
|
72 | var modelBuilder = new ModelBuilder();
|
73 |
|
74 | // Describe the user model
|
75 | var UserDefinition = {
|
76 | id: Number,
|
77 | firstName: String,
|
78 | lastName: String
|
79 | }
|
80 |
|
81 | // Compile the user model definition into a JavaScript constructor
|
82 | var User = modelBuilder.define('User', UserDefinition);
|
83 |
|
84 | // Create a new instance of User
|
85 | var user = new User({id: 1, firstName: 'John', lastName: 'Smith'});
|
86 |
|
87 | console.log(user.id); // 1
|
88 | console.log(user.firstName); // 'John'
|
89 | console.log(user.lastName); // 'Smith'
|
90 |
|
91 |
|
92 | That's it. Now you have a User constructor representing the user model.
|
93 |
|
94 | At this point, the constructor only has a set of accessors to model properties.
|
95 | No behaviors have been introduced yet.
|
96 |
|
97 | ## Adding logic to a model
|
98 |
|
99 | Models describe the shape of data. To leverage the data, we'll add logic to the
|
100 | model for various purposes, such as:
|
101 |
|
102 | - Interact with the data store for CRUD
|
103 | - Add behavior around a model instance
|
104 | - Add service operations using the model as the context
|
105 |
|
106 | There are a few ways to add methods to a model constructor:
|
107 |
|
108 | ### Create the model constructor from a data source
|
109 | A LoopBack data source injects methods on the model.
|
110 |
|
111 |
|
112 | var DataSource = require('loopback-datasource-juggler').DataSource;
|
113 | var ds = new DataSource('memory');
|
114 |
|
115 | // Compile the user model definition into a JavaScript constructor
|
116 | var User = ds.define('User', UserDefinition);
|
117 |
|
118 | // Create a new instance of User
|
119 | User.create({id: 1, firstName: 'John', lastName: 'Smith'}, function(err, user) {
|
120 | console.log(user); // The newly created user instance
|
121 | User.findById(1, function(err, user) {
|
122 | console.log(user); // The user instance for id 1
|
123 | user.firstName = 'John1'; // Change the property
|
124 | user.save(function(err, user) {
|
125 | console.log(user); // The modified user instance for id 1
|
126 | });
|
127 | };
|
128 | });
|
129 |
|
130 |
|
131 | ### Attach the model to a data source
|
132 | A plain model constructor created from `ModelBuilder` can be attached a `DataSource`.
|
133 |
|
134 |
|
135 | var DataSource = require('loopback-datasource-juggler').DataSource;
|
136 | var ds = new DataSource('memory');
|
137 |
|
138 | User.attachTo(ds); // The CRUD methods will be mixed into the User constructor
|
139 |
|
140 | ### Manually add methods to the model constructor
|
141 | Static methods can be added by declaring a function as a member of the model
|
142 | constructor. Within a class method, other class methods can be called using the
|
143 | model as usual.
|
144 |
|
145 |
|
146 | // Define a static method
|
147 | User.findByLastName = function(lastName, cb) {
|
148 | User.find({where: {lastName: lastName}, cb);
|
149 | };
|
150 |
|
151 | User.findByLastName('Smith', function(err, users) {
|
152 | console.log(users); // Print an array of user instances
|
153 | });
|
154 |
|
155 | Instance methods can be added to the prototype. Within instance methods, the
|
156 | model instance itself can be referenced with this keyword.
|
157 |
|
158 | // Define a prototype method
|
159 | User.prototype.getFullName = function () {
|
160 | return this.firstName + ' ' + this.lastName;
|
161 | };
|
162 |
|
163 | var user = new User({id: 1, firstName: 'John', lastName: 'Smith'});
|
164 | console.log(user.getFullName()); // 'John Smith'
|
165 |
|
166 |
|
167 | ## Exploring advanced LDL features
|
168 |
|
169 | As we mentioned before, a complete model definition is an object with three
|
170 | properties:
|
171 |
|
172 | - name: The model name
|
173 | - options: An object of options, optional
|
174 | - properties: An object of property definitions
|
175 |
|
176 | ### Model level options
|
177 | There are a set of options to control the model definition.
|
178 |
|
179 | - strict:
|
180 | - true: Only properties defined in the model are accepted. Use this
|
181 | mode if you want to make sure only predefined properties are accepted. Relational databases only support this setting.
|
182 | - false: The model will be an open model. All properties are accepted,
|
183 | including the ones that not predefined with the model. This mode is useful
|
184 | if the mobile application just wants to store free form JSON data to
|
185 | a schema-less database such as MongoDB. For relational databases, the value will be converted back to true.
|
186 | - undefined: Default to false unless the data source is backed by a
|
187 | relational database such as Oracle or MySQL.
|
188 |
|
189 |
|
190 | - idInjection:
|
191 | - true: An `id` property will be added to the model automatically
|
192 | - false: No `id` property will be added to the model
|
193 |
|
194 |
|
195 | - plural: The plural form of the model name. If not present, it will be derived from the model name following English
|
196 | conventions.
|
197 |
|
198 |
|
199 | - Data source specific mappings
|
200 | The model can be decorated with connector-specific options to customize the
|
201 | mapping between the model and the connector. For example, we can define the
|
202 | corresponding schema/table names for Oracle as follows:
|
203 |
|
204 | {
|
205 | "name": "Location",
|
206 | "options": {
|
207 | "idInjection": false,
|
208 | "oracle": {
|
209 | "schema": "BLACKPOOL",
|
210 | "table": "LOCATION"
|
211 | }
|
212 | },
|
213 | ...
|
214 | }
|
215 |
|
216 | ### Property definitions
|
217 | A model consists of a list of properties. The basic example use
|
218 | `propertyName: type` to describe a property.
|
219 |
|
220 | Properties can have options in addition to the type. LDL uses a JSON object to
|
221 | describe such properties, for example:
|
222 |
|
223 | "id": {"type": "number", "id": true, "doc": "User ID"}
|
224 |
|
225 | "firstName": {"type": "string", "required": true, "oracle": {"column": "FIRST_NAME", "type": "VARCHAR(32)"}}
|
226 |
|
227 | **Note** `"id": "number"` is a short form of `"id": {"type": "number"}`.
|
228 |
|
229 | #### Data types
|
230 | LDL supports the following data types.
|
231 |
|
232 | - String/Text
|
233 | - Number
|
234 | - Date
|
235 | - Boolean
|
236 | - Buffer/Binary
|
237 | - Array
|
238 | - Any/Object/JSON
|
239 | - GeoPoint
|
240 |
|
241 | ##### Array types
|
242 | LDL supports array types as follows:
|
243 |
|
244 | - `{emails: [String]}`
|
245 | - `{"emails": ["String"]}`
|
246 | - `{emails: [{type: String, length: 64}]}`
|
247 |
|
248 | ##### Object types
|
249 | A model often has properties that consist of other properties. For example, the
|
250 | user model can have an `address` property
|
251 | that in turn has properties such as `street`, `city`, `state`, and `zipCode`.
|
252 |
|
253 | LDL allows inline declaration of such properties, for example,
|
254 |
|
255 | var UserModel = {
|
256 | firstName: String,
|
257 | lastName: String,
|
258 | address: {
|
259 | street: String,
|
260 | city: String,
|
261 | state: String,
|
262 | zipCode: String
|
263 | },
|
264 | ...
|
265 | }
|
266 |
|
267 | The value of the address is the definition of the `address` type, which can be
|
268 | also considered as an anonymous model.
|
269 |
|
270 | If you intend to reuse the address model, we can define it independently and
|
271 | reference it in the user model. For example,
|
272 |
|
273 | var AddressModel = {
|
274 | street: String,
|
275 | city: String,
|
276 | state: String,
|
277 | zipCode: String
|
278 | };
|
279 |
|
280 | var Address = ds.define('Address', AddressModel);
|
281 |
|
282 | var UserModel = {
|
283 | firstName: String,
|
284 | lastName: String,
|
285 | address: 'Address', // or address: Address
|
286 | ...
|
287 | }
|
288 |
|
289 | var User = ds.define('User', UserModel);
|
290 |
|
291 | **Note**: The user model has to reference the Address constructor or the model
|
292 | name - `'Address'`.
|
293 |
|
294 |
|
295 | #### ID(s) for a model
|
296 | A model representing data to be persisted in a database usually has one or more
|
297 | properties as an id to uniquely identify the model instance. For example, the
|
298 | `user` model can have user ids.
|
299 |
|
300 | By default, if no id properties are defined and the `idInjection` of the model
|
301 | options is false, LDL will automatically add an id property to the model as follows:
|
302 |
|
303 | id: {type: Number, generated: true, id: true}
|
304 |
|
305 | To explicitly specify a property as `id`, LDL provides an `id` property for the
|
306 | option. The value can be true, false, or a number.
|
307 |
|
308 | - true: It's an id
|
309 | - false or any falsey values: It's not an id (default)
|
310 | - a positive number, such as 1 or 2: It's the index of the composite id
|
311 |
|
312 | LDL supports the definition of a composite id that has more than one properties.
|
313 | For example,
|
314 |
|
315 | var InventoryDefinition =
|
316 | {
|
317 | productId: {type: String, id: 1},
|
318 | locationId: {type: String, id: 2},
|
319 | qty: Number
|
320 | }
|
321 |
|
322 | The composite id is (productId, locationId) for an inventory model.
|
323 |
|
324 | **Note: Composite ids are NOT supported as query parameters in REST APIs yet.**
|
325 |
|
326 | #### Property documentation
|
327 | * doc: Documentation of the property
|
328 |
|
329 | #### Constraints
|
330 | Constraints are modeled as options too, for example:
|
331 |
|
332 | * default: The default value of the property
|
333 | * required: Indicate if the property is required
|
334 | * pattern: A regular expression pattern that a string should match
|
335 | * min/max: The minimal and maximal value
|
336 | * length: The maximal length of a string
|
337 |
|
338 |
|
339 | #### Conversion and formatting
|
340 | Format conversions can also be declared as options, for example:
|
341 |
|
342 | * trim: Trim the string
|
343 | * lowercase: Convert the string to be lowercase
|
344 | * uppercase: Convert the string to be uppercase
|
345 | * format: Format a Date
|
346 |
|
347 | #### Mapping
|
348 | Data source specific mappings can be added to the property options, for example,
|
349 | to map a property to be a column in Oracle database table, you can use the
|
350 | following syntax:
|
351 |
|
352 | "oracle": {"column": "FIRST_NAME", "type": "VARCHAR", "length": 32}
|
353 |
|
354 |
|
355 | ### Relations between models
|
356 |
|
357 | #### belongsTo
|
358 |
|
359 | A `belongsTo` relation sets up a one-to-one connection with another model, such
|
360 | that each instance of the declaring model "belongs to" one instance of the other
|
361 | model. For example, if your application includes customers and orders, and each order
|
362 | can be placed by exactly one customer.
|
363 |
|
364 | ![belongsTo](belongs-to.png "belongsTo")
|
365 |
|
366 | var Order = ds.createModel('Order', {
|
367 | customerId: Number,
|
368 | orderDate: Date
|
369 | });
|
370 |
|
371 | var Customer = ds.createModel('Customer', {
|
372 | name: String
|
373 | });
|
374 |
|
375 | Order.belongsTo(Customer);
|
376 |
|
377 |
|
378 | The code above basically says Order has a reference called `customer` to User using
|
379 | the `customerId` property of Order as the foreign key. Now we can access the customer
|
380 | in one of the following styles:
|
381 |
|
382 |
|
383 | order.customer(callback); // Get the customer for the order
|
384 | order.customer(); // Get the customer for the order synchronously
|
385 | order.customer(customer); // Set the customer for the order
|
386 |
|
387 |
|
388 | #### hasMany
|
389 |
|
390 | A `hasMany` relation builds a one-to-many connection with another model. You'll
|
391 | often find this relation on the "other side" of a `belongsTo` relation. This
|
392 | relation indicates that each instance of the model has zero or more instances
|
393 | of another model. For example, in an application containing customers and orders, a
|
394 | customer has zero or more orders.
|
395 |
|
396 | ![hasMany](has-many.png "hasMany")
|
397 |
|
398 | var Order = ds.createModel('Order', {
|
399 | customerId: Number,
|
400 | orderDate: Date
|
401 | });
|
402 |
|
403 | var Customer = ds.createModel('Customer', {
|
404 | name: String
|
405 | });
|
406 |
|
407 | Customer.hasMany(Order, {as: 'orders', foreignKey: 'customerId'});
|
408 |
|
409 |
|
410 |
|
411 | Scope methods created on the base model by hasMany allows to build, create and
|
412 | query instances of other class. For example,
|
413 |
|
414 | customer.orders(filter, callback); // Find orders for the customer
|
415 | customer.orders.build(data); // Build a new order
|
416 | customer.orders.create(data, callback); // Create a new order for the customer
|
417 | customer.orders.destroyAll(callback); // Remove all orders for the customer
|
418 | customer.orders.findById(orderId, callback); // Find an order by id
|
419 | customer.orders.destroy(orderId, callback); // Delete and order by id
|
420 |
|
421 |
|
422 | #### hasMany through
|
423 |
|
424 | A `hasMany through` relation is often used to set up a many-to-many connection with another model. This relation
|
425 | indicates that the declaring model can be matched with zero or more instances of another model by proceeding through
|
426 | a third model. For example, consider a medical practice where patients make appointments to see physicians. The
|
427 | relevant association declarations could look like this:
|
428 |
|
429 | ![hasManyThrough](has-many-through.png "hasManyThrough")
|
430 |
|
431 | var Physician = ds.createModel('Physician', {name: String});
|
432 | var Patient = ds.createModel('Patient', {name: String});
|
433 | var Appointment = ds.createModel('Appointment', {
|
434 | physicianId: Number,
|
435 | patientId: Number,
|
436 | appointmentDate: Date
|
437 | });
|
438 |
|
439 | Appointment.belongsTo(Patient);
|
440 | Appointment.belongsTo(Physician);
|
441 |
|
442 | Physician.hasMany(Patient, {through: Appointment});
|
443 | Patient.hasMany(Physician, {through: Appointment});
|
444 |
|
445 | Now the Physician model has a virtual property called `patients`:
|
446 |
|
447 | physician.patients(filter, callback); // Find patients for the physician
|
448 | physician.patients.build(data); // Build a new patient
|
449 | physician.patients.create(data, callback); // Create a new patient for the physician
|
450 | physician.patients.destroyAll(callback); // Remove all patients for the physician
|
451 | physician.patients.add(patient, callback); // Add an patient to the physician
|
452 | physician.patients.remove(patient, callback); // Remove an patient from the physician
|
453 | physician.patients.findById(patientId, callback); // Find an patient by id
|
454 |
|
455 |
|
456 | #### hasAndBelongsToMany
|
457 |
|
458 | A `hasAndBelongsToMany` relation creates a direct many-to-many connection with
|
459 | another model, with no intervening model. For example, if your application
|
460 | includes users and groups, with each group having many users and each user
|
461 | appearing in many groups, you could declare the models this way,
|
462 |
|
463 | ![hasAndBelongsToMany](has-and-belongs-to-many.png "hasAndBelongsToMany")
|
464 |
|
465 | User.hasAndBelongsToMany('groups', {model: Group, foreignKey: 'groupId'});
|
466 | user.groups(callback); // get groups of the user
|
467 | user.groups.create(data, callback); // create a new group and connect it with the user
|
468 | user.groups.add(group, callback); // connect an existing group with the user
|
469 | user.groups.remove(group, callback); // remove the user from the group
|
470 |
|
471 |
|
472 | ### Extend from a base model
|
473 | LDL allows a new model to extend from an existing model. For example, Customer
|
474 | can extend from User as follows. The Customer model will inherit properties and
|
475 | methods from the User model.
|
476 |
|
477 | var Customer = User.extend('customer', {
|
478 | accountId: String,
|
479 | vip: Boolean
|
480 | });
|
481 |
|
482 | ### Mix in model definitions
|
483 | Some models share the common set of properties and logic around. LDL allows a
|
484 | model to mix in one or more other models. For example,
|
485 |
|
486 | var TimeStamp = modelBuilder.define('TimeStamp', {created: Date, modified: Date});
|
487 | var Group = modelBuilder.define('Group', {groups: [String]});
|
488 | User.mixin(Group, TimeStamp);
|