@naanlang/naan 1.0.1 → 1.0.4

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 (37) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +5 -5
  3. package/bin/index.js +18 -15
  4. package/dist/env_web.js +146 -16
  5. package/dist/naan.min.js +5 -5
  6. package/frameworks/browser/sworker.js +299 -91
  7. package/frameworks/browser/terminals.nlg +23 -10
  8. package/frameworks/browser/workers.nlg +26 -12
  9. package/frameworks/client/apiclient.nlg +17 -4
  10. package/frameworks/client/psm_client.nlg +178 -81
  11. package/frameworks/common/common.nlg +14 -4
  12. package/frameworks/node/apiserver.nlg +31 -15
  13. package/frameworks/node/filesystem.nlg +230 -46
  14. package/frameworks/node/gitter.nlg +27 -6
  15. package/frameworks/node/psm_server.nlg +113 -37
  16. package/frameworks/project/build.nlg +40 -23
  17. package/frameworks/project/projects.nlg +50 -27
  18. package/frameworks/running/debugnub.nlg +2 -5
  19. package/frameworks/storage/csv.nlg +120 -0
  20. package/frameworks/storage/dbt_pouch.nlg +41 -25
  21. package/frameworks/storage/psm.nlg +108 -28
  22. package/frameworks/storage/psm_dbtables.nlg +7 -3
  23. package/frameworks/storage/resources.nlg +30 -2
  24. package/lib/browser/env_web.js +148 -18
  25. package/lib/browser/env_webworker.js +1 -1
  26. package/lib/browser/require.js +1 -1
  27. package/lib/core/naanlib.js +5 -5
  28. package/lib/env_node.js +9 -1
  29. package/package.json +2 -1
  30. package/plugins/serviceAws/aws_cloudwatchlogs.nlg +1 -1
  31. package/plugins/serviceAws/aws_dynamo.nlg +625 -141
  32. package/plugins/serviceAws/aws_dynextra.nlg +294 -0
  33. package/plugins/serviceAws/dbt_aws.nlg +31 -11
  34. package/plugins/serviceAws/psm_aws.nlg +42 -26
  35. package/plugins/serviceAws/serviceAws.nlg +1 -0
  36. package/plugins/serviceGitHub/psm_github.nlg +41 -25
  37. package/plugins/serviceGitLab/psm_gitlab.nlg +41 -25
