@sap/cds 1.15.0 → 1.17.0
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 +19 -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 +175 -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/TUTORIAL.md +0 -1236
- package/dependencies +0 -56
- package/sapcds.manifest +0 -1747
package/manager.js
CHANGED
|
@@ -1,30 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
const async = require('async');
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
var transaction = require('./transaction');
|
|
3
|
+
const metadata = require('./metadata');
|
|
4
|
+
const exprs = require('./exprs');
|
|
5
|
+
const utils = require('./utils');
|
|
6
|
+
const Queue = require('./util/Queue');
|
|
7
|
+
const ppp = utils.ppp; // @DEBUG
|
|
8
|
+
const transaction = require('./transaction');
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
const logger = utils.logger;
|
|
12
11
|
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
// / CRUD operations ////////////////////////////////////////////////////////////
|
|
15
14
|
|
|
16
15
|
// cache for fully built entity instances; indexed by entityName x cacheId
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const instanceCache = {};
|
|
17
|
+
let nextInstanceId = 1; // keeping track of objects @DEBUG
|
|
19
18
|
|
|
20
19
|
exports.extensionPoints = {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
instanceMethods: function(im) {
|
|
21
|
+
return im;
|
|
22
|
+
},
|
|
24
23
|
}; // hook for extenders
|
|
25
24
|
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
// / retrieve ///////////////////////////////////////////////////////////////////
|
|
28
27
|
|
|
29
28
|
/*
|
|
30
29
|
* fetchInstance, _find: public $get, $findAll functions
|
|
@@ -45,50 +44,56 @@ exports.extensionPoints = {
|
|
|
45
44
|
*/
|
|
46
45
|
|
|
47
46
|
// central request queue for serializing CRUD operations
|
|
48
|
-
|
|
47
|
+
const queue = new Queue.Queue();
|
|
49
48
|
|
|
50
49
|
exports._get = _get;
|
|
51
|
-
exports._find = function
|
|
52
|
-
|
|
50
|
+
exports._find = function(client, entity, condition, callback) {
|
|
51
|
+
_get(client, entity, condition, callback, true);
|
|
53
52
|
};
|
|
54
53
|
|
|
55
54
|
// retrieve instance(s) from database
|
|
56
55
|
function _get(client, entity, key, callback, isFind) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
56
|
+
if (!('$_metadata' in entity)) {
|
|
57
|
+
return callback('not an entity');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const entityName = entity.$_metadata.entityName;
|
|
61
|
+
const pool = {}; // work in progress
|
|
62
|
+
logger.debug(`node-cds: getting ${ entityName } with key ${
|
|
63
|
+
JSON.stringify(projectOnKeys(entity, key))}`);
|
|
64
|
+
|
|
65
|
+
if (entity.$_metadata.isUnmanaged) {
|
|
66
|
+
return callback(`Cannot get instance of unmanaged entity ${ entityName}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// convert to instances and update cache
|
|
70
|
+
function done(err) {
|
|
71
|
+
logger.debug(`node-cds: ${ entityName
|
|
72
|
+
}${JSON.stringify(projectOnKeys(entity, key)) } retrieved`);
|
|
73
|
+
if (err) {
|
|
74
|
+
return callback(err);
|
|
75
|
+
}
|
|
67
76
|
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// add XS interface and wire up associations in wip skeletons, add to cache
|
|
76
|
-
for (var i in pool)
|
|
77
|
-
if (pool[i]) {
|
|
78
|
-
makeInstance(client, pool[i], pool, false);
|
|
79
|
-
}
|
|
77
|
+
// add XS interface and wire up associations in wip skeletons, add to cache
|
|
78
|
+
for (const i in pool) {
|
|
79
|
+
if (pool[i]) {
|
|
80
|
+
makeInstance(client, pool[i], pool, false);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
// return result via cache
|
|
85
|
+
const result = isFind ?
|
|
83
86
|
findCached(client, entity, key) :
|
|
84
|
-
getCached(client, entity, key, true);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
getCached(client, entity, key, true); // could be null
|
|
88
|
+
// indicates error
|
|
89
|
+
if (typeof result === 'string') {
|
|
90
|
+
return callback(result);
|
|
91
|
+
} else {
|
|
92
|
+
return callback(null, result);
|
|
89
93
|
}
|
|
94
|
+
}
|
|
90
95
|
|
|
91
|
-
|
|
96
|
+
fetchInstance(client, entity, key, pool, isFind, done);
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
|
|
@@ -96,698 +101,775 @@ function _get(client, entity, key, callback, isFind) {
|
|
|
96
101
|
|
|
97
102
|
// retrieve instance plus associations by JOIN
|
|
98
103
|
function fetchInstance(client, entity, key, pool, isFind, callback) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
104
|
+
const entityName = entity.$_metadata.entityName;
|
|
105
|
+
|
|
106
|
+
// request queue for fetching root and potential target instances
|
|
107
|
+
const todo = [];
|
|
108
|
+
|
|
109
|
+
// initiate actual request for root instance
|
|
110
|
+
const debugId = entityName;
|
|
111
|
+
if (isFind) {
|
|
112
|
+
todo.push({entity: entity, key: key, isFind: true});
|
|
113
|
+
} else {
|
|
114
|
+
todo.push({entity: entity, key: projectOnKeys(entity, key)});
|
|
115
|
+
}
|
|
116
|
+
queue.push(start, callback, debugId);
|
|
117
|
+
|
|
118
|
+
// main queue
|
|
119
|
+
|
|
120
|
+
function start(callback) {
|
|
121
|
+
async.whilst(pending, loop, callback);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function pending() {
|
|
125
|
+
return todo.length > 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// main query loop
|
|
129
|
+
function loop(callback) {
|
|
130
|
+
const item = todo.shift();
|
|
131
|
+
if (!item) {
|
|
132
|
+
throw new Error('*** ASSERT FAIL *** empty todo list');
|
|
110
133
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
134
|
+
if (item.itemId && item.itemId in pool) {
|
|
135
|
+
return callback(null);
|
|
136
|
+
} // already retrieved
|
|
137
|
+
logger.debug(`node-cds fetch loop: processing ${ item.itemId}`);
|
|
138
|
+
|
|
139
|
+
// check if cached
|
|
140
|
+
if (!item.isFind) {
|
|
141
|
+
const instance = getCached(client, entity, item.key);
|
|
142
|
+
if (instance && !instance.$_reload) {
|
|
143
|
+
return callback(null);
|
|
144
|
+
}
|
|
117
145
|
}
|
|
118
146
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
147
|
+
// optimize: pre-compute at import
|
|
148
|
+
const relations = metadata.computeRelations(item.entity);
|
|
149
|
+
query(item.entity, item.key, relations, callback);
|
|
150
|
+
}
|
|
122
151
|
|
|
123
|
-
|
|
124
|
-
function loop(callback) {
|
|
125
|
-
var item = todo.shift();
|
|
126
|
-
if (!item)
|
|
127
|
-
throw new Error("*** ASSERT FAIL *** empty todo list");
|
|
128
|
-
if (item.itemId && item.itemId in pool)
|
|
129
|
-
return callback(null); // already retrieved
|
|
130
|
-
logger.debug("node-cds fetch loop: processing " + item.itemId);
|
|
131
|
-
|
|
132
|
-
// check if cached
|
|
133
|
-
if (!item.isFind) {
|
|
134
|
-
var instance = getCached(client, entity, item.key);
|
|
135
|
-
if (instance && !instance.$_reload)
|
|
136
|
-
return callback(null);
|
|
137
|
-
}
|
|
152
|
+
// queue items
|
|
138
153
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
154
|
+
// query database for key
|
|
155
|
+
function query(entity, condition, relations, callback) {
|
|
156
|
+
if (condition.$_id) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`*** ASSERT FAIL *** query condition is instance: ${ ppp(condition)}`,
|
|
159
|
+
);
|
|
142
160
|
}
|
|
143
161
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
162
|
+
const q = entity.$query().$matching(condition).$project(relations.projection);
|
|
163
|
+
// logger.debug("node-cds SQL: " + q.$sql());
|
|
164
|
+
q.$execute(client, {$factorized: true, $noCommit: true}, function(err, skeletons) {
|
|
165
|
+
if (!err) {
|
|
166
|
+
logger.debug(`node-cds: query returns ${ skeletons.length } results`);
|
|
167
|
+
addSkeletonsToWipPool(entity, skeletons, relations.associations);
|
|
168
|
+
fetchRecursiveAssocs(skeletons, relations.cycles);
|
|
169
|
+
}
|
|
170
|
+
callback(err);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
150
173
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
callback(err);
|
|
160
|
-
});
|
|
161
|
-
}
|
|
174
|
+
// add skeletons to work-in-progress pool
|
|
175
|
+
function addSkeletonsToWipPool(entity, skeletons, associations) {
|
|
176
|
+
const _add = function(entity, skeletons, index) {
|
|
177
|
+
const skeleton = skeletons[index];
|
|
178
|
+
if (skeleton === null) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
162
181
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
skeleton.$__entity = entity;
|
|
189
|
-
pool[itemId] = skeleton;
|
|
190
|
-
logger.debug("node-cds: added skeleton to pool: " + itemId);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
};
|
|
182
|
+
const itemId = computeItemId(entity, skeleton);
|
|
183
|
+
// NOTE: this may happen when (1) an association is key and
|
|
184
|
+
// (2) the foreign-key does not point to a valid instance
|
|
185
|
+
if (itemId === null) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const instance = getCached(client, entity, skeleton);
|
|
190
|
+
if (instance && !instance.$_reload) {
|
|
191
|
+
if (!(itemId in pool)) {
|
|
192
|
+
pool[itemId] = instance;
|
|
193
|
+
} // add to wip pool to choke recursive gets
|
|
194
|
+
logger.debug(`node-cds: added cached instance to pool: ${ itemId}`);
|
|
195
|
+
} else {
|
|
196
|
+
if (itemId in pool) {
|
|
197
|
+
const existingSkeleton = pool[itemId];
|
|
198
|
+
skeletons[index] = existingSkeleton; // replace newcomer
|
|
199
|
+
logger.debug(`node-cds: replaced skeleton in pool: ${ itemId}`);
|
|
200
|
+
} else {
|
|
201
|
+
skeleton.$__entity = entity;
|
|
202
|
+
pool[itemId] = skeleton;
|
|
203
|
+
logger.debug(`node-cds: added skeleton to pool: ${ itemId}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
194
207
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
208
|
+
for (const i in skeletons) {
|
|
209
|
+
_add(entity, skeletons, i);
|
|
210
|
+
for (const a in associations) {
|
|
211
|
+
const assoc = associations[a];
|
|
212
|
+
const targetEntity = assoc.target;
|
|
213
|
+
const targetSkeletons = utils.getPropPathSet(skeletons[i], assoc.field);
|
|
214
|
+
if (!assoc.lazy) {
|
|
215
|
+
let hasTarget = false;
|
|
216
|
+
for (const j in targetSkeletons) {
|
|
217
|
+
_add(targetEntity, targetSkeletons, j);
|
|
218
|
+
hasTarget = true;
|
|
219
|
+
}
|
|
220
|
+
if (!hasTarget) {
|
|
221
|
+
utils.setPropPath(skeletons[i], assoc.field, assoc.toMany ? [] : null);
|
|
222
|
+
}
|
|
211
223
|
}
|
|
224
|
+
}
|
|
212
225
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// fetch targets of recursive associations
|
|
229
|
+
function fetchRecursiveAssocs(skeletons, cycles) {
|
|
230
|
+
for (const i in skeletons) {
|
|
231
|
+
for (const c in cycles) {
|
|
232
|
+
const entity = cycles[c].assoc.$class;
|
|
233
|
+
const links = utils.getPropPathSet(skeletons[i], cycles[c].field);
|
|
234
|
+
for (const f in links) {
|
|
235
|
+
const link = links[f];
|
|
236
|
+
if (link && !link.$_id) {
|
|
237
|
+
const itemId = computeItemId(entity, link);
|
|
238
|
+
todo.push({itemId: itemId, entity: entity, key: link});
|
|
239
|
+
logger.debug(`node-cds: requesting recursive assoc ${
|
|
240
|
+
entity.$_metadata.entityName }${JSON.stringify(link)}`);
|
|
241
|
+
}
|
|
230
242
|
}
|
|
243
|
+
}
|
|
231
244
|
}
|
|
232
|
-
|
|
245
|
+
}
|
|
233
246
|
}
|
|
234
247
|
|
|
235
248
|
// build instance from property skeleton, swap in cached instances,
|
|
236
249
|
// lock key properties, and add XS-specific interface components
|
|
237
250
|
function makeInstance(client, skeleton, pool, needsDeepCopy) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
251
|
+
const entity = skeleton.$__entity || skeleton.$_entity; // || skeleton.$
|
|
252
|
+
const cacheId = computeKeyId(entity, skeleton);
|
|
253
|
+
|
|
254
|
+
// check for cached instance
|
|
255
|
+
let instance = getCached(client, entity, skeleton);
|
|
256
|
+
if (instance) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// build instance from property skeleton
|
|
261
|
+
if (needsDeepCopy) {
|
|
262
|
+
instance = utils.deepCopy(skeleton);
|
|
263
|
+
} else {
|
|
264
|
+
instance = skeleton;
|
|
265
|
+
}
|
|
266
|
+
delete instance.$__entity;
|
|
267
|
+
|
|
268
|
+
logger.debug(`node-cds: making instance for ${ entity } : ${ cacheId}`);
|
|
269
|
+
makeAssociations(client, entity, instance, pool);
|
|
270
|
+
lockKeys(entity, instance);
|
|
271
|
+
addXSInterface(client, entity, instance);
|
|
272
|
+
|
|
273
|
+
addCache(client, instance, cacheId);
|
|
260
274
|
}
|
|
261
275
|
|
|
262
276
|
// build associations from skeletons in wip pool
|
|
263
277
|
function makeAssociations(client, entity, instance, pool) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
};
|
|
278
|
+
// load function for lazy associations
|
|
279
|
+
const load = function(entity, field, value) {
|
|
280
|
+
return function(cb) {
|
|
281
|
+
// load based on skeleton key information
|
|
282
|
+
client.$get(entity, value[field], function(err, target) {
|
|
283
|
+
value[field] = target;
|
|
284
|
+
cb(err, target);
|
|
285
|
+
});
|
|
273
286
|
};
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
v[f] = getFromWip(targetEntity, v[f]);
|
|
303
|
-
}
|
|
304
|
-
if (a.$viaBacklink || a.$on) {
|
|
305
|
-
utils.addInternalProp(v[f], "$reload", reloadInstance(client, entity, instance, p + f));
|
|
306
|
-
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// find existing instance or wip item for skeleton
|
|
290
|
+
const getFromWip = function(entity, skeleton) {
|
|
291
|
+
const itemId = computeItemId(entity, skeleton);
|
|
292
|
+
if (!itemId) {
|
|
293
|
+
return null;
|
|
294
|
+
} // missing target
|
|
295
|
+
return getCached(client, entity, skeleton) || pool[itemId] || null;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// process all associations
|
|
299
|
+
utils.forInstance(instance, entity.$_mapping, {
|
|
300
|
+
$association: function(p, f, v, m) {
|
|
301
|
+
const a = m[f].$association;
|
|
302
|
+
const targetEntity = a.$class;
|
|
303
|
+
if (typeof v[f] === 'undefined') {
|
|
304
|
+
const isToMany = utils.isToMany(a);
|
|
305
|
+
v[f] = isToMany ? [] : null; // @DESIGN
|
|
306
|
+
}
|
|
307
|
+
if (a.$lazy) {
|
|
308
|
+
utils.addInternalProp(v[f], '$load', load(targetEntity, f, v));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (utils.isArray(v[f])) {
|
|
312
|
+
const is = [];
|
|
313
|
+
for (const j in v[f]) {
|
|
314
|
+
is.push(getFromWip(targetEntity, v[f][j]));
|
|
307
315
|
}
|
|
308
|
-
|
|
316
|
+
v[f] = is;
|
|
317
|
+
} else {
|
|
318
|
+
v[f] = getFromWip(targetEntity, v[f]);
|
|
319
|
+
}
|
|
320
|
+
if (a.$viaBacklink || a.$on) {
|
|
321
|
+
utils.addInternalProp(
|
|
322
|
+
v[f],
|
|
323
|
+
'$reload',
|
|
324
|
+
reloadInstance(client, entity, instance, p + f),
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
});
|
|
309
329
|
}
|
|
310
330
|
|
|
311
331
|
// reload instance function, e.g., for unmanaged assocs
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
332
|
+
const reloadInstance = function(client, entity, instance, path) {
|
|
333
|
+
return function(cb) {
|
|
334
|
+
const reloadPool = {};
|
|
335
|
+
const key = projectOnKeys(entity, instance);
|
|
336
|
+
++instance.$_reload; // instance.$_reload = true;
|
|
337
|
+
|
|
338
|
+
function done(err) {
|
|
339
|
+
if (err) {
|
|
340
|
+
cb(err);
|
|
341
|
+
}
|
|
342
|
+
const itemId = computeItemId(entity, instance);
|
|
343
|
+
const reloadedInstance = reloadPool[itemId];
|
|
344
|
+
if (!reloadedInstance) {
|
|
345
|
+
throw new Error('*** ASSERT FAIL *** missing reload instance');
|
|
346
|
+
}
|
|
347
|
+
makeAssociations(client, entity, reloadedInstance, reloadPool);
|
|
348
|
+
const targets = utils.getPropPath(reloadedInstance, path) || [];
|
|
349
|
+
utils.setPropPath(instance, path, targets);
|
|
350
|
+
utils.addInternalProp(targets, '$reload',
|
|
351
|
+
reloadInstance(client, entity, instance, path)); // rearm reloader
|
|
352
|
+
--instance.$_reload; // delete(instance.$_reload);
|
|
353
|
+
|
|
354
|
+
for (const i in reloadPool) {
|
|
355
|
+
if (i !== itemId && reloadPool[i]) {
|
|
356
|
+
makeInstance(client, reloadPool[i], reloadPool, false);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
cb(null, targets); // pass only reloaded assoc target to callback
|
|
339
360
|
};
|
|
361
|
+
fetchInstance(client, entity, key, reloadPool, false, done);
|
|
362
|
+
};
|
|
340
363
|
};
|
|
341
364
|
|
|
342
365
|
// add XS1-style interface
|
|
343
366
|
function addXSInterface(client, entity, instance) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
367
|
+
Object.defineProperties(instance, exports.extensionPoints.instanceMethods({
|
|
368
|
+
$_id: {value: nextInstanceId++},
|
|
369
|
+
$_client: client,
|
|
370
|
+
$_entity: {value: entity},
|
|
371
|
+
$_tx: {value: 0, writable: true},
|
|
372
|
+
$_reload: {value: 0, writable: true},
|
|
373
|
+
}));
|
|
351
374
|
}
|
|
352
375
|
|
|
353
376
|
|
|
354
|
-
|
|
377
|
+
// / create/update /////////////////////////////////////////////////////////
|
|
355
378
|
|
|
356
379
|
exports._save = function(client, instance, callback) {
|
|
357
|
-
|
|
380
|
+
_save(client, instance.$_entity || instance.$entity, instance, callback);
|
|
358
381
|
};
|
|
359
382
|
|
|
360
383
|
// persist instance to database
|
|
361
384
|
function _save(client, entity, instance, callback) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
385
|
+
const _tx = Date.now();
|
|
386
|
+
const keys = []; const saves = []; const links = []; const reloads = [];
|
|
387
|
+
collectInstancesToSave(client, entity, instance, _tx, keys, saves, links, reloads);
|
|
388
|
+
logger.debug(`node-cds: saving instance ${ instance.$_id }, #keys=${
|
|
389
|
+
keys.length } #saves=${ saves.length } #links=${ links.length
|
|
390
|
+
} #reloads=${ reloads.length}`);
|
|
391
|
+
async.series([].concat(keys, saves, links, reloads), function(err) {
|
|
392
|
+
logger.debug(`node-cds: saving instance ${ instance.$_id } done`);
|
|
393
|
+
if (err) {
|
|
394
|
+
return callback(err);
|
|
395
|
+
}
|
|
396
|
+
if (client.$_autoCommit) {
|
|
397
|
+
client.$commit(function(err) {
|
|
398
|
+
callback(err, instance);
|
|
399
|
+
});
|
|
400
|
+
} else {
|
|
401
|
+
callback(err, instance);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
377
404
|
}
|
|
378
405
|
|
|
379
406
|
// (sync) recursively collect all instances that need to be updated
|
|
380
|
-
function collectInstancesToSave(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
407
|
+
function collectInstancesToSave(
|
|
408
|
+
client, entity, instance, _tx, keys, saves, links, reloads,
|
|
409
|
+
) {
|
|
410
|
+
if (instance.$_tx && instance.$_tx >= _tx) {
|
|
411
|
+
return;
|
|
412
|
+
} // already saved or to be saved
|
|
413
|
+
if (instance.$load) {
|
|
414
|
+
return;
|
|
415
|
+
} // don't attempt to save unresolved lazy associations
|
|
416
|
+
|
|
417
|
+
// (async) insert or update single instance
|
|
418
|
+
function upsert(entity, instance, isCreate, callback) {
|
|
419
|
+
const cloneForSave = createInstance(client, entity, instance);
|
|
420
|
+
const key = projectOnKeys(instance.$_entity, instance);
|
|
421
|
+
let query = entity.$query().$matching(key).$values(cloneForSave);
|
|
422
|
+
query = isCreate ? query.$insert() : query.$update();
|
|
423
|
+
query.$execute(client, {$noCommit: true}, callback);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// update via entity links
|
|
427
|
+
const updateLinks = function(entity, instance, assoc, targets, linkEntity, callback) {
|
|
428
|
+
const qs = [];
|
|
429
|
+
const sourceKey = projectOnKeys(entity, instance);
|
|
430
|
+
// delete old links
|
|
431
|
+
qs.push(function(cb) {
|
|
432
|
+
const link = {};
|
|
433
|
+
link[assoc.$source] = sourceKey;
|
|
434
|
+
linkEntity
|
|
435
|
+
.$query()
|
|
436
|
+
.$matching(link)
|
|
437
|
+
.$discard()
|
|
438
|
+
.$execute(client, {$noCommit: true}, cb);
|
|
439
|
+
});
|
|
440
|
+
// create new links
|
|
441
|
+
for (let i = 0; i < targets.length; ++i) {
|
|
442
|
+
qs.push((function(t) {
|
|
443
|
+
return function(cb) {
|
|
444
|
+
const link = {}; // must create unique object in each loop iteration
|
|
445
|
+
link[assoc.$source] = sourceKey;
|
|
446
|
+
link[assoc.$target] = t;
|
|
447
|
+
linkEntity
|
|
448
|
+
.$query()
|
|
449
|
+
.$values(link)
|
|
450
|
+
.$insert()
|
|
451
|
+
.$execute(client, {$noCommit: true}, cb);
|
|
452
|
+
};
|
|
453
|
+
})(targets[i]));
|
|
393
454
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
455
|
+
async.series(qs, callback);
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const isCreate = typeof instance.$_id === 'undefined';
|
|
459
|
+
const isUnmanaged = entity.$_metadata.isUnmanaged;
|
|
460
|
+
if (isCreate) {
|
|
461
|
+
Object.defineProperty(instance, '$_tx', {value: 0, writable: true});
|
|
462
|
+
}
|
|
463
|
+
instance.$_tx = _tx;
|
|
464
|
+
|
|
465
|
+
// collect associated instances
|
|
466
|
+
utils.forInstance(instance, entity.$_mapping, {
|
|
467
|
+
$column: function(p, f, v, m) {
|
|
468
|
+
// generate missing keys
|
|
469
|
+
if (typeof m[f].$key === 'string' && v && typeof v[f] === 'undefined') {
|
|
470
|
+
// v[f] = { $key: utils.quoteTable(m[f].$key };
|
|
471
|
+
// @NOTE: The current architecture doesn't support the creation of keys
|
|
472
|
+
// during INSERT, as the application wouldn't know about the
|
|
473
|
+
// key values. We thus have to retrieve the keys manually from
|
|
474
|
+
// the database sequence.
|
|
475
|
+
keys.push(function(cb) {
|
|
476
|
+
querySequence(m[f].$key, function(err, seq) {
|
|
477
|
+
v[f] = seq;
|
|
478
|
+
cb(err);
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
$association: function(p, f, v, m, e) {
|
|
484
|
+
if (isUnmanaged || v[f] === null) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const assoc = m[f].$association;
|
|
488
|
+
// fix missing target property
|
|
489
|
+
if (typeof v[f] === 'undefined') {
|
|
490
|
+
v[f] = utils.isToMany(assoc) ? [] : null;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const targets = utils.isArray(v[f]) ? v[f] : [v[f]];
|
|
494
|
+
if (assoc.$viaEntity) {
|
|
495
|
+
const linkEntity = assoc.$viaClass;
|
|
496
|
+
links.push(function(cb) {
|
|
497
|
+
updateLinks(entity, instance, assoc, targets, linkEntity, cb);
|
|
404
498
|
});
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
499
|
+
} else if (assoc.$viaBacklink) {
|
|
500
|
+
// update backlink in children (thus adding children to association)
|
|
501
|
+
// only if instance is newly created -- updating backlinks unconditionally
|
|
502
|
+
// would pose potential conflict when updating multiple children in
|
|
503
|
+
// parallel (see spec text case "save backlinks/parallel child update")
|
|
504
|
+
if (isCreate) {
|
|
505
|
+
logger.debug(`node-cds: updating target backlinks for ${ instance.$_id}`);
|
|
506
|
+
const backlink = assoc.$viaBacklink;
|
|
507
|
+
for (const i in targets) {
|
|
508
|
+
utils.setPropPath(targets[i], backlink, instance);
|
|
509
|
+
}
|
|
413
510
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
// collect associated instances
|
|
424
|
-
utils.forInstance(instance, entity.$_mapping, {
|
|
425
|
-
$column: function (p, f, v, m) {
|
|
426
|
-
// generate missing keys
|
|
427
|
-
if (typeof m[f].$key === "string" && v && typeof v[f] === "undefined") {
|
|
428
|
-
//v[f] = { $key: utils.quoteTable(m[f].$key };
|
|
429
|
-
//@NOTE: The current architecture doesn't support the creation of keys
|
|
430
|
-
// during INSERT, as the application wouldn't know about the
|
|
431
|
-
// key values. We thus have to retrieve the keys manually from
|
|
432
|
-
// the database sequence.
|
|
433
|
-
keys.push(function (cb) {
|
|
434
|
-
querySequence(m[f].$key, function (err, seq) {
|
|
435
|
-
v[f] = seq;
|
|
436
|
-
cb(err);
|
|
437
|
-
});
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
},
|
|
441
|
-
$association: function(p, f, v, m, e) {
|
|
442
|
-
if (isUnmanaged || v[f] === null)
|
|
443
|
-
return;
|
|
444
|
-
var assoc = m[f].$association;
|
|
445
|
-
// fix missing target property
|
|
446
|
-
if (typeof v[f] === "undefined")
|
|
447
|
-
v[f] = utils.isToMany(assoc) ? [] : null;
|
|
448
|
-
|
|
449
|
-
var targets = utils.isArray(v[f]) ? v[f] : [v[f]];
|
|
450
|
-
if (assoc.$viaEntity) {
|
|
451
|
-
var linkEntity = assoc.$viaClass;
|
|
452
|
-
links.push(function(cb) {
|
|
453
|
-
updateLinks(entity, instance, assoc, targets, linkEntity, cb);
|
|
454
|
-
});
|
|
455
|
-
} else if (assoc.$viaBacklink) {
|
|
456
|
-
// update backlink in children (thus adding children to association)
|
|
457
|
-
// only if instance is newly created -- updating backlinks unconditionally
|
|
458
|
-
// would pose potential conflict when updating multiple children in
|
|
459
|
-
// parallel (see spec text case "save backlinks/parallel child update")
|
|
460
|
-
if (isCreate) {
|
|
461
|
-
logger.debug("node-cds: updating target backlinks for " + instance.$_id);
|
|
462
|
-
var backlink = assoc.$viaBacklink;
|
|
463
|
-
for (var i in targets)
|
|
464
|
-
utils.setPropPath(targets[i], backlink, instance);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// add reload functions
|
|
469
|
-
if (assoc.$viaBacklink || assoc.$on) {
|
|
470
|
-
if (!v[f].$reload)
|
|
471
|
-
utils.addInternalProp(v[f], "$reload", reloadInstance(client, entity, instance, p + f));
|
|
472
|
-
// trigger reload of unmanaged associations
|
|
473
|
-
reloads.push(function (cb) {
|
|
474
|
-
if (v[f].$reload)
|
|
475
|
-
v[f].$reload(cb);
|
|
476
|
-
else
|
|
477
|
-
logger.warn("node-cds: missing $reload function"); // removed by user
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// save target instances
|
|
482
|
-
for (var i in targets)
|
|
483
|
-
collectInstancesToSave(client, assoc.$class, targets[i], _tx, keys, saves, links, reloads);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// add reload functions
|
|
514
|
+
if (assoc.$viaBacklink || assoc.$on) {
|
|
515
|
+
if (!v[f].$reload) {
|
|
516
|
+
utils.addInternalProp(
|
|
517
|
+
v[f], '$reload', reloadInstance(client, entity, instance, p + f),
|
|
518
|
+
);
|
|
484
519
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
520
|
+
// trigger reload of unmanaged associations
|
|
521
|
+
reloads.push(function(cb) {
|
|
522
|
+
if (v[f].$reload) {
|
|
523
|
+
v[f].$reload(cb);
|
|
524
|
+
} else {
|
|
525
|
+
logger.warn('node-cds: missing $reload function');
|
|
526
|
+
} // removed by user
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// save target instances
|
|
531
|
+
for (const i in targets) {
|
|
532
|
+
collectInstancesToSave(
|
|
533
|
+
client, assoc.$class, targets[i], _tx, keys, saves, links, reloads,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// register upsert of root instance
|
|
540
|
+
saves.push(function(cb) {
|
|
541
|
+
upsert(entity, instance, isCreate, cb);
|
|
542
|
+
});
|
|
491
543
|
}
|
|
492
544
|
|
|
493
545
|
// like makeInstance but specifically for save
|
|
494
546
|
function createInstance(client, entity, skeleton) {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
547
|
+
// missing target instance?
|
|
548
|
+
if (skeleton === undefined) {
|
|
549
|
+
// e.g., missing assoc target from get
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
// clone instance for $query, which might get confused
|
|
555
|
+
const colValueClone = {};
|
|
556
|
+
utils.forInstance(skeleton, entity.$_mapping, {
|
|
557
|
+
$column: function(p, f, v, m) {
|
|
558
|
+
// clone column values
|
|
559
|
+
const x = v && f in v ? v[f] : undefined;
|
|
560
|
+
utils.setPropPath(colValueClone, p + f, x);
|
|
561
|
+
// lock keys
|
|
562
|
+
if (m[f].$key) {
|
|
563
|
+
Object.defineProperty(v, f, {writable: false});
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
if (!skeleton.$_id) {
|
|
569
|
+
// add XS-style interface}
|
|
570
|
+
Object.defineProperties(skeleton, exports.extensionPoints.instanceMethods({
|
|
571
|
+
$_id: {value: nextInstanceId++},
|
|
572
|
+
$_client: client,
|
|
573
|
+
$_entity: {value: entity},
|
|
574
|
+
$_reload: {value: 0, writable: true},
|
|
575
|
+
}));
|
|
511
576
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
$_client: client,
|
|
517
|
-
$_entity: {value: entity},
|
|
518
|
-
$_reload: {value: 0, writable: true},
|
|
519
|
-
}));
|
|
520
|
-
|
|
521
|
-
// add to cache
|
|
522
|
-
var cacheId = computeKeyId(entity, skeleton);
|
|
523
|
-
addCache(client, skeleton, cacheId);
|
|
524
|
-
}
|
|
577
|
+
// add to cache
|
|
578
|
+
const cacheId = computeKeyId(entity, skeleton);
|
|
579
|
+
addCache(client, skeleton, cacheId);
|
|
580
|
+
}
|
|
525
581
|
|
|
526
|
-
|
|
582
|
+
return colValueClone;
|
|
527
583
|
}
|
|
528
584
|
|
|
529
585
|
|
|
530
|
-
|
|
586
|
+
// / discard /////////////////////////////////////////////////////////
|
|
531
587
|
|
|
532
|
-
exports._discard = function
|
|
533
|
-
|
|
588
|
+
exports._discard = function(client, instance, callback) {
|
|
589
|
+
_discard(client, instance.$_entity || instance.$entity, instance, callback);
|
|
534
590
|
};
|
|
535
591
|
|
|
536
592
|
// discard instance(s) from database
|
|
537
593
|
function _discard(client, entity, instance, callback) {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
}
|
|
594
|
+
const _tx = Date.now();
|
|
595
|
+
const deletes = [];
|
|
596
|
+
const id = instance.$_id;
|
|
597
|
+
collectInstancesToDiscard(client, entity, instance, _tx, deletes);
|
|
598
|
+
logger.debug(`node-cds: discarding instance ${ id }, #deletes=${ deletes.length}`);
|
|
599
|
+
async.series(deletes, function(err) {
|
|
600
|
+
logger.debug(`node-cds: discarding of instance ${ id } complete`);
|
|
601
|
+
if (!err && client.$_autoCommit) {
|
|
602
|
+
client.$commit(callback);
|
|
603
|
+
} else {
|
|
604
|
+
callback(err);
|
|
605
|
+
} // strip result of series call
|
|
606
|
+
});
|
|
550
607
|
}
|
|
551
608
|
|
|
552
609
|
function collectInstancesToDiscard(client, entity, instance, _tx, deletes) {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
610
|
+
if (instance.$_tx && instance.$_tx >= _tx) {
|
|
611
|
+
return;
|
|
612
|
+
} // already marked for deletion
|
|
613
|
+
instance.$_tx = _tx;
|
|
614
|
+
|
|
615
|
+
function discard(entity, key) {
|
|
616
|
+
deletes.push(function(cb) {
|
|
617
|
+
const query = entity.$query().$matching(key).$discard();
|
|
618
|
+
query.$execute(client, {$noCommit: true}, cb);
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// discard root instance
|
|
623
|
+
removeCache(client, instance);
|
|
624
|
+
const key = projectOnKeys(entity, instance);
|
|
625
|
+
discard(entity, key);
|
|
626
|
+
|
|
627
|
+
// cascade discard to associated instances
|
|
628
|
+
utils.forInstance(instance, entity.$_mapping, {
|
|
629
|
+
$association: function(p, f, v, m, e) {
|
|
630
|
+
// delete via entity links
|
|
631
|
+
let linkEntity;
|
|
632
|
+
let link;
|
|
633
|
+
if (m[f].$association.$viaEntity) {
|
|
634
|
+
linkEntity = m[f].$association.$viaClass;
|
|
635
|
+
link = {};
|
|
636
|
+
link[m[f].$association.$source] = key;
|
|
637
|
+
discard(linkEntity, link);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (!m[f].$association.$cascadeDiscard) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
556
643
|
|
|
557
|
-
|
|
644
|
+
// discard unresolved lazy associations
|
|
645
|
+
if (m[f].$association.$lazy && v[f].$load) {
|
|
558
646
|
deletes.push(function(cb) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// discard root instance
|
|
565
|
-
removeCache(client, instance);
|
|
566
|
-
var key = projectOnKeys(entity, instance);
|
|
567
|
-
discard(entity, key);
|
|
568
|
-
|
|
569
|
-
// cascade discard to associated instances
|
|
570
|
-
utils.forInstance(instance, entity.$_mapping, {
|
|
571
|
-
$association: function (p, f, v, m, e) {
|
|
572
|
-
// delete via entity links
|
|
573
|
-
if (m[f].$association.$viaEntity) {
|
|
574
|
-
var linkEntity = m[f].$association.$viaClass;
|
|
575
|
-
var link = {};
|
|
576
|
-
link[m[f].$association.$source] = key;
|
|
577
|
-
discard(linkEntity, link);
|
|
647
|
+
v[f].$load(function(err, targets) {
|
|
648
|
+
if (err) {
|
|
649
|
+
return cb(err);
|
|
578
650
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
if (targets[i]) {
|
|
601
|
-
collectInstancesToDiscard(client, targetEntity, targets[i], _tx, deletes);
|
|
602
|
-
|
|
603
|
-
// delete backlinking via entity entries for discarded targets
|
|
604
|
-
if (m[f].$association.$viaEntity) {
|
|
605
|
-
link = {};
|
|
606
|
-
var targetKey = projectOnKeys(targetEntity, targets[i]);
|
|
607
|
-
link[m[f].$association.$target] = targetKey;
|
|
608
|
-
discard(linkEntity, link);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
651
|
+
const ts = utils.isArray(targets) ? targets : [targets];
|
|
652
|
+
client.$discardAll(ts, cb);
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// discard target instances
|
|
659
|
+
const targetEntity = m[f].$association.$class;
|
|
660
|
+
const targets = utils.isArray(v[f]) ? v[f] : [v[f]];
|
|
661
|
+
for (const i in targets) {
|
|
662
|
+
if (targets[i]) {
|
|
663
|
+
collectInstancesToDiscard(client, targetEntity, targets[i], _tx, deletes);
|
|
664
|
+
|
|
665
|
+
// delete backlinking via entity entries for discarded targets
|
|
666
|
+
if (m[f].$association.$viaEntity) {
|
|
667
|
+
link = {};
|
|
668
|
+
const targetKey = projectOnKeys(targetEntity, targets[i]);
|
|
669
|
+
link[m[f].$association.$target] = targetKey;
|
|
670
|
+
discard(linkEntity, link);
|
|
671
|
+
}
|
|
611
672
|
}
|
|
612
|
-
|
|
673
|
+
}
|
|
674
|
+
},
|
|
675
|
+
});
|
|
613
676
|
}
|
|
614
677
|
|
|
615
678
|
|
|
616
|
-
|
|
679
|
+
// / unmanaged operations ///////////////////////////////////////////////////////
|
|
617
680
|
|
|
618
681
|
// unmanaged delete: delete without associations, ignoring cache
|
|
619
|
-
exports._delete = function
|
|
620
|
-
|
|
621
|
-
|
|
682
|
+
exports._delete = function(client, entity, condition, callback) {
|
|
683
|
+
entity.$query().$matching(condition).$discard()
|
|
684
|
+
.$execute(client, function(err) {
|
|
685
|
+
callback(err);
|
|
686
|
+
});
|
|
622
687
|
};
|
|
623
688
|
|
|
624
689
|
|
|
625
|
-
|
|
690
|
+
// / internal functions /////////////////////////////////////////////////////////
|
|
626
691
|
|
|
627
692
|
// install new cache for given transaction
|
|
628
693
|
exports.openCache = function(tx) {
|
|
629
|
-
|
|
630
|
-
|
|
694
|
+
instanceCache[tx.$_tx_id] = {};
|
|
695
|
+
exports._clearCaches(tx);
|
|
631
696
|
};
|
|
632
697
|
|
|
633
698
|
// purge and close cache for given transaction
|
|
634
699
|
exports.closeCache = function(tx) {
|
|
635
|
-
|
|
700
|
+
delete(instanceCache[tx.$_tx_id]);
|
|
636
701
|
};
|
|
637
702
|
|
|
638
703
|
// clear all caches (for testing)
|
|
639
704
|
exports._clearCaches = function(tx) {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
705
|
+
const entities = metadata.getKnownEntities();
|
|
706
|
+
for (const name in entities) {
|
|
707
|
+
instanceCache[tx.$_tx_id][name] = {};
|
|
708
|
+
}
|
|
643
709
|
};
|
|
644
710
|
|
|
645
711
|
// (sync) check for cached instance with given key properties
|
|
646
712
|
function getCached(tx, entity, props, reportErrors) {
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
713
|
+
const cacheId = computeKeyId(entity, props);
|
|
714
|
+
if (!cacheId) {
|
|
715
|
+
return reportErrors ? 'invalid key' : null;
|
|
716
|
+
}
|
|
717
|
+
const name = entity.$_metadata.entityName;
|
|
718
|
+
if (!(tx.$_tx_id in instanceCache)) {
|
|
719
|
+
throw new Error(`*** ASSERT FAIL *** missing cache for tx ${ tx.$_tx_id}`);
|
|
720
|
+
}
|
|
721
|
+
if (!(name in instanceCache[tx.$_tx_id])) {
|
|
722
|
+
throw new Error(`*** ASSERT FAIL *** unregistered entity ${ name}`);
|
|
723
|
+
}
|
|
724
|
+
return instanceCache[tx.$_tx_id][name][cacheId] || null;
|
|
656
725
|
}
|
|
657
726
|
|
|
658
727
|
function findCached(tx, entity, condition) {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
728
|
+
const name = entity.$_metadata.entityName;
|
|
729
|
+
const txid = tx.$_tx_id;
|
|
730
|
+
if (!(name in instanceCache[txid])) {
|
|
731
|
+
throw new Error(`*** ASSERT FAIL *** unregistered entity: ${ name}`);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// build filter function
|
|
735
|
+
const matches = exprs.buildInstanceFilter(entity, condition);
|
|
736
|
+
|
|
737
|
+
// apply filter to cache (synchronous)
|
|
738
|
+
const result = [];
|
|
739
|
+
try {
|
|
740
|
+
for (const i in instanceCache[txid][name]) {
|
|
741
|
+
if (matches(instanceCache[txid][name][i])) {
|
|
742
|
+
result.push(instanceCache[txid][name][i]);
|
|
743
|
+
}
|
|
675
744
|
}
|
|
745
|
+
} catch (e) {
|
|
746
|
+
return `Error: ${ e}`; // return type string -> error
|
|
747
|
+
}
|
|
676
748
|
|
|
677
|
-
|
|
749
|
+
return result;
|
|
678
750
|
}
|
|
679
751
|
|
|
680
752
|
function addCache(tx, instance, cacheId) {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
753
|
+
const entity = instance.$_entity;
|
|
754
|
+
if (entity === undefined) {
|
|
755
|
+
throw new Error('*** ASSERT FAIL *** not an entity instance');
|
|
756
|
+
}
|
|
757
|
+
const name = entity.$_metadata.entityName;
|
|
758
|
+
if (!cacheId) {
|
|
759
|
+
logger.warn(`node-cds: invalid key for instance of ${ name}`);
|
|
760
|
+
} else {
|
|
761
|
+
instanceCache[tx.$_tx_id][name][cacheId] = instance;
|
|
762
|
+
}
|
|
689
763
|
}
|
|
690
764
|
|
|
691
765
|
function removeCache(tx, instance) {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
766
|
+
const entity = instance.$_entity;
|
|
767
|
+
if (entity === undefined) {
|
|
768
|
+
return;
|
|
769
|
+
} // not an instance, but discard by key
|
|
770
|
+
|
|
771
|
+
const name = entity.$_metadata.entityName;
|
|
772
|
+
const cacheId = computeKeyId(entity, instance);
|
|
773
|
+
if (!cacheId) {
|
|
774
|
+
throw new Error('*** ASSERT FAIL *** invalid cache id');
|
|
775
|
+
}
|
|
776
|
+
delete instanceCache[tx.$_tx_id][name][cacheId];
|
|
701
777
|
}
|
|
702
778
|
|
|
703
779
|
function computeItemId(entity, properties) {
|
|
704
|
-
|
|
705
|
-
|
|
780
|
+
const cacheId = computeKeyId(entity, properties);
|
|
781
|
+
return cacheId ? `${entity.$_metadata.entityName }##${ cacheId}` : null;
|
|
706
782
|
}
|
|
707
783
|
|
|
708
784
|
function computeKeyId(entity, properties) {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
785
|
+
const keyValues = getKeyValues(entity, properties);
|
|
786
|
+
if (!keyValues) {
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
let id = '#';
|
|
790
|
+
for (const i in keyValues) {
|
|
791
|
+
id += hash(keyValues[i]);
|
|
792
|
+
}
|
|
793
|
+
return id;
|
|
716
794
|
}
|
|
717
795
|
|
|
718
796
|
function getKeyValues(entity, properties, relaxed) {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
797
|
+
const keyValues = [];
|
|
798
|
+
const keyFields = entity.$_metadata.keyFields;
|
|
799
|
+
for (const k in keyFields) {
|
|
800
|
+
const v = utils.getPropPath(properties, k);
|
|
801
|
+
if (typeof v === 'undefined' && !relaxed) {
|
|
802
|
+
return null;
|
|
803
|
+
} // incomplete key yields null if not in relaxed mode
|
|
804
|
+
keyValues.push(v);
|
|
805
|
+
}
|
|
806
|
+
return keyValues;
|
|
728
807
|
}
|
|
729
808
|
|
|
730
809
|
// remove non-(secondary-)key values from properties
|
|
731
810
|
function projectOnKeys(entity, instance) {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
}
|
|
739
|
-
|
|
811
|
+
const key = {};
|
|
812
|
+
utils.forInstance(instance, entity.$_mapping, {
|
|
813
|
+
$column: function(p, f, v, m) {
|
|
814
|
+
if (m[f].$key) {
|
|
815
|
+
utils.setPropPath(key, p + f, v[f]);
|
|
816
|
+
}
|
|
817
|
+
},
|
|
818
|
+
});
|
|
819
|
+
return key;
|
|
740
820
|
}
|
|
741
821
|
|
|
742
822
|
// lock keys
|
|
743
823
|
function lockKeys(entity, skeleton) {
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
}
|
|
824
|
+
utils.forInstance(skeleton, entity.$_mapping, {
|
|
825
|
+
$column: function(p, f, v, m) {
|
|
826
|
+
if (m[f].$key) {
|
|
827
|
+
Object.defineProperty(v, f, {writable: false});
|
|
828
|
+
}
|
|
829
|
+
},
|
|
830
|
+
});
|
|
750
831
|
}
|
|
751
832
|
|
|
752
833
|
function querySequence(key, callback) {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
834
|
+
logger.debug(`node-cds: query sequence ${ key}`);
|
|
835
|
+
transaction.getClient(null, function(err, client) {
|
|
836
|
+
if (err) {
|
|
837
|
+
return callback(err);
|
|
838
|
+
}
|
|
839
|
+
client.exec(`SELECT ${ key }.NEXTVAL AS V FROM DUMMY`, function(err, result) {
|
|
840
|
+
transaction.releaseClient(client);
|
|
841
|
+
logger.debug(`node-cds: received sequence value ${ result[0].V}`);
|
|
842
|
+
callback(err, result[0].V);
|
|
762
843
|
});
|
|
844
|
+
});
|
|
763
845
|
}
|
|
764
846
|
|
|
765
847
|
// simple hash function
|
|
766
848
|
function hash(data) {
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
849
|
+
switch (typeof data) {
|
|
850
|
+
case 'undefined':
|
|
851
|
+
return 'U';
|
|
852
|
+
case 'number':
|
|
853
|
+
return `N${ data}`;
|
|
854
|
+
case 'boolean':
|
|
855
|
+
return data ? 'B1' : 'B0';
|
|
856
|
+
case 'string':
|
|
857
|
+
const str = data.toString();
|
|
858
|
+
let hash = 0;
|
|
859
|
+
for (let i = 0; i < str.length; i++) {
|
|
860
|
+
const c = str.charCodeAt(i);
|
|
861
|
+
hash = c + (hash << 6) + (hash << 16) - hash;
|
|
862
|
+
}
|
|
863
|
+
return `S${ hash}`;
|
|
864
|
+
case 'object':
|
|
865
|
+
if (data === null) {
|
|
866
|
+
return 'Q';
|
|
867
|
+
}
|
|
868
|
+
if (data instanceof Date) {
|
|
869
|
+
return `D${ data.getTime()}`;
|
|
870
|
+
}
|
|
871
|
+
/* fall-through */
|
|
872
|
+
default:
|
|
873
|
+
}
|
|
874
|
+
throw new Error(`*** ASSERT FAIL *** hash: invalid type: ${ typeof data}`);
|
|
793
875
|
}
|