@naanlang/naan 1.0.2 → 1.0.3

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 (35) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +3 -3
  3. package/bin/index.js +2 -2
  4. package/dist/env_web.js +149 -20
  5. package/dist/naan.min.js +5 -5
  6. package/frameworks/browser/sworker.js +309 -91
  7. package/frameworks/browser/terminals.nlg +22 -9
  8. package/frameworks/browser/workers.nlg +19 -11
  9. package/frameworks/client/apiclient.nlg +17 -4
  10. package/frameworks/client/psm_client.nlg +116 -53
  11. package/frameworks/common/common.nlg +12 -2
  12. package/frameworks/node/apiserver.nlg +25 -13
  13. package/frameworks/node/filesystem.nlg +173 -30
  14. package/frameworks/node/psm_server.nlg +95 -32
  15. package/frameworks/project/build.nlg +40 -23
  16. package/frameworks/project/projects.nlg +47 -26
  17. package/frameworks/running/debugnub.nlg +2 -5
  18. package/frameworks/storage/csv.nlg +120 -0
  19. package/frameworks/storage/dbt_pouch.nlg +41 -25
  20. package/frameworks/storage/psm.nlg +108 -28
  21. package/frameworks/storage/psm_dbtables.nlg +7 -3
  22. package/frameworks/storage/resources.nlg +30 -2
  23. package/lib/browser/env_web.js +147 -18
  24. package/lib/browser/env_webworker.js +1 -1
  25. package/lib/browser/require.js +1 -1
  26. package/lib/core/naanlib.js +5 -5
  27. package/package.json +1 -1
  28. package/plugins/serviceAws/aws_cloudwatchlogs.nlg +1 -1
  29. package/plugins/serviceAws/aws_dynamo.nlg +625 -141
  30. package/plugins/serviceAws/aws_dynextra.nlg +294 -0
  31. package/plugins/serviceAws/dbt_aws.nlg +31 -11
  32. package/plugins/serviceAws/psm_aws.nlg +42 -26
  33. package/plugins/serviceAws/serviceAws.nlg +1 -0
  34. package/plugins/serviceGitHub/psm_github.nlg +41 -25
  35. package/plugins/serviceGitLab/psm_gitlab.nlg +41 -25
@@ -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 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,219 @@ 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
+ // genAttributeAdder
156
303
  //
