@naanlang/naan 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +46 -0
  3. package/dist/env_web.js +431 -0
  4. package/dist/naan.min.js +73 -0
  5. package/frameworks/browser/browser.nlg +30 -0
  6. package/frameworks/browser/pouchdb/pouchdb.min.js +7 -0
  7. package/frameworks/browser/spark-md5/spark-md5.min.js +1 -0
  8. package/frameworks/browser/sworker.js +192 -0
  9. package/frameworks/browser/terminals.nlg +787 -0
  10. package/frameworks/browser/workers.nlg +386 -0
  11. package/frameworks/client/apiclient.nlg +183 -0
  12. package/frameworks/client/client.nlg +29 -0
  13. package/frameworks/client/psm_client.nlg +572 -0
  14. package/frameworks/common/common.nlg +464 -0
  15. package/frameworks/common/preferences.nlg +131 -0
  16. package/frameworks/common/repltools.nlg +100 -0
  17. package/frameworks/common/watching.nlg +97 -0
  18. package/frameworks/node/apiserver.nlg +399 -0
  19. package/frameworks/node/filesystem.nlg +1012 -0
  20. package/frameworks/node/gitter.nlg +234 -0
  21. package/frameworks/node/node.nlg +81 -0
  22. package/frameworks/node/psm_server.nlg +465 -0
  23. package/frameworks/node/worker.nlg +560 -0
  24. package/frameworks/project/build.nlg +1229 -0
  25. package/frameworks/project/jszip.js +30 -0
  26. package/frameworks/project/jszip.nlg +87 -0
  27. package/frameworks/project/proj_cliser.nlg +139 -0
  28. package/frameworks/project/proj_console.nlg +137 -0
  29. package/frameworks/project/proj_folder.nlg +143 -0
  30. package/frameworks/project/proj_lambda.nlg +137 -0
  31. package/frameworks/project/proj_static.nlg +138 -0
  32. package/frameworks/project/project.nlg +29 -0
  33. package/frameworks/project/projects.nlg +587 -0
  34. package/frameworks/project/uglifyjs.js +1 -0
  35. package/frameworks/project/uglifyjs.nlg +79 -0
  36. package/frameworks/running/debugcom.nlg +163 -0
  37. package/frameworks/running/debugnub.nlg +565 -0
  38. package/frameworks/running/debugutil.nlg +510 -0
  39. package/frameworks/running/executors.nlg +767 -0
  40. package/frameworks/running/running.nlg +151 -0
  41. package/frameworks/running/sourcecode.nlg +346 -0
  42. package/frameworks/running/taskexec.nlg +196 -0
  43. package/frameworks/storage/dbt_pouch.nlg +773 -0
  44. package/frameworks/storage/file_manager.nlg +772 -0
  45. package/frameworks/storage/psm.nlg +326 -0
  46. package/frameworks/storage/psm_dbtables.nlg +814 -0
  47. package/frameworks/storage/resources.nlg +445 -0
  48. package/frameworks/storage/storage.nlg +52 -0
  49. package/lib/browser/env_web.js +431 -0
  50. package/lib/browser/env_webworker.js +146 -0
  51. package/lib/browser/require.js +1653 -0
  52. package/lib/core/naanlib.js +73 -0
  53. package/lib/env_node.js +282 -0
  54. package/lib/env_nodeworker.js +165 -0
  55. package/lib/node_repl.js +25 -0
  56. package/lib/node_repl_init.nlg +52 -0
  57. package/package.json +13 -0
  58. package/plugins/gitClient/clientops.nlg +317 -0
  59. package/plugins/gitClient/gitClient.nlg +63 -0
  60. package/plugins/gitClient/gitutils.nlg +1331 -0
  61. package/plugins/gitClient/minimatch.js +945 -0
  62. package/plugins/gitClient/minimatch_license.txt +15 -0
  63. package/plugins/gitClient/minimatch_readme.md +208 -0
  64. package/plugins/nodeLib/nodeLib.nlg +26 -0
  65. package/plugins/nodeLib/node_cp.nlg +65 -0
  66. package/plugins/nodeLib/node_fs.nlg +137 -0
  67. package/plugins/nodeLib/node_https.nlg +172 -0
  68. package/plugins/nodeLib/node_util.nlg +180 -0
  69. package/plugins/serviceAws/aws/aws-sdk.min.js +88 -0
  70. package/plugins/serviceAws/aws/lambda_rest_index.js +122 -0
  71. package/plugins/serviceAws/aws/lambda_rest_init.nlg +58 -0
  72. package/plugins/serviceAws/aws/lambda_ws_index.js +498 -0
  73. package/plugins/serviceAws/aws/lambda_ws_init.nlg +49 -0
  74. package/plugins/serviceAws/aws_cloudwatchlogs.nlg +122 -0
  75. package/plugins/serviceAws/aws_cognito.nlg +76 -0
  76. package/plugins/serviceAws/aws_dynamo.nlg +509 -0
  77. package/plugins/serviceAws/aws_lambda.nlg +95 -0
  78. package/plugins/serviceAws/aws_s3.nlg +134 -0
  79. package/plugins/serviceAws/aws_utils.nlg +86 -0
  80. package/plugins/serviceAws/dbt_aws.nlg +769 -0
  81. package/plugins/serviceAws/lambda_naan_control.nlg +175 -0
  82. package/plugins/serviceAws/psm_aws.nlg +258 -0
  83. package/plugins/serviceAws/serviceAws.nlg +38 -0
  84. package/plugins/serviceGitHub/gitops.nlg +522 -0
  85. package/plugins/serviceGitHub/psm_github.nlg +205 -0
  86. package/plugins/serviceGitHub/serviceGitHub.nlg +65 -0
  87. package/plugins/serviceGitLab/gitops.nlg +528 -0
  88. package/plugins/serviceGitLab/psm_gitlab.nlg +205 -0
  89. package/plugins/serviceGitLab/serviceGitLab.nlg +65 -0
  90. package/test/harness.nlg +312 -0
  91. package/test/node_test.js +33 -0
  92. package/test/test_01_core.nlg +768 -0
  93. package/test/test_02_context.nlg +269 -0
  94. package/test/test_03_jsinterop.nlg +137 -0
  95. package/test/test_04_numerics.nlg +450 -0
  96. package/test/test_05_strings.nlg +63 -0
  97. package/test/test_06_lingoparse.nlg +500 -0
  98. package/test/test_07_lingo.nlg +623 -0
