foxhound 2.0.18 → 2.0.19
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/package.json +2 -2
- package/source/Foxhound-Dialects.js +4 -0
- package/source/dialects/DGraph/FoxHound-Dialect-DGraph.js +954 -0
- package/source/dialects/MongoDB/FoxHound-Dialect-MongoDB.js +902 -0
- package/source/dialects/PostgreSQL/FoxHound-Dialect-PostgreSQL.js +865 -0
- package/source/dialects/Solr/FoxHound-Dialect-Solr.js +895 -0
- package/test/FoxHound-Dialect-DGraph_tests.js +547 -0
- package/test/FoxHound-Dialect-MongoDB_tests.js +485 -0
- package/test/FoxHound-Dialect-PostgreSQL_tests.js +342 -0
- package/test/FoxHound-Dialect-Solr_tests.js +551 -0
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FoxHound DGraph Dialect
|
|
3
|
+
*
|
|
4
|
+
* Generates DQL query strings and JSON mutation descriptors for DGraph.
|
|
5
|
+
* The query body is a JSON string; the parsed operation object is also
|
|
6
|
+
* stored in query.parameters.dgraphOperation for direct provider consumption.
|
|
7
|
+
*
|
|
8
|
+
* @license MIT
|
|
9
|
+
*
|
|
10
|
+
* @author Steven Velozo <steven@velozo.com>
|
|
11
|
+
* @class FoxHoundDialectDGraph
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
var FoxHoundDialectDGraph = function(pFable)
|
|
15
|
+
{
|
|
16
|
+
_Fable = pFable;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Strip any table-name prefix from a column name.
|
|
20
|
+
* DGraph uses plain predicate names without table qualification.
|
|
21
|
+
*
|
|
22
|
+
* @method stripTablePrefix
|
|
23
|
+
* @param {String} pColumn Column name, possibly table-qualified
|
|
24
|
+
* @return {String} Plain column name
|
|
25
|
+
*/
|
|
26
|
+
var stripTablePrefix = function(pColumn)
|
|
27
|
+
{
|
|
28
|
+
if (typeof pColumn !== 'string')
|
|
29
|
+
{
|
|
30
|
+
return pColumn;
|
|
31
|
+
}
|
|
32
|
+
// Remove backtick and double-quote quoting
|
|
33
|
+
var tmpColumn = pColumn.replace(/[`"]/g, '');
|
|
34
|
+
// Strip table prefix (e.g. "Animal.Name" -> "Name")
|
|
35
|
+
if (tmpColumn.indexOf('.') >= 0)
|
|
36
|
+
{
|
|
37
|
+
var tmpParts = tmpColumn.split('.');
|
|
38
|
+
if (tmpParts[tmpParts.length - 1] === '*')
|
|
39
|
+
{
|
|
40
|
+
return '*';
|
|
41
|
+
}
|
|
42
|
+
return tmpParts[tmpParts.length - 1];
|
|
43
|
+
}
|
|
44
|
+
return tmpColumn;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Find the schema entry for a given column name.
|
|
49
|
+
*
|
|
50
|
+
* @method findSchemaEntry
|
|
51
|
+
* @param {String} pColumn Column name
|
|
52
|
+
* @param {Array} pSchema Schema array
|
|
53
|
+
* @return {Object} Schema entry or default
|
|
54
|
+
*/
|
|
55
|
+
var findSchemaEntry = function(pColumn, pSchema)
|
|
56
|
+
{
|
|
57
|
+
for (var i = 0; i < pSchema.length; i++)
|
|
58
|
+
{
|
|
59
|
+
if (pColumn == pSchema[i].Column)
|
|
60
|
+
{
|
|
61
|
+
return pSchema[i];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { Column: pColumn, Type: 'Default' };
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Format a value for inclusion in a DQL string.
|
|
69
|
+
* Strings are double-quoted, numbers stay bare, arrays become bracketed.
|
|
70
|
+
*
|
|
71
|
+
* @method formatDGraphValue
|
|
72
|
+
* @param {*} pValue The value to format
|
|
73
|
+
* @return {String} Formatted value string
|
|
74
|
+
*/
|
|
75
|
+
var formatDGraphValue = function(pValue)
|
|
76
|
+
{
|
|
77
|
+
if (Array.isArray(pValue))
|
|
78
|
+
{
|
|
79
|
+
var tmpItems = [];
|
|
80
|
+
for (var i = 0; i < pValue.length; i++)
|
|
81
|
+
{
|
|
82
|
+
tmpItems.push(formatDGraphValue(pValue[i]));
|
|
83
|
+
}
|
|
84
|
+
return '[' + tmpItems.join(', ') + ']';
|
|
85
|
+
}
|
|
86
|
+
if (typeof pValue === 'number')
|
|
87
|
+
{
|
|
88
|
+
return String(pValue);
|
|
89
|
+
}
|
|
90
|
+
if (typeof pValue === 'boolean')
|
|
91
|
+
{
|
|
92
|
+
return pValue ? 'true' : 'false';
|
|
93
|
+
}
|
|
94
|
+
// Escape double quotes inside strings
|
|
95
|
+
return '"' + String(pValue).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Translate a single FoxHound filter entry into a DGraph DQL filter function call.
|
|
100
|
+
*
|
|
101
|
+
* @method translateOperator
|
|
102
|
+
* @param {Object} pFilterEntry A FoxHound filter object
|
|
103
|
+
* @return {String} DQL filter function string
|
|
104
|
+
*/
|
|
105
|
+
var translateOperator = function(pFilterEntry)
|
|
106
|
+
{
|
|
107
|
+
var tmpColumn = stripTablePrefix(pFilterEntry.Column);
|
|
108
|
+
var tmpValue = pFilterEntry.Value;
|
|
109
|
+
|
|
110
|
+
switch (pFilterEntry.Operator)
|
|
111
|
+
{
|
|
112
|
+
case '=':
|
|
113
|
+
return 'eq(' + tmpColumn + ', ' + formatDGraphValue(tmpValue) + ')';
|
|
114
|
+
case '!=':
|
|
115
|
+
return 'NOT eq(' + tmpColumn + ', ' + formatDGraphValue(tmpValue) + ')';
|
|
116
|
+
case '>':
|
|
117
|
+
return 'gt(' + tmpColumn + ', ' + formatDGraphValue(tmpValue) + ')';
|
|
118
|
+
case '>=':
|
|
119
|
+
return 'ge(' + tmpColumn + ', ' + formatDGraphValue(tmpValue) + ')';
|
|
120
|
+
case '<':
|
|
121
|
+
return 'lt(' + tmpColumn + ', ' + formatDGraphValue(tmpValue) + ')';
|
|
122
|
+
case '<=':
|
|
123
|
+
return 'le(' + tmpColumn + ', ' + formatDGraphValue(tmpValue) + ')';
|
|
124
|
+
case 'LIKE':
|
|
125
|
+
// Convert SQL LIKE pattern to regex: % -> .*, _ -> .
|
|
126
|
+
var tmpPattern = String(tmpValue).replace(/%/g, '.*').replace(/_/g, '.');
|
|
127
|
+
return 'regexp(' + tmpColumn + ', /' + tmpPattern + '/i)';
|
|
128
|
+
case 'IN':
|
|
129
|
+
// DGraph eq() supports array values as IN
|
|
130
|
+
var tmpInValues = Array.isArray(tmpValue) ? tmpValue : [tmpValue];
|
|
131
|
+
return 'eq(' + tmpColumn + ', ' + formatDGraphValue(tmpInValues) + ')';
|
|
132
|
+
case 'NOT IN':
|
|
133
|
+
var tmpNinValues = Array.isArray(tmpValue) ? tmpValue : [tmpValue];
|
|
134
|
+
return 'NOT eq(' + tmpColumn + ', ' + formatDGraphValue(tmpNinValues) + ')';
|
|
135
|
+
case 'IS NULL':
|
|
136
|
+
return 'NOT has(' + tmpColumn + ')';
|
|
137
|
+
case 'IS NOT NULL':
|
|
138
|
+
return 'has(' + tmpColumn + ')';
|
|
139
|
+
default:
|
|
140
|
+
// Unknown operator, treat as equality
|
|
141
|
+
return 'eq(' + tmpColumn + ', ' + formatDGraphValue(tmpValue) + ')';
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Generate the DGraph @filter(...) clause from the FoxHound filter array.
|
|
147
|
+
* Uses a stack-based approach for parenthetical groups.
|
|
148
|
+
*
|
|
149
|
+
* @method generateFilter
|
|
150
|
+
* @param {Object} pParameters Query Parameters
|
|
151
|
+
* @return {String} DQL @filter clause or empty string
|
|
152
|
+
*/
|
|
153
|
+
var generateFilter = function(pParameters)
|
|
154
|
+
{
|
|
155
|
+
var tmpFilter = Array.isArray(pParameters.filter) ? pParameters.filter.slice() : [];
|
|
156
|
+
|
|
157
|
+
// Auto-add Deleted filter if applicable
|
|
158
|
+
if (!pParameters.query.disableDeleteTracking)
|
|
159
|
+
{
|
|
160
|
+
var tmpSchema = Array.isArray(pParameters.query.schema) ? pParameters.query.schema : [];
|
|
161
|
+
for (var i = 0; i < tmpSchema.length; i++)
|
|
162
|
+
{
|
|
163
|
+
if (tmpSchema[i].Type === 'Deleted')
|
|
164
|
+
{
|
|
165
|
+
var tmpHasDeletedParam = false;
|
|
166
|
+
for (var x = 0; x < tmpFilter.length; x++)
|
|
167
|
+
{
|
|
168
|
+
if (stripTablePrefix(tmpFilter[x].Column) === tmpSchema[i].Column)
|
|
169
|
+
{
|
|
170
|
+
tmpHasDeletedParam = true;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (!tmpHasDeletedParam)
|
|
175
|
+
{
|
|
176
|
+
tmpFilter.push(
|
|
177
|
+
{
|
|
178
|
+
Column: tmpSchema[i].Column,
|
|
179
|
+
Operator: '=',
|
|
180
|
+
Value: 0,
|
|
181
|
+
Connector: 'AND',
|
|
182
|
+
Parameter: 'Deleted'
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (tmpFilter.length < 1)
|
|
191
|
+
{
|
|
192
|
+
return '';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Stack-based processing for parenthetical groups
|
|
196
|
+
// Each stack level is an array of { text: 'eq(...)', connector: 'AND' }
|
|
197
|
+
var tmpStack = [[]];
|
|
198
|
+
|
|
199
|
+
for (var i = 0; i < tmpFilter.length; i++)
|
|
200
|
+
{
|
|
201
|
+
var tmpEntry = tmpFilter[i];
|
|
202
|
+
|
|
203
|
+
if (tmpEntry.Operator === '(')
|
|
204
|
+
{
|
|
205
|
+
tmpStack.push([]);
|
|
206
|
+
}
|
|
207
|
+
else if (tmpEntry.Operator === ')')
|
|
208
|
+
{
|
|
209
|
+
var tmpGroupConditions = tmpStack.pop();
|
|
210
|
+
|
|
211
|
+
// Check if any condition inside the group used OR
|
|
212
|
+
var tmpHasOR = false;
|
|
213
|
+
for (var g = 0; g < tmpGroupConditions.length; g++)
|
|
214
|
+
{
|
|
215
|
+
if (tmpGroupConditions[g].connector === 'OR')
|
|
216
|
+
{
|
|
217
|
+
tmpHasOR = true;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Join conditions in the group
|
|
223
|
+
var tmpGroupText = '';
|
|
224
|
+
for (var g2 = 0; g2 < tmpGroupConditions.length; g2++)
|
|
225
|
+
{
|
|
226
|
+
if (g2 > 0)
|
|
227
|
+
{
|
|
228
|
+
tmpGroupText += tmpHasOR ? ' OR ' : ' AND ';
|
|
229
|
+
}
|
|
230
|
+
tmpGroupText += tmpGroupConditions[g2].text;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
tmpStack[tmpStack.length - 1].push({
|
|
234
|
+
text: '(' + tmpGroupText + ')',
|
|
235
|
+
connector: tmpEntry.Connector || 'AND'
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
else
|
|
239
|
+
{
|
|
240
|
+
tmpStack[tmpStack.length - 1].push({
|
|
241
|
+
text: translateOperator(tmpEntry),
|
|
242
|
+
connector: tmpEntry.Connector || 'AND'
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Collapse root level
|
|
248
|
+
var tmpRootConditions = tmpStack[0];
|
|
249
|
+
if (tmpRootConditions.length === 0)
|
|
250
|
+
{
|
|
251
|
+
return '';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
var tmpFilterText = '';
|
|
255
|
+
for (var r = 0; r < tmpRootConditions.length; r++)
|
|
256
|
+
{
|
|
257
|
+
if (r > 0)
|
|
258
|
+
{
|
|
259
|
+
tmpFilterText += ' ' + tmpRootConditions[r].connector + ' ';
|
|
260
|
+
}
|
|
261
|
+
tmpFilterText += tmpRootConditions[r].text;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return ' @filter(' + tmpFilterText + ')';
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Generate the field list for a DQL query from dataElements.
|
|
269
|
+
* Always includes uid. Falls back to all schema columns if no dataElements.
|
|
270
|
+
*
|
|
271
|
+
* @method generateFieldList
|
|
272
|
+
* @param {Object} pParameters Query Parameters
|
|
273
|
+
* @return {String} Space-separated field list
|
|
274
|
+
*/
|
|
275
|
+
var generateFieldList = function(pParameters)
|
|
276
|
+
{
|
|
277
|
+
var tmpFields = ['uid'];
|
|
278
|
+
var tmpDataElements = pParameters.dataElements;
|
|
279
|
+
|
|
280
|
+
if (Array.isArray(tmpDataElements) && tmpDataElements.length > 0)
|
|
281
|
+
{
|
|
282
|
+
for (var i = 0; i < tmpDataElements.length; i++)
|
|
283
|
+
{
|
|
284
|
+
var tmpField = tmpDataElements[i];
|
|
285
|
+
if (Array.isArray(tmpField))
|
|
286
|
+
{
|
|
287
|
+
tmpField = tmpField[0];
|
|
288
|
+
}
|
|
289
|
+
tmpField = stripTablePrefix(tmpField);
|
|
290
|
+
if (tmpField !== '*' && tmpFields.indexOf(tmpField) < 0)
|
|
291
|
+
{
|
|
292
|
+
tmpFields.push(tmpField);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
else
|
|
297
|
+
{
|
|
298
|
+
// Use all schema columns
|
|
299
|
+
var tmpSchema = Array.isArray(pParameters.query.schema) ? pParameters.query.schema : [];
|
|
300
|
+
for (var j = 0; j < tmpSchema.length; j++)
|
|
301
|
+
{
|
|
302
|
+
var tmpCol = tmpSchema[j].Column;
|
|
303
|
+
if (tmpFields.indexOf(tmpCol) < 0)
|
|
304
|
+
{
|
|
305
|
+
tmpFields.push(tmpCol);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// If we still only have uid, include dgraph.type for completeness
|
|
311
|
+
if (tmpFields.length === 1)
|
|
312
|
+
{
|
|
313
|
+
tmpFields.push('dgraph.type');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return tmpFields.join(' ');
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Generate DQL sort parameters from sort array.
|
|
321
|
+
*
|
|
322
|
+
* @method generateSort
|
|
323
|
+
* @param {Object} pParameters Query Parameters
|
|
324
|
+
* @return {String} Sort parameters (e.g. "orderasc: Name") or empty string
|
|
325
|
+
*/
|
|
326
|
+
var generateSort = function(pParameters)
|
|
327
|
+
{
|
|
328
|
+
var tmpSort = pParameters.sort;
|
|
329
|
+
if (!Array.isArray(tmpSort) || tmpSort.length < 1)
|
|
330
|
+
{
|
|
331
|
+
return '';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
var tmpParts = [];
|
|
335
|
+
for (var i = 0; i < tmpSort.length; i++)
|
|
336
|
+
{
|
|
337
|
+
var tmpColumn = stripTablePrefix(tmpSort[i].Column);
|
|
338
|
+
var tmpDir = (tmpSort[i].Direction === 'Descending') ? 'orderdesc' : 'orderasc';
|
|
339
|
+
tmpParts.push(tmpDir + ': ' + tmpColumn);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return tmpParts.join(', ');
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Generate DQL pagination parameters from begin/cap.
|
|
347
|
+
*
|
|
348
|
+
* @method generatePagination
|
|
349
|
+
* @param {Object} pParameters Query Parameters
|
|
350
|
+
* @return {String} Pagination parameters (e.g. "first: 10, offset: 5") or empty string
|
|
351
|
+
*/
|
|
352
|
+
var generatePagination = function(pParameters)
|
|
353
|
+
{
|
|
354
|
+
var tmpParts = [];
|
|
355
|
+
|
|
356
|
+
if (pParameters.cap)
|
|
357
|
+
{
|
|
358
|
+
tmpParts.push('first: ' + pParameters.cap);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (pParameters.begin !== false && pParameters.begin > 0)
|
|
362
|
+
{
|
|
363
|
+
tmpParts.push('offset: ' + pParameters.begin);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return tmpParts.join(', ');
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Generate the document for a create (mutation set) operation.
|
|
371
|
+
* Walks the record through the schema to handle special column types.
|
|
372
|
+
*
|
|
373
|
+
* @method generateCreateDocument
|
|
374
|
+
* @param {Object} pParameters Query Parameters
|
|
375
|
+
* @return {Object|false} Document object or false if no record
|
|
376
|
+
*/
|
|
377
|
+
var generateCreateDocument = function(pParameters)
|
|
378
|
+
{
|
|
379
|
+
var tmpRecords = pParameters.query.records;
|
|
380
|
+
if (!Array.isArray(tmpRecords) || tmpRecords.length < 1)
|
|
381
|
+
{
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
var tmpSchema = Array.isArray(pParameters.query.schema) ? pParameters.query.schema : [];
|
|
386
|
+
var tmpDocument = {};
|
|
387
|
+
|
|
388
|
+
for (var tmpColumn in tmpRecords[0])
|
|
389
|
+
{
|
|
390
|
+
var tmpSchemaEntry = findSchemaEntry(tmpColumn, tmpSchema);
|
|
391
|
+
|
|
392
|
+
if (!pParameters.query.disableDeleteTracking)
|
|
393
|
+
{
|
|
394
|
+
if (tmpSchemaEntry.Type === 'DeleteDate' ||
|
|
395
|
+
tmpSchemaEntry.Type === 'DeleteIDUser')
|
|
396
|
+
{
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
switch (tmpSchemaEntry.Type)
|
|
402
|
+
{
|
|
403
|
+
case 'AutoIdentity':
|
|
404
|
+
if (pParameters.query.disableAutoIdentity)
|
|
405
|
+
{
|
|
406
|
+
tmpDocument[tmpColumn] = tmpRecords[0][tmpColumn];
|
|
407
|
+
}
|
|
408
|
+
else
|
|
409
|
+
{
|
|
410
|
+
tmpDocument[tmpColumn] = '$$AUTOINCREMENT';
|
|
411
|
+
}
|
|
412
|
+
break;
|
|
413
|
+
case 'AutoGUID':
|
|
414
|
+
if (pParameters.query.disableAutoIdentity)
|
|
415
|
+
{
|
|
416
|
+
tmpDocument[tmpColumn] = tmpRecords[0][tmpColumn];
|
|
417
|
+
}
|
|
418
|
+
else if (tmpRecords[0][tmpColumn] &&
|
|
419
|
+
tmpRecords[0][tmpColumn].length >= 5 &&
|
|
420
|
+
tmpRecords[0][tmpColumn] !== '0x0000000000000000')
|
|
421
|
+
{
|
|
422
|
+
tmpDocument[tmpColumn] = tmpRecords[0][tmpColumn];
|
|
423
|
+
}
|
|
424
|
+
else
|
|
425
|
+
{
|
|
426
|
+
tmpDocument[tmpColumn] = pParameters.query.UUID;
|
|
427
|
+
}
|
|
428
|
+
break;
|
|
429
|
+
case 'UpdateDate':
|
|
430
|
+
case 'CreateDate':
|
|
431
|
+
if (pParameters.query.disableAutoDateStamp)
|
|
432
|
+
{
|
|
433
|
+
tmpDocument[tmpColumn] = tmpRecords[0][tmpColumn];
|
|
434
|
+
}
|
|
435
|
+
else
|
|
436
|
+
{
|
|
437
|
+
tmpDocument[tmpColumn] = '$$NOW';
|
|
438
|
+
}
|
|
439
|
+
break;
|
|
440
|
+
case 'DeleteIDUser':
|
|
441
|
+
case 'UpdateIDUser':
|
|
442
|
+
case 'CreateIDUser':
|
|
443
|
+
if (pParameters.query.disableAutoUserStamp)
|
|
444
|
+
{
|
|
445
|
+
tmpDocument[tmpColumn] = tmpRecords[0][tmpColumn];
|
|
446
|
+
}
|
|
447
|
+
else
|
|
448
|
+
{
|
|
449
|
+
tmpDocument[tmpColumn] = pParameters.query.IDUser;
|
|
450
|
+
}
|
|
451
|
+
break;
|
|
452
|
+
case 'Deleted':
|
|
453
|
+
tmpDocument[tmpColumn] = 0;
|
|
454
|
+
break;
|
|
455
|
+
default:
|
|
456
|
+
tmpDocument[tmpColumn] = tmpRecords[0][tmpColumn];
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (Object.keys(tmpDocument).length === 0)
|
|
462
|
+
{
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return tmpDocument;
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Generate the update fields for a mutation operation.
|
|
471
|
+
* Walks the record through the schema, skipping identity/create/delete columns.
|
|
472
|
+
*
|
|
473
|
+
* @method generateUpdateDocument
|
|
474
|
+
* @param {Object} pParameters Query Parameters
|
|
475
|
+
* @return {Object|false} Update document or false if no record
|
|
476
|
+
*/
|
|
477
|
+
var generateUpdateDocument = function(pParameters)
|
|
478
|
+
{
|
|
479
|
+
var tmpRecords = pParameters.query.records;
|
|
480
|
+
if (!Array.isArray(tmpRecords) || tmpRecords.length < 1)
|
|
481
|
+
{
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
var tmpSchema = Array.isArray(pParameters.query.schema) ? pParameters.query.schema : [];
|
|
486
|
+
var tmpUpdateDoc = {};
|
|
487
|
+
|
|
488
|
+
for (var tmpColumn in tmpRecords[0])
|
|
489
|
+
{
|
|
490
|
+
var tmpSchemaEntry = findSchemaEntry(tmpColumn, tmpSchema);
|
|
491
|
+
|
|
492
|
+
if (pParameters.query.disableAutoDateStamp &&
|
|
493
|
+
tmpSchemaEntry.Type === 'UpdateDate')
|
|
494
|
+
{
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
if (pParameters.query.disableAutoUserStamp &&
|
|
498
|
+
tmpSchemaEntry.Type === 'UpdateIDUser')
|
|
499
|
+
{
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
switch (tmpSchemaEntry.Type)
|
|
504
|
+
{
|
|
505
|
+
case 'AutoIdentity':
|
|
506
|
+
case 'CreateDate':
|
|
507
|
+
case 'CreateIDUser':
|
|
508
|
+
case 'DeleteDate':
|
|
509
|
+
case 'DeleteIDUser':
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
switch (tmpSchemaEntry.Type)
|
|
514
|
+
{
|
|
515
|
+
case 'UpdateDate':
|
|
516
|
+
tmpUpdateDoc[tmpColumn] = '$$NOW';
|
|
517
|
+
break;
|
|
518
|
+
case 'UpdateIDUser':
|
|
519
|
+
tmpUpdateDoc[tmpColumn] = pParameters.query.IDUser;
|
|
520
|
+
break;
|
|
521
|
+
default:
|
|
522
|
+
tmpUpdateDoc[tmpColumn] = tmpRecords[0][tmpColumn];
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (Object.keys(tmpUpdateDoc).length === 0)
|
|
528
|
+
{
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return tmpUpdateDoc;
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Generate the soft-delete setters.
|
|
537
|
+
* Returns false if no Deleted column exists or delete tracking is disabled.
|
|
538
|
+
*
|
|
539
|
+
* @method generateDeleteSetters
|
|
540
|
+
* @param {Object} pParameters Query Parameters
|
|
541
|
+
* @return {Object|false} Delete setters or false
|
|
542
|
+
*/
|
|
543
|
+
var generateDeleteSetters = function(pParameters)
|
|
544
|
+
{
|
|
545
|
+
if (pParameters.query.disableDeleteTracking)
|
|
546
|
+
{
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
var tmpSchema = Array.isArray(pParameters.query.schema) ? pParameters.query.schema : [];
|
|
551
|
+
var tmpHasDeletedField = false;
|
|
552
|
+
var tmpSetters = {};
|
|
553
|
+
|
|
554
|
+
for (var i = 0; i < tmpSchema.length; i++)
|
|
555
|
+
{
|
|
556
|
+
var tmpSchemaEntry = tmpSchema[i];
|
|
557
|
+
switch (tmpSchemaEntry.Type)
|
|
558
|
+
{
|
|
559
|
+
case 'Deleted':
|
|
560
|
+
tmpSetters[tmpSchemaEntry.Column] = 1;
|
|
561
|
+
tmpHasDeletedField = true;
|
|
562
|
+
break;
|
|
563
|
+
case 'DeleteDate':
|
|
564
|
+
tmpSetters[tmpSchemaEntry.Column] = '$$NOW';
|
|
565
|
+
break;
|
|
566
|
+
case 'UpdateDate':
|
|
567
|
+
tmpSetters[tmpSchemaEntry.Column] = '$$NOW';
|
|
568
|
+
break;
|
|
569
|
+
case 'DeleteIDUser':
|
|
570
|
+
tmpSetters[tmpSchemaEntry.Column] = pParameters.query.IDUser;
|
|
571
|
+
break;
|
|
572
|
+
default:
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!tmpHasDeletedField || Object.keys(tmpSetters).length === 0)
|
|
578
|
+
{
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return tmpSetters;
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Generate the undelete setters.
|
|
587
|
+
* Returns false if no Deleted column exists.
|
|
588
|
+
*
|
|
589
|
+
* @method generateUndeleteSetters
|
|
590
|
+
* @param {Object} pParameters Query Parameters
|
|
591
|
+
* @return {Object|false} Undelete setters or false
|
|
592
|
+
*/
|
|
593
|
+
var generateUndeleteSetters = function(pParameters)
|
|
594
|
+
{
|
|
595
|
+
var tmpSchema = Array.isArray(pParameters.query.schema) ? pParameters.query.schema : [];
|
|
596
|
+
var tmpHasDeletedField = false;
|
|
597
|
+
var tmpSetters = {};
|
|
598
|
+
|
|
599
|
+
for (var i = 0; i < tmpSchema.length; i++)
|
|
600
|
+
{
|
|
601
|
+
var tmpSchemaEntry = tmpSchema[i];
|
|
602
|
+
switch (tmpSchemaEntry.Type)
|
|
603
|
+
{
|
|
604
|
+
case 'Deleted':
|
|
605
|
+
tmpSetters[tmpSchemaEntry.Column] = 0;
|
|
606
|
+
tmpHasDeletedField = true;
|
|
607
|
+
break;
|
|
608
|
+
case 'UpdateDate':
|
|
609
|
+
tmpSetters[tmpSchemaEntry.Column] = '$$NOW';
|
|
610
|
+
break;
|
|
611
|
+
case 'UpdateIDUser':
|
|
612
|
+
tmpSetters[tmpSchemaEntry.Column] = pParameters.query.IDUser;
|
|
613
|
+
break;
|
|
614
|
+
default:
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (!tmpHasDeletedField || Object.keys(tmpSetters).length === 0)
|
|
620
|
+
{
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return tmpSetters;
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Build the func: arguments portion of a DQL query root.
|
|
629
|
+
* Combines type filter, pagination, and sort.
|
|
630
|
+
*
|
|
631
|
+
* @method buildFuncArgs
|
|
632
|
+
* @param {String} pType DGraph type name
|
|
633
|
+
* @param {String} pPagination Pagination string
|
|
634
|
+
* @param {String} pSort Sort string
|
|
635
|
+
* @return {String} Func arguments (e.g. "func: type(Animal), first: 10, orderasc: Name")
|
|
636
|
+
*/
|
|
637
|
+
var buildFuncArgs = function(pType, pPagination, pSort)
|
|
638
|
+
{
|
|
639
|
+
var tmpParts = ['func: type(' + pType + ')'];
|
|
640
|
+
|
|
641
|
+
if (pPagination)
|
|
642
|
+
{
|
|
643
|
+
tmpParts.push(pPagination);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (pSort)
|
|
647
|
+
{
|
|
648
|
+
tmpParts.push(pSort);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return tmpParts.join(', ');
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Create a new record
|
|
657
|
+
*
|
|
658
|
+
* @method Create
|
|
659
|
+
* @param {Object} pParameters Query parameters
|
|
660
|
+
* @return {String} JSON operation descriptor or false
|
|
661
|
+
*/
|
|
662
|
+
var Create = function(pParameters)
|
|
663
|
+
{
|
|
664
|
+
var tmpDocument = generateCreateDocument(pParameters);
|
|
665
|
+
|
|
666
|
+
if (!tmpDocument)
|
|
667
|
+
{
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Add DGraph type predicate
|
|
672
|
+
tmpDocument['dgraph.type'] = pParameters.scope;
|
|
673
|
+
|
|
674
|
+
// Determine if we need a counter scope for auto-increment
|
|
675
|
+
var tmpCounterScope = false;
|
|
676
|
+
var tmpSchema = Array.isArray(pParameters.query.schema) ? pParameters.query.schema : [];
|
|
677
|
+
for (var i = 0; i < tmpSchema.length; i++)
|
|
678
|
+
{
|
|
679
|
+
if (tmpSchema[i].Type === 'AutoIdentity' && !pParameters.query.disableAutoIdentity)
|
|
680
|
+
{
|
|
681
|
+
tmpCounterScope = pParameters.scope + '.' + tmpSchema[i].Column;
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
var tmpResult = {
|
|
687
|
+
type: pParameters.scope,
|
|
688
|
+
operation: 'mutate',
|
|
689
|
+
mutationType: 'set',
|
|
690
|
+
document: tmpDocument
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
if (tmpCounterScope)
|
|
694
|
+
{
|
|
695
|
+
tmpResult.counterScope = tmpCounterScope;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
pParameters.query.parameters.dgraphOperation = tmpResult;
|
|
699
|
+
|
|
700
|
+
return JSON.stringify(tmpResult);
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Read one or many records
|
|
706
|
+
*
|
|
707
|
+
* @method Read
|
|
708
|
+
* @param {Object} pParameters Query parameters
|
|
709
|
+
* @return {String} JSON operation descriptor
|
|
710
|
+
*/
|
|
711
|
+
var Read = function(pParameters)
|
|
712
|
+
{
|
|
713
|
+
if (pParameters.join && Array.isArray(pParameters.join) && pParameters.join.length > 0)
|
|
714
|
+
{
|
|
715
|
+
_Fable.log.warn('DGraph dialect does not support JOINs; join parameter will be ignored.');
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
var tmpFilterClause = generateFilter(pParameters);
|
|
719
|
+
var tmpFieldList = generateFieldList(pParameters);
|
|
720
|
+
var tmpSort = generateSort(pParameters);
|
|
721
|
+
var tmpPagination = generatePagination(pParameters);
|
|
722
|
+
|
|
723
|
+
var tmpFuncArgs = buildFuncArgs(pParameters.scope, tmpPagination, tmpSort);
|
|
724
|
+
|
|
725
|
+
var tmpDQL = '{ results(' + tmpFuncArgs + ')' + tmpFilterClause + ' { ' + tmpFieldList + ' } }';
|
|
726
|
+
|
|
727
|
+
var tmpResult = {
|
|
728
|
+
type: pParameters.scope,
|
|
729
|
+
operation: 'query',
|
|
730
|
+
query: tmpDQL,
|
|
731
|
+
queryName: 'results'
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
if (pParameters.distinct)
|
|
735
|
+
{
|
|
736
|
+
tmpResult.distinct = true;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
pParameters.query.parameters.dgraphOperation = tmpResult;
|
|
740
|
+
|
|
741
|
+
return JSON.stringify(tmpResult);
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Update one or many records
|
|
746
|
+
*
|
|
747
|
+
* @method Update
|
|
748
|
+
* @param {Object} pParameters Query parameters
|
|
749
|
+
* @return {String} JSON operation descriptor or false
|
|
750
|
+
*/
|
|
751
|
+
var Update = function(pParameters)
|
|
752
|
+
{
|
|
753
|
+
var tmpFilterClause = generateFilter(pParameters);
|
|
754
|
+
var tmpUpdateDoc = generateUpdateDocument(pParameters);
|
|
755
|
+
|
|
756
|
+
if (!tmpUpdateDoc)
|
|
757
|
+
{
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Build a DQL query to find the UIDs of matching nodes
|
|
762
|
+
var tmpFuncArgs = buildFuncArgs(pParameters.scope, '', '');
|
|
763
|
+
var tmpQueryForUIDs = '{ updateTargets(' + tmpFuncArgs + ')' + tmpFilterClause + ' { uid } }';
|
|
764
|
+
|
|
765
|
+
var tmpResult = {
|
|
766
|
+
type: pParameters.scope,
|
|
767
|
+
operation: 'upsert',
|
|
768
|
+
queryForUIDs: tmpQueryForUIDs,
|
|
769
|
+
queryName: 'updateTargets',
|
|
770
|
+
update: tmpUpdateDoc
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
pParameters.query.parameters.dgraphOperation = tmpResult;
|
|
774
|
+
|
|
775
|
+
return JSON.stringify(tmpResult);
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Delete one or many records (soft or hard depending on schema)
|
|
780
|
+
*
|
|
781
|
+
* @method Delete
|
|
782
|
+
* @param {Object} pParameters Query parameters
|
|
783
|
+
* @return {String} JSON operation descriptor
|
|
784
|
+
*/
|
|
785
|
+
var Delete = function(pParameters)
|
|
786
|
+
{
|
|
787
|
+
var tmpDeleteSetters = generateDeleteSetters(pParameters);
|
|
788
|
+
var tmpFilterClause = generateFilter(pParameters);
|
|
789
|
+
|
|
790
|
+
// Build a DQL query to find the UIDs of matching nodes
|
|
791
|
+
var tmpFuncArgs = buildFuncArgs(pParameters.scope, '', '');
|
|
792
|
+
var tmpQueryForUIDs = '{ deleteTargets(' + tmpFuncArgs + ')' + tmpFilterClause + ' { uid } }';
|
|
793
|
+
|
|
794
|
+
if (tmpDeleteSetters)
|
|
795
|
+
{
|
|
796
|
+
// Soft delete via mutation
|
|
797
|
+
var tmpResult = {
|
|
798
|
+
type: pParameters.scope,
|
|
799
|
+
operation: 'upsert',
|
|
800
|
+
queryForUIDs: tmpQueryForUIDs,
|
|
801
|
+
queryName: 'deleteTargets',
|
|
802
|
+
update: tmpDeleteSetters
|
|
803
|
+
};
|
|
804
|
+
pParameters.query.parameters.dgraphOperation = tmpResult;
|
|
805
|
+
return JSON.stringify(tmpResult);
|
|
806
|
+
}
|
|
807
|
+
else
|
|
808
|
+
{
|
|
809
|
+
// Hard delete
|
|
810
|
+
var tmpHardResult = {
|
|
811
|
+
type: pParameters.scope,
|
|
812
|
+
operation: 'delete',
|
|
813
|
+
queryForUIDs: tmpQueryForUIDs,
|
|
814
|
+
queryName: 'deleteTargets'
|
|
815
|
+
};
|
|
816
|
+
pParameters.query.parameters.dgraphOperation = tmpHardResult;
|
|
817
|
+
return JSON.stringify(tmpHardResult);
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Undelete (restore) a soft-deleted record
|
|
823
|
+
*
|
|
824
|
+
* @method Undelete
|
|
825
|
+
* @param {Object} pParameters Query parameters
|
|
826
|
+
* @return {String} JSON operation descriptor
|
|
827
|
+
*/
|
|
828
|
+
var Undelete = function(pParameters)
|
|
829
|
+
{
|
|
830
|
+
var tmpUndeleteSetters = generateUndeleteSetters(pParameters);
|
|
831
|
+
|
|
832
|
+
// Temporarily disable delete tracking for filter generation
|
|
833
|
+
// so we can find records where Deleted=1
|
|
834
|
+
var tmpDeleteTrackingState = pParameters.query.disableDeleteTracking;
|
|
835
|
+
pParameters.query.disableDeleteTracking = true;
|
|
836
|
+
var tmpFilterClause = generateFilter(pParameters);
|
|
837
|
+
pParameters.query.disableDeleteTracking = tmpDeleteTrackingState;
|
|
838
|
+
|
|
839
|
+
if (tmpUndeleteSetters)
|
|
840
|
+
{
|
|
841
|
+
var tmpFuncArgs = buildFuncArgs(pParameters.scope, '', '');
|
|
842
|
+
var tmpQueryForUIDs = '{ undeleteTargets(' + tmpFuncArgs + ')' + tmpFilterClause + ' { uid } }';
|
|
843
|
+
|
|
844
|
+
var tmpResult = {
|
|
845
|
+
type: pParameters.scope,
|
|
846
|
+
operation: 'upsert',
|
|
847
|
+
queryForUIDs: tmpQueryForUIDs,
|
|
848
|
+
queryName: 'undeleteTargets',
|
|
849
|
+
update: tmpUndeleteSetters
|
|
850
|
+
};
|
|
851
|
+
pParameters.query.parameters.dgraphOperation = tmpResult;
|
|
852
|
+
return JSON.stringify(tmpResult);
|
|
853
|
+
}
|
|
854
|
+
else
|
|
855
|
+
{
|
|
856
|
+
// No-op -- can't undelete without a Deleted column
|
|
857
|
+
var tmpNoopResult = {
|
|
858
|
+
type: pParameters.scope,
|
|
859
|
+
operation: 'noop'
|
|
860
|
+
};
|
|
861
|
+
pParameters.query.parameters.dgraphOperation = tmpNoopResult;
|
|
862
|
+
return JSON.stringify(tmpNoopResult);
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Count records
|
|
868
|
+
*
|
|
869
|
+
* @method Count
|
|
870
|
+
* @param {Object} pParameters Query parameters
|
|
871
|
+
* @return {String} JSON operation descriptor
|
|
872
|
+
*/
|
|
873
|
+
var Count = function(pParameters)
|
|
874
|
+
{
|
|
875
|
+
var tmpFilterClause = generateFilter(pParameters);
|
|
876
|
+
|
|
877
|
+
var tmpFuncArgs = buildFuncArgs(pParameters.scope, '', '');
|
|
878
|
+
|
|
879
|
+
var tmpDQL = '{ results(' + tmpFuncArgs + ')' + tmpFilterClause + ' { total: count(uid) } }';
|
|
880
|
+
|
|
881
|
+
var tmpResult = {
|
|
882
|
+
type: pParameters.scope,
|
|
883
|
+
operation: 'query',
|
|
884
|
+
query: tmpDQL,
|
|
885
|
+
queryName: 'results',
|
|
886
|
+
isCount: true
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
if (pParameters.distinct)
|
|
890
|
+
{
|
|
891
|
+
tmpResult.distinct = true;
|
|
892
|
+
var tmpDataElements = pParameters.dataElements;
|
|
893
|
+
if (Array.isArray(tmpDataElements) && tmpDataElements.length > 0)
|
|
894
|
+
{
|
|
895
|
+
var tmpFields = [];
|
|
896
|
+
for (var i = 0; i < tmpDataElements.length; i++)
|
|
897
|
+
{
|
|
898
|
+
var tmpField = Array.isArray(tmpDataElements[i]) ? tmpDataElements[i][0] : tmpDataElements[i];
|
|
899
|
+
tmpField = stripTablePrefix(tmpField);
|
|
900
|
+
if (tmpField !== '*')
|
|
901
|
+
{
|
|
902
|
+
tmpFields.push(tmpField);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (tmpFields.length > 0)
|
|
906
|
+
{
|
|
907
|
+
tmpResult.distinctFields = tmpFields;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
else
|
|
911
|
+
{
|
|
912
|
+
// Fall back to AutoIdentity column from schema
|
|
913
|
+
var tmpSchema = Array.isArray(pParameters.query.schema) ? pParameters.query.schema : [];
|
|
914
|
+
for (var j = 0; j < tmpSchema.length; j++)
|
|
915
|
+
{
|
|
916
|
+
if (tmpSchema[j].Type === 'AutoIdentity')
|
|
917
|
+
{
|
|
918
|
+
tmpResult.distinctFields = [tmpSchema[j].Column];
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
pParameters.query.parameters.dgraphOperation = tmpResult;
|
|
926
|
+
|
|
927
|
+
return JSON.stringify(tmpResult);
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
var tmpDialect = ({
|
|
931
|
+
Create: Create,
|
|
932
|
+
Read: Read,
|
|
933
|
+
Update: Update,
|
|
934
|
+
Delete: Delete,
|
|
935
|
+
Undelete: Undelete,
|
|
936
|
+
Count: Count
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Dialect Name
|
|
941
|
+
*
|
|
942
|
+
* @property name
|
|
943
|
+
* @type string
|
|
944
|
+
*/
|
|
945
|
+
Object.defineProperty(tmpDialect, 'name',
|
|
946
|
+
{
|
|
947
|
+
get: function() { return 'DGraph'; },
|
|
948
|
+
enumerable: true
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
return tmpDialect;
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
module.exports = FoxHoundDialectDGraph;
|