157
- // dynQueryPager
304
+ // Return a function that, when called repeatedly, will add attribute definitions to the
305
+ // specified parameter block. Symbols are interpreted as literal names, e.g. attribute names, and
306
+ // are not defined as values here.
158
307
  //
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.
308
+ closure genAttributeAdder(params, local adex) {
309
+ function attrib(expr, local adef, atype, key, data) {
310
+ if symbol(expr) && !member(expr, `(true, false, null))
311
+ return (expr.tostring) // literal attribute name
312
+ if !params.ExpressionAttributeValues
313
+ params.ExpressionAttributeValues = { }
314
+ if !adex
315
+ adex = (keys(params.ExpressionAttributeValues).length || 0) + 1
316
+ adef = datumNaanToDyn(expr)
317
+ atype = keys(adef).0
318
+ for `(key, data) in params.ExpressionAttributeValues
319
+ if data[atype] == adef[atype]
320
+ return (key) // found matching attribute
321
+ key = ":v".concat(adex++)
322
+ params.ExpressionAttributeValues[key] = adef
323
+ key
324
+ }
325
+ }
326
+
161
327
  //
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
328
+ // createKeyExpression
329
+ //
330
+ // Update the specified parameter block with one or more condition expressions, preserving any
331
+ // that are already present. Specify each condition with a dictionary as follows:
332
+ // {
333
+ // [KeyConditionExpression: <condition-dictionary>]
334
+ // [FilterExpression: <condition-dictionary]
335
+ // [etc.]
336
+ // }
337
+ // If successful the parameter block is updated, otherwise it may be partly updated. The return
338
+ // value is a standard (error, data) tuple where the data is just { ok: true } if no error.
339
+ // This will parse only one expression per dictionary, but as many such conditions as you
340
+ // you wish to use by specifying an array. This does not check for semantics enforced by the
341
+ // API. E.g. Key conditions can only have one condition expression for the sort key, and only
342
+ // equality is allowed for the partition key. Only filter expressions can use the <> operator.
343
+ //
344
+ // condition dictionary:
345
+ // {
346
+ // key: <keyname, e.g. partition key name> // required
347
+ // "=": <expression> // exclusive of other comparisons
348
+ // ">": <expression> // exclusive of other comparisons
349
+ // "<": <expression> // exclusive of other comparisons
350
+ // "<>": <expression> // exclusive of other comparisons
351
+ // beginsWith: <string-expression> // exclusive of other comparisons
352
+ // ">=": <expression> // with <= or alone
353
+ // "<=": <expression> // with >= or alone
354
+ // }
355
+ //
356
+
357
+ conv.createKeyExpression = function createKeyExpression(params, conds
358
+ local error, attrib, rangename, range, keyname, op, expr, rangex, arg1, arg2) {
359
+
360
+ // dupeop
361
+ //
362
+ // Return a duplicate operation error.
363
+ function dupeop() {
364
+ list(Error("key expression cannot combine", op, "with", rangex.0))
365
+ }
366
+
367
+ //
368
+ // build the expression
369
+ //
370
+ attrib = genAttributeAdder(params)
371
+ for `(rangename, ranges) in conds {
372
+ if !array(ranges)
373
+ ranges = [ranges]
374
+ for range in ranges {
375
+ rangex = false
376
+ keyname = tostring(range.key)
377
+ if !keyname
378
+ return (list(Error("key expression requires key name")))
379
+ for `(op, expr) in range {
380
+ if op == "key"
381
+ continue
382
+ expr = attrib(expr)
383
+ if op == ">=" {
384
+ if rangex.0 == "<="
385
+ rangex = list("between", expr, rangex.1)
386
+ else if rangex.0
387
+ return (dupeop())
388
+ else
389
+ rangex = list(op, expr) }
390
+ else if op == "<=" {
391
+ if rangex.0 == ">="
392
+ rangex = list("between", rangex.1, expr)
393
+ else if rangex.0
394
+ return (dupeop())
395
+ else
396
+ rangex = list(op, expr) }
397
+ else if rangex.0
398
+ return (dupeop())
399
+ else
400
+ rangex = list(op, expr)
401
+ }
402
+ if rangex {
403
+ arg1 = rangex.1
404
+ if rangex.0 == "between" {
405
+ arg2 = rangex.2
406
+ expr = keyname.concat(" between ", arg1, " and ", arg2)
407
+ }
408
+ else if rangex.0 == "beginsWith"
409
+ expr = "begins_with(".concat(keyname, ", ", arg1, ")")
410
+ else
411
+ expr = keyname.concat(space, rangex.0, space, arg1)
412
+ if params[rangename]
413
+ params[rangename] = params[rangename].concat(" and ", expr)
414
+ else
415
+ params[rangename] = expr
416
+ }
173
417
  }
174
- })
418
+ }
419
+ list(false, { ok: true })
420
+ }
421
+
422
+ // createUpdateExpression
423
+ //
424
+ // Update the specified parameter block with one or more update expressions, preserving any that
425
+ // are already present. Specify the updates with a dictionary as follows:
426
+ // {
427
+ // set: [`(attribute, value)] // set attributes to values
428
+ // sum: [`(attribute, value)] // attributes += values
429
+ // diff: [`(attribute, value)] // attributes -= values
430
+ // remove: [attribute] // remove attributes from the record
431
+ // }
432
+ // If successful the parameter block is updated, otherwise it may be partly updated. The return
433
+ // value is a standard (error, data) tuple where the data is just { ok: true } if no error.
434
+ //
435
+ conv.createUpdateExpression = function createUpdateExpression(params, updates,
436
+ local attrib, output, item) {
437
+
438
+ if params.UpdateExpression
439
+ return (list(Error("conv.createUpdateExpression: UpdateExpression already exists")))
440
+ attrib = genAttributeAdder(params)
441
+ output = {
442
+ SET: []
443
+ REMOVE: []
444
+ }
445
+ for item in updates.set
446
+ output.SET.push(strcat(item.0, "=", attrib(item.1)))
447
+ for item in updates.sum
448
+ output.SET.push(strcat(item.0, "=", item.0, "+", attrib(item.1)))
449
+ for item in updates.diff
450
+ output.SET.push(strcat(item.0, "=", item.0, "-", attrib(item.1)))
451
+ for item in updates.remove
452
+ output.REMOVE.push(tostring(item))
453
+ params.UpdateExpression = ""
454
+ for `(key, value) in output
455
+ params.UpdateExpression = params.UpdateExpression.concat(key, space, value.join(", "))
456
+ list(false, { ok: true })
175
457
  }
176
458
 
459
+ // genBackoffDelayer
177
460
  //
461
+ // Return a function that, when called repeatedly, will sleep for an exponentially increasing
462
+ // maximum time interval starting with 50 msec and doubling each time. The actual delay varies by
463
+ // a random jitter of up to 100% of the delay. The function returns the maximum milliseconds of
464
+ // the next delay, so the caller can give up if this becomes too large.
465
+ //
466
+ conv.genBackoffDelayer = closure genBackoffDelayer(local delay) {
467
+ delay = 50
468
+ function delayer() {
469
+ sleep(delay * Math.random()) // jitter up to 100%
470
+ delay *= 2
471
+ }
472
+ }
473
+
474
+ // finis
475
+
476
+ conv
477
+ };
478
+
479
+
480
+ /*
481
+ * DynaTable
482
+ *
483
+ * DynamoDB table.
484
+ *
485
+ */
486
+
487
+ closure DynaTable(dyna, tablename, local table) {
488
+ table = new(object, this)
489
+ table.name = tablename
490
+
178
491
  // info
179
492
  //
180
493
  // Return info about the table, or an error, in standard (error, dictionary) tuple format.
@@ -203,6 +516,7 @@ closure DynaTable(dyna, tablename, local table) {
203
516
  if error {
204
517
  dynaTableError = error
205
518
  debuglog("aws.describeTable failed", tablename, error)
519
+ error = Error("table.info(".concat(tablename, ") failed"), error)
206
520
  } else {
207
521
  data = data.Table
208
522
  output = {
@@ -219,18 +533,17 @@ closure DynaTable(dyna, tablename, local table) {
219
533
  output.range = key.AttributeName
220
534
  for key in data.AttributeDefinitions
221
535
  if key.AttributeName == output.hash
222
- output.hashType = fieldTypeDynToNaan(key.AttributeType)
536
+ output.hashType = dyna.conv.fieldTypeDynToNaan(key.AttributeType)
223
537
  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
538
+ output.rangeType = dyna.conv.fieldTypeDynToNaan(key.AttributeType)
539
+ table.hashKey = compress(output.hash) // hash attribute name
540
+ table.rangeKey = compress(output.range) // range attribute name or false
227
541
  }
228
542
  pending.signal(list(error, output))
229
543
  })
230
544
  pending.wait()
231
545
  }
232
546
 
233
- //
234
547
  // create
235
548
  //
236
549
  // Create the table with specified parameters, blocking until the table is ready for use.
@@ -242,11 +555,11 @@ closure DynaTable(dyna, tablename, local table) {
242
555
  // range: <name> // primary range key attribute name (optional)
243
556
  // rangeType: <name> // string | numeric (if range specified)
244
557
  // }
245
-
558
+ //
246
559
  table.create = closure create(parameters, local hashType, rangeType, params, pending, error, data, start) {
247
560
  if !dyna.aws
248
561
  return (list(Error("DynaTable.create: not logged into AWS")))
249
- hashType = fieldTypeNaanToDyn(parameters.hashType)
562
+ hashType = dyna.conv.fieldTypeNaanToDyn(parameters.hashType)
250
563
  if !parameters.hash || !hashType
251
564
  return (list(Error("DynoTable.create: primary key hash/hashType required")))
252
565
  params = {
@@ -265,7 +578,7 @@ closure DynaTable(dyna, tablename, local table) {
265
578
  ]
266
579
  }
267
580
  if parameters.range {
268
- rangeType = fieldTypeNaanToDyn(parameters.rangeType)
581
+ rangeType = dyna.conv.fieldTypeNaanToDyn(parameters.rangeType)
269
582
  if !rangeType
270
583
  return (list(Error("DynoTable.create: range requires rangeType")))
271
584
  params.AttributeDefinitions.push({
@@ -282,6 +595,7 @@ closure DynaTable(dyna, tablename, local table) {
282
595
  if error {
283
596
  dynaTableError = error
284
597
  debuglog("aws.createTable failed", tablename, error)
598
+ error = Error("table.create(".concat(tablename, ") failed"), error)
285
599
  }
286
600
  pending.signal(list(error, data))
287
601
  })
@@ -293,7 +607,7 @@ closure DynaTable(dyna, tablename, local table) {
293
607
  `(error, data) = info()
294
608
  if data.status == "ACTIVE"
295
609
  break
296
- if milliseconds() - start > 20000 { // 20 seconds
610
+ if milliseconds() - start > dyna.timeout { // 20 seconds
297
611
  if !error
298
612
  error = data.status
299
613
  return(list(Error("DynoTable.create: timeout waiting for new table to become active", error)))
@@ -302,27 +616,81 @@ closure DynaTable(dyna, tablename, local table) {
302
616
  }
303
617
  list(false, data)
304
618
  }
305
-
619
+
620
+ // preflightError
306
621
  //
307
- // putRecord
622
+ // Ensure that the table is ready before executing an operation. This returns an error tuple or
623
+ // false to proceed.
308
624
  //
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) {
625
+ function preflightError(op) {
626
+ if !dyna.aws
627
+ return (list(Error("DynaTable.".concat(op, ": not logged into AWS"))))
312
628
  if !table.hashKey {
313
629
  result = info()
314
630
  if result.0
315
631
  return(result)
316
632
  }
633
+ }
634
+
635
+ // makeDynKey
636
+ //
637
+ // Make a dynamoDB key dictionary for the table using the specified primary key, which can be a
638
+ // tuple of hash/range or just a hash if only a hash is needed for this table.
639
+ //
640
+ conv.makeDynKey = function makeDynKey(primaryKey, local hashValue, rangeValue, key) {
641
+ if tuple(primaryKey)
642
+ `(hashValue, rangeValue) = primaryKey
643
+ else
644
+ hashValue = primaryKey
645
+ key = { }
646
+ key[table.hashKey] = dyna.conv.datumNaanToDyn(hashValue)
647
+ if table.rangeKey
648
+ key[table.rangeKey] = dyna.conv.datumNaanToDyn(rangeValue)
649
+ key
650
+ }
651
+
652
+ // getRecord
653
+ //
654
+ // Get a record from the table with the specified hash value, and optional range value.
655
+ //
656
+ table.getRecord = closure getRecord(hashValue, rangeValue, local result, params, pending) {
657
+ if preflightError(this)
658
+ return
659
+ params = {
660
+ TableName: tablename
661
+ }
662
+ params.Key = makeDynKey(list(hashValue, rangeValue))
663
+ pending = new(nonce)
664
+ dyna.aws.getItem(params, function(error, data, local output) {
665
+ if error {
666
+ dynaTableError = error
667
+ debuglog("aws.getItem failed", tablename, error)
668
+ error = Error("table.getRecord(".concat(tablename, ") failed"), error)
669
+ } else
670
+ data = dyna.conv.recordDynToNaan(data.Item)
671
+ pending.signal(list(error, data))
672
+ })
673
+ pending.wait()
674
+ }
675
+
676
+ //
677
+ // putRecord
678
+ //
679
+ // Put an record in the table, replacing any existing record with the same key.
680
+
681
+ table.putRecord = closure putRecord(record, local result, params, pending) {
682
+ if preflightError(this)
683
+ return
317
684
  params = {
318
685
  TableName: tablename
319
- Item: recordNaanToDyn(record)
686
+ Item: dyna.conv.recordNaanToDyn(record)
320
687
  }
321
688
  pending = new(nonce)
322
689
  dyna.aws.putItem(params, function(error, data, local output) {
323
690
  if error {
324
691
  dynaTableError = error
325
692
  debuglog("aws.putItem failed", tablename, error)
693
+ error = Error("table.putRecord(".concat(tablename, ") failed"), error)
326
694
  }
327
695
  pending.signal(list(error, data))
328
696
  })
@@ -330,113 +698,197 @@ closure DynaTable(dyna, tablename, local table) {
330
698
  }
331
699
 
332
700
  //
333
- // getRecord
701
+ // updateRecord
334
702
  //
335
- // Get a record from the table with the specified hash value, and optional range value.
703
+ // Update a record in the table. This will update the existing record if it exists, or add a new
704
+ // one. Options include conditional updates, and adding or removing attributes as follows:
705
+ // {
706
+ // condition: <range-expression> // conditional expression as in createKeyExpression()
707
+ // set: [`(attribute, value)] // set attributes to values
708
+ // sum: [`(attribute, value)] // attributes += values
709
+ // diff: [`(attribute, value)] // attributes -= values
710
+ // remove: [attribute] // remove attributes from the record
711
+ // return: "all_old" | "all_new" | "updated_old" | "updated_new"
712
+ // // return the record as chosen, otherwise { }
713
+ // }
714
+ // The result is a standard result tuple `(error, record).
336
715
 
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)
342
- }
716
+ table.updateRecord = closure updateRecord(hashValue, rangeValue, options, local error, result, params, actions, pending) {
717
+ if preflightError(this)
718
+ return
343
719
  params = {
344
720
  TableName: tablename
345
- Key: { }
721
+ Item: dyna.conv.recordNaanToDyn(record)
722
+ }
723
+ params.Key = makeDynKey(list(hashValue, rangeValue))
724
+ if options.condition {
725
+ `(error) = dyna.conv.createKeyExpression(params, {
726
+ KeyConditionExpression: {
727
+ key: table.hashKey
728
+ "=": hashValue
729
+ }
730
+ })
731
+ if error
732
+ return (list(Error("table.updateRecord:", error)))
733
+ }
734
+ dyna.conv.createUpdateExpression(params, `SET, options)
735
+ if options.return {
736
+ params.ReturnValues = {
737
+ all_old: "ALL_OLD"
738
+ all_new: "ALL_NEW"
739
+ updated_old: "UPDATED_OLD"
740
+ updated_new: "UPDATED_NEW"
741
+ }
742
+ if !params.ReturnValues
743
+ params.ReturnValues = "NONE"
346
744
  }
347
- params.Key[table.hashKey] = datumNaanToDyn(hashValue)
348
- if table.rangeKey
349
- params.Key[table.rangeKey] = datumNaanToDyn(rangeValue)
350
745
  pending = new(nonce)
351
- dyna.aws.getItem(params, function(error, data, local output) {
746
+ dyna.aws.putItem(params, function(error, data) {
352
747
  if error {
353
748
  dynaTableError = error
354
- debuglog("aws.getItem failed", tablename, error)
355
- } else
356
- data = recordDynToNaan(data.Item)
749
+ debuglog("aws.putItem failed", tablename, error)
750
+ error = Error("table.updateRecord(".concat(tablename, ") failed"), error)
751
+ }
752
+ else
753
+ data = dyna.conv.recordDynToNaan(data.Attributes)
357
754
  pending.signal(list(error, data))
358
755
  })
359
756
  pending.wait()
360
757
  }
361
758
 
759
+ //
760
+ // dynQueryPager
761
+ //
762
+ // Perform a paged query. This implements paging conventions specific to DynamoDB. The callback
763
+ // should return the number of items found in the data, or false to stop looping.
764
+ //
765
+ closure dynQueryPager(procname, params, remaining, doneCB) {
766
+ if !remaining
767
+ remaining = 10 // default retrieve limit
768
+ params.Limit = remaining
769
+ QueryPager(dyna.aws, procname, params, function(data, local processed) {
770
+ processed = doneCB(data)
771
+ if processed
772
+ remaining -= processed
773
+ if processed && remaining > 0 && data.LastEvaluatedKey {
774
+ params.limit = remaining // set up for next iteration
775
+ params.ExclusiveStartKey = data.LastEvaluatedKey
776
+ true // continue looping
777
+ }
778
+ })
779
+ }
780
+
362
781
  //
363
782
  // queryRecords
364
783
  //
365
784
  // Options:
366
785
  // {
367
786
  // index: <string> // index name in table
368
- // range: <expression-dictionary> // range of keys to retrieve
787
+ // range: <key-expression> // range of keys to retrieve
788
+ // filter: <key-expression> // conditions for items to return
789
+ // attmap: <dictionary> // map attribute names to #shortcuts
369
790
  // reverse: <boolean> // reverse order
791
+ // project: <array> // project only specified attributes
792
+ // exstart: <exclusive start key> // for paging
370
793
  // limit: <number> // maximum count of results
371
794
  // }
372
795
  //
373
- // range dictionary:
374
- // {
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
381
- // }
382
- //
383
796
 
384
797
  table.queryRecords = closure queryRecords(hashValue, options
385
- local result, params, op, expr, rangex, output, error)
798
+ local error, result, params, op, expr, output, error)
386
799
  {
387
- if !table.hashKey {
388
- result = info()
389
- if result.0
390
- return(result)
391
- }
800
+ if preflightError(this)
801
+ return
392
802
  params = {
393
803
  TableName: tablename
394
- ExpressionAttributeValues: {
395
- ":v1": datumNaanToDyn(hashValue)
396
- }
397
- KeyConditionExpression: strcat(table.hashKey, " = :v1")
398
804
  }
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)
805
+ dyna.conv.createKeyExpression(params, {
806
+ KeyConditionExpression: {
807
+ key: table.hashKey
808
+ "=": hashValue
430
809
  }
810
+ })
811
+ if options.index
812
+ params.IndexName = options.index
813
+ if options.range {
814
+ `(error) = dyna.conv.createKeyExpression(params, {
815
+ KeyConditionExpression: options.range
816
+ })
817
+ if error
818
+ return (list(Error("table.queryRecords range:", error)))
431
819
  }
820
+ if options.filter {
821
+ `(error) = dyna.conv.createKeyExpression(params, {
822
+ FilterExpression: options.filter
823
+ })
824
+ if error
825
+ return (list(Error("table.queryRecords filter:", error)))
826
+ }
827
+ if options.attmap
828
+ params.ExpressionAttributeNames = attmap
829
+ if options.reverse
830
+ params.ScanIndexForward = false
831
+ if options.project
832
+ params.ProjectionExpression = options.project.join(", ")
833
+ if options.exstart
834
+ params.ExclusiveStartKey = options.exstart
432
835
  output = []