@@ -0,0 +1,769 @@
1
+ /*
2
+ * dbt_aws.nlg
3
+ * serviceAws
4
+ *
5
+ * Naan database tables using AWS for browser and NodeJS.
6
+ *
7
+ * column positioning: // // !
8
+ *
9
+ * Copyright (c) 2020-2021 by Richard C. Zulch
10
+ *
11
+ */
12
+
13
+
14
+ /*
15
+ * dawsValidateName
16
+ *
17
+ * True iff the specified name is valid here.
18
+ *
19
+ */
20
+
21
+ function dawsValidateName(key, path) {
22
+
23
+ };
24
+
25
+
26
+ /*
27
+ * dawsBucket
28
+ *
29
+ * Create an S3 access object for a bucket, with as few added semantics as possible. Callbacks
30
+ * are required for these member functions; they don't provide a sync adapter. AWS Credentials
31
+ * including the desired region must be preconfigured in AWS.config before creating a dawsBucket
32
+ * instance. The return value is tuple of (error, bucket).
33
+ *
34
+ */
35
+
36
+ closure dawsBucket(s3, local bucket) {
37
+ bucket = new(object, this)
38
+ bucket.s3 = s3
39
+ bucket.name = s3.config.params.Bucket
40
+ bucket.region = s3.config.region
41
+ if not s3 || !string(bucket.region) || bucket.region.length == 0 || !string(bucket.name) || bucket.bucket.length == 0
42
+ return (list(Error("invalid S3 parameter")))
43
+
44
+ // asyncResult
45
+ //
46
+ // Invoke a callback function asynchronously. The first argument is the callback, followed by
47
+ // any others desired.
48
+
49
+ closure asyncResult args {
50
+ future(function() { apply(pop(args), args) }).run()
51
+ }
52
+
53
+ // close
54
+ //
55
+ // Close the bucket, preventing future i/o. It's important that the methods below check
56
+ // that the bucket is still open before beginning (or continuing) any operations.
57
+
58
+ bucket.close = function close() {
59
+ bucket.s3 = false // just remove access
60
+ list(false, { })
61
+ }
62
+
63
+ // get
64
+ //
65
+ // Get an object from the bucket. This omits the content if "nodata" is true.
66
+
67
+ bucket.get = closure get(key, nodata, callback, local params) {
68
+ if !string(key) || length(key) == 0
69
+ return (asyncResult(callback, Error("invalid arguments")))
70
+ if !s3
71
+ return (asyncResult(callback, Error("database closed")))
72
+ params = {
73
+ Key: key
74
+ }
75
+
76
+ function s3GetCB(error, resp) {
77
+ if error
78
+ error = Error("can't get object", error, key)
79
+ else if resp.ETag.length == 34
80
+ resp.md5 = resp.ETag.substring(1,33) // ETags have to have quotes, but our hash doesn't
81
+ callback(error, resp)
82
+ }
83
+
84
+ if nodata
85
+ s3.headObject(params, s3GetCB.proc)
86
+ else
87
+ s3.getObject(params, s3GetCB.proc)
88
+ }
89
+
90
+ // put
91
+ //
92
+ // Put an object in the bucket. Parameters include:
93
+ // key - the object name, which may not be empty or start with "/"
94
+ // data - optional content to store
95
+ // metadata - optional dictionary of keys to store
96
+
97
+ bucket.put = closure put(key, data, metadata, callback, local params, mimeType, md5) {
98
+ if !string(key) || length(key) == 0 || key.substring(0,1) == "/" || metadata && !dictionary(metadata)
99
+ return (asyncResult(callback, Error("invalid arguments")))
100
+ if !s3
101
+ return (asyncResult(callback, Error("database closed")))
102
+ params = {
103
+ Key: key
104
+ }
105
+ if data {
106
+ if !integer(data.byteLength) && !string(data) { // non-buffer/non-string translated to JSON string
107
+ data = JSONstringify(data)
108
+ params.ContentType = "application/json" }
109
+ else {
110
+ mimeType = {
111
+ "css": "text/css",
112
+ "csv": "text/csv",
113
+ "htm": "text/html",
114
+ "html": "text/html",
115
+ "ico": "image/x-icon",
116
+ "ics": "application/octet-stream",
117
+ "jpeg": "image/jpeg",
118
+ "jpg": "image/jpeg",
119
+ "js": "text/javascript",
120
+ "json": "application/json",
121
+ "mp3": "audio/mpeg",
122
+ "mp4": "video/mp4",
123
+ "nlg": "text/plain",
124
+ "npk": "application/json",
125
+ "svg": "image/svg+xml",
126
+ "text": "text/plain",
127
+ "txt": "text/plain",
128
+ "zip": "application/octet-stream"
129
+ }[JSpath.extname(key).substring(1)]
130
+ if !mimeType {
131
+ if string(data)
132
+ mimeType = "text/plain"
133
+ else
134
+ mimeType = "application/octet-stream"
135
+ }
136
+ if mimeType.startsWith("text/")
137
+ mimeType = mimeType.concat("; charset=UTF-8")
138
+ params.ContentType = mimeType
139
+ }
140
+ }
141
+ if metadata
142
+ params.Metadata = metadata
143
+ if data
144
+ params.Body = data
145
+ else
146
+ data = "" // empty string for default MD5 hash
147
+ params.ContentMD5 = HashMD5_base64(data) // base64 string
148
+ md5 = HashMD5(data) // hex character string
149
+ s3.putObject(params, function(error, resp) {
150
+ if error
151
+ error = Error("can't put object", error, key)
152
+ else {
153
+ resp.length = length(data)
154
+ if resp.ETag.length == 34 { // multipart S3 ETag will is longer and will be ignored
155
+ resp.md5 = resp.ETag.substring(1,33) // ETags have to have quotes, but our hash doesn't
156
+ if md5 != resp.md5 {
157
+ error = Error("hash match failure", md5, resp.md5) // has match failed on round-trip
158
+ resp = false } } }
159
+ callback(error, resp)
160
+ })
161
+ }
162
+
163
+ // delete
164
+ //
165
+ // Delete an object.
166
+
167
+ bucket.delete = closure delete(key, callback, local params) {
168
+ if !string(key) || length(key) == 0
169
+ return (asyncResult(callback, Error("invalid arguments")))
170
+ if !s3
171
+ return (asyncResult(callback, Error("database closed")))
172
+ params = {
173
+ Key: key
174
+ }
175
+ s3.deleteObject(params, function(error, resp) {
176
+ if error
177
+ error = Error("can't delete object", error, key)
178
+ callback(error, resp)
179
+ })
180
+ }
181
+
182
+ // copy
183
+ //
184
+ // Copy an object within a bucket.
185
+
186
+ bucket.copy = closure copy(srckey, destkey, callback, local params) {
187
+ if !string(srckey) || length(srckey) == 0 || !string(destkey) || length(destkey) == 0
188
+ return (asyncResult(callback, Error("invalid arguments")))
189
+ if !s3
190
+ return (asyncResult(callback, Error("database closed")))
191
+ params = {
192
+ Bucket: bucket.name
193
+ CopySource: js.w.encodeURIComponent(bucket.name).concat("/", js.w.encodeURIComponent(srckey))
194
+ Key: destkey
195
+ }
196
+ s3.copyObject(params, function(error, resp) {
197
+ if error
198
+ error = Error("can't copy object", error)
199
+ callback(error, resp)
200
+ })
201
+ }
202
+
203
+ // list
204
+ //
205
+ // List objects matching an (optional) prefix. This does not attempt parallel i/o so that
206
+ // sorting is not required. If onelevel is true then this only lists one level of object. Use
207
+ // this to build a tree in parallel.
208
+ // The result is an (error, result) tuple, with the following result dictionary on success:
209
+ //
210
+ // { Prefix : "joe/bill/",
211
+ // CommonPrefixes : [
212
+ // { Prefix: "test/folderA/" },
213
+ // { Prefix: "test/folderB/" }],
214
+ // Contents : [
215
+ // {
216
+ // Key : "john",
217
+ // LastModified: [Date Sat Apr 04 2020 17:05:38 GMT-0700 (Pacific Daylight Time)]
218
+ // ETag : "55ff07fb005494c4bd42322d4845e858"
219
+ // Size : 156043
220
+ // StorageClass: "STANDARD" },
221
+ // {
222
+ // Key : "jill",
223
+ // LastModified: [Date Sat Apr 22 2020 17:05:38 GMT-0700 (Pacific Daylight Time)]
224
+ // ETag : "f53f07fb00d42322dc4bd42322d4845e"
225
+ // Size : 155
226
+ // StorageClass: "STANDARD" } ] }
227
+ //
228
+
229
+ bucket.list = closure listobjects(prefix, onelevel, callback, local params, output) {
230
+
231
+ function listOne() { // list one chunk of objects
232
+ s3.listObjectsV2(params, function(error, resp) {
233
+ if error
234
+ callback(Error("list objects failed", error))
235
+ else {
236
+ if output {
237
+ output.Contents = output.Contents.concat(resp.Contents)
238
+ output.CommonPrefixes = output.CommonPrefixes.concat(resp.CommonPrefixes) }
239
+ else
240
+ output = {
241
+ Prefix: resp.Prefix
242
+ CommonPrefixes: resp.CommonPrefixes
243
+ Contents: resp.Contents
244
+ }
245
+ if resp.IsTruncated {
246
+ params.ContinuationToken = resp.NextContinuationToken
247
+ listOne()
248
+ } else
249
+ callback(false, output)
250
+ } }) }
251
+
252
+ if !s3
253
+ return (asyncResult(callback, Error("database closed")))
254
+ params = { }
255
+ if string(prefix)
256
+ params.Prefix = prefix
257
+ if onelevel
258
+ params.Delimiter = "/" // don't go deep; just list children
259
+ listOne()
260
+ }
261
+
262
+ list(false, bucket)
263
+
264
+ };
265
+
266
+
267
+ /*
268
+ * dawsTable
269
+ *
270
+ * Create a database table access object.
271
+ *
272
+ * If a callback function is specified then it is called when the operation completes with the
273
+ * usual (error, result) arguments. If a callback function is not specified then the call blocks
274
+ * until the operation is complete, and an (error, result) tuple is returned.
275
+ * Records are presented as dictionaries to the client of this interface. The client's data is
276
+ * stored under <record>.content, while the record key is available under <record>.key.
277
+ *
278
+ * Internals
279
+ * AWS offers a limited number of buckets per account (initially 100) but we may need a large
280
+ * number of records. The key structuring scheme here allows us to segregate buckets into independent
281
+ * folders sharing a single bucket.
282
+ * An S3DB instance has an S3 context that specifies the region and bucketname, along with all
283
+ * necessary access credentials. This is normally initialized from a credentials vault.
284
+ * Within each bucket, a table instance has a path starting with "" representing the root of the
285
+ * database. All records in the bucket having that path as a prefix belong to this table instance.
286
+ * instance. A pseudo-folder structure is implemented using "/" (slash) characters as delimiters.
287
+ * AWS S3 by itself does not offer conflict detection or change notification. Those can be done
288
+ * by lambdas if required.
289
+ *
290
+ */
291
+
292
+ closure dawsTable(database, path, local table) {
293
+ table = new(object, this)
294
+ table.type = database.type
295
+ table.db = database
296
+ table.path = path
297
+ table.s3prefix = makePrefix(path)
298
+
299
+ // makePrefix
300
+ //
301
+ // Make a key prefix from a path.
302
+
303
+ function makePrefix(path) {
304
+ if path == ""
305
+ path // in the root
306
+ else if path.slice(-1) == "/"
307
+ path // already complete prefix
308
+ else
309
+ path.concat("/") // non-root prefix always ends in slash
310
+ }
311
+
312
+ // makeID
313
+ //
314
+ // Make an internal ID string from an external key, using the table's pre-determined prefix.
315
+
316
+ function makeID(key) {
317
+ if key eq `prefix
318
+ table.s3prefix
319
+ else if integer(key)
320
+ table.s3prefix.concat(tostring(key))
321
+ else if not string(key)
322
+ return (Error("invalid key type: ", typeof(key)))
323
+ else
324
+ table.s3prefix.concat(key)
325
+ }
326
+
327
+ // s3resp2doc
328
+ //
329
+ // Convert an S3 get response to a new table document.
330
+
331
+ function s3resp2doc(resp, local doc, metadata, metakey) {
332
+ if resp.Metadata.naandbt
333
+ try {
334
+ doc = new(JSONparse(resp.Metadata.naandbt))
335
+ } catch {
336
+ if true { }
337
+ }
338
+ if !dictionary(doc)
339
+ doc = { } // invalid metadata
340
+ if resp.key
341
+ doc.key = resp.key
342
+ if resp.Body
343
+ doc.content = resp.Body
344
+ if resp.LastModified
345
+ doc.modified = resp.LastModified
346
+ if resp.ContentLength
347
+ doc.length = resp.ContentLength
348
+ if resp.ContentType
349
+ doc.contentType = resp.ContentType
350
+ if resp.md5
351
+ doc.md5 = resp.md5
352
+ doc
353
+ }
354
+
355
+ // metadata2s3
356
+ //
357
+ // Convert a dictionary to S3 params metadata. If valid this returns the data to be defined
358
+ // on params.Metadata, otherwise it returns false.
359
+
360
+ function metadata2s3(metadata, local metakeep, metakey) {
361
+ if metadata {
362
+ metakeep = { }
363
+ for metakey in metadata
364
+ if !member(metakey, `("key", "content")) && metakey.substring(0,1) != "_"
365
+ metakeep[metakey] = metadata[metakey] // valid metadata
366
+ if length(metakeep) > 0
367
+ return ({ naandbt: JSONstringify(metakeep) })
368
+ }
369
+ false
370
+ }
371
+
372
+ // add
373
+ //
374
+ // Add a new record to the table with optional metadata. This will fail if the key already
375
+ // exists. The metadata is stored with the record and cannot be changed without calling update.
376
+ // Metadata keys cannot begin with underscore ("_").
377
+ //
378
+ // The returned document, which can be used with update, has the following keys:
379
+ // key - the key name of the record
380
+ // content - the body content stored under in the record
381
+ // md5 - the MD5 hash of the content
382
+ // <...> - the various metadata keys
383
+
384
+ table.add = closure add(key, content, metadata, callback, local id, doc) {
385
+ if !callback
386
+ return (syncAdapter(add, key, content, metadata))
387
+ id = makeID(key)
388
+ if !string(id)
389
+ return (asyncResult(callback, id)) // error from makeID
390
+ if metadata && !dictionary(metadata)
391
+ return (asyncResult(callback, Error("invalid arguments")))
392
+ if metadata
393
+ doc = new(metadata) // start with metadata
394
+ else
395
+ doc = { }
396
+ doc.key = key
397
+ doc.id = id
398
+ if content
399
+ doc.content = content
400
+ metadata = metadata2s3(metadata) // filter out any invalid metadata
401
+ database.bucket.put(id, content, metadata, function(error, resp) {
402
+ if error
403
+ error = Error("add failed", error, id, { status: error.statusCode })
404
+ else {
405
+ doc.md5 = resp.md5 // update the original doc
406
+ doc.length = resp.length }
407
+ callback(error, doc)
408
+ })
409
+ }
410
+
411
+ // update
412
+ //
413
+ // Update a record in the table using the document previously returned by add or get.
414
+
415
+ table.update = closure update(doc, callback, local metadata) {
416
+ if !callback
417
+ return (syncAdapter(update, doc))
418
+ if !doc._id || !doc._id.startsWith(table.s3prefix) || !doc.md5
419
+ return (asyncResult(callback, Error("invalid argument")))
420
+ metadata = metadata2s3(doc)
421
+ database.bucket.put(doc._id, doc.content, metadata, function(error, resp) {
422
+ if error
423
+ error = Error("update failed", error, doc._id, { status: error.statusCode })
424
+ else {
425
+ doc.md5 = resp.md5 // update the original doc
426
+ doc.modified = undefined // we don't know exact modify date
427
+ doc.length = resp.length }
428
+ callback(error, doc)
429
+ })
430
+ }
431
+
432
+ // delete
433
+ //
434
+ // Delete a record in the table using the document previously returned by add or get. This
435
+ // marks the record deleted but does not actually reclaim storage.
436
+
437
+ table.delete = closure delete(doc, callback) {
438
+ if !callback
439
+ return (syncAdapter(delete, doc))
440
+ if !doc._id || !doc._id.startsWith(table.s3prefix) || !doc.md5
441
+ return (asyncResult(callback, Error("invalid argument")))
442
+ database.bucket.delete(doc._id, function(error, resp) {
443
+ if error
444
+ error = Error("delete failed", error, doc._id, { status: error.statusCode })
445
+ callback(error, resp)
446
+ })
447
+ }
448
+
449
+ // get
450
+ //
451
+ // Get the record in the table stored in the specified key.
452
+
453
+ table.get = closure get(key, callback, local id) {
454
+ if !callback
455
+ return (syncAdapter(get, key))
456
+ id = makeID(key)
457
+ if !string(id)
458
+ return (asyncResult(callback, id)) // error from makeID
459
+ database.bucket.get(id, false, function(error, resp) {
460
+ if error
461
+ error = Error("get failed", error, id, { status: error.statusCode })
462
+ else {
463
+ resp.key = key
464
+ resp = s3resp2doc(resp)
465
+ if resp.contentType == "application/json; charset=UTF-8" {
466
+ try {
467
+ resp.content = new(JSONparse(resp.content))
468
+ } catch {
469
+ if true
470
+ resp._error = "JSON decoding failed"
471
+ } }
472
+ else if resp.contentType.startsWith("image/")
473
+ { } // already "decoded", e.g. base64
474
+ else if jsTypedArray(resp.content)
475
+ resp.content = xnew(TextDecoder, "utf-8").decode(resp.content)
476
+ resp._id = id }
477
+ callback(error, resp)
478
+ })
479
+ }
480
+
481
+ // md5
482
+ //
483
+ // Compute the MD5 hash of the content of the specified record if it exists. The result is "" if
484
+ // no content exists. The metadata is not part of the hash.
485
+
486
+ table.md5 = closure md5(key, callback, local id) {
487
+ if !callback
488
+ return (syncAdapter(md5, key))
489
+ id = makeID(key)
490
+ if !string(id)
491
+ return (asyncResult(callback, id)) // error from makeID
492
+ database.bucket.get(id, true, function(error, resp) {
493
+ if error
494
+ error = Error("md5 failed", error, id, { status: error.statusCode })
495
+ else if resp._md5
496
+ resp = resp._md5
497
+ else
498
+ resp = ""
499
+ callback(error, resp)
500
+ })
501
+ }
502
+
503
+ // head
504
+ //
505
+ // Return the header of the specified record, i.e. everything except the content.
506
+
507
+ table.head = closure head(key, callback, local id) {
508
+ if !callback
509
+ return (syncAdapter(head, key))
510
+ id = makeID(key)
511
+ if !string(id)
512
+ return (asyncResult(callback, id)) // error from makeID
513
+ database.bucket.get(id, true, function(error, resp) {
514
+ if error
515
+ error = Error("head failed", error, id, { status: error.statusCode })
516
+ else {
517
+ resp.key = key
518
+ resp = s3resp2doc(resp)
519
+ resp._id = id }
520
+ callback(error, resp)
521
+ })
522
+ }
523
+
524
+ // copy
525
+ //
526
+ // Copy a record from a source key to a new record under a destination key.
527
+
528
+ table.copy = closure copy(srckey, destkey, callback, local srcid, destid) {
529
+ if !callback
530
+ return (syncAdapter(copy, srckey, destkey))
531
+ srcid = makeID(srckey)
532
+ if !string(srcid)
533
+ return (asyncResult(callback, srcid)) // error from makeID
534
+ destid = makeID(destkey)
535
+ if !string(destid)
536
+ return (asyncResult(callback, destid)) // error from makeID
537
+ database.bucket.copy(srcid, destid, function(error, resp) {
538
+ if error
539
+ error = Error("copy failed", {
540
+ source: srckey
541
+ destination: destkey
542
+ status: error.statusCode
543
+ }, error)
544
+ callback(error, resp)
545
+ })
546
+ }
547
+
548
+ // listfetch
549
+ //
550
+ // Update each element in the input array with a doc record from get or head.
551
+
552
+ closure listfetch(input, nodata, local kqdex, fetcher, pending) {
553
+
554
+ // queuemore
555
+ //
556
+ // Make more requests, or signal when all are completed
557
+
558
+ function queuemore() {
559
+ while kqdex < input.length {
560
+ if pending.active >= 10
561
+ return // too busy
562
+ closure (kdex, local key) { // separate closure to keep record context
563
+ key = input[kdex].key
564
+ if key { // this record is fetchable
565
+ ++pending.active
566
+ fetcher(key, function(error, doc) {
567
+ if error
568
+ input[kdex].error = error // this record failed
569
+ else
570
+ input[kdex].doc = doc // this record succeeded
571
+ --pending.active
572
+ queuemore()
573
+ }) }
574
+ } (kqdex++) // issue one request
575
+ }
576
+ if pending.active == 0
577
+ pending.signal() // no more to do, and all done
578
+ }
579
+
580
+ if nodata
581
+ fetcher = table.head
582
+ else
583
+ fetcher = table.get
584
+ pending = new(nonce)
585
+ pending.active = 0
586
+ kqdex = 0
587
+ queuemore()
588
+ pending.wait()
589
+ }
590
+
591
+ // records
592
+ //
593
+ // List all records in the table with the following options:
594
+ // prefix: <string> -- only records with the specified key prefix are included
595
+ // deep: true|false -- if true, include transitive children; default false
596
+ // head: true|false -- if true, include doc element with header for each record
597
+ // get: true|false -- if true, include doc element with content for each record
598
+ //
599
+ // The return structure has the following form:
600
+ //
601
+ // {prefix : "joe/bill/",
602
+ // rows : [
603
+ // {
604
+ // key : "john",
605
+ // _modified : [Date Sat Apr 04 2020 17:05:38 GMT-0700 (Pacific Daylight Time)]
606
+ // _md5 : "55ff07fb005494c4bd42322d4845e858"
607
+ // _length : 156043
608
+ // [ doc | error: <document from "get" or "head"> | <error from get|head> ] },
609
+ // {
610
+ // key : "jill",
611
+ // _modified : [Date Sat Apr 22 2020 17:05:38 GMT-0700 (Pacific Daylight Time)]
612
+ // _md5 : "f53f07fb00d42322dc4bd42322d4845e"
613
+ // _length : 155
614
+ // [ doc : <document from "get" or "head"> ] } ] }
615
+ //
616
+ // All transitive nested children are included with the options.deep flag non-false. Otherwise
617
+ // only immediate children are listed, with directories at the end--and no metadata for them.
618
+ //
619
+
620
+ table.records = closure records(options, callback, local s3prefix, output) {
621
+ if !callback
622
+ return (syncAdapter(records, options))
623
+ output = {
624
+ rows: [] }
625
+ if options.prefix {
626
+ if !string(options.prefix)
627
+ return (asyncResult(callback, Error("prefix option must be string")))
628
+ output.prefix = options.prefix
629
+ s3prefix = table.s3prefix.concat(makePrefix(options.prefix)) }
630
+ else
631
+ s3prefix = table.s3prefix
632
+ database.bucket.list(s3prefix, !options.deep, function(error, objects, local obj, key) {
633
+ if error
634
+ error = Error("records list failed", error, options.prefix, { status: error.statusCode })
635
+ else {
636
+ for obj in objects.Contents {
637
+ if obj.Key.startsWith(table.s3prefix)
638
+ key = obj.Key.substring(table.s3prefix.length)
639
+ else
640
+ key = "*key error*" // should not happen
641
+ output.rows.push({
642
+ key: key
643
+ _id: obj.Key
644
+ modified: obj.LastModified
645
+ md5: obj.ETag.substring(1,33)
646
+ length: obj.Size
647
+ }) }
648
+ for obj in objects.CommonPrefixes // children, if !options.deep
649
+ if obj.Prefix.startsWith(s3prefix)
650
+ output.rows.push({
651
+ key: obj.Prefix.slice(table.s3prefix.length)
652
+ _id: obj.Prefix })
653
+ if options.head || options.get
654
+ listfetch(output.rows, options.head) // fetch doc records
655
+ }
656
+ callback(error, output)
657
+ })
658
+ }
659
+
660
+ // finis
661
+
662
+ list(false, table)
663
+ };
664
+
665
+
666
+ /*
667
+ * S3DB
668
+ *
669
+ * Make a database access object for an S3 bucket. Naan databases create an abstraction of
670
+ * "tables" that segment a database for different usage. For S3 databases the "table" is a name
671
+ * prefix shared among multiple objects. Subtables may also exist, like any hierarchy.
672
+ * Return values from the various methods are typically a two-element tuple of (error, data),
673
+ * where at most one of the two values is non-false.
674
+ *
675
+ */
676
+
677
+ closure S3DB(s3, local database, result) {
678
+ database = new(object, this)
679
+ result = dawsBucket(s3)
680
+ if result.0
681
+ return (result) // can't access bucket
682
+ database.bucket = result.1
683
+ database.s3 = s3
684
+ database.type = "Amazon S3"
685
+
686
+ // isopen
687
+ //
688
+ // False iff the database is not open.
689
+
690
+ database.isopen = function isopen() {
691
+ !!database.bucket
692
+ }
693
+
694
+ // close
695
+ //
696
+ // Close the database. In-progress io may continue for a while.
697
+
698
+ database.close = function close() {
699
+ if !database.bucket
700
+ return (list(Error("database closed")))
701
+ database.bucket.close()
702
+ database.bucket = false
703
+ database.s3 = false
704
+ list(false, { ok: true })
705
+ }
706
+
707
+ // info
708
+ //
709
+ // Return implementation-specific database info.
710
+
711
+ database.info = function info(local result) {
712
+ if !database.bucket
713
+ return (list(Error("database closed")))
714
+ result = {
715
+ type: database.type
716
+ info: {
717
+ region: s3.config.region
718
+ bucket: s3.config.params.Bucket
719
+ }
720
+ }
721
+ list(false, result)
722
+ }
723
+
724
+ // destroy
725
+ //
726
+ // Delete the database and close it.
727
+
728
+ database.destroy = function destroy(local pending) {
729
+ if !database.bucket
730
+ return (list(Error("database closed")))
731
+ list(Error("not sure how to destroy this database")) // ### delete the base object?
732
+ }
733
+
734
+ // table
735
+ //
736
+ // Return a table-access object for the table.
737
+
738
+ database.table = function table(path) {
739
+ if !database.bucket
740
+ return (list(Error("database closed")))
741
+ if !string(path)
742
+ return (list(Error("invalid table path")))
743
+ dawsTable(database, path)
744
+ }
745
+
746
+ // finis
747
+
748
+ list(false, database)
749
+ };
750
+
751
+
752
+ /*
753
+ * dawsInit
754
+ *
755
+ * Initialize the Database module.
756
+ *
757
+ */
758
+
759
+ function dawsInit(local manifest) {
760
+
761
+ manifest = `(dawsValidateName, dawsBucket, dawsTable, S3DB, dawsInit)
762
+
763
+ Naan.module.build(module.id, "dbt_aws", function(modobj, compobj) {
764
+ require("./serviceAws.nlg")
765
+ compobj.manifest = manifest
766
+ modobj.exports.S3DB = S3DB
767
+ })
768
+
769
+ }();