@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,7 +6,7 @@
6
6
  *
7
7
  * column positioning: // // !
8
8
  *
9
- * Copyright (c) 2016-2021 by Richard C. Zulch
9
+ * Copyright (c) 2016-2022 by Richard C. Zulch
10
10
  *
11
11
  */
12
12
 
@@ -23,6 +23,7 @@ closure ProjectManager(locate, track, dbname, local error, proj) {
23
23
  `(error, proj) = storageResources.ResourceManager(dbname, "Projects/")
24
24
  if error
25
25
  return(list(error))
26
+ proj.configLocal = proj.vault("configlocal") // local config storage
26
27
  proj.locate = locate // storage locations
27
28
  proj.track = track // execution tracking
28
29
  proj.types = []
@@ -150,40 +151,36 @@ closure projConnector(projman, projtype, local connector, watch) {
150
151
  connector.label = projtype.classLabel
151
152
  connector.vault = projman.vault(projtype.classID)
152
153
  watch = projman.watchable(connector)
153
-
154
- // hashResourceID
155
- //
156
- // Make a stable hash based on the resource credentials that we can persist or share between
157
- // different domains accessing the same resource.
158
- connector.hashResourceID = function hashResourceID(resource, local ident) {
159
- ident = resource.name.concat(resource.locationID, resource.path)
160
- HashSHA256(ident)
161
- }
162
154
 
163
155
  // findResource
164
156
  //
165
- // Find a resource with the specified contents, returning the resID or false. The loose criteria
166
- // is: if added, would the specified contents be redundant to an existing resource?
157
+ // Find a resource with the specified contents, returning the resID or false.
167
158
  connector.findResource = function findResource(resource, local resID, resInfo) {
168
159
  for resID in listResources().1 {
169
160
  resInfo = access(resID).1
170
- if resource.name == resInfo.name
171
- && resource.locationID == resInfo.locationID
172
- && resource.path == resInfo.path
161
+ if ((!resource.name || resource.name == resInfo.name)
162
+ && (!resource.locationID || resource.locationID == resInfo.locationID)
163
+ && (!resource.path || resource.path == resInfo.path))
173
164
  return (resID)
174
165
  }
175
166
  false
176
167
  }
177
168
 
178
- // addResource
169
+ // writeResource
179
170
  //
180
- // Add credentials for a named resource. The resource is a dictionary comprising these keys:
171
+ // Add or update credentials for a named resource. If no resID is specified this adds a new
172
+ // resource; otherwise it updates an existing one. The resource is a dictionary comprising these
173
+ // keys:
181
174
  // name: <string> - the user visible name of the project
182
175
  // locationID: <string> - resID of location resource where this project resides
183
176
  // path: <string> - path to project within location
184
- // Error, if non-false, is a dictionary of field keys and error diagnostic strings.
185
- connector.addResource = function addResource(resource, local errors, error, resID, data) {
177
+ // The return value is an (error, resID) tuple. Error, if non-false, is a dictionary of field
178
+ // keys and error diagnostic strings.
179
+ connector.writeResource = function writeResource(resource, resID,
180
+ local errors, error, data, change) {
181
+ // badField - test for a bad string
186
182
  function badField(str) { !string(str) || str.trim().length == 0 }
183
+
187
184
  errors = { }
188
185
  if badField(resource.name)
189
186
  errors.name = "invalid name"
@@ -197,19 +194,42 @@ closure projConnector(projman, projtype, local connector, watch) {
197
194
  if length(errors) > 0
198
195
  error = errors
199
196
  else {
200
- resID = hashResourceID(resource)
201
- `(error, data) = connector.vault.addResource(resID, resource)
197
+ if resID {
198
+ `(error, data) = connector.vault.updateResource(resID, resource)
199
+ change = { changed: [resID] }
200
+ } else {
201
+ resID = UUID()
202
+ `(error, data) = connector.vault.addResource(resID, resource)
203
+ change = { added: [resID] }
204
+ }
202
205
  if data {
203
- watch.notify(projtype.classID, { added: [resID] })
204
- data = resID }
206
+ watch.notify(projtype.classID, change)
207
+ data = resID
208
+ }
205
209
  }