433
- `(error) = cwlQueryPager(`query, params, options.limit, function(data) {
836
+ `(error) = dynQueryPager(`query, params, options.limit, function(data) {
837
+ if data.Count > 0
838
+ output = output.concat(dyna.conv.recordDynToNaan(data.Items))
839
+ data.Count
840
+ })
841
+ if error {
842
+ error = Error("table.queryRecords(".concat(tablename, ") failed"), error)
843
+ output = false
844
+ }
845
+ list(error, output)
846
+ }
847
+
848
+ //
849
+ // scanRecords
850
+ //
851
+ // Options:
852
+ // {
853
+ // index: <string> // index name in table
854
+ // filter: <key-expression> // conditions for items to return
855
+ // attmap: <dictionary> // map attribute names to #shortcuts
856
+ // project: <array> // project only specified attributes
857
+ // exstart: <exclusive start key> // for paging
858
+ // limit: <number> // maximum count of results
859
+ // }
860
+ //
861
+
862
+ table.scanRecords = closure scanRecords(options
863
+ local result, params, op, expr, output, error)
864
+ {
865
+ if preflightError(this)
866
+ return
867
+ params = {
868
+ TableName: tablename
869
+ }
870
+ if options.index
871
+ params.IndexName = options.index
872
+ if options.filter
873
+ dyna.conv.createKeyExpression(params, {
874
+ FilterExpression: options.filter
875
+ })
876
+ if options.attmap
877
+ params.ExpressionAttributeNames = attmap
878
+ if options.project
879
+ params.ProjectionExpression = options.project.join(", ")
880
+ if options.exstart
881
+ params.ExclusiveStartKey = options.exstart
882
+ output = []
883
+ `(error) = dynQueryPager(`scan, params, options.limit, function(data) {
434
884
  if data.Count >= 0
435
- output = output.concat(recordDynToNaan(data.Items))
885
+ output = output.concat(dyna.conv.recordDynToNaan(data.Items))
436
886
  data.Count
437
887
  })