@@ -0,0 +1,120 @@
1
+ /*
2
+ * csv.nlg
3
+ * Naanlib/frameworks/storage
4
+ *
5
+ * CSV file parsing and generation.
6
+ *
7
+ * column positioning: // // !
8
+ *
9
+ * Copyright (c) 2022 by Richard C. Zulch
10
+ *
11
+ */
12
+
13
+
14
+ /*
15
+ * csvParse
16
+ *
17
+ * Parse text lines in CSV format into a row array of column arrays. The delimiter defaults to
18
+ * comma, but can be replaced with anything that's not crazy. Normally missing columns are replaced
19
+ * with false, but the empty parameter can be used to override this. For example, specify "" to
20
+ * use an empty string for the empty column.
21
+ * The parse regex is derived from the following Stack Overflow article:
22
+ * https://stackoverflow.com/questions/1293147/example-javascript-code-to-parse-csv-data
23
+ * Please see the answer by Joshua McLauchlan.
24
+ *
25
+ */
26
+
27
+ function csvParse(text, delim, empty, local res, parseRx, quoteRx, result, matches) {
28
+ if !string(text)
29
+ return (false) // don't hang or be stupid
30
+ res = '(,|\r?\n|\r|^)(?:"([^"]*(?:""[^"]*)*)"|([^,\r\n]*))'
31
+ if delim
32
+ res = res.replace(RegExp(',', 'g'), delim)
33
+ else
34
+ delim = ','
35
+ parseRx = RegExp(res, 'gi')
36
+ quoteRx = RegExp('""', 'g')
37
+ result = [[]]
38
+ while !!(matches = parseRx.exec(text)) {
39
+ if !matches[0]
40
+ break // should never happen, but don't hang
41
+ if matches[1].length > 0 && matches[1] !== delim
42
+ result.push([]) // first column in row
43
+ result[result.length-1].push(cond(
44
+ if matches[2] !== undefined
45
+ matches[2].replace(quoteRx, '"') // quoted string, including ""
46
+ else if matches[3] !== ""
47
+ matches[3] // unquoted, non-empty
48
+ else
49
+ empty // empty column
50
+ ))
51
+ }
52
+ result
53
+ };
54
+
55
+
56
+ /*
57
+ * csvPrint
58
+ *
59
+ * Print an array of row arrays of columns to a string, reversing csvParse.
60
+ *
61
+ */
62
+
63
+ function csvPrint(rows, local output, quoter, row, line, delim, col) {
64
+ quoter = csvQuoteGenerator()
65
+ output = ""
66
+ for row in rows {
67
+ delim = line = ""
68
+ for col in row {
69
+ line = line.concat(delim, quoter(col))
70
+ delim = ","
71
+ }
72
+ output = output.concat(line, "\n")
73
+ }
74
+ output
75
+ };
76
+
77
+
78
+ /*
79
+ * csvQuoteGenerator
80
+ *
81
+ * Return a function that quotes a string as needed for a CSV file with the given delimiter. This
82
+ * is a generator because it will likely be needed frequently. Comma is the default delimeter, just
83
+ * like God intended for a *C*sv file.
84
+ *
85
+ */
86
+
87
+ closure csvQuoteGenerator(delimiter, local rxComma, rxQuote) {
88
+ if !delimiter
89
+ delimiter = ','
90
+ rxDelimiter = RegExp('['.concat('"\\', delimiter, ']'))
91
+ rxQuote = RegExp("\"", "g")
92
+
93
+ // csvQuote
94
+ // Quote a string for a CSV file as specified.
95
+ function csvQuote(str) {
96
+ if rxDelimiter.test(str)
97
+ return ("\"".concat(str.replace(rxQuote, "\"\""), "\""))
98
+ return (str)
99
+ }
100
+ };
101
+
102
+
103
+ /*
104
+ * csvInit
105
+ *
106
+ * Initialize the component.
107
+ *
108
+ */
109
+
110
+ function csvInit(local manifest) {
111
+ manifest = `(csvParse, csvPrint, csvQuoteGenerator, csvInit)
112
+
113
+ Naan.module.build(module.id, "csv", function(modobj, compobj) {
114
+ require("./storage.nlg")
115
+ compobj.manifest = manifest
116
+ module.exports.csvParse = csvParse
117
+ module.exports.csvPrint = csvPrint
118
+ module.exports.csvQuoteGenerator = csvQuoteGenerator
119
+ })
120
+ }();
@@ -540,36 +540,26 @@ closure DbConnector(psm, local connClassID, connector, watch) {
540
540
  type: "password" },
541
541
  ]
542
542
  }
543
-
544
- // hashResourceID
545
- //
546
- // Make a stable hash based on the resource credentials that we can persist or share between
547
- // different domains accessing the same resource.
548
- connector.hashResourceID = function hashResourceID(resource, local ident) {
549
- ident = resource.urlName
550
- if resource.label
551
- ident = ident.concat(resource.label)
552
- HashSHA256(ident)
553
- }
554
543
 
555
544
  // findResource
556
545
  //
557
- // Find a resource with the specified contents, returning the resID or false. The loose criteria
558
- // is: if added, would the specified contents be redundant to an existing resource?
546
+ // Find a resource with the specified contents, returning the resID or false.
559
547
  connector.findResource = function findResource(resource, local resID, creds) {
560
548
  for resID in listResources().1 {
561
549
  creds = access(resID).1
562
- if resource.urlName == creds.urlName
550
+ if ((!resource.urlName ||resource.urlName == creds.urlName)
563
551
  && (!resource.label || resource.label == creds.label)
564
- && resource.authName == creds.authName
552
+ && (!resource.authName || resource.authName == creds.authName))
565
553
  return (resID)
566
554
  }
567
555
  false
568
556
  }
569
557
 
570
- // addResource
558
+ // writeResource
571
559
  //
572
- // Add credentials for a named resource. The resource is a dictionary comprising these keys:
560
+ // Add or update credentials for a named resource. If no resID is specified this adds a new
561
+ // resource; otherwise it updates an existing one. The resource is a dictionary comprising these
562
+ // keys:
573
563
  // label - [optional] label we use for this resource
574
564
  // urlName - URL (e.g. "http://domain.com/dbname")
575
565
  // - filepath (e.g. "/Users/richardz/dbname") - NodeJS filesystem access
@@ -579,10 +569,13 @@ closure DbConnector(psm, local connClassID, connector, watch) {
579
569
  // hidden - [optional] don't enumerate to user
580
570
  // locked - [optional] don't allow user delete
581
571
  // existng - [optional] true to enable using existing database
582
- // The return value is (error, result) tuple. Error, if non-false, is a dictionary of field
583
- // keys and error diagnostic strings. The special key "label" refers to the label string
584
- connector.addResource = function addResource(resource, local errors, creds, original, error, resID, data) {
572
+ // The return value is an (error, resID) tuple. Error, if non-false, is a dictionary of field
573
+ // keys and error diagnostic strings.
574
+ connector.writeResource = function writeResource(resource, resID,
575
+ local errors, creds, original, error, data) {
576
+ // badField - test for a bad string
585
577
  function badField(str) { !string(str) || str.trim().length == 0 }
578
+
586
579
  creds = { urlName: resource.urlName }
587
580
  errors = { }
588
581
  if resource.label
@@ -591,6 +584,8 @@ closure DbConnector(psm, local connClassID, connector, watch) {
591
584
  creds.label = resource.urlName
592
585
  if badField(creds.label)
593
586
  errors.label = "invalid label"
587
+ else if !resID && findResource({ label: creds.label })
588
+ errors.label = "duplicate name"
594
589
  if badField(resource.urlName)
595
590
  errors.urlName = "required field"
596
591
  else if !resource.existing {
@@ -612,15 +607,36 @@ closure DbConnector(psm, local connClassID, connector, watch) {
612
607
  creds.hidden = resource.hidden
613
608
  if resource.locked
614
609
  creds.locked = resource.locked
615
- resID = hashResourceID(resource)
616
- `(error, data) = connector.vault.addResource(resID, creds)
610
+ if resID {
611
+ `(error, data) = connector.vault.updateResource(resID, creds)
612
+ change = { changed: [resID] }
613
+ } else {
614
+ resID = UUID()
615
+ `(error, data) = connector.vault.addResource(resID, creds)
616
+ change = { added: [resID] }
617
+ }
617
618
  if data {
618
- watch.notify(connClassID, { added: [resID] })
619
- data = resID }
619
+ watch.notify(connClassID, change)
620
+ data = resID
621
+ }
620
622
  }