206
210
  list(error, data)
207
211
  }
208
212
 
213
+ // addResource
214
+ //
215
+ // Add a new resource, returning a standard error tuple per writeResource.
216
+ connector.addResource = function addResource(resource) {
217
+ writeResource(resource)
218
+ }
219
+
220
+ // updateResource
221
+ //
222
+ // Update an existing resource, returning a standard error tuple per writeResource.
223
+ connector.updateResource = function updateResource(resID, resource) {
224
+ writeResource(resource, resID)
225
+ }
226
+
209
227
  // deleteResource
210
228
  //
211
229
  // Delete information for a project. The return value is (error, result) tuple.
212
- connector.deleteResource = function deleteResource(resID, local error, creds, db) {
230
+ connector.deleteResource = function deleteResource(resID, local localID) {
231
+ localID = resID.concat("-data")
232
+ projman.configLocal.deleteResource(localID) // remove local storage if there is any
213
233
  connector.vault.deleteResource(resID)
214
234
  watch.notify(projtype.classID, { deleted: [resID] })
215
235
  list(false, { ok: true})
@@ -276,7 +296,7 @@ closure projConnector(projman, projtype, local connector, watch) {
276
296
  *
277
297
  * Nide.proj/
278
298
  * nide_cliser.cfg - JSON definition of the project (see below)
279
- * NaanIDE_version.txt - [default] substitution file defining 1.0.2-1 etc.
299
+ * NaanIDE_version.txt - [default] substitution file defining 1.0.5-1 etc.
280
300
  * NaanIDE_version_builds.txt - [default] contains current build number as decimal string
281
301
  * build/ - [optional] default build output location
282
302
  * NaanIDE.lic - [optional] defines Zulch Laboratories, Inc. and other license info
@@ -289,6 +309,10 @@ closure projConnector(projman, projtype, local connector, watch) {
289
309
  * build: <build instructions dictionary>
290
310
  * }
291
311
  *
312
+ * A project configuration also stores local data that is not in any file, which is useful for
313
+ * items like accounts & locations, or credentials that should not be "on disk". Project data from
314
+ * the .cfg file is available in config.configDict, while local data is in config.localDict.
315
+ *
292
316
  * Given a new projConfig instance, you can either load (which fails of the project is not valid)
293
317
  * or create (which fails if the project already exists.) The load operation will update the where
294
318
  * argument with the actual name and type found. Unless other noted the methods all return standard
@@ -299,9 +323,11 @@ closure projConnector(projman, projtype, local connector, watch) {
299
323
  proj_configDataFolder = "Nide.proj/";
300
324
  proj_cliserConfigFile = "nide_cliser.cfg";
301
325
 
302
- closure projConfig(projman, where, local procon, fs, configpath) {
326
+ closure projConfig(projman, where, projID, local procon, fs, configpath) {
303
327
  procon = new(object, this)
304
328
  procon.name = where.name
329
+ if projID
330
+ procon.localID = projID.concat("-data")
305
331
 
306
332
  // validate
307
333
  //
@@ -359,6 +385,15 @@ closure projConfig(projman, where, local procon, fs, configpath) {
359
385
  return (list(Error("can't parse project configuration", where.path, exception))) }
360
386
  procon.where_name = where.name // copy name to config
361
387
  procon.where_typeID = where.typeID // copy typeID to config
388
+ if procon.localID {
389
+ `(error, data) = projman.configLocal.accessResource(procon.localID)
390
+ if data
391
+ procon.localDict = data
392
+ else {
393
+ procon.localDict = { }
394
+ projman.configLocal.addResource(procon.localID, procon.localDict)
395
+ }
396
+ }
362
397
  list(false, procon.configDict)
363
398
  }
364
399
 
@@ -367,6 +402,11 @@ closure projConfig(projman, where, local procon, fs, configpath) {
367
402
  // Write procon.configDict to our configuration file, returning a result tuple.
368
403
  //
369
404
  closure writeConfig(local error, info, data) {
405
+ if procon.localID {
406
+ `(error, data) = projman.configLocal.updateResource(procon.localID, procon.localDict)
407
+ if error
408
+ debuglog("projConfig.writeConfig cannot write local storage:", ErrorString(error))
409
+ }
370
410
  `(error, data) = fs.writeFile(configpath, JSONstringify(procon.configDict))
371
411
  if !error {
372
412
  procon.configMD5 = data
@@ -385,6 +425,8 @@ closure projConfig(projman, where, local procon, fs, configpath) {
385
425
  // not already be loaded. The provided name overrides any name in the where argument above, but
386
426
  // path and locationID must be specified. Returns a standard result tuple.
387
427
  //
428
+ // ### we need to update this for optional multiple-configuraiton builds
429
+ //
388
430
  procon.create = closure create(name, local error, data, configpath) {
389
431
  if procon.loaded
390
432
  return (list(Error("Project.create: project already loaded")))
@@ -440,7 +482,9 @@ closure projConfig(projman, where, local procon, fs, configpath) {
440
482
  error = Error("project configuration not a directory", where.path)
441
483
  else
442
484
  `(error, data) = readConfig() }
443
- if error
485
+ if error.code == "ENOENT"
486
+ return (list(Error("Project <b>".concat(where.name, "</b>:<br>not found at ", where.path))))
487
+ else if error
444
488
  return (list(Error("Project.load failed", error)))
445
489
  procon.loaded = true
446
490
  list(false, procon.configDict)
@@ -479,15 +523,52 @@ closure projConfig(projman, where, local procon, fs, configpath) {
479
523
  procon.configDict = newconfig
480
524
  `(error, data) = writeConfig()
481
525
  }
482
-
526
+
527
+ // stages
528
+ //
529
+ // Return a tuple of stages in the project configuration.
530
+ //
531
+ procon.stages = function stages() {
532
+ procon.configDict."build-rules".*
533
+ }
534
+
535
+ // currentStage
536
+ //
537
+ // Validate and return the current stage of the project, or the first one if there isn't a
538
+ // previous selection, or false if no valid stages exist.
539
+ //
540
+ procon.currentStage = function currentStage(local stage, stagelist) {
541
+ stage = procon.localDict.currentStage
542
+ stagelist = stages()
543
+ if stage && !member(stage, stagelist)
544
+ stage = false
545
+ if stagelist && !stage
546
+ stage = stagelist.0
547
+ stage
548
+ }
549
+
483
550
  // buildrules
484
551
  //
485
- // Get the current build rules.
552
+ // Get the build rules for the named stage.
486
553
  //
487
- function buildrules(local rules) {
488
- rules = procon.configDict."build-rules".default // "default" will be switchable later
554
+ function buildrules(stage, local rules, common, rule, key) {
555
+ rules = procon.configDict."build-rules"[stage]
489
556
  if !rules
490
557
  rules = procon.configDict.build // fallback to old config structure
558
+ rules = new(rules)
559
+ common = procon.configDict."build-common"
560
+ if common {
561
+ for rule in common
562
+ if !rules[rule]
563
+ rules[rule] = common[rule] // common rule was not overridden
564
+ else if array(common[rule], rules[rule])
565
+ rules[rule] = common[rule].concat(rules[rule]) // merge common array with rule
566
+ else if dictionary(common[rule], rules[rule]) {
567
+ for key in common[rule] // merge common dictionary with rule
568
+ if !rules[rule][key]
569
+ rules[rule][key] = common[rule][key]
570
+ }
571
+ }
491
572
  rules
492
573
  }
493
574
 
@@ -495,16 +576,18 @@ closure projConfig(projman, where, local procon, fs, configpath) {
495
576
  //
496
577
  // Run the project by building and then launching.
497
578
  //
498
- procon.run = closure run(buildonly, local rules, builder, buildpath, error, buildnum) {
579
+ procon.run = closure run(stage, buildonly, local rules, builder, buildpath, error, buildnum) {
499
580
  if !procon.loaded
500
581
  return (list(Error("project not loaded")))
501
- rules = buildrules()
582
+ rules = buildrules(stage)
502
583
  if !rules || !rules.products
503
584
  return (list(Error("build rules missing or incomplete")))
504
585
  buildpath = fs.path.join(proj_configDataFolder, rules.products)
505
586
  rules.name = procon.where_name
506
587
  rules.typeID = procon.where_typeID
507
- `(error, builder) = Builder(rules, "debug", fs, "", buildpath)
588
+ if !rules.operation
589
+ rules.operation = "debug"
590
+ `(error, builder) = Builder(rules, rules.operation, fs, "", buildpath)
508
591
  if error
509
592
  return (list(error))
510
593
  `(error, buildnum) = builder.increment()
@@ -530,16 +613,21 @@ closure projConfig(projman, where, local procon, fs, configpath) {
530
613
  //
531
614
  // Publish the project to its configured location.
532
615
  //
533
- procon.publish = closure publish(local rules, publish, error, outfs, inpath) {
534
- rules = buildrules()
535
- publish = procon.configDict.publish
536
- if !rules || !publish.where
616
+ procon.publish = closure publish(stage, local rules, where, publish, error, outfs, inpath) {
617
+ rules = buildrules(stage)
618
+ publish = rules.publish
619
+ where = procon.localDict.publish[stage].where
620
+ if !rules || !where
537
621
  return (list(Error("no project publish location configured")))
538
- `(error, outfs) = projman.locate.connect(publish.where.locationID, "NideFS", publish.where.path)
622
+ `(error, outfs) = projman.locate.connect(where.locationID, "NideFS", where.path)
539
623
  if error
540
624
  return (list(Error("can't access publishing filesystem", error)))
541
625
  inpath = fs.path.join(proj_configDataFolder, rules.products, publish.input)
542
- fs.util.deepcopy(inpath, publish.sources, outfs, publish.output)
626
+ fs.util.deepcopy(inpath, publish.sources, outfs, publish.output, {
627
+ erase: true
628
+ overwrite: true
629
+ follow: true
630
+ })
543
631
  }
544
632
 
545
633
  // close
@@ -217,7 +217,7 @@ closure DebugNub(local nub, debug, paused) {
217
217
  // constant string, with corresponding arguments defined as follows:
218
218
  //
219
219
  // "startpause" - pause at start of evaluating an expression
220
- // tuple - expression being evaluated
220
+ // tuple - dictionary of current step info
221
221
  // "breakpoint" - hit a breakpoint
222
222
  // where - dictionary
223
223
  // value - applied value
@@ -237,10 +237,7 @@ closure DebugNub(local nub, debug, paused) {
237
237
  if entry != "steptrap"
238
238
  lineStepClear()
239
239
  if entry == "startpause" {
240
- if atom(arg1)
241
- msg.error = Error("can't debug atom:", arg1)
242
- else
243
- msg.expr = arg1
240
+ msg.expr = arg1
244
241
  } else if entry == "breakpoint" {
245
242
  if breakpointHit(msg, arg1, arg2)
246
243
  return
@@ -438,6 +438,7 @@ closure ExecutorTracker(local xtrak) {
438
438
  // Notify watchers of an update on an execution instance.
439
439
 
440
440
  xtrak.update = closure update(executor) {
441
+ debuglog("xtrack.update", executor.title)
441
442
  notify({ updated: [executor] })
442
443
  }
443
444
 
@@ -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