438
- if error
888
+ if error {
889
+ error = Error("table.scanRecords(".concat(tablename, ") failed"), error)
439
890
  output = false
891
+ }
440
892
  list(error, output)
441
893
  }
442
894
 
@@ -455,35 +907,67 @@ closure DynaTable(dyna, tablename, local table) {
455
907
 
456
908
  closure DynamoDB(local dyna) {
457
909
  dyna = new(object, this)
910
+ dyna.timeout = 20000 // default 20-second timeout
911
+ dyna.conv = DynaConverter()
912
+ dyna.multi = DynaMultiTable(dyna)
458
913
 
459
- //
460
914
  // login
461
915
  //
462
- dyna.login = closure login(creds) {
916
+ dyna.login = function login(creds) {
463
917
  dyna.aws = xnew(awsSDK.DynamoDB, {
464
918
  apiVersion: "2012-08-10",
465
919
  accessKeyId: creds.keyID,
466
920
  secretAccessKey: creds.keySecret,
467
921
  region: creds.region })
922
+ list(false, { ok: true })
468
923
  }
469
924
 
470
- //
471
925
  // table
472
926
  //
473
927
  // Return a new table access object for the table of the specified name. The table may not
474
928
  // yet exist; see DynaTable for methods.
475
929
  //
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) {
930
+ dyna.table = function table(name) {
485
931
  DynaTable(dyna, name)
486
932
  }
933
+
934
+ // timekeys
935
+ //
936
+ // Return a TimeKeys object with a default no prefix, and baseMS of 1/1/2022.
937
+ //
938
+ dyna.timekeys = function timekeys(prefix, baseMS) {
939
+ TimeKeys(prefix, baseMS)
940
+ }
941
+
942
+ // batchGetRecords
943
+ //
944
+ dyna.batchGetRecords = function loadBatchGetRecords(request, options) {
945
+ require("./aws_dynextra.nlg")
946
+ dyna.batchGetRecords = function bgr(request, options) {
947
+ dynexBatchGetRecords(dyna, request, options)
948
+ }
949
+ dynexBatchGetRecords(dyna, request, options)
950
+ }
951
+
952
+ // dynexBatchWriteRecords
953
+ //
954
+ dyna.batchWriteRecords = function batchWriteRecords(request, options) {
955
+ require("./aws_dynextra.nlg")
956
+ dyna.batchWriteRecords = function bwr(request, options) {
957
+ dynexBatchWriteRecords(dyna, request, options)
958
+ }
959
+ dynexBatchWriteRecords(dyna, request, options)
960
+ }
961
+
962
+ // dynexTransactGetRecords
963
+ //
964
+ dyna.transactGetRecords = function loadTransactGetRecords(request, options) {
965
+ require("./aws_dynextra.nlg")
966
+ dyna.transactGetRecords = function tgr(request, options) {
967
+ dynexTransactGetRecords(dyna, request, options)
968
+ }
969
+ dynexTransactGetRecords(dyna, request, options)
970
+ }
487
971
 
488
972
  // finis
489
973
  dyna
@@ -491,19 +975,19 @@ closure DynamoDB(local dyna) {
491
975
 
492
976
 
493
977
  /*
494
- * dynadaInit
978
+ * dynaInit
495
979
  *
496
- * Initialize the DynamoDB module.
980
+ * Initialize the DynamoDB component.
497
981
  *
498
982
  */
499
983
 
500
- function dynadaInit(local manifest) {
501
- manifest = `(DynaTable, DynamoDB, dynadaInit)
984
+ function dynaInit(local manifest) {
985
+ manifest = `(TimeKeys, DynaConverter, DynaTable, DynamoDB, dynaInit)
502
986
 
503
987
  Naan.module.build(module.id, "aws_dynamo", function(modobj, compobj) {
504
- require("./serviceAws.nlg")
505
988
  compobj.manifest = manifest
989
+ modobj.exports.TimeKeys = TimeKeys
506
990
  modobj.exports.DynamoDB = DynamoDB
991
+ require("./serviceAws.nlg")
507
992
  })
508
-
509
993
  } ();