@sap/cds 1.15.1 → 1.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/{developer-license-3.1.txt → LICENSE} +37 -35
- package/_hdbext/README.md +373 -0
- package/_hdbext/index.js +4 -0
- package/_hdbext/lib/client-factory.js +62 -0
- package/_hdbext/lib/client-session.js +96 -0
- package/_hdbext/lib/conn-options.js +84 -0
- package/_hdbext/lib/constants.js +79 -0
- package/_hdbext/lib/internal-constants.js +7 -0
- package/_hdbext/lib/middleware.js +46 -0
- package/_hdbext/lib/pool.js +236 -0
- package/_hdbext/lib/safe-sql.js +17 -0
- package/_hdbext/lib/sql-injection-utils.js +149 -0
- package/cds-queries-geo.js +347 -371
- package/cds-queries.js +2692 -2229
- package/cds.js +111 -104
- package/exprs.js +118 -107
- package/manager.js +696 -614
- package/metadata.js +604 -542
- package/npm-shrinkwrap.json +268 -0
- package/package.json +40 -1
- package/transaction.js +45 -51
- package/util/Queue.js +32 -30
- package/utils.js +182 -159
- package/xsjs-cds.js +231 -221
- package/.project +0 -11
- package/SIGNATURE.SMF +0 -1747
- package/TUTORIAL.md +0 -1236
- package/dependencies +0 -56
package/TUTORIAL.md
DELETED
|
@@ -1,1236 +0,0 @@
|
|
|
1
|
-
node-cds: Core Data Services for node.js
|
|
2
|
-
========================================
|
|
3
|
-
|
|
4
|
-
Important note:
|
|
5
|
-
---------------
|
|
6
|
-
The node-cds library is now considered feature complete.
|
|
7
|
-
It will remain fully supported but will not receive further
|
|
8
|
-
enhancements in future releases.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Entity Declarations
|
|
12
|
-
-------------------
|
|
13
|
-
|
|
14
|
-
The data declaration part of your JavaScript file defines the entity
|
|
15
|
-
types based on existing CDS and/or HANA tables and views:
|
|
16
|
-
|
|
17
|
-
cds.$importEntities([
|
|
18
|
-
// for CDS:
|
|
19
|
-
{ $entity: "cds.namespace::cds_context.cds_entity" },
|
|
20
|
-
// for plain SQL:
|
|
21
|
-
{ $schema: "SCHEMA", $table: "package::TABLE"), $name: "name" }
|
|
22
|
-
], function (error, entities) { … });
|
|
23
|
-
|
|
24
|
-
For single entity imports the `$importEntity` function may also be
|
|
25
|
-
used.
|
|
26
|
-
|
|
27
|
-
Both import methods pass a dictionary of imported entities indexed by
|
|
28
|
-
entity name to the callback function. This dictionary comprises both
|
|
29
|
-
explicitly and implicitly imported entities, including previously
|
|
30
|
-
imported entities if required.
|
|
31
|
-
|
|
32
|
-
cds.$importEntities([{
|
|
33
|
-
$entity: "sap.hana.xs2.cds.examples.data::bboard.post"
|
|
34
|
-
}], function (error, entities) {
|
|
35
|
-
var post = entities["sap.hana.xs2.cds.examples.data::bboard.post"];
|
|
36
|
-
var user = entities["sap.hana.xs2.cds.examples.data::bboard.user"];
|
|
37
|
-
// automatically imported because of association from Post to User
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
For most CDS data models and simple SQL data models this is all that
|
|
41
|
-
is required to define an entity.
|
|
42
|
-
|
|
43
|
-
Advanced options allow you to override certain properties of the
|
|
44
|
-
columns to import. In the simplest case, you simply want to rename a
|
|
45
|
-
table column for your JavaScript object property:
|
|
46
|
-
|
|
47
|
-
cds.$importEntities([
|
|
48
|
-
{ $entity: "sap.hana.xs2.cds.examples.data::bboard.post",
|
|
49
|
-
$fields: {
|
|
50
|
-
PostedOn: { $column: "Created" }
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
], callback);
|
|
54
|
-
|
|
55
|
-
Primary key information is automatically imported, but you may also
|
|
56
|
-
add or change key information, e.g., for CDS entities defined with the
|
|
57
|
-
`@nokey` attribute:
|
|
58
|
-
|
|
59
|
-
cds.$importEntities([{
|
|
60
|
-
$entity: "sap.hana.xs2.cds.examples.data::bboard.post",
|
|
61
|
-
$fields: {
|
|
62
|
-
pid: { $key: true }
|
|
63
|
-
}
|
|
64
|
-
}], callback);
|
|
65
|
-
|
|
66
|
-
You may also provide an existing HDB sequence to generate keys
|
|
67
|
-
automatically for you. By using a sequence key fields may be
|
|
68
|
-
populated automatically by the database.
|
|
69
|
-
|
|
70
|
-
cds.$importEntities([{
|
|
71
|
-
$entity: "sap.hana.xs2.cds.examples.data::bboard.user",
|
|
72
|
-
$fields: {
|
|
73
|
-
uid: { $key: "\"sap.hana.xs2.cds.examples.data::bb_keyid\"" }
|
|
74
|
-
}
|
|
75
|
-
}], callback);
|
|
76
|
-
|
|
77
|
-
Note that node-cds does not apply any additional logic to key
|
|
78
|
-
generation. It is left to the application and the sequence to
|
|
79
|
-
generate valid, unique keys for your entities.
|
|
80
|
-
|
|
81
|
-
`$importEntities()` passes a dictionary of entity objects for the
|
|
82
|
-
entity types defined to its callback. If you need to access an entity
|
|
83
|
-
in a different scope you can use the `$getEntity()` function to
|
|
84
|
-
retrieve the entity object by entity name:
|
|
85
|
-
|
|
86
|
-
function foo() {
|
|
87
|
-
cds.$importEntity({ $entity: "cds_namespace::cds_context.cds_entity" }),
|
|
88
|
-
callback);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function bar() {
|
|
92
|
-
cds.$getEntity("cds_namespace::cds_context.cds_entity"),
|
|
93
|
-
function(err, entity) { … }
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
The `$getEntity` function not only saves you from storing your
|
|
97
|
-
entities in global variables but also offers flow control to ensure
|
|
98
|
-
that the entity and all its dependencies have been imported
|
|
99
|
-
completely. Note that the callback of a `$getEntity` call for an
|
|
100
|
-
entity that is not imported will never be called!
|
|
101
|
-
|
|
102
|
-
There is also a synchronous version `$getEntitySync` that will return
|
|
103
|
-
the entity immediately or null if the import has not been completed
|
|
104
|
-
yet.
|
|
105
|
-
|
|
106
|
-
var someEntity = cds.$getEntitySync("cds_namespace::cds_context.cds_entity");
|
|
107
|
-
if (!someEntity)
|
|
108
|
-
console.warn("well, now what?!");
|
|
109
|
-
|
|
110
|
-
Finally we would like to stress that node-cds is a consumption-only
|
|
111
|
-
framework that will only import externally defined data models. It is
|
|
112
|
-
beyond the scope of node-cds to create tables and modify existing CDS
|
|
113
|
-
data models permanently. This scope definition will not change in the
|
|
114
|
-
future.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
Associations
|
|
118
|
-
------------
|
|
119
|
-
|
|
120
|
-
One of the benefits of the semantically rich data models of CDS is the
|
|
121
|
-
ability to handle associations conveniently. node-cds supports plain
|
|
122
|
-
one-to-one, one-to-many, and many-to-many CDS associations in all of
|
|
123
|
-
their flavors.
|
|
124
|
-
|
|
125
|
-
While the CDS core implementation for managed one-to-many and
|
|
126
|
-
many-to-many associations is still incomplete node-cds already
|
|
127
|
-
supports these types of associations in a way that makes it easy to
|
|
128
|
-
migrate once they become available in CDS as well.
|
|
129
|
-
|
|
130
|
-
CDS one-to-one (and one-to-zero-or-one) associations are imported
|
|
131
|
-
automatically. To define one-to-one associations manually you provide
|
|
132
|
-
an `$association` property that specifies the associated entity type
|
|
133
|
-
and the foreign key that links the parent entity to the associated
|
|
134
|
-
target entity:
|
|
135
|
-
|
|
136
|
-
cds.$importEntity({
|
|
137
|
-
$entity: "sap.hana.xs2.cds.examples.data::bboard.post",
|
|
138
|
-
$fields: {
|
|
139
|
-
PostedBy: {
|
|
140
|
-
$association: { $entity: "sap.hana.xs2.cds.examples.data::bboard.user" },
|
|
141
|
-
uid: { $column: "Author" }
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}, callback);
|
|
145
|
-
|
|
146
|
-
In this example the link between Post and User is defined through the
|
|
147
|
-
foreign key relation so that `USERS.uid = POSTS.Author` holds. Note
|
|
148
|
-
that the `Author` column of table `POSTS` is not mapped to a field of
|
|
149
|
-
the `Post` entity; it’s used exclusively for following the
|
|
150
|
-
association. The foreign key may reference any number of columns of
|
|
151
|
-
the underlying database table.
|
|
152
|
-
|
|
153
|
-
Entities referenced by name are imported automatically with default
|
|
154
|
-
options if no matching `$importEntities()` call can be found.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
### One-to-Many Associations
|
|
158
|
-
|
|
159
|
-
The CDS specification defines one-to-many associations by using
|
|
160
|
-
backlinks from the targets to the parent. Assume for our example that
|
|
161
|
-
each post may have any number of unique comments, where each comment
|
|
162
|
-
contains a reference to its parent post:
|
|
163
|
-
|
|
164
|
-
entity comment {
|
|
165
|
-
key cid: Integer not null;
|
|
166
|
-
Text: text;
|
|
167
|
-
Author: association [1] to user;
|
|
168
|
-
Parent: association [1] to post; // as backlink
|
|
169
|
-
Created: UTCDateTime;
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
Since the CDS implementation does not support backlinks yet, we cannot
|
|
173
|
-
add comments to our CDS definition of post, but we may augment the
|
|
174
|
-
`Post` data model in our import:
|
|
175
|
-
|
|
176
|
-
cds.$importEntity({
|
|
177
|
-
$entity: "sap.hana.xs2.cds.examples.data::bboard.post",
|
|
178
|
-
$fields: {
|
|
179
|
-
Comments: {
|
|
180
|
-
$association: {
|
|
181
|
-
$entity: "sap.hana.xs2.cds.examples.data::bboard.comment",
|
|
182
|
-
$viaBacklink: "Parent"
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}, callback);
|
|
187
|
-
|
|
188
|
-
This is equivalent to importing the following, still unsupported CDS
|
|
189
|
-
definition:
|
|
190
|
-
|
|
191
|
-
entity post {
|
|
192
|
-
key pid: Integer not null;
|
|
193
|
-
...
|
|
194
|
-
Comments: association [0..*] to comment via backlink Parent;
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
Once CDS support for backlinks is implemented, the node-cds import
|
|
198
|
-
above simplifies to
|
|
199
|
-
|
|
200
|
-
cds.$importEntity({ $entity: "sap.hana.xs2.cds.examples.data::bboard.post" },
|
|
201
|
-
callback);
|
|
202
|
-
|
|
203
|
-
Either way, the comments will appear as an array in our instances of
|
|
204
|
-
the Post entity:
|
|
205
|
-
|
|
206
|
-
tx.$get(Post, { pid: 69 }, function(error, post) {
|
|
207
|
-
for (var i = 0; i < post.Comments.length; ++i) {
|
|
208
|
-
if (post.Comments[i].Author.Name === "Alice") { … }
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
Note that the association array should be seen as a read-only array
|
|
212
|
-
that gathers the association targets for convenience. While
|
|
213
|
-
individual array elements may be manipulated directly their membership
|
|
214
|
-
to the association and thus the array is governed by their backlink
|
|
215
|
-
property only. This has some potentially unexpected consequences when
|
|
216
|
-
saving instances with backlink associations that are further detailed
|
|
217
|
-
in the section “Working with entities”.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
### Many-to-Many Associations
|
|
221
|
-
|
|
222
|
-
The CDS specification defines many-to-many associations by using
|
|
223
|
-
linking entities that connect parent and target instances. Assume for
|
|
224
|
-
our example that any number of tags may be attached to our posts:
|
|
225
|
-
|
|
226
|
-
entity tag {
|
|
227
|
-
key tid: Integer not null;
|
|
228
|
-
Name: String(63);
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
The actual linkage data is stored in a separate table. For plain SQL,
|
|
232
|
-
this table may be specified directly, but for CDS we assume a
|
|
233
|
-
corresponding entity definition:
|
|
234
|
-
|
|
235
|
-
@nokey entity tags_by_post {
|
|
236
|
-
lid: Integer;
|
|
237
|
-
LinkTag: association to tag;
|
|
238
|
-
LinkPost: association to post;
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
Since the CDS implementation does not support linking entities yet, we
|
|
242
|
-
can manually augment our data model in the import statement:
|
|
243
|
-
|
|
244
|
-
cds.$importEntity({
|
|
245
|
-
$entity: "sap.hana.xs2.cds.examples.data::bboard.post",
|
|
246
|
-
$fields: {
|
|
247
|
-
Tags: {
|
|
248
|
-
$association: {
|
|
249
|
-
$entity: "sap.hana.xs2.cds.examples.data::bboard.tag",
|
|
250
|
-
$viaEntity: "sap.hana.xs2.cds.examples.data::bboard.tags_by_post",
|
|
251
|
-
$source: "LinkPost",
|
|
252
|
-
$target: "LinkTag"
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}, callback);
|
|
257
|
-
|
|
258
|
-
The `$source` and `$target` properties indicate the direction of the
|
|
259
|
-
association. This is strictly required if source and target refer to
|
|
260
|
-
the same entity; for simplicity, however, we enforce that these
|
|
261
|
-
properties are always supplied.
|
|
262
|
-
|
|
263
|
-
Above annotation is equivalent to importing the following, still
|
|
264
|
-
unsupported CDS definition:
|
|
265
|
-
|
|
266
|
-
entity post {
|
|
267
|
-
key pid: Integer not null;
|
|
268
|
-
...
|
|
269
|
-
Tags: association [0..*] to ds_comments via entity tags_by_post;
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
Once CDS support for backlinks is implemented, the node-cds import
|
|
273
|
-
simplifies to
|
|
274
|
-
|
|
275
|
-
cds.$importEntity({ $entity: "sap.hana.xs2.cds.examples.data::bboard.post" },
|
|
276
|
-
callback);
|
|
277
|
-
|
|
278
|
-
Note again that an explicit import of `Tags` or `Tags_by_post` is not
|
|
279
|
-
required. The linking entities such as `Tags_by_post` may or may not
|
|
280
|
-
have a key, and they could have additional fields besides the
|
|
281
|
-
`$source` and `$target` fields.
|
|
282
|
-
|
|
283
|
-
The tags will appear as a native JavaScript array in our instances of
|
|
284
|
-
the `Post` entity:
|
|
285
|
-
|
|
286
|
-
tx.$get(Post, { pid: 69 }, function (error, post) {
|
|
287
|
-
tx.$find(Tag, { Text: "cool" }, function (error, tags) {
|
|
288
|
-
if (post.Tags.indexOf(tags[0]) < 0) {
|
|
289
|
-
// ...
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
Via entity association arrays behave differently from via backlink
|
|
295
|
-
association arrays in that element membership is controlled directly
|
|
296
|
-
through the array. In fact, direct manipulation of linking entities
|
|
297
|
-
such as `Tags_by_post` is discouraged, although it is possible. For
|
|
298
|
-
further details see the section "Working with entities".
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
### Unmanaged Associations
|
|
302
|
-
|
|
303
|
-
The most general form of associations supported by CDS are unmanaged
|
|
304
|
-
associations that define an arbitrary `JOIN ... ON` condition:
|
|
305
|
-
|
|
306
|
-
entity toppost {
|
|
307
|
-
key pid: Integer not null;
|
|
308
|
-
...
|
|
309
|
-
TopReplies: association to Post on TopReplies.Parent.pid = pid and
|
|
310
|
-
TopReplies.Rating >= 3;
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
Unmanaged associations are imported automatically and need not be
|
|
314
|
-
supplied to the import call:
|
|
315
|
-
|
|
316
|
-
cds.$importEntity({ $entity: "sap.hana.xs2.cds.examples.data::bboard.topposts" },
|
|
317
|
-
callback);
|
|
318
|
-
|
|
319
|
-
Similarly to backlink associations, unmanaged target instances are
|
|
320
|
-
stored in a conceptually read-only array. Due to the unmanaged nature
|
|
321
|
-
of these associations the contents of the array cannot reflect unsaved
|
|
322
|
-
changes to the relation and must be refreshed manually.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
Working With Managed Entities
|
|
326
|
-
-----------------------------
|
|
327
|
-
|
|
328
|
-
Once we have imported all our entities into our application we can use
|
|
329
|
-
the resulting handles to work with their instances.
|
|
330
|
-
|
|
331
|
-
For this node-cds provides two different modes to interact with the
|
|
332
|
-
database: managed mode and unmanaged mode. Managed mode works with
|
|
333
|
-
entity instances, i.e., singleton objects with “personality” that
|
|
334
|
-
correspond one-to-one to database records. Unmanaged mode, on the
|
|
335
|
-
other hand, works with plain values that are simple, flat JavaScript
|
|
336
|
-
object values.
|
|
337
|
-
|
|
338
|
-
In this tutorial we will start off with managed mode and cover
|
|
339
|
-
unmanaged mode in the corresponding section below.
|
|
340
|
-
|
|
341
|
-
To retrieve an existing entity instance in managed mode, we may query
|
|
342
|
-
it by its key:
|
|
343
|
-
|
|
344
|
-
tx.$get(Post, { pid: 1 }, function (error, post) {
|
|
345
|
-
if (error)
|
|
346
|
-
console.error(error)
|
|
347
|
-
else
|
|
348
|
-
notify(post.Author.Email);
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
This will retrieve the post with `pid == 1` if it exists, or null
|
|
352
|
-
otherwise. Note that specifying a non-existing key is not an error
|
|
353
|
-
condition. The $get() method requires that all key properties be
|
|
354
|
-
supplied.
|
|
355
|
-
|
|
356
|
-
The more general `$find()` method will search for instances that match
|
|
357
|
-
the given property conditions:
|
|
358
|
-
|
|
359
|
-
tx.$find(User, { Name: "Alice" }, function (error, users) {
|
|
360
|
-
console.log("Number of users called Alice: " + users.length);
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
The instance filter expression may be built using the following basic
|
|
364
|
-
expression language that is valid for all JavaScript and SQL types:
|
|
365
|
-
|
|
366
|
-
<expr> ::= { <cond>, <cond>, … }
|
|
367
|
-
<cond> ::= prop: value | prop: { <op>: value, … }
|
|
368
|
-
<op> ::= $eq | $ne | $lt | $le | $gt | $ge | $like | $unlike | $null
|
|
369
|
-
|
|
370
|
-
All conditions in an expression are joined by logical-AND. The
|
|
371
|
-
expression `{ prop: value }` is a shorthand for `{ prop: { $eq: value
|
|
372
|
-
} }`.
|
|
373
|
-
|
|
374
|
-
The comparison operators `$eq`, `$ne`, `$lt`, `$le`, `$gt`, `$ge`
|
|
375
|
-
apply to all suitable types if the value is supplied in appropriate
|
|
376
|
-
format:
|
|
377
|
-
|
|
378
|
-
tx.$find(E, { p: { $lt: 100 } }); // returns all instances where p < 100
|
|
379
|
-
tx.$find(E, { p: { $ge: 100, $le: 200 } }); // … p between 100 and 200
|
|
380
|
-
tx.$find(E, { p: "Bob", q: { $ne: "Jones" } }); // … p is Bob but q is not Jones
|
|
381
|
-
|
|
382
|
-
The `$like` and `$unlike` operators can be used for SQL-like pattern
|
|
383
|
-
matching:
|
|
384
|
-
|
|
385
|
-
tx.$find(User, { Name: { $like: "B%" } }); // returns all users whose name starts with B
|
|
386
|
-
tx.$find(User, { Name: { $unlike: "J.." } }); // returns Bill but not Jim
|
|
387
|
-
|
|
388
|
-
The `$null` operator checks if a given property is `NULL` in the database:
|
|
389
|
-
|
|
390
|
-
tx.$find(Post, { Rating: { $null: true } }); // returns posts with unknown rating
|
|
391
|
-
tx.$find(Post, { Created: { $null: false } }); // returns posts with known authors
|
|
392
|
-
|
|
393
|
-
The `$empty` operator checks if an association is missing or empty:
|
|
394
|
-
|
|
395
|
-
tx.$find(Post, { Parent: { $empty: true } }); // returns posts without parent
|
|
396
|
-
tx.$find(Post, { Comments: { $empty: false } }); // returns posts with comments
|
|
397
|
-
|
|
398
|
-
Expressions are evaluated by the database but also by JavaScript when
|
|
399
|
-
checking the entity cache for matching instances (see section on
|
|
400
|
-
Entity Management below). This works for simple types such as integers
|
|
401
|
-
and strings, but requires user intervention for complex types, such as
|
|
402
|
-
`DATE`, and for types that have no native JavaScript equivalent, such
|
|
403
|
-
as `DECIMALS`.
|
|
404
|
-
|
|
405
|
-
Handling those types meaningfully requires a comparison function
|
|
406
|
-
with `$using` that matches the given implementation for non-native types.
|
|
407
|
-
|
|
408
|
-
The `DATETIME` type, for example, is returned as string. To retrieve
|
|
409
|
-
instances relative to a certain date the following code could be used:
|
|
410
|
-
|
|
411
|
-
var datecomp = function(arg1, arg2) {
|
|
412
|
-
return (+(new Date(arg1))) - (+(new Date(arg2)))
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
tx.$find(Entity, {
|
|
416
|
-
datevalue: {
|
|
417
|
-
$le: "2012-02-29T01:02:03.000Z",
|
|
418
|
-
$using: datecomp }
|
|
419
|
-
}, callback);
|
|
420
|
-
|
|
421
|
-
Note that HANA does not store timezone information, so passing JavaScript
|
|
422
|
-
`Date` objects to the query (including `$query()`) is likely to cause
|
|
423
|
-
issues.
|
|
424
|
-
|
|
425
|
-
node-cds also supports batch retrieval to reduce the number of
|
|
426
|
-
required callback functions:
|
|
427
|
-
|
|
428
|
-
tx.$getAll([
|
|
429
|
-
{ $entity: Post, pid: 1 },
|
|
430
|
-
{ $entity: User, uid: 2 }
|
|
431
|
-
], function (error, instances) {
|
|
432
|
-
console.log("Post " + instances[0] + " was not made by user " + instances[1]);
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
If you abhor the mixing of entity type and entity key the $prepare
|
|
436
|
-
auxiliary function also provides the required entity type information
|
|
437
|
-
to `$getAll()`:
|
|
438
|
-
|
|
439
|
-
tx.$getAll([
|
|
440
|
-
Post.$prepare({ pid: 1 }),
|
|
441
|
-
User.$prepare({ uid: 2 })
|
|
442
|
-
], function (error, instances) { … });
|
|
443
|
-
|
|
444
|
-
Note that there is no `$findAll()` method, as `$find()` already
|
|
445
|
-
returns a list of instances.
|
|
446
|
-
|
|
447
|
-
Managed queries are inherently limited in their expressiveness, as the
|
|
448
|
-
node-cds runtime needs to check its instance cache against the filter
|
|
449
|
-
condition provided to `$find()`. Applications requiring advanced
|
|
450
|
-
query capabilities should use unmanaged queries described in the next
|
|
451
|
-
section. Of course, both managed and unmanaged queries can be used
|
|
452
|
-
side by side.
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
### Updating Entities
|
|
456
|
-
|
|
457
|
-
Entity instances are regular JavaScript objects and can be used and
|
|
458
|
-
modified as one would expect:
|
|
459
|
-
|
|
460
|
-
post.Ranking++;
|
|
461
|
-
post.Author = users[0];
|
|
462
|
-
post.Author.Name = "Newt";
|
|
463
|
-
|
|
464
|
-
All changes are made in memory only. If we want to persist our
|
|
465
|
-
changes for an instance we invoke the `$save()` method:
|
|
466
|
-
|
|
467
|
-
tx.$save(post, function (error, result) {
|
|
468
|
-
// post and its associated entity instances updated
|
|
469
|
-
// ...
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
Calling `$save()` will flush our in-memory changes of that instance to
|
|
473
|
-
the database, following all associations reachable from the root
|
|
474
|
-
instance. Note that all instances will be updated, including
|
|
475
|
-
instances not actually modified.
|
|
476
|
-
|
|
477
|
-
If auto commit is active, then all changes written to the database are
|
|
478
|
-
automatically committed. Without auto commit, you will have to call
|
|
479
|
-
`tx.$commit()` and/or `tx.$rollback()` yourself in order to persist or
|
|
480
|
-
undo your changes on the database.
|
|
481
|
-
|
|
482
|
-
The result argument of the callback function receives the updated
|
|
483
|
-
instance as written on the database. It is identical to the instance
|
|
484
|
-
argument the `$save()` method was called on.
|
|
485
|
-
|
|
486
|
-
As instance keys must not be changed, all key properties are locked
|
|
487
|
-
and cannot be modified; attempts to do are silently ignored:
|
|
488
|
-
|
|
489
|
-
var post = tx.$get(Post, { pid: 1 });
|
|
490
|
-
post.pid = 69; // silently ignored
|
|
491
|
-
console.log(JSON.stringigy(post)); // still pid == 1
|
|
492
|
-
|
|
493
|
-
There is an additional batch persist for persisting multiple instances
|
|
494
|
-
in one operation:
|
|
495
|
-
|
|
496
|
-
tx.$saveAll([ post1, user2 ]); // persists post1 and user2 plus their dependencies
|
|
497
|
-
|
|
498
|
-
Note that modified instances in memory always take precedence over
|
|
499
|
-
their unmodified versions on the database. For more details and
|
|
500
|
-
further information on some of the resulting subtleties please refer
|
|
501
|
-
to the section on Entity Management and Consistency below.
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
### Creating new Entities
|
|
505
|
-
|
|
506
|
-
New instances are created simply by saving new values with
|
|
507
|
-
non-existing keys:
|
|
508
|
-
|
|
509
|
-
tx.$find(User, { Name: "Alice" }, function (error, users) {
|
|
510
|
-
var alice = users[0] || null;
|
|
511
|
-
var post = {
|
|
512
|
-
$entity: Post,
|
|
513
|
-
id: 101,
|
|
514
|
-
Title: "New Post", Text: "Hello BBoard!", Rating: 1, Created: new Date(),
|
|
515
|
-
Author: alice
|
|
516
|
-
};
|
|
517
|
-
tx.$save(post, function (error) { … });
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
Since the original post object is not a `Post` instance yet, you’ll need
|
|
521
|
-
to tell node-cds about its entity type by including a `$entity`
|
|
522
|
-
property. Alternatively, you could use the trivial $prepare helper
|
|
523
|
-
function:
|
|
524
|
-
|
|
525
|
-
var post = { pid: … }; // no $entity: … property
|
|
526
|
-
tx.$save(Post.$prepare(post), function (error) { … });
|
|
527
|
-
|
|
528
|
-
New and existing instances may be mixed, as in the example
|
|
529
|
-
above. node-cds tracks if an object represents an new or an existing
|
|
530
|
-
instance and will use an `INSERT` or an `UPDATE` statements accordingly.
|
|
531
|
-
Trying to save an existing instance with a value that was not
|
|
532
|
-
retrieved from the database will yield a duplicate key exception from
|
|
533
|
-
the database.
|
|
534
|
-
|
|
535
|
-
var user = { $entity: User, uid: 42, Name: "Newt" };
|
|
536
|
-
tx.$save(user, function(error) {
|
|
537
|
-
// user #42 created
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
var update = { $entity: User, uid: 42, Name: "Ann Newt" }; // must use $get instead
|
|
541
|
-
tx.$save(user, function(error) { … }); // duplicate key error
|
|
542
|
-
|
|
543
|
-
Key properties for which an HDB sequence or an appropriate `$init`
|
|
544
|
-
function has been supplied may be omitted from the entity call.
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
### Discarding Entities
|
|
548
|
-
|
|
549
|
-
Retrieved entity instances are stored in the entity manager cache and
|
|
550
|
-
subject to general JavaScript garbage collection. The `$discard()`
|
|
551
|
-
method permanently deletes an instance from the database:
|
|
552
|
-
|
|
553
|
-
tx.$get(Post, { pid: 99 }, function (error, post) {
|
|
554
|
-
tx.$discard(post, function (error) { … });
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
The `$discard()` callback has a single argument for potential errors.
|
|
558
|
-
|
|
559
|
-
Note that after calling `$discard()` on an instance, the actual
|
|
560
|
-
JavaScript object remains in memory as an unmanaged entity instance,
|
|
561
|
-
i.e., `$find()` will no longer not return references to it. It is up to
|
|
562
|
-
the application to not use any remaining references to the object that
|
|
563
|
-
may still be stored in some JavaScript variables.
|
|
564
|
-
|
|
565
|
-
Note that `$discard()` cannot be used to delete instances based on a
|
|
566
|
-
condition. To delete multiple instances without instantiating them
|
|
567
|
-
before use the `$delete()` method described below.
|
|
568
|
-
|
|
569
|
-
The `$discardAll()` method for batch discards works analogous to
|
|
570
|
-
the `$saveAll()` method:
|
|
571
|
-
|
|
572
|
-
tx.$discardAll([ post1, post2 ], function (error) {
|
|
573
|
-
// ...
|
|
574
|
-
}); // discards post1 and post2
|
|
575
|
-
|
|
576
|
-
For the special use case of deleting instances on the database
|
|
577
|
-
without instantiating them in memory first node-cds provides the
|
|
578
|
-
`$delete()` operation for unmanaged deletes:
|
|
579
|
-
|
|
580
|
-
tx.$delete(Post, { Rating: { $lt: 3 } }, function (err) {
|
|
581
|
-
// deletes all posts where Rating < 3
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
An unmanaged delete will delete all matching records on the database,
|
|
585
|
-
ignoring the state of the entity cache. Likewise, `$delete`s will not
|
|
586
|
-
cascade to target instances. Thus, the set of affected entity instances
|
|
587
|
-
may differ from that of the sequence
|
|
588
|
-
|
|
589
|
-
tx.$findAll(Post, <cond>, function (error, posts) {
|
|
590
|
-
tx.$discardAll(posts, function (error) { … });
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
In fact, `$delete()` is merely syntactic sugar for
|
|
594
|
-
`$query().$matching().$delete()` (see below). Please use `$delete()` with
|
|
595
|
-
care.
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
### Associations and Custom Types
|
|
599
|
-
|
|
600
|
-
Both CDS types and CDS associations are supported and will yield
|
|
601
|
-
nested structures in the resulting JavaScript entity. For example,
|
|
602
|
-
the import of the CDS definitions
|
|
603
|
-
|
|
604
|
-
entity user {
|
|
605
|
-
key uid: Integer not null;
|
|
606
|
-
Name: String(63) not null;
|
|
607
|
-
};
|
|
608
|
-
|
|
609
|
-
type text {
|
|
610
|
-
Text: String(255);
|
|
611
|
-
Lang: String(2);
|
|
612
|
-
};
|
|
613
|
-
|
|
614
|
-
entity post {
|
|
615
|
-
key pid: Integer not null;
|
|
616
|
-
Title: String(63) not null;
|
|
617
|
-
Text: text;
|
|
618
|
-
Author: association [1] to user;
|
|
619
|
-
Created: UTCDateTime;
|
|
620
|
-
};
|
|
621
|
-
|
|
622
|
-
will yield a Post entity whose instances may look like
|
|
623
|
-
|
|
624
|
-
{
|
|
625
|
-
"pid": 102,
|
|
626
|
-
"Title": "Re: First Post!",
|
|
627
|
-
"Text": {
|
|
628
|
-
"Lang": "EN",
|
|
629
|
-
"Text": "You beat me to it."
|
|
630
|
-
},
|
|
631
|
-
"Author": {
|
|
632
|
-
"uid": 2,
|
|
633
|
-
"Name": "Bob"
|
|
634
|
-
},
|
|
635
|
-
"Created": "2014-04-25T09:11:30.000Z"
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
Overriding properties in such structured CDS entities works analogous
|
|
639
|
-
to the flat case:
|
|
640
|
-
|
|
641
|
-
cds.$importEntity({
|
|
642
|
-
$entity: "sap.hana.xs2.cds.examples.data::bboard.post",
|
|
643
|
-
$fields: {
|
|
644
|
-
Text: { Lang: { $init: "EN" } }
|
|
645
|
-
}
|
|
646
|
-
}, callback);
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
### Lazy Navigation
|
|
650
|
-
|
|
651
|
-
By default, all associations are eagerly resolved, i.e., their targets
|
|
652
|
-
are included in the parent instance object. For heavily connected
|
|
653
|
-
data this may lead to very large data structures in memory.
|
|
654
|
-
|
|
655
|
-
To control which associations are being followed associations may be
|
|
656
|
-
declared `$lazy`:
|
|
657
|
-
|
|
658
|
-
cds.$importEntity({
|
|
659
|
-
$entity: "sap.hana.xs2.cds.examples.data::bboard.post",
|
|
660
|
-
$fields: {
|
|
661
|
-
Parent: { $association: { $lazy: true } }
|
|
662
|
-
},
|
|
663
|
-
$name: "LazyPost"
|
|
664
|
-
}, callback);
|
|
665
|
-
|
|
666
|
-
A lazy association will delay the retrieval of the associated instance
|
|
667
|
-
or instances until their property values are actually required:
|
|
668
|
-
|
|
669
|
-
tx.$get(LazyPost, { pid: 1 }, function (error, post) {
|
|
670
|
-
// retrieves single Post and Author from database
|
|
671
|
-
post.Rating++;
|
|
672
|
-
if (post.Author.Name === "Chris") {
|
|
673
|
-
// now retrieve parent post from database, if it exists
|
|
674
|
-
post.Parent.$load(function (error, parent) {
|
|
675
|
-
if (parent)
|
|
676
|
-
parent.Rating++;
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
The $load() function asynchronously retrieved that target instance or
|
|
682
|
-
instances from the database and passes them to the callback function.
|
|
683
|
-
Additionally, the target value is added to the association parent
|
|
684
|
-
instance.
|
|
685
|
-
|
|
686
|
-
Note that updates to an entity instance will not update
|
|
687
|
-
associated lazy instances if they haven’t been followed yet!
|
|
688
|
-
|
|
689
|
-
A lot may happen between the retrieval of some entity instance and the
|
|
690
|
-
navigation to any of its lazy association targets. It is left to the
|
|
691
|
-
application to ensure the consistency of data.
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
Unmanaged Queries
|
|
695
|
-
-----------------
|
|
696
|
-
|
|
697
|
-
In unmanaged mode we work with plain structured values retrieved from
|
|
698
|
-
HANA by arbitrary queries. Unlike in managed mode, these general
|
|
699
|
-
queries support all of the advanced HANA functionality for retrieving
|
|
700
|
-
data.
|
|
701
|
-
|
|
702
|
-
A general query related to an entity is built by calling the `$query()`
|
|
703
|
-
method of the entity function:
|
|
704
|
-
|
|
705
|
-
var qPosts = Post.$query();
|
|
706
|
-
|
|
707
|
-
The resulting object returned by `$query()` represents an initial query
|
|
708
|
-
for all of the fields of the underlying entity, but without any
|
|
709
|
-
associated entities.
|
|
710
|
-
|
|
711
|
-
Queries may be built from existing queries by chaining `$where`,
|
|
712
|
-
`$matching`, `$project`, and further operators. With these operators,
|
|
713
|
-
results can be constructed incrementally without accessing the
|
|
714
|
-
database for intermediate results. A typical query construction could
|
|
715
|
-
look as follows:
|
|
716
|
-
|
|
717
|
-
var qInterestingPosts = Post.$query().$where(/* condition */)
|
|
718
|
-
.$where(/* further condition */)
|
|
719
|
-
.$project(/* projection paths */)
|
|
720
|
-
.$limit(5);
|
|
721
|
-
|
|
722
|
-
The final query is executed by invoking its `$execute()` method, and is
|
|
723
|
-
the only asynchronous operation in unmanaged mode:
|
|
724
|
-
|
|
725
|
-
qInterestingPosts.$execute(tx, function (error, result) { … });
|
|
726
|
-
|
|
727
|
-
It is important to note again that the result of the query is a plain
|
|
728
|
-
value, not an entity instance managed by node-cds.
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
### Projections
|
|
732
|
-
|
|
733
|
-
The `$project()` method specifies the fields the query should return:
|
|
734
|
-
|
|
735
|
-
var qTitleAndComments = Post.$query().$project({
|
|
736
|
-
Title: true,
|
|
737
|
-
Comments: {
|
|
738
|
-
Author: {
|
|
739
|
-
Name: "CommentAuthorName"
|
|
740
|
-
},
|
|
741
|
-
Text: {
|
|
742
|
-
Text: true
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
});
|
|
746
|
-
qTitleAndComments.$execute(tx, function (error, result) { … });
|
|
747
|
-
|
|
748
|
-
The list of projected fields is a JavaScript object, where desired
|
|
749
|
-
fields are marked by either true or a String literal such as
|
|
750
|
-
`CommentAuthorName` denoting an alias name. Above query may thus
|
|
751
|
-
yield the result:
|
|
752
|
-
|
|
753
|
-
{
|
|
754
|
-
Title: "First Post!",
|
|
755
|
-
Comments: {
|
|
756
|
-
Author: {
|
|
757
|
-
CommentAuthorName: "Bob"
|
|
758
|
-
},
|
|
759
|
-
Text: {
|
|
760
|
-
Text: "Can we prevent this by software?"
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
Note that the actual database query automatically `JOIN`s all required
|
|
766
|
-
tables based on the associations involved. For above example, the
|
|
767
|
-
generated SQL looks like (omitting the package prefix from the table
|
|
768
|
-
name for readability):
|
|
769
|
-
|
|
770
|
-
SELECT "t0"."Title" AS "t0.Title",
|
|
771
|
-
"t0.Comments.Author"."Name" AS "t0.Comments.Author.Name",
|
|
772
|
-
"t0.Comments"."Text.Text" AS "t0.Comments.Text,Text"
|
|
773
|
-
FROM "bboard.post" "t0"
|
|
774
|
-
LEFT OUTER JOIN "bboard.comment" "t0.Comments" ON "t0"."pid"="t0.Comments"."Post.pid"
|
|
775
|
-
LEFT OUTER JOIN "bboard.user" "t0.Comments.Author" ON
|
|
776
|
-
"t0.Comments"."Author.uid"="t0.Comments.Author"."uid"
|
|
777
|
-
LEFT OUTER JOIN "bboard.user" "t0.Author" ON "t0"."Author.uid"="t0.Author"."uid"
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
### Selections using $where
|
|
781
|
-
|
|
782
|
-
The `$where()` method filters the query result by some conditional
|
|
783
|
-
expression. For example, to select all posts which were commented by
|
|
784
|
-
a person with the same name as the post author, we write:
|
|
785
|
-
|
|
786
|
-
var qStrangePosts = qTitleAndComments.$where(
|
|
787
|
-
Post.Comments.Author.Name.$eq(Post.Author.Name));
|
|
788
|
-
|
|
789
|
-
References to fields and associations such as `Comments` are available
|
|
790
|
-
as properties of the entity object, e.g., `Post.Comments`. As in the
|
|
791
|
-
case with projections, node-cds generates all required `JOIN`s for
|
|
792
|
-
associations referenced by the conditions automatically, even if they
|
|
793
|
-
are not part of the current projection.
|
|
794
|
-
|
|
795
|
-
To build complex query expressions, the node-cds expression language
|
|
796
|
-
provides a number of predefined operators that work on all data types:
|
|
797
|
-
|
|
798
|
-
* `$eq`, `$ne` for SQL equality and inequality, resp.
|
|
799
|
-
* `$gt`, `$lt`, `$gt`, `$le` for the SQL operators `>`, `<`, `<=`, `>=`, resp.
|
|
800
|
-
* `$null` for the SQL operator `IS NULL`
|
|
801
|
-
* `$like`, $unlike for the SQL operators `LIKE` and `NOT LIKE`
|
|
802
|
-
* `$and`, $or for SQL junctors `AND` and `OR`
|
|
803
|
-
|
|
804
|
-
A more complex selection statement could thus look like
|
|
805
|
-
|
|
806
|
-
Post.$query().$where(
|
|
807
|
-
Post.Tags.Name.$eq("+1").$and(Post.Rating.$lt(2).$or(Post.Rating.$gt(5))))
|
|
808
|
-
|
|
809
|
-
yielding the following SQL query:
|
|
810
|
-
|
|
811
|
-
SELECT "t0"."Created" AS "t0.Created",
|
|
812
|
-
"t0"."Rating" AS "t0.Rating",
|
|
813
|
-
"t0"."Parent.pid" AS "t0.Parent,pid",
|
|
814
|
-
"t0"."Author.uid" AS "t0.Author,uid",
|
|
815
|
-
"t0"."Text.Lang" AS "t0.Text,Lang",
|
|
816
|
-
"t0"."Text.Text" AS "t0.Text,Text",
|
|
817
|
-
"t0"."Title" AS "t0.Title",
|
|
818
|
-
"t0"."pid" AS "t0.pid"
|
|
819
|
-
FROM "bboard.post" "t0"
|
|
820
|
-
LEFT OUTER JOIN "bboard.tags_by_post" "t0.Tags$viaEntity"
|
|
821
|
-
ON "t0.Tags$viaEntity"."Post.pid" = "t0"."pid"
|
|
822
|
-
LEFT OUTER JOIN "bboard.tag" "t0.Tags" ON "t0.Tags$viaEntity"."Tag.tid" = "t0.Tags"."tid"
|
|
823
|
-
WHERE ("t0.Tags"."Name" = '+1') AND (("t0"."Rating" < 2) OR ("t0"."Rating" > 5))
|
|
824
|
-
|
|
825
|
-
For other SQL operators not part of the node-cds expression language
|
|
826
|
-
you may use generic operators such as $prefixOp
|
|
827
|
-
|
|
828
|
-
qStrangePosts = qStrangePosts.$where(Post.Rating.$prefixOp("SQRT").$gt(1));
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
### Selections using $matching
|
|
832
|
-
|
|
833
|
-
The `$matching()` method provides an alternative way to specify
|
|
834
|
-
conditional expressions using the JSON-like syntax of the `$find()`
|
|
835
|
-
method (see above).
|
|
836
|
-
|
|
837
|
-
var q1 = Posts.$query().$matching(
|
|
838
|
-
{ Rating: { $gt: 2 });
|
|
839
|
-
var q2 = Posts.$query().$matching(
|
|
840
|
-
{ Rating: { $ge: 1, $lt: 5 }, Parent: { $null: false } });
|
|
841
|
-
|
|
842
|
-
The main difference between $matching() and $findAll() is that the
|
|
843
|
-
former returns an unmanaged, plain value and ignores all unpersisted
|
|
844
|
-
changes to any entity instances.
|
|
845
|
-
|
|
846
|
-
We can think of the JSON-like conditional expression as a “template”
|
|
847
|
-
that the result should match. Compared to the node-cds expression
|
|
848
|
-
language used by `$where()`, the matching syntax is more concise, but
|
|
849
|
-
also less expressive. Also note that the expression language does not
|
|
850
|
-
apply to managed queries, e.g., to `$find()`.
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
### Calculated Fields and Aggregations
|
|
854
|
-
|
|
855
|
-
Arbitrary calculated values may be added to the result set by using
|
|
856
|
-
the $addFields() method. As an example, we return the square root of
|
|
857
|
-
the post’s rating as an additional field `MyRating`:
|
|
858
|
-
|
|
859
|
-
var qRootedRatings = Posts.$query().$addFields(
|
|
860
|
-
{ MyRating: Post.Rating.$prefixOp("SQRT") });
|
|
861
|
-
|
|
862
|
-
Aggregations are a special case of calculated fields that combine the
|
|
863
|
-
$addFields operator with an additional $aggregate() method. For
|
|
864
|
-
example, to retrieve the average rating per user, we would write:
|
|
865
|
-
|
|
866
|
-
var qUserRating = Post.$query()
|
|
867
|
-
.$aggregate({ Author: { uid: true, Name: true } })
|
|
868
|
-
.$addFields({ AverageRating: Post.Rating.$avg() });
|
|
869
|
-
|
|
870
|
-
In SQL terms, the `$aggregate` operator creates a `GROUP BY` expression
|
|
871
|
-
for the specified paths and automatically projects the result on
|
|
872
|
-
those. For an even more restrictive projection you may replace the
|
|
873
|
-
true by a false in the `$aggregate` call:
|
|
874
|
-
|
|
875
|
-
var qUserRating = Post.$query()
|
|
876
|
-
.$aggregate({ Author: { uid: false, Name: true } })
|
|
877
|
-
.$addFields({ AverageRating: Post.Rating.$avg() });
|
|
878
|
-
|
|
879
|
-
This will remove the users’ IDs from the in the result set.
|
|
880
|
-
|
|
881
|
-
Currently, node-cds supports aggregation operators `$avg`, `$sum`, `$count`,
|
|
882
|
-
`$max`, and `$min`.
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
### Ordering, Size Limits, and Duplicate Elimination
|
|
886
|
-
|
|
887
|
-
The order of the result set is defined by the `$order()` method. Each
|
|
888
|
-
order criteria contains a property by with an expression according
|
|
889
|
-
which to order. Optionally each criterion can contain a flag desc to
|
|
890
|
-
require a descending order and a nullsLast flag. The following
|
|
891
|
-
example uses two criteria to first order descending by rating and then
|
|
892
|
-
order ascending by author name:
|
|
893
|
-
|
|
894
|
-
var qOrderedPosts = Post.$query()
|
|
895
|
-
.$order({ $by: Post.Rating, $desc: true },
|
|
896
|
-
{ $by: Post.Author.Name });
|
|
897
|
-
|
|
898
|
-
The $distinct operator removes duplicates from the result set. To get
|
|
899
|
-
the set of all ratings we can write:
|
|
900
|
-
|
|
901
|
-
var qAllRatings = Post.$query().$project({ Rating: true }).$distinct();
|
|
902
|
-
|
|
903
|
-
The number of records returned may be specified by using the $limit
|
|
904
|
-
operator, which introduces the `LIMIT` and `OFFSET` keywords into the SQL
|
|
905
|
-
query:
|
|
906
|
-
|
|
907
|
-
// skip posts 1-3, return posts 4-8
|
|
908
|
-
var qNextFivePosts = qStrangePosts.$limit(5, 3);
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
Entity Manegement and Consistency
|
|
912
|
-
---------------------------------
|
|
913
|
-
|
|
914
|
-
Entities retrieved from the database are stored in the entity manager
|
|
915
|
-
cache. Any subsequent query for that entity will be served from the
|
|
916
|
-
cache instead of the database.
|
|
917
|
-
|
|
918
|
-
It is important to realize that if we modify an entity instance in
|
|
919
|
-
memory, then all node-cds queries for that entity instance through
|
|
920
|
-
`$get()` and `$find()` will return the modified, in-memory version of the
|
|
921
|
-
entity, even if it hasn’t been persisted to the database yet.
|
|
922
|
-
|
|
923
|
-
// assume post #1 and post #2 share same author alice@sap.com
|
|
924
|
-
var post1 = tx.$get(Post, { pid: 1 });
|
|
925
|
-
post1.Author.Email = "alice@saphana.com";
|
|
926
|
-
var post2 = tx.$get(Post, { pid: 2 });
|
|
927
|
-
|
|
928
|
-
In above example, the value of `post2.Author.Email` equals the new value
|
|
929
|
-
`alice@saphana.com`, even though post1 has not been `$save()`ed yet.
|
|
930
|
-
|
|
931
|
-
An unmanaged query, on the other hand, will ignore unpersisted changes
|
|
932
|
-
to our data and return the database view instead, so continuing the
|
|
933
|
-
example from above,
|
|
934
|
-
|
|
935
|
-
var post2_value = Post.$query().$matching({ pid: 2 }).$execute();
|
|
936
|
-
|
|
937
|
-
will yield `post2_value.Author.Email === "alice@hana.com"`.
|
|
938
|
-
|
|
939
|
-
You may use a transaction rollback to revert to the last committed
|
|
940
|
-
state of your data, irrespective of whether data was persisted or not
|
|
941
|
-
(see below).
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
### Associations
|
|
945
|
-
|
|
946
|
-
There are some additional subtleties about the consistency of
|
|
947
|
-
CDS-based associations that impose certain restrictions on using
|
|
948
|
-
backlinks and entity links in managed mode.
|
|
949
|
-
|
|
950
|
-
For backlink associations, the associated instances are stored in an
|
|
951
|
-
array in the parent instance:
|
|
952
|
-
|
|
953
|
-
tx.$get(Post, { pid: 1 }, function (error, post) {
|
|
954
|
-
post.Comments.forEach(function (comment) { … });
|
|
955
|
-
});
|
|
956
|
-
|
|
957
|
-
For any given target instance `t` with backlink `b` there are two ways to
|
|
958
|
-
express that `t` is a target of backlink association `a` of parent `p`:
|
|
959
|
-
|
|
960
|
-
1. `t` is a member of `p.a`
|
|
961
|
-
2. `t.b` equals `p`
|
|
962
|
-
|
|
963
|
-
node-cds uses (2) exclusively to express the relationship between p
|
|
964
|
-
and t. Supporting association updates through the association array,
|
|
965
|
-
i.e., option (1), would impose an unduly amount of runtime overhead
|
|
966
|
-
on the application and may lead to conflicting membership states.
|
|
967
|
-
|
|
968
|
-
For convenience, however, the `$save` function will set the backlink
|
|
969
|
-
of newly created targets currently stored in `p.a` to `p`. Thus,
|
|
970
|
-
`p.a` may be used to add newly created instances, but it cannot be
|
|
971
|
-
used to add or remove already existing instances.
|
|
972
|
-
|
|
973
|
-
As a consequence of the above, updating an instance with backlinks
|
|
974
|
-
may lead to situations where `p.a` contains elements that are not part of
|
|
975
|
-
the association or where `p.a` misses elements that are part of the
|
|
976
|
-
association. To re-sync the association array `p.a` all backlink
|
|
977
|
-
associations have a `$reload()` function that will update the
|
|
978
|
-
current array:
|
|
979
|
-
|
|
980
|
-
tx.$get(Post, { pid: 1 }, function (error, post) {
|
|
981
|
-
var firstComment = post.Comments[0];
|
|
982
|
-
firstComment.Parent = 0; // remove first comment
|
|
983
|
-
post.Comments.$reload(function (error, comments) {
|
|
984
|
-
// comments == post.Comments
|
|
985
|
-
// firstComment not in comments
|
|
986
|
-
});
|
|
987
|
-
});
|
|
988
|
-
|
|
989
|
-
Saving an instance with backlinks will update all array members
|
|
990
|
-
individually, but this will not change their backlinks. Thus, simply
|
|
991
|
-
adding and/or removing members from the association array and
|
|
992
|
-
`$save()`ing the parent will not change the relationship between parent
|
|
993
|
-
and target instances.
|
|
994
|
-
|
|
995
|
-
tx.$get(Post, { pid: 1 }, function (error, post) {
|
|
996
|
-
post.Comments.splice(1); // incorrect: won’t remove comment
|
|
997
|
-
tx.$save(post, function (error, updatedPost) {
|
|
998
|
-
// $reload restores first comment back to post.Comments
|
|
999
|
-
});
|
|
1000
|
-
});
|
|
1001
|
-
|
|
1002
|
-
When saving an instance with backlink associations, the `$reload()`
|
|
1003
|
-
function will be invoked automatically.
|
|
1004
|
-
|
|
1005
|
-
For many-to-many associations, on the other hand, the association
|
|
1006
|
-
array is the primary means to control the membership of individual
|
|
1007
|
-
target instances:
|
|
1008
|
-
|
|
1009
|
-
tx.$find(Tag, { Text: "cool" }, function (error, tag) { // find certain tag
|
|
1010
|
-
tx.$get(Post, { pid: 1 }, function (error, post) { // get particular post
|
|
1011
|
-
post.Tags.push(tag); // attach tag to post
|
|
1012
|
-
tx.$save(post, callback); // update database
|
|
1013
|
-
});
|
|
1014
|
-
});
|
|
1015
|
-
|
|
1016
|
-
This example attaches the tag “cool” to the first post without
|
|
1017
|
-
modifying any already existing tags.
|
|
1018
|
-
|
|
1019
|
-
Note that the direct manipulation of linking entities is discouraged,
|
|
1020
|
-
as this may lead to inconsistencies between the views on the
|
|
1021
|
-
association arrays of the entities with many-to-many associations and
|
|
1022
|
-
their link tables:
|
|
1023
|
-
|
|
1024
|
-
post.Tags = [ tag1 ]; // post has one tag
|
|
1025
|
-
tx.$save({ $entity: TagsByPost,
|
|
1026
|
-
lid: 69, Tag: tag2, Post: post }, callback); // don’t do this!
|
|
1027
|
-
|
|
1028
|
-
Unmanaged associations are handled very similar to backlink
|
|
1029
|
-
associations in that the target association status cannot be
|
|
1030
|
-
controlled by array membership:
|
|
1031
|
-
|
|
1032
|
-
tx.$get(PostWithTopReplies, { pid: 101 }, function (error, thread) { … });
|
|
1033
|
-
|
|
1034
|
-
But unlike managed associations such as those defined by
|
|
1035
|
-
backlinks, an unmanaged association is static and ignores both updates
|
|
1036
|
-
to the database and to the cache unless reloaded explicitly.
|
|
1037
|
-
|
|
1038
|
-
var reply = thread[0];
|
|
1039
|
-
reply.Rating = 0;
|
|
1040
|
-
tx.$save(reply);
|
|
1041
|
-
|
|
1042
|
-
// reply still contained in thread.TopReplies even
|
|
1043
|
-
// though Rating >= 3 no longer holds
|
|
1044
|
-
var i1 = thread.TopReplies.indexOf(reply); // === 0
|
|
1045
|
-
|
|
1046
|
-
// explicit reload from database
|
|
1047
|
-
thread.TopReplies.$reload(function (error, instance) {
|
|
1048
|
-
// now reply is no longer contained in thread.TopReplies
|
|
1049
|
-
var i2 = thread.TopReplies.indexOf(reply); // === -1
|
|
1050
|
-
});
|
|
1051
|
-
|
|
1052
|
-
Saving the parent of an unmanaged association will also save all
|
|
1053
|
-
elements stored in the association array. Finally, before invoking
|
|
1054
|
-
the callback function, the `$reload` function of the parent is
|
|
1055
|
-
called.
|
|
1056
|
-
|
|
1057
|
-
Discarding an instance will delete the root instance, but not
|
|
1058
|
-
associated entities by default, even for one-to-one associations. If
|
|
1059
|
-
you do want to delete associated entities as well, you can add a
|
|
1060
|
-
$cascadeDiscard property to your entity import statement:
|
|
1061
|
-
|
|
1062
|
-
cds.$importEntity({
|
|
1063
|
-
$entity: "…",
|
|
1064
|
-
$fields: {
|
|
1065
|
-
MyAssoc: {
|
|
1066
|
-
$association: {
|
|
1067
|
-
…
|
|
1068
|
-
$cascadeDiscard: true
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
}, callback);
|
|
1073
|
-
|
|
1074
|
-
node-cds supports explicit cascading for deletion only at the moment.
|
|
1075
|
-
All other operations such as creation, tracking, and updates are
|
|
1076
|
-
always cascaded to all associated entities automatically.
|
|
1077
|
-
|
|
1078
|
-
Note that node-cds currently doesn’t support orphan removal. It is
|
|
1079
|
-
left to the application to maintain integrity of associations and
|
|
1080
|
-
references. You can let HANA help you there by defining key
|
|
1081
|
-
constraints for your associations.
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
Transactions
|
|
1085
|
-
------------
|
|
1086
|
-
|
|
1087
|
-
All node-cds instance operations are tied to transactions. To open a
|
|
1088
|
-
new transaction, call the corresponding node-cds function:
|
|
1089
|
-
|
|
1090
|
-
cds.$getTransaction(function (error, tx) { … });
|
|
1091
|
-
|
|
1092
|
-
By default, all `$save()` and `$discard()` operations will auto
|
|
1093
|
-
commit upon completion. To change this behavior you can set auto
|
|
1094
|
-
committing explicitly:
|
|
1095
|
-
|
|
1096
|
-
tx.$setAutoCommit(<boolean>);
|
|
1097
|
-
|
|
1098
|
-
When not in auto commit mode, transactions can be explicitly committed
|
|
1099
|
-
or rolled back:
|
|
1100
|
-
|
|
1101
|
-
tx.$commit(callback);
|
|
1102
|
-
tx.$rollback(callback);
|
|
1103
|
-
|
|
1104
|
-
If you call `$rollback`, the instance cache for that transaction will be
|
|
1105
|
-
reset. You need to make sure that you do not use any references pointing to
|
|
1106
|
-
obsolete instances predating the cache reset.
|
|
1107
|
-
|
|
1108
|
-
Note that auto commit refers to node-cds operations, not database operations.
|
|
1109
|
-
In other words, if you `$save` an entity with a single target entity the two
|
|
1110
|
-
low-level `INSERT` statements are grouped into a single node-cds transaction
|
|
1111
|
-
that will `commit` or `rollback` together.
|
|
1112
|
-
|
|
1113
|
-
When done, close and release the transaction by calling its `$close`
|
|
1114
|
-
method:
|
|
1115
|
-
|
|
1116
|
-
tx.$close();
|
|
1117
|
-
|
|
1118
|
-
If your application is passed an existing database connection you can
|
|
1119
|
-
pass the connection to $getTransaction():
|
|
1120
|
-
|
|
1121
|
-
cds.$getTransaction(<dbconn>, function (error, tx) { … });
|
|
1122
|
-
|
|
1123
|
-
This will reuse the existing connection instead of requesting a new
|
|
1124
|
-
connection from the pool. If `dbconn` is in state disconnected node-cds
|
|
1125
|
-
will try to open the connection. Note, however, that multiple calls
|
|
1126
|
-
using the same connection will also return the same transaction!
|
|
1127
|
-
|
|
1128
|
-
Calling `$close()` on a transaction tied to an existing database
|
|
1129
|
-
connection will not have any effects.
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
Support for geospatial HANA data types
|
|
1133
|
-
--------------------------------------
|
|
1134
|
-
|
|
1135
|
-
node-cds also provides handling of entities with spatial attributes:
|
|
1136
|
-
|
|
1137
|
-
entity attraction {
|
|
1138
|
-
...
|
|
1139
|
-
position: hana.ST_GEOMETRY(4326);
|
|
1140
|
-
};
|
|
1141
|
-
|
|
1142
|
-
For processing such entities node-cds uses an extension 'cds-queries-geo',
|
|
1143
|
-
which can be optinally be required and allows to seamlessly create/insert
|
|
1144
|
-
spatial objects and perform spatial functions with/on them in combination
|
|
1145
|
-
with normal node-cds query API. For example a query like
|
|
1146
|
-
|
|
1147
|
-
attractions.$query().$where( attractions.position.$distance( geo.stPoint([49.008981, 8.403897])).$le(1500))
|
|
1148
|
-
|
|
1149
|
-
would result in:
|
|
1150
|
-
|
|
1151
|
-
[
|
|
1152
|
-
{ "id":5 ,
|
|
1153
|
-
"name":"SAP Karlsruhe" ,
|
|
1154
|
-
"info":"SAP location Karlsruhe." ,
|
|
1155
|
-
"year":1999 ,
|
|
1156
|
-
"position": {"type":"Point","coordinates":[49.013143,8.424231]}
|
|
1157
|
-
},
|
|
1158
|
-
...
|
|
1159
|
-
]
|
|
1160
|
-
|
|
1161
|
-
The extension allows for converting the resulting spatial objects to
|
|
1162
|
-
[GeoJSON](http://geojson.org/) objects so that results can seamlessly
|
|
1163
|
-
be processed in JavaScript.
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
XSJS Compatibility Mode
|
|
1167
|
-
-----------------------
|
|
1168
|
-
|
|
1169
|
-
For code in the xsjs compatibility mode, an extension
|
|
1170
|
-
compatible with the XS Basic library XSDS can be used.
|
|
1171
|
-
|
|
1172
|
-
Note, that it is intended to be used *only within the XSJS
|
|
1173
|
-
Compatibility Layer*.
|
|
1174
|
-
|
|
1175
|
-
To initialize it from an xsjs file, it needs to be invoked as follows:
|
|
1176
|
-
|
|
1177
|
-
var conn = $.db.getConnection();
|
|
1178
|
-
var cds = $.require("cds").xsjs(conn);
|
|
1179
|
-
|
|
1180
|
-
Note, that the connection to the database is managed by the connection
|
|
1181
|
-
provided in the argument to the xsjs call. The connection object can
|
|
1182
|
-
be modified through the `cds.Transaction` object. E.g. you can close
|
|
1183
|
-
the connection by `cds.Transaction.$close();`.
|
|
1184
|
-
|
|
1185
|
-
Like in plain node-cds, entities can then be imported following the
|
|
1186
|
-
syntax and the synchronous style of XSDS:
|
|
1187
|
-
|
|
1188
|
-
var Post = cds.$importEntity("sap.hana.xs2.cds.examples.data", "bboard.post");
|
|
1189
|
-
|
|
1190
|
-
Note, that namespace and entity name are submitted as separate
|
|
1191
|
-
arguments. Moreover only one entity at a time is imported. In order to
|
|
1192
|
-
enhance the entity, a third argument can be passed, as demonstrated in
|
|
1193
|
-
the following example:
|
|
1194
|
-
|
|
1195
|
-
var Post = cds.$importEntity("sap.hana.xs2.cds.examples.data", "bboard.post",
|
|
1196
|
-
{ Comments:
|
|
1197
|
-
{ $association:
|
|
1198
|
-
{ $entity:
|
|
1199
|
-
"sap.hana.xs2.cds.examples.data::bboard.user",
|
|
1200
|
-
$viaBacklink: "Post"
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
});
|
|
1204
|
-
|
|
1205
|
-
Next, entities can be retrieved in managed or unmanaged mode, using
|
|
1206
|
-
the operations already known from plain node-cds, but in the
|
|
1207
|
-
synchronous approach of XSJS.
|
|
1208
|
-
|
|
1209
|
-
For example, the $get method of an entity will retrieve an entity
|
|
1210
|
-
instance by its key:
|
|
1211
|
-
|
|
1212
|
-
var post = Post.$get({ pid: 101 });
|
|
1213
|
-
|
|
1214
|
-
Creating a new instance is just as simple as invoking the 'new'
|
|
1215
|
-
operator and persisting the resulting object:
|
|
1216
|
-
|
|
1217
|
-
// create new post
|
|
1218
|
-
var newpost = new Post({ pid: 102,
|
|
1219
|
-
Title: "New Post",
|
|
1220
|
-
Text: { Text: "Hello BBoard!", Text: "EN" },
|
|
1221
|
-
Author: post.Author,
|
|
1222
|
-
Rating: 1,
|
|
1223
|
-
Created: new Date() });
|
|
1224
|
-
newpost.$save();
|
|
1225
|
-
|
|
1226
|
-
For complex ad-hoc queries the same query builder as in plain node-cds
|
|
1227
|
-
can be used. The $query method of the entity constructor returns a
|
|
1228
|
-
query object for the step-wise building of complex queries:
|
|
1229
|
-
|
|
1230
|
-
var results = Post.$query().$project({
|
|
1231
|
-
Author: {
|
|
1232
|
-
Name: true,
|
|
1233
|
-
Email: true
|
|
1234
|
-
},
|
|
1235
|
-
Title: true
|
|
1236
|
-
}).$execute();
|