621
623
  list(error, data)
622
624
  }
623
-
625
+
626
+ // addResource
627
+ //
628
+ // Add a new resource, returning a standard error tuple per writeResource.
629
+ connector.addResource = function addResource(resource) {
630
+ writeResource(resource)
631
+ }
632
+
633
+ // updateResource
634
+ //
635
+ // Update an existing resource, returning a standard error tuple per writeResource.
636
+ connector.updateResource = function updateResource(resID, resource) {
637
+ writeResource(resource, resID)
638
+ }
639
+
624
640
  // getResource
625
641
  //
626
642
  // Get credentials for a named resource, matching the dictionary semantics of addResource. The
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * column positioning: // // !
8
8
  *
9
- * Copyright (c) 2019-2021 by Richard C. Zulch
9
+ * Copyright (c) 2019-2022 by Richard C. Zulch
10
10
  *
11
11
  */
12
12
 
@@ -75,7 +75,7 @@ closure MakePSMutil(fs, local util) {
75
75
  } (tree, inpath.split(tree.data.pathsep).reverse())
76
76
  if !treedir
77
77
  return (callback(Error("treeDirUpdate: can't find directory within tree", inpath)))
78
- `(error, files) = fs.dirList(dirpath, { head: true })
78
+ `(error, files) = fs.dirList(dirpath, { stat: true })
79
79
  if error
