@naanlang/naan 1.0.2 → 1.0.5

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.
Files changed (56) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +10 -8
  3. package/bin/index.js +2 -2
  4. package/dist/env_web.js +172 -22
  5. package/dist/naan.min.js +6 -6
  6. package/frameworks/browser/https_request.nlg +120 -0
  7. package/frameworks/browser/sworker.js +299 -91
  8. package/frameworks/browser/terminals.nlg +28 -13
  9. package/frameworks/browser/workers.nlg +27 -12
  10. package/frameworks/client/apiclient.nlg +17 -4
  11. package/frameworks/client/psm_client.nlg +184 -81
  12. package/frameworks/common/common.nlg +105 -25
  13. package/frameworks/common/preferences.nlg +1 -0
  14. package/frameworks/common/watching.nlg +1 -0
  15. package/frameworks/node/apiserver.nlg +31 -15
  16. package/frameworks/node/filesystem.nlg +230 -46
  17. package/frameworks/node/gitter.nlg +27 -6
  18. package/frameworks/node/https_request.nlg +119 -0
  19. package/frameworks/node/node.nlg +1 -0
  20. package/frameworks/node/psm_server.nlg +113 -37
  21. package/frameworks/project/build.nlg +40 -23
  22. package/frameworks/project/proj_cliser.nlg +11 -3
  23. package/frameworks/project/proj_console.nlg +13 -4
  24. package/frameworks/project/proj_folder.nlg +10 -1
  25. package/frameworks/project/proj_lambda.nlg +13 -4
  26. package/frameworks/project/proj_static.nlg +11 -3
  27. package/frameworks/project/projects.nlg +128 -40
  28. package/frameworks/running/debugnub.nlg +2 -5
  29. package/frameworks/running/executors.nlg +1 -0
  30. package/frameworks/storage/csv.nlg +120 -0
  31. package/frameworks/storage/dbt_pouch.nlg +41 -25
  32. package/frameworks/storage/psm.nlg +108 -28
  33. package/frameworks/storage/psm_dbtables.nlg +7 -3
  34. package/frameworks/storage/resources.nlg +30 -2
  35. package/lib/browser/env_web.js +170 -20
  36. package/lib/browser/env_webworker.js +1 -1
  37. package/lib/browser/require.js +1 -1
  38. package/lib/core/naanlib.js +6 -6
  39. package/package.json +2 -1
  40. package/plugins/serviceAws/aws/aws-sdk-node.min.js +2 -0
  41. package/plugins/serviceAws/aws/aws-sdk.min.js +2 -88
  42. package/plugins/serviceAws/aws_cloudwatchlogs.nlg +8 -7
  43. package/plugins/serviceAws/aws_dynamo.nlg +715 -148
  44. package/plugins/serviceAws/aws_dynextra.nlg +294 -0
  45. package/plugins/serviceAws/aws_lambda.nlg +11 -4
  46. package/plugins/serviceAws/aws_s3.nlg +44 -14
  47. package/plugins/serviceAws/dbt_aws.nlg +58 -45
  48. package/plugins/serviceAws/psm_aws.nlg +61 -40
  49. package/plugins/serviceAws/serviceAws.nlg +7 -4
  50. package/plugins/serviceGitHub/psm_github.nlg +41 -25
  51. package/plugins/serviceGitLab/psm_gitlab.nlg +41 -25
  52. package/test/harness.nlg +3 -1
  53. package/test/test_01_core.nlg +5 -5
  54. package/test/test_02_context.nlg +4 -4
  55. package/test/test_03_jsinterop.nlg +9 -3
  56. package/plugins/serviceAws/aws_cognito.nlg +0 -76
@@ -6,26 +6,164 @@
6
6
  *
7
7
  * column positioning: // // !
8
8
  *
9
- * Copyright (c) 2021 by Richard C. Zulch
9
+ * Copyright (c) 2021-2022 by Richard C. Zulch
10
10
  *
11
11
  */
12
12
 
13
13
 
14
14
  /*
15
- * DynaTable
15
+ * TimeKeys
16
16
  *
17
- * DynamoDB table.
17
+ * TimeKeys implement a time-ordered key paradigm for DynamoDB and similar databases. Each item
18
+ * stored under a timeKey is assigned an ID comprising a timestamp and a random suffix derived from
19
+ * a version-4 UUID. The ID has a 26-character format: <13-digit timestamp>-<12-character-random>.
20
+ * A prefix option inserts a fixed-length string before the timestamp, enabling categorization when
21
+ * multiple datatypes inhabit the same keyspace. From the ID is derived a partition key in the form
22
+ * YYYYDDD, where DDD is the day of the year from 1 to 365. TimeKeys have the following useful
23
+ * properties:
24
+ * - IDs sort in date order
25
+ * - IDs allow direct access
26
+ * - Over time IDs become distributed over partitions
27
+ * - Can iterate partitions to query longer timeframes
28
+ *
29
+ * The principal challenge is that IDs can cause a hot partition problem if you try to create them
30
+ * too quickly, because they all share the same day. Likewise if you access recently-created IDs
31
+ * frequently. In that case a separate table for today's items might be preferable.
32
+ * A potential challenge is that rarely-created keys will be sparsely distributed among the
33
+ * potential partition keys. For data is not created every day, you may want to cache the available
34
+ * days, or choose a different key paradigm.
18
35
  *
19
36
  */
20
37
 