80
80
  return (callback(error))
81
81
  //
@@ -238,46 +238,126 @@ closure MakePSMutil(fs, local util) {
238
238
  //
239
239
  // Perform a deep copy of an array of sources from the current filesystem and specified inpath to
240
240
  // the outfs filesystem and specified outpath. The input path is used to find the source files,
241
- // however it is not added to the output. The result is a standard error tuple, where the errors
242
- // are an array if any occur.
241
+ // however it is not added to the output. If the source array is false then the file or directory
242
+ // at the input path is copied to the destination. The result is a standard error tuple, where
243
+ // the errors are an array if any occur.
244
+ //
245
+ // Supported options are:
246
+ // {
247
+ // overwrite: <boolean> -- overwrite existing files
248
+ // erase: <boolean> -- remove extraneous files in destination folders
249
+ // force: <boolean> -- copy files even if older (default is only copy if new/newer)
250
+ // follow: <boolean> -- follow links instead of treating them as files
251
+ // preserve: <boolean> -- preserve file info where possible
252
+ // (e.g. mtime/atime/mode/uid/gid on NodeJS)
253
+ // }
254
+ //
255
+ // Limitations:
256
+ // - this does not copy info about source folders to destination folders
257
+ // (it would need to make destination folder writable, then update after files copied)
243
258
 
244
- util.deepcopy = closure deepcopy(inpath, sources, outfs, outpath, options, callback, local errors, copies) {
259
+ util.deepcopy = closure deepcopy(inpath, sources, outfs, outpath, options, callback,
260
+ local errors, copies, fsopt, folders) {
245
261
  if !callback
246
262
  return (syncAdapter(deepcopy, inpath, sources, outfs, outpath, options))
247
- if !array(sources) || !string(inpath) || !string(outpath)
263
+ if sources && !array(sources) || !string(inpath) || !string(outpath)
248
264
  return (callback(Error("deepcopy: invalid arguments")))
265
+ if options.follow
266
+ fsopt = { follow: true }
249
267
  errors = []
250
268
  copies = []
251
- sources = new(sources) // don't modify the original!
252
- asyncArray(sources, 10, closure mklist(entry, index, elements, local srcpath, destpath, error, folder, node) {
269
+ folders = { }
270
+ if sources
271
+ sources = new(sources) // don't modify the original!
272
+ else {
273
+ sources = [fs.path.basename(inpath)] // copy inpath itself
274
+ inpath = fs.path.dirname(inpath)
275
+ }
276
+ //
277
+ // Pass 1 - Get info on all source files, and make destination directories
278
+ //
279
+ asyncArray(sources, 10, closure mklist(entry, index, elements,
280
+ local srcpath, destpath, epath, error, stat, folder, node) {
253
281
  srcpath = fs.path.join(inpath, entry)
254
- `(error, folder) = fs.dirList(srcpath)
255
- if error && error.code != "ENOTDIR"
256
- return (errors.push(error))
257
- destpath = outfs.path.join(outpath, entry) // "." if both empty strings
258
- if !error {
259
- if destpath != "."
260
- `(error, node) = outfs.mkdir(destpath) // may already exist but we don't care
261
- if error
262
- return (errors.push(error)) // don't do children if we can't form destination
263
- for node in folder.children
264
- sources.push(fs.path.join(entry, node.name)) // omit inpath from sources
265
- }
266
- else
267
- copies.push({ // copy the file
282
+ destpath = outfs.path.join(outpath, entry)
283
+ if destpath == "."
284
+ destpath = "" // joined empty strings
285
+ `(error, stat) = fs.info(srcpath, fsopt)
286
+ if stat.type != "directory" && (!options.follow || stat.type != "symlink") {
287
+ copies.push({ // copy file or symlink
268
288
  source: srcpath
269
289
  dest: destpath
270
290
  })
291
+ return (list(false, { ok: true })) // generic success not used
292
+ }
293
+ `(error, folder) = fs.dirList(srcpath)
294
+ if error
295
+ return (errors.push(error))
296
+ if destpath == ""
297
+ epath = ""
298
+ else
299
+ epath = outfs.path.join(destpath, outfs.path.sep) // canonical name
300
+ folders[epath] = folder // sources for this folder
301
+ `(error, node) = outfs.mkdir(destpath) // may already exist but we don't care
302
+ if error
303
+ return (errors.push(error)) // don't do children if we can't form destination
304
+ for node in folder.children
305
+ sources.push(fs.path.join(entry, node.name)) // add more sources, without inpath
271
306
  }).wait()
307
+ //
308
+ // Pass 2 - erase extraneous files and folders from destination
309
+ //
310
+ if options.erase {
311
+ asyncArray(keys(folders).toarray, 10, closure eraser(destpath, index, elements
312
+ local error, outfolder, folder, namesin, removes, delopt) {
313
+ `(error, outfolder) = outfs.dirList(destpath) // existing files in our destination
314
+ if error
315
+ return
316
+ folder = folders[destpath] // our source folders, if any
317
+ namesin = { }
318
+ for node in folder.children // make list of top-level dest names
319
+ namesin[node.name] = true
320
+ removes = []
321
+ for node in outfolder.children
322
+ if !namesin[node.name] {
323
+ if node.info.type == "directory"
324
+ delopt = { recursive: true } // deleting a directory
325
+ else
326
+ delopt = false // deleting a file
327
+ removes.push(list(outfs.path.join(destpath, node.name), delopt))
328
+ }
329
+ asyncArray(removes, 10, closure cleardest(delitem, local error, data) {
330
+ `(error, data) = outfs.delete(delitem.0, delitem.1)
331
+ if error
332
+ errors.push(Error("deepcopy: can't remove", error))
333
+ }).wait()
334
+ }).wait()
335
+ }
336
+ //
337
+ // Pass 3 - copy files and links
338
+ //
272
339
  asyncArray(copies, 10, closure cpfiles(entry, index, elements, local error, data, instat, outstat) {
273
- `(error, instat) = fs.info(entry.source)
340
+ `(error, instat) = fs.info(entry.source, fsopt)
274
341
  if !error {
275
342
  `(error, outstat) = outfs.info(entry.dest)
276
- if !error && (instat.md5 && instat.md5 == outstat.md5 || instat.mtimeMs <= outstat.mtimeMs)
277
- return // file unchanged
278
- `(error, data) = fs.readFile(entry.source, { encoding: "binary"})
279
- if !error
280
- `(error, data) = outfs.writeFile(entry.dest, data, { encoding: "binary"})
343
+ if !error &&
344
+ (instat.md5 && instat.md5 == outstat.md5
345
+ || !options.force && instat.mtimeMs <= outstat.mtimeMs)
346
+ return // file "unchanged"
347
+ if !error && !options.overwrite
348
+ error = Error("deepcopy: can't overwrite", entry.dest)
349
+ else if instat.type == "symlink" && !options.follow {
350
+ `(error, data) = fs.readlink(entry.source)
351
+ if !error
352
+ `(error, data) = outfs.symlink(data, entry.dest)
353
+ }
354
+ else {
355
+ `(error, data) = fs.readFile(entry.source, { encoding: "binary"})
356
+ if !error
357
+ `(error, data) = outfs.writeFile(entry.dest, data, { encoding: "binary"})
358
+ if !error && options.preserve
359
+ `(error) = outfs.setInfo(entry.dest, instat)
360
+ }
281
361
  }
282
362
  if error
283
363
  errors.push(error)
@@ -287,7 +287,7 @@ closure FSview(table, rootpath, local view, error) {
287
287
  })))
288
288
  if buildpath.slice(-1) != "/"
289
289
  buildpath = buildpath.concat("/")
290
- `(error, data) = view.table.get(buildpath) // get desired directory
290
+ `(error, data) = view.table.head(buildpath) // get desired directory
291
291
  if error.status == 404
292
292
  `(error, data) = dbWriteRecord(buildpath) // create desired directory
293
293
  if error
@@ -684,8 +684,12 @@ closure FSview(table, rootpath, local view, error) {
684
684
  if !finfo
685
685
  finfo = { }
686
686
  name = item.key.slice(offset)
687
- if name.slice(-1) == "/"
687
+ if name.slice(-1) == "/" {
688
688
  name = name.slice(0,-1) // remove trailing "/" from directory
689
+ if !finfo.type
690
+ finfo.type = "directory"
691
+ } else if !finfo.type
692
+ finfo.type = "file" // everything is a file in UNIX
689
693
  if item.doc._md5
690
694
  finfo.md5 = item.doc._md5
691
695
  if item.doc._length
@@ -703,7 +707,7 @@ closure FSview(table, rootpath, local view, error) {
703
707
  // Read a directory at the specified path
704
708
 
705
709
  view.readir = closure readir(path, callback, local name, dirdict) {
706
- dirList(path, { head: true }, callback)
710
+ dirList(path, { stat: true }, callback)
707
711
  }
708
712
 
709
713
  // tree
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * column positioning: // // !
13
13
  *
14
- * Copyright (c) 2019-2021 by Richard C. Zulch
14
+ * Copyright (c) 2019-2022 by Richard C. Zulch
15
15
  *
16
16
  */
17
17
 
@@ -38,6 +38,21 @@ closure rsrcVault(db, tablename, local vault) {
38
38
  vault.table.add(resID, data)
39
39
  }
40
40
 
41
+ // updateResource
42
+ //
43
+ // Update credentials for accessing a resource, returning the added credential record.
44
+
45
+ vault.updateResource = function updateResource(resID, data, local error, existing) {
46
+ `(error, existing) = vault.table.get(resID)
47
+ if error
48
+ data = false
49
+ else {
50
+ existing.content = data
51
+ `(error, existing) = vault.table.update(existing)
52
+ }
53
+ list(error, data)
54
+ }
55
+
41
56
  // deleteResource
42
57
  //
43
58
  // Delete resource credentials.
@@ -111,8 +126,9 @@ closure rsrcVault(db, tablename, local vault) {
111
126
  * conn.uifields() - entry fields for add this kind of resource in UI
112
127
  * conn.findResource(resource) - find a resource by content, returning resID or false
113
128
  * conn.addResource(resource) - add the resource dict and return `(error, resID)
114
- * conn.info(resID) - return `(error, infoDictionary) tuple
129
+ * conn.updateResource(resID, resource) - update a resource
115
130
  * conn.deleteResource(resID) - delete the specified resource, returning a tuple
131
+ * conn.info(resID) - return `(error, infoDictionary) tuple
116
132
  * conn.connect(resID, serviceID, args) - connect to resource and return object
117
133
  *
118
134
  */
@@ -309,6 +325,18 @@ closure ResourceTracker(reman, local retor, watch) {
309
325
  reman.conns[classID].addResource(resource)
310
326
  }
311
327
 
328
+ // updateResource
329
+ //
330
+ // Update credentials for accessing a resource, returning the added credential record.
331
+
332
+ retor.updateResource = function updateResource(resID, resource, local error, classI) {
333
+ `(error, classID) = resourceClassID(resID)
334
+ if error
335
+ list(error)
336
+ else
337
+ reman.conns[classID].updateResource(resID, resource)
338
+ }
339
+
312
340
  // getResource
313
341
  //
314
342
  // Get a resource for the resID as a standard result tuple.