21
- closure DynaTable(dyna, tablename, local table) {
22
- table = new(object, this)
23
- table.name = tablename
38
+ closure TimeKeys(prefix, baseMS, local timekeys, prelen) {
39
+ if !UUID.proc
40
+ throw("TimeKeys requires UUID()")
41
+ timekeys = new(object, this)
42
+ if !prefix
43
+ prefix = ""
44
+ prelen = prefix.length
45
+ if !baseMS
46
+ baseMS = 1640995200000 // 01/01/2022 UTC - earliest valid date
47
+
48
+ // utcYearStartMS
49
+ //
50
+ // Get the millisecond timestamp for the start of the given UTC year.
51
+ //
52
+ function utcYearStartMS(year) {
53
+ Date("01/01/".concat(year, "GMT")).getTime()
54
+ }
55
+
56
+ // makePartID
57
+ //
58
+ // Return the partition ID for a millisecond timestamp, in the form YYYYDDD where DDD is the day
59
+ // of the year from 001 to 365. If the timestamp is invalid it returns false.
60
+ //
61
+ timekeys.makePartID = function makePartID(msec, local tsdate, yyyy, doy) {
62
+ if msec >= baseMS {
63
+ tsdate = Date(msec)
64
+ yyyy = tsdate.getUTCFullYear()
65
+ doy = Math.floor((msec - utcYearStartMS(yyyy))/(1000*3600*24))
66
+ prefix.concat(yyyy.tostring, "000".concat(doy+1).slice(-3))
67
+ }
68
+ }
69
+
70
+ // comparePartID
71
+ //
72
+ // Compare two partID values, returning -1, 0, 1 if the first is <, ==, or > to the second.
73
+ //
74
+ timekeys.comparePartID = function comparePartID(a, b) {
75
+ a = a.slice(prelen)
76
+ b = b.slice(prelen)
77
+ if (a < b)
78
+ -1
79
+ else if a > b
80
+ 1
81
+ else
82
+ 0
83
+ }
84
+
85
+ // adjustPartID
86
+ //
87
+ // Increment or decrement the partition key by the specified number of days returning the next
88
+ // partition key in the specified direction, or false if there are no more keys. Dates in the
89
+ // future are considered invalid and return false.
90
+ //
91
+ timekeys.adjustPartID = function adjustPartID(partID, adjust, local year, doy, dayms, msec) {
92
+ partID = partID.slice(prelen)
93
+ yyyy = toint(partID.substring(0,4))
94
+ doy = toint(partID.substring(4,7))
95
+ dayms = 1000*3600*24
96
+ msec = utcYearStartMS(yyyy) + (doy-1) * dayms
97
+ msec += adjust * dayms
98
+ if msec < Date.now()
99
+ makePartID(msec)
100
+ }
101
+
102
+ // partIDfromTimeID
103
+ //
104
+ // Return the partID for a timeID.
105
+ //
106
+ timekeys.partIDfromTimeID = function partIDfromTimeID(timeID) {
107
+ makePartID(timeIDtimestamp(timeID))
108
+ }
109
+
110
+ // newTimeID
111
+ //
112
+ // Return a new, unique timeID for the present moment.
113
+ //
114
+ timekeys.newTimeID = function newTimeID(now) {
115
+ if !now
116
+ now = Date.now()
117
+ prefix.concat(now.tostring, UUID().slice(-13))
118
+ }
119
+
120
+ // isTimeID
121
+ //
122
+ // Return true iff this looks like our timeID with the right prefix, as opposed to a timestamp.
123
+ //
124
+ timekeys.isTimeID = function isTimeID(tstid) {
125
+ string(tstid) && tstid.startsWith(prefix) && tstid.length-prelen == 26
126
+ }
127
+
128
+ // timeIDtimestamp
129
+ //
130
+ // Return the numeric millisecond timestamp for a timeID.
131
+ //
132
+ timekeys.timeIDtimestamp = function timeIDtimestamp(timeID) {
133
+ toint(timeID.slice(prelen, prelen+13))
134
+ }
135
+
136
+ // makeTimeKey
137
+ //
138
+ // Make a range key from a timestamp, creating the earliest possible range key, which will sort
139
+ // earlier than all possible timeIDs for that timestamp.
140
+ //
141
+ timekeys.makeTimeKey = function makeTimeKey(timestamp) {
142
+ if !integer(timestamp) || timestamp < baseMS
143
+ timestamp = baseMS
144
+ prefix.concat(timestamp.tostring, "-", space.repeat(12))
145
+ }
146
+
147
+ // finis
148
+
149
+ timekeys
150
+ };
151
+
152
+
153
+ /*
154
+ * DynaConverter
155
+ *
156
+ * Translate between Naan and DynamoDB.
157
+ *
158
+ */
159
+
160
+ closure DynaConverter(local conv) {
161
+ conv = new(object, this)
24
162
 
25
163
  // fieldTypeDynToNaan
26
164
  // Convert field type from DynamoDB to Naan
27
165
  //
28
- function fieldTypeDynToNaan(dtype) {
166
+ conv.fieldTypeDynToNaan = function fieldTypeDynToNaan(dtype) {
29
167
  if dtype == "S",
30
168
  "utf8"
31
169
  else if dtype == "N",
@@ -39,7 +177,7 @@ closure DynaTable(dyna, tablename, local table) {
39
177
  // fieldTypeNaanToDyn
40
178
  // Convert field type from Naan to DynamoDB
41
179
  //
42
- function fieldTypeNaanToDyn(ntype) {
180
+ conv.fieldTypeNaanToDyn = function fieldTypeNaanToDyn(ntype) {
43
181
  if ntype == "utf8",
44
182
  "S"
45
183
  else if ntype == "numeric",
@@ -53,7 +191,7 @@ closure DynaTable(dyna, tablename, local table) {
53
191
  // datumNaanToDyn
54
192
  // Convert a Naan datum to DynamoDB format.
55
193
  //
56
- function datumNaanToDyn(datum, local output, ntype, dtype) {
194
+ conv.datumNaanToDyn = function datumNaanToDyn(datum, local output, ntype, dtype) {
57
195
  output = { }
58
196
  if tuple(datum)
59
197
  datum = datum.toarray
@@ -84,11 +222,12 @@ closure DynaTable(dyna, tablename, local table) {
84
222
  else if datum === true || datum === false
85
223
  dtype = "BOOL"
86
224
  else if datum === undefined
87
- datum = undefined
225
+ return (undefined) // Zen!
88
226
  else
89
- dtype = "S" // some other symbol is string
227
+ return (datum.tostring)
228
+
90
229
  } else
91
- debuglog("DynaTable.datumNaanToDyn: invalid type", ntype)
230
+ debuglog("DynaConverter.datumNaanToDyn: invalid type", ntype)
92
231
  }
93
232
  output[dtype] = datum
94
233
  output
@@ -97,7 +236,7 @@ closure DynaTable(dyna, tablename, local table) {
97
236
  // recordNaanToDyn
98
237
  // Convert a Naan dictionary to DynamoDB format.
99
238
  //
100
- function recordNaanToDyn(nitem, local output, key, data) {
239
+ conv.recordNaanToDyn = function recordNaanToDyn(nitem, local output, key, data) {
101
240
  output = { }
102
241
  for `(key, data) in nitem
103
242
  output[key] = datumNaanToDyn(data)
@@ -107,7 +246,7 @@ closure DynaTable(dyna, tablename, local table) {
107
246
  // datumDynToNaan
108
247
  // Convert a DynamoDB typed attribute to Naan format.
109
248
  //
110
- function datumDynToNaan(dtype, dvalue, local output) {
249
+ conv.datumDynToNaan = function datumDynToNaan(dtype, dvalue, local output) {
111
250
  if dtype == "BS" || dtype == "SS"
112
251
  dvalue
113
252
  else if dtype == "NS"
@@ -136,45 +275,247 @@ closure DynaTable(dyna, tablename, local table) {
136
275
  else if dtype == "B" || dtype == "S"
137
276
  dvalue
138
277
  else {
139
- debuglog("DynaTable.datumDynToNaan: unknown type", dtype, typeof(dvalue), dvalue)
278
+ debuglog("DynaConverter.datumDynToNaan: unknown type", dtype, typeof(dvalue), dvalue)
140
279
  false }
141
280
  }
142
281
 
143
282
  // recordDynToNaan
144
283
  // Convert a DynamoDB typed item dictionary to Naan format.
145
284
  //
146
- function recordDynToNaan(ditem, local output, key, data, dtype, dvalue) {
147
- output = { }
148
- for `(key, data) in ditem {
149
- dtype = data.*.0
150
- dvalue = data[dtype]
151
- output[key] = datumDynToNaan(dtype, dvalue)
285
+ conv.recordDynToNaan = function recordDynToNaan(ditem, local output, key, data, dtype, dvalue) {
286
+ if ditem.constructor === Array.prototype.constructor || array(ditem) { // process an array
287
+ output = []
288
+ for data in ditem
289
+ output.push(recordDynToNaan(data))
290
+ }
291
+ else { // process a single expression
292
+ output = { }
293
+ for `(key, data) in ditem {
294
+ dtype = data.*.0
295
+ dvalue = data[dtype]
296
+ output[key] = datumDynToNaan(dtype, dvalue)
297
+ }
152
298
  }
153
299
  output
154
300
  }
155
-
301
+
302
+ // genAttributeNameAdder
156
303
  //
157
- // dynQueryPager
304
+ // Return a function that, when called repeatedly, adds attribute name definitions to the
305
+ // specified parameter block.
158
306
  //
159
- // Perform a dynamo paged query. This implements paging conventions specific to CWL. The callback
160
- // should return the number of items found in the data, or false to stop looping.
307
+ closure genAttributeNameAdder(params, local adex) {
308
+ function attrib(name, local adef, atype, key, data) {
309
+ if !string(name)
310
+ name = tostring(name) // try to make it a string
311
+ if !params.ExpressionAttributeNames
312
+ params.ExpressionAttributeNames = { }
313
+ if !adex
314
+ adex = (keys(params.ExpressionAttributeNames).length || 0) + 1
315
+ for `(key, data) in params.ExpressionAttributeNames
316
+ if data == name
317
+ return (key) // found matching name
318
+ key = "#n".concat(adex++)
319
+ params.ExpressionAttributeNames[key] = name
320
+ key
321
+ }
322
+ }
323
+
324
+ // genAttributeValueAdder
161
325
  //
162
- closure dynQueryPager(procname, params, remaining, doneCB) {
163
- params.Limit = remaining
164
- QueryPager(cwlogs.aws, procname, params, function(data, local processed) {
165
- processed = doneCB(data)
166
- if processed
167
- remaining -= processed
168
- if processed && (!remaining || remaining > 0)
169
- && data.LastEvaluatedKey && data.LastEvaluatedKey != params.ExclusiveStartKey {
170
- params.Limit = remaining // set up for next iteration
171
- params.ExclusiveStartKey = data.LastEvaluatedKey
172
- true // continue looping
326
+ // Return a function that, when called repeatedly, adds attribute value definitions to the
327
+ // specified parameter block. Symbols are interpreted as literal names, e.g. attribute names, and
328
+ // are not defined as values here.
329
+ //
330
+ closure genAttributeValueAdder(params, local adex) {
331
+ function attrib(expr, local adef, atype, key, data) {
332
+ if symbol(expr) && !member(expr, `(true, false, null))
333
+ return (expr.tostring) // literal attribute name
334
+ if !params.ExpressionAttributeValues
335
+ params.ExpressionAttributeValues = { }
336
+ if !adex
337
+ adex = (keys(params.ExpressionAttributeValues).length || 0) + 1
338
+ adef = datumNaanToDyn(expr)
339
+ atype = keys(adef).0
340
+ for `(key, data) in params.ExpressionAttributeValues
341
+ if data[atype] == adef[atype]
342
+ return (key) // found matching attribute
343
+ key = ":v".concat(adex++)
344
+ params.ExpressionAttributeValues[key] = adef
345
+ key
346
+ }
347
+ }
348
+
349
+ //
350
+ // createKeyExpression
351
+ //
352
+ // Update the specified parameter block with one or more condition expressions, preserving any
353
+ // that are already present. Specify each condition with a dictionary as follows:
354
+ // {
355
+ // [KeyConditionExpression: <condition-dictionary>]
356
+ // [FilterExpression: <condition-dictionary]
357
+ // [etc.]
358
+ // }
359
+ // If successful the parameter block is updated, otherwise it may be partly updated. The return
360
+ // value is a standard (error, data) tuple where the data is just { ok: true } if no error.
361
+ // This will parse only one expression per dictionary, but as many such conditions as you
362
+ // you wish to use by specifying an array. This does not check for semantics enforced by the
363
+ // API. E.g. Key conditions can only have one condition expression for the sort key, and only
364
+ // equality is allowed for the partition key. Only filter expressions can use the <> operator.
365
+ //
366
+ // condition dictionary:
367
+ // {
368
+ // key: <keyname, e.g. partition key name> // required
369
+ // "=": <expression> // exclusive of other comparisons
370
+ // ">": <expression> // exclusive of other comparisons
371
+ // "<": <expression> // exclusive of other comparisons
372
+ // "<>": <expression> // exclusive of other comparisons
373
+ // beginsWith: <string-expression> // exclusive of other comparisons
374
+ // ">=": <expression> // with <= or alone
375
+ // "<=": <expression> // with >= or alone
376
+ // }
377
+ //
378
+
379
+ conv.createKeyExpression = function createKeyExpression(params, conds
380
+ local error, attval, rangename, range, keyname, op, expr, rangex, arg1, arg2) {
381
+
382
+ // dupeop
383
+ //
384
+ // Return a duplicate operation error.
385
+ function dupeop() {
386
+ list(Error("key expression cannot combine", op, "with", rangex.0))
387
+ }
388
+
389
+ //
390
+ // build the expression
391
+ //
392
+ attval = genAttributeValueAdder(params)
393
+ for `(rangename, ranges) in conds {
394
+ if !array(ranges)
395
+ ranges = [ranges]
396
+ for range in ranges {
397
+ rangex = false
398
+ keyname = tostring(range.key)
399
+ if !keyname
400
+ return (list(Error("key expression requires key name")))
401
+ for `(op, expr) in range {
402
+ if op == "key"
403
+ continue
404
+ expr = attval(expr)
405
+ if op == ">=" {
406
+ if rangex.0 == "<="
407
+ rangex = list("between", expr, rangex.1)
408
+ else if rangex.0
409
+ return (dupeop())
410
+ else
411
+ rangex = list(op, expr) }
412
+ else if op == "<=" {
413
+ if rangex.0 == ">="
414
+ rangex = list("between", rangex.1, expr)
415
+ else if rangex.0
416
+ return (dupeop())
417
+ else
418
+ rangex = list(op, expr) }
419
+ else if rangex.0
420
+ return (dupeop())
421
+ else
422
+ rangex = list(op, expr)
423
+ }
424
+ if rangex {
425
+ arg1 = rangex.1
426
+ if rangex.0 == "between" {
427
+ arg2 = rangex.2
428
+ expr = keyname.concat(" between ", arg1, " and ", arg2)
429
+ }
430
+ else if rangex.0 == "beginsWith"
431
+ expr = "begins_with(".concat(keyname, ", ", arg1, ")")
432
+ else
433
+ expr = keyname.concat(space, rangex.0, space, arg1)
434
+ if params[rangename]
435
+ params[rangename] = params[rangename].concat(" and ", expr)
436
+ else
437
+ params[rangename] = expr
438
+ }
173
439
  }
174
- })
440
+ }
441
+ list(false, { ok: true })
442
+ }
443
+
444
+ // createUpdateExpression
445
+ //
446
+ // Update the specified parameter block with one or more update expressions, preserving any that
447
+ // are already present. Specify the updates with a dictionary as follows:
448
+ // {
449
+ // set: [`(attribute, value)] // set attributes to values
450
+ // sum: [`(attribute, value)] // attributes += values
451
+ // diff: [`(attribute, value)] // attributes -= values
452
+ // add: [`(attribute, value)] // ADD(attributes, values) -- attrib need not exist
453
+ // remove: [attribute] // remove attributes from the record
454
+ // }
455
+ // If successful the parameter block is updated, otherwise it may be partly updated. The return
456
+ // value is a standard (error, data) tuple where the data is just { ok: true } if no error.
457
+ //
458
+ conv.createUpdateExpression = function createUpdateExpression(params, updates,
459
+ local attval, attname, output, item) {
460
+
461
+ if params.UpdateExpression
462
+ return (list(Error("conv.createUpdateExpression: UpdateExpression already exists")))
463
+ attval = genAttributeValueAdder(params)
464
+ attname = genAttributeNameAdder(params)
465
+ output = {
466
+ SET: []
467
+ ADD: []
468
+ REMOVE: []
469
+ }
470
+ for item in updates.set
471
+ output.SET.push(strcat(attname(item.0), "=", attval(item.1)))
472
+ for item in updates.sum
473
+ output.SET.push(strcat(attname(item.0), "=", attname(item.0), "+", attval(item.1)))
474
+ for item in updates.diff
475
+ output.SET.push(strcat(attname(item.0), "=", attname(item.0), "-", attval(item.1)))
476
+ for item in updates.add
477
+ output.ADD.push(strcat(attname(item.0), space, attval(item.1)))
478
+ for item in updates.remove
479
+ output.REMOVE.push(attname(item))
480
+ params.UpdateExpression = ""
481
+ for `(key, value) in output
482
+ if value.length > 0
483
+ params.UpdateExpression = params.UpdateExpression.concat(key, space, value.join(", "), space)
484
+ list(false, { ok: true })
175
485
  }
176
486
 
487
+ // genBackoffDelayer
177
488
  //
489
+ // Return a function that, when called repeatedly, will sleep for an exponentially increasing
490
+ // maximum time interval starting with 50 msec and doubling each time. The actual delay varies by
491
+ // a random jitter of up to 100% of the delay. The function returns the maximum milliseconds of
492
+ // the next delay, so the caller can give up if this becomes too large.
493
+ //
494
+ conv.genBackoffDelayer = closure genBackoffDelayer(local delay) {
495
+ delay = 50
496
+ function delayer() {
497
+ sleep(delay * Math.random()) // jitter up to 100%
498
+ delay *= 2
499
+ }
500
+ }
501
+
502
+ // finis
503
+
504
+ conv
505
+ };
506
+
507
+
508
+ /*
509
+ * DynaTable
510
+ *
511
+ * DynamoDB table.
512
+ *
513
+ */
514
+
515
+ closure DynaTable(dyna, tablename, local table) {
516
+ table = new(object, this)
517
+ table.name = tablename
518
+
178
519
  // info
179
520
  //
180
521
  // Return info about the table, or an error, in standard (error, dictionary) tuple format.
@@ -202,7 +543,8 @@ closure DynaTable(dyna, tablename, local table) {
202
543
  dyna.aws.describeTable(params, function(error, data, local output, key) {
203
544
  if error {
204
545
  dynaTableError = error
205
- debuglog("aws.describeTable failed", tablename, error)
546
+ debuglog("aws.describeTable failed", tablename, ErrorString(error))
547
+ error = Error("table.info(".concat(tablename, ") failed"), error)
206
548
  } else {
207
549
  data = data.Table
208
550
  output = {
@@ -219,18 +561,17 @@ closure DynaTable(dyna, tablename, local table) {
219
561
  output.range = key.AttributeName
220
562
  for key in data.AttributeDefinitions
221
563
  if key.AttributeName == output.hash
222
- output.hashType = fieldTypeDynToNaan(key.AttributeType)
564
+ output.hashType = dyna.conv.fieldTypeDynToNaan(key.AttributeType)
223
565
  else if key.AttributeName == output.range
224
- output.rangeType = fieldTypeDynToNaan(key.AttributeType)
225
- table.hashKey = output.hash // hash attribute name
226
- table.rangeKey = output.range // range attribute name or false
566
+ output.rangeType = dyna.conv.fieldTypeDynToNaan(key.AttributeType)
567
+ table.hashKey = compress(output.hash) // hash attribute name
568
+ table.rangeKey = compress(output.range) // range attribute name or false
227
569
  }
228
570
  pending.signal(list(error, output))
229
571
  })
230
572
  pending.wait()
231
573
  }
232
574
 
233
- //
234
575
  // create
235
576
  //
236
577
  // Create the table with specified parameters, blocking until the table is ready for use.
@@ -242,11 +583,11 @@ closure DynaTable(dyna, tablename, local table) {
242
583
  // range: <name> // primary range key attribute name (optional)
243
584
  // rangeType: <name> // string | numeric (if range specified)
244
585
  // }
245
-
586
+ //
246
587
  table.create = closure create(parameters, local hashType, rangeType, params, pending, error, data, start) {
247
588
  if !dyna.aws
248
589
  return (list(Error("DynaTable.create: not logged into AWS")))
249
- hashType = fieldTypeNaanToDyn(parameters.hashType)
590
+ hashType = dyna.conv.fieldTypeNaanToDyn(parameters.hashType)
250
591
  if !parameters.hash || !hashType
251
592
  return (list(Error("DynoTable.create: primary key hash/hashType required")))
252
593
  params = {
@@ -265,7 +606,7 @@ closure DynaTable(dyna, tablename, local table) {
265
606
  ]
266
607
  }
267
608
  if parameters.range {
268
- rangeType = fieldTypeNaanToDyn(parameters.rangeType)
609
+ rangeType = dyna.conv.fieldTypeNaanToDyn(parameters.rangeType)
269
610
  if !rangeType
270
611
  return (list(Error("DynoTable.create: range requires rangeType")))
271
612
  params.AttributeDefinitions.push({
@@ -281,7 +622,8 @@ closure DynaTable(dyna, tablename, local table) {
281
622
  dyna.aws.createTable(params, function(error, data) {
282
623
  if error {
283
624
  dynaTableError = error
284
- debuglog("aws.createTable failed", tablename, error)
625
+ debuglog("aws.createTable failed", tablename, ErrorString(error))
626
+ error = Error("table.create(".concat(tablename, ") failed"), error)
285
627
  }
286
628
  pending.signal(list(error, data))
287
629
  })
@@ -293,7 +635,7 @@ closure DynaTable(dyna, tablename, local table) {
293
635
  `(error, data) = info()
294
636
  if data.status == "ACTIVE"
295
637
  break
296
- if milliseconds() - start > 20000 { // 20 seconds
638
+ if milliseconds() - start > dyna.timeout { // 20 seconds
297
639
  if !error
298
640
  error = data.status
299
641
  return(list(Error("DynoTable.create: timeout waiting for new table to become active", error)))
@@ -302,27 +644,81 @@ closure DynaTable(dyna, tablename, local table) {
302
644
  }
303
645
  list(false, data)
304
646
  }
305
-
647
+
648
+ // preflightError
306
649
  //
307
- // putRecord
650
+ // Ensure that the table is ready before executing an operation. This returns an error tuple or
651
+ // false to proceed.
308
652
  //
309
- // Put an record in the table, replacing any existing record with the same key.
310
-
311
- table.putRecord = closure putRecord(record, local result, params, pending) {
653
+ function preflightError(op) {
654
+ if !dyna.aws
655
+ return (list(Error("DynaTable.".concat(op, ": not logged into AWS"))))
312
656
  if !table.hashKey {
313
657
  result = info()
314
658
  if result.0
315
659
  return(result)
316
660
  }
661
+ }
662
+
663
+ // makeDynKey
664
+ //
665
+ // Make a dynamoDB key dictionary for the table using the specified primary key, which can be a
666
+ // tuple of hash/range or just a hash if only a hash is needed for this table.
667
+ //
668
+ conv.makeDynKey = function makeDynKey(primaryKey, local hashValue, rangeValue, key) {
669
+ if tuple(primaryKey)
670
+ `(hashValue, rangeValue) = primaryKey
671
+ else
672
+ hashValue = primaryKey
673
+ key = { }
674
+ key[table.hashKey] = dyna.conv.datumNaanToDyn(hashValue)
675
+ if table.rangeKey
676
+ key[table.rangeKey] = dyna.conv.datumNaanToDyn(rangeValue)
677
+ key
678
+ }
679
+
680
+ // getRecord
681
+ //
682
+ // Get a record from the table with the specified hash value, and optional range value.
683
+ //
684
+ table.getRecord = closure getRecord(hashValue, rangeValue, local result, params, pending) {
685
+ if preflightError(this)
686
+ return
317
687
  params = {
318
688
  TableName: tablename
319
- Item: recordNaanToDyn(record)
689
+ }
690
+ params.Key = makeDynKey(list(hashValue, rangeValue))
691
+ pending = new(nonce)
692
+ dyna.aws.getItem(params, function(error, data, local output) {
693
+ if error {
694
+ dynaTableError = error
695
+ debuglog("aws.getItem failed", tablename, ErrorString(error))
696
+ error = Error("table.getRecord(".concat(tablename, ") failed"), error)
697
+ } else
698
+ data = dyna.conv.recordDynToNaan(data.Item)
699
+ pending.signal(list(error, data))
700
+ })
701
+ pending.wait()
702
+ }
703
+
704
+ //
705
+ // putRecord
706
+ //
707
+ // Put a record in the table, replacing any existing record with the same key.
708
+
709
+ table.putRecord = closure putRecord(record, local result, params, pending) {
710
+ if preflightError(this)
711
+ return
712
+ params = {
713
+ TableName: tablename
714
+ Item: dyna.conv.recordNaanToDyn(record)
320
715
  }
321
716
  pending = new(nonce)
322
717
  dyna.aws.putItem(params, function(error, data, local output) {
323
718
  if error {
324
719
  dynaTableError = error
325
- debuglog("aws.putItem failed", tablename, error)
720
+ debuglog("aws.putItem failed", tablename, ErrorString(error))
721
+ error = Error("table.putRecord(".concat(tablename, ") failed"), error)
326
722
  }
327
723
  pending.signal(list(error, data))
328
724
  })
@@ -330,113 +726,250 @@ closure DynaTable(dyna, tablename, local table) {
330
726
  }
331
727
 
332
728
  //
333
- // getRecord
729
+ // updateRecord
334
730
  //
335
- // Get a record from the table with the specified hash value, and optional range value.
731
+ // Update a record in the table. This will update the existing record if it exists, or add a new
732
+ // one. Options include conditional updates, and adding or removing attributes as follows:
733
+ // {
734
+ // condition: <range-expression> // conditional expression as in createKeyExpression()
735
+ // set: [`(attribute, value)] // set attributes to values
736
+ // sum: [`(attribute, value)] // attributes += values
737
+ // diff: [`(attribute, value)] // attributes -= values
738
+ // remove: [attribute] // remove attributes from the record
739
+ // return: "all_old" | "all_new" | "updated_old" | "updated_new"
740
+ // // return the record as chosen, otherwise { }
741
+ // }
742
+ // The result is a standard result tuple `(error, record).
336
743
 
337
- table.getRecord = closure getRecord(hashValue, rangeValue, local result, params, pending) {
338
- if !table.hashKey {
339
- result = info()
340
- if result.0
341
- return(result)
744
+ table.updateRecord = closure updateRecord(hashValue, rangeValue, options, local error, result, params, actions, pending) {
745
+ if preflightError(this)
746
+ return
747
+ params = {
748
+ TableName: tablename
342
749
  }
750
+ params.Key = makeDynKey(list(hashValue, rangeValue))
751
+ if options.condition {
752
+ `(error) = dyna.conv.createKeyExpression(params, {
753
+ KeyConditionExpression: {
754
+ key: table.hashKey
755
+ "=": hashValue
756
+ }
757
+ })
758
+ if error
759
+ return (list(Error("table.updateRecord:", error)))
760
+ }
761
+ dyna.conv.createUpdateExpression(params, options)
762
+ if options.return {
763
+ params.ReturnValues = {
764
+ all_old: "ALL_OLD"
765
+ all_new: "ALL_NEW"
766
+ updated_old: "UPDATED_OLD"
767
+ updated_new: "UPDATED_NEW"
768
+ }[options.return]
769
+ if !params.ReturnValues
770
+ params.ReturnValues = "NONE"
771
+ }
772
+ pending = new(nonce)
773
+ dyna.aws.updateItem(params, function(error, data) {
774
+ if error {
775
+ dynaTableError = error
776
+ debuglog("aws.updateItem failed", tablename, ErrorString(error), Dialect.print(params))
777
+ error = Error("table.updateRecord(".concat(tablename, ") failed"), error)
778
+ }
779
+ else
780
+ data = dyna.conv.recordDynToNaan(data.Attributes)
781
+ pending.signal(list(error, data))
782
+ })
783
+ pending.wait()
784
+ }
785
+
786
+ //
787
+ // deleteRecord
788
+ //
789
+ // Delete a record in the table. Options include conditional updates as follows:
790
+ // {
791
+ // condition: <range-expression> // conditional expression as in createKeyExpression()
792
+ // return: "all_old" // return previous attribute values, otherwise { }
793
+ // }
794
+
795
+
796
+ table.deleteRecord = closure deleteRecord(hashValue, rangeValue, options, local result, params, pending) {
797
+ if preflightError(this)
798
+ return
343
799
  params = {
344
800
  TableName: tablename
345
- Key: { }
346
801
  }
347
- params.Key[table.hashKey] = datumNaanToDyn(hashValue)
348
- if table.rangeKey
349
- params.Key[table.rangeKey] = datumNaanToDyn(rangeValue)
802
+ params.Key = makeDynKey(list(hashValue, rangeValue))
803
+ if options.condition {
804
+ `(error) = dyna.conv.createKeyExpression(params, {
805
+ KeyConditionExpression: {
806
+ key: table.hashKey
807
+ "=": hashValue
808
+ }
809
+ })
810
+ if error
811
+ return (list(Error("table.deleteRecord:", error)))
812
+ }
813
+ if options.return {
814
+ params.ReturnValues = {
815
+ all_old: "ALL_OLD"
816
+ }[options.return]
817
+ if !params.ReturnValues
818
+ params.ReturnValues = "NONE"
819
+ }
350
820
  pending = new(nonce)
351
- dyna.aws.getItem(params, function(error, data, local output) {
821
+ dyna.aws.deleteItem(params, function(error, data, local output) {
352
822
  if error {
353
823
  dynaTableError = error
354
- debuglog("aws.getItem failed", tablename, error)
355
- } else
356
- data = recordDynToNaan(data.Item)
824
+ debuglog("aws.deleteItem failed", tablename, ErrorString(error))
825
+ error = Error("table.deleteRecord(".concat(tablename, ") failed"), error)
826
+ }
357
827
  pending.signal(list(error, data))
358
828
  })
359
829
  pending.wait()
360
830
  }
361
831
 
832
+ //
833
+ // dynQueryPager
834
+ //
835
+ // Perform a paged query. This implements paging conventions specific to DynamoDB. The callback
836
+ // should return the number of items found in the data, or false to stop looping.
837
+ //
838
+ closure dynQueryPager(procname, params, remaining, doneCB) {
839
+ if !remaining
840
+ remaining = 10 // default retrieve limit
841
+ params.Limit = remaining
842
+ QueryPager(dyna.aws, procname, params, function(data, local processed) {
843
+ processed = doneCB(data)
844
+ if processed
845
+ remaining -= processed
846
+ if processed && remaining > 0 && data.LastEvaluatedKey {
847
+ params.limit = remaining // set up for next iteration
848
+ params.ExclusiveStartKey = data.LastEvaluatedKey
849
+ true // continue looping
850
+ }
851
+ })
852
+ }
853
+
362
854
  //
363
855
  // queryRecords
364
856
  //
365
857
  // Options:
366
858
  // {
367
- // index: <string> // index name in table
368
- // range: <expression-dictionary> // range of keys to retrieve
369
- // reverse: <boolean> // reverse order
370
- // limit: <number> // maximum count of results
859
+ // indexName: <string> // index name in table
860
+ // indexHashKey: <string> // name of index hashKey
861
+ // range: <key-expression> // range of keys to retrieve
862
+ // filter: <key-expression> // conditions for items to return
863
+ // attmap: <dictionary> // map attribute names to #shortcuts
864
+ // reverse: <boolean> // reverse order
865
+ // project: <array> // project only specified attributes
866
+ // exstart: <exclusive start key> // for paging
867
+ // limit: <number> // maximum count of results
371
868
  // }
372
869
  //
373
- // range dictionary:
870
+
871
+ table.queryRecords = closure queryRecords(hashValue, options
872
+ local error, result, params, hashKey, op, expr, output, error)
873
+ {
874
+ if preflightError(this)
875
+ return
876
+ params = {
877
+ TableName: tablename
878
+ }
879
+ hashKey = table.hashKey
880
+ if options.indexName {
881
+ params.IndexName = options.indexName
882
+ if options.indexHashKey
883
+ hashKey = options.indexHashKey
884
+ }
885
+ dyna.conv.createKeyExpression(params, {
886
+ KeyConditionExpression: {
887
+ key: hashKey
888
+ "=": hashValue
889
+ }
890
+ })
891
+ if options.range {
892
+ `(error) = dyna.conv.createKeyExpression(params, {
893
+ KeyConditionExpression: options.range
894
+ })
895
+ if error
896
+ return (list(Error("table.queryRecords range:", error)))
897
+ }
898
+ if options.filter {
899
+ `(error) = dyna.conv.createKeyExpression(params, {
900
+ FilterExpression: options.filter
901
+ })
902
+ if error
903
+ return (list(Error("table.queryRecords filter:", error)))
904
+ }
905
+ if options.attmap
906
+ params.ExpressionAttributeNames = options.attmap
907
+ if options.reverse
908
+ params.ScanIndexForward = false
909
+ if options.project
910
+ params.ProjectionExpression = options.project.join(", ")
911
+ if options.exstart
912
+ params.ExclusiveStartKey = options.exstart
913
+ output = []
914
+ `(error) = dynQueryPager(`query, params, options.limit, function(data) {
915
+ if data.Count > 0
916
+ output = output.concat(dyna.conv.recordDynToNaan(data.Items))
917
+ data.Count
918
+ })
919
+ if error {
920
+ error = Error("table.queryRecords(".concat(tablename, ") failed"), error)
921
+ output = false
922
+ }
923
+ list(error, output)
924
+ }
925
+
926
+ //
927
+ // scanRecords
928
+ //
929
+ // Options:
374
930
  // {
375
- // "=": <expression> // exclusive of other comparisons
376
- // ">": <expression> // exclusive of other comparisons
377
- // "<": <expression> // exclusive of other comparisons
378
- // beginsWith: <string-expression> // exclusive of other comparisons
379
- // ">=": <expression> // with <= or alone
380
- // "<=": <expression> // with >= or alone
931
+ // index: <string> // index name in table
932
+ // filter: <key-expression> // conditions for items to return
933
+ // attmap: <dictionary> // map attribute names to #shortcuts
934
+ // project: <array> // project only specified attributes
935
+ // exstart: <exclusive start key> // for paging
936
+ // limit: <number> // maximum count of results
381
937
  // }
382
938
  //
383
939
 
384
- table.queryRecords = closure queryRecords(hashValue, options
385
- local result, params, op, expr, rangex, output, error)
940
+ table.scanRecords = closure scanRecords(options
941
+ local result, params, op, expr, output, error)
386
942
  {
387
- if !table.hashKey {
388
- result = info()
389
- if result.0
390
- return(result)
391
- }
943
+ if preflightError(this)
944
+ return
392
945
  params = {
393
946
  TableName: tablename
394
- ExpressionAttributeValues: {
395
- ":v1": datumNaanToDyn(hashValue)
396
- }
397
- KeyConditionExpression: strcat(table.hashKey, " = :v1")
398
947
  }
399
- if options.range {
400
- for `(op, expr) in options.range {
401
- function dupeop() { list(Error("DynaTable.queryRecords: cannot combine", op, "with", rangex.0)) }
402
- if op == ">=" {
403
- if rangex.0 == "<="
404
- rangex = list("between", expr, rangex.1)
405
- else if rangex.0
406
- return (dupeop())
407
- else
408
- rangex = list(op, expr) }
409
- else if op == "<=" {
410
- if rangex.0 == ">="
411
- rangex = list("between", rangex.1, expr)
412
- else if rangex.0
413
- return (dupeop())
414
- else
415
- rangex = list(op, expr) }
416
- else if rangex.0
417
- return (dupeop())
418
- else
419
- rangex = list(op, expr)
420
- }
421
- if rangex {
422
- params.ExpressionAttributeValues[":v2"] = datumNaanToDyn(rangex.1)
423
- if rangex.0 == "between" {
424
- params.ExpressionAttributeValues[":v3"] = datumNaanToDyn(rangex.2)
425
- expr = strcat(" between :v2 and :v3")
426
- }
427
- else
428
- expr = strcat(space, rangex.0, " :v2")
429
- params.KeyConditionExpression = params.KeyConditionExpression.concat(" and ", table.rangeKey, expr)
430
- }
948
+ if options.index
949
+ params.IndexName = options.index
950
+ if options.filter {
951
+ `(error) = dyna.conv.createKeyExpression(params, {
952
+ FilterExpression: options.filter
953
+ })
954
+ if error
955
+ return (list(Error("table.scanRecords filter:", error)))
431
956
  }
957
+ if options.attmap
958
+ params.ExpressionAttributeNames = attmap
959
+ if options.project
960
+ params.ProjectionExpression = options.project.join(", ")
961
+ if options.exstart
962
+ params.ExclusiveStartKey = options.exstart
432
963
  output = []
433
- `(error) = cwlQueryPager(`query, params, options.limit, function(data) {
964
+ `(error) = dynQueryPager(`scan, params, options.limit, function(data) {
434
965
  if data.Count >= 0
435
- output = output.concat(recordDynToNaan(data.Items))
966
+ output = output.concat(dyna.conv.recordDynToNaan(data.Items))
436
967
  data.Count
437
968
  })
438
- if error
969
+ if error {
970
+ error = Error("table.scanRecords(".concat(tablename, ") failed"), error)
439
971
  output = false
972
+ }
440
973
  list(error, output)
441
974
  }
442
975
 
@@ -455,35 +988,69 @@ closure DynaTable(dyna, tablename, local table) {
455
988
 
456
989
  closure DynamoDB(local dyna) {
457
990
  dyna = new(object, this)
991
+ dyna.timeout = 20000 // default 20-second timeout
992
+ dyna.conv = DynaConverter()
993
+ dyna.multi = DynaMultiTable(dyna)
458
994
 
459
- //
460
995
  // login
461
996
  //
462
- dyna.login = closure login(creds) {
997
+ dyna.login = function login(creds) {
463
998
  dyna.aws = xnew(awsSDK.DynamoDB, {
464
- apiVersion: "2012-08-10",
465
- accessKeyId: creds.keyID,
466
- secretAccessKey: creds.keySecret,
467
- region: creds.region })
999
+ region: creds.region
1000
+ credentials: {
1001
+ accessKeyId: creds.keyID
1002
+ secretAccessKey: creds.keySecret
1003
+ }
1004
+ })
1005
+ list(false, { ok: true })
468
1006
  }
469
1007
 
470
- //
471
1008
  // table
472
1009
  //
473
1010
  // Return a new table access object for the table of the specified name. The table may not
474
1011
  // yet exist; see DynaTable for methods.
475
1012
  //
476
- // parameters:
477
- // {
478
- // hash: <name> // primary hash key attribute name (required)
479
- // hashType: <type> // string | numeric (required)
480
- // range: <name> // primary range key attribute name (optional)
481
- // rangeType: <name> // string | numeric (required if range specified)
482
- // }
483
- //
484
- dyna.table = closure table(name) {
1013
+ dyna.table = function table(name) {
485
1014
  DynaTable(dyna, name)
486
1015
  }
1016
+
1017
+ // timekeys
1018
+ //
1019
+ // Return a TimeKeys object with a default no prefix, and baseMS of 1/1/2022.
1020
+ //
1021
+ dyna.timekeys = function timekeys(prefix, baseMS) {
1022
+ TimeKeys(prefix, baseMS)
1023
+ }
1024
+
1025
+ // batchGetRecords
1026
+ //
1027
+ dyna.batchGetRecords = function loadBatchGetRecords(request, options) {
1028
+ require("./aws_dynextra.nlg")
1029
+ dyna.batchGetRecords = function bgr(request, options) {
1030
+ dynexBatchGetRecords(dyna, request, options)
1031
+ }
1032
+ dynexBatchGetRecords(dyna, request, options)
1033
+ }
1034
+
1035
+ // dynexBatchWriteRecords
1036
+ //
1037
+ dyna.batchWriteRecords = function batchWriteRecords(request, options) {
1038
+ require("./aws_dynextra.nlg")
1039
+ dyna.batchWriteRecords = function bwr(request, options) {
1040
+ dynexBatchWriteRecords(dyna, request, options)
1041
+ }
1042
+ dynexBatchWriteRecords(dyna, request, options)
1043
+ }
1044
+
1045
+ // dynexTransactGetRecords
1046
+ //
1047
+ dyna.transactGetRecords = function loadTransactGetRecords(request, options) {
1048
+ require("./aws_dynextra.nlg")
1049
+ dyna.transactGetRecords = function tgr(request, options) {
1050
+ dynexTransactGetRecords(dyna, request, options)
1051
+ }
1052
+ dynexTransactGetRecords(dyna, request, options)
1053
+ }
487
1054
 
488
1055
  // finis
489
1056
  dyna
@@ -491,19 +1058,19 @@ closure DynamoDB(local dyna) {
491
1058
 
492
1059
 
493
1060
  /*
494
- * dynadaInit
1061
+ * dynaInit
495
1062
  *
496
- * Initialize the DynamoDB module.
1063
+ * Initialize the DynamoDB component.
497
1064
  *
498
1065
  */
499
1066
 
500
- function dynadaInit(local manifest) {
501
- manifest = `(DynaTable, DynamoDB, dynadaInit)
1067
+ function dynaInit(local manifest) {
1068
+ manifest = `(TimeKeys, DynaConverter, DynaTable, DynamoDB, dynaInit)
502
1069
 
503
1070
  Naan.module.build(module.id, "aws_dynamo", function(modobj, compobj) {
504
1071
  require("./serviceAws.nlg")
505
1072
  compobj.manifest = manifest
1073
+ modobj.exports.TimeKeys = TimeKeys
506
1074
  modobj.exports.DynamoDB = DynamoDB
507
1075
  })
508
-
509
1076
  } ();