@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.
- package/LICENSE.md +1 -1
- package/README.md +3 -3
- package/bin/index.js +2 -2
- package/dist/env_web.js +149 -20
- package/dist/naan.min.js +5 -5
- package/frameworks/browser/sworker.js +309 -91
- package/frameworks/browser/terminals.nlg +22 -9
- package/frameworks/browser/workers.nlg +19 -11
- package/frameworks/client/apiclient.nlg +17 -4
- package/frameworks/client/psm_client.nlg +116 -53
- package/frameworks/common/common.nlg +12 -2
- package/frameworks/node/apiserver.nlg +25 -13
- package/frameworks/node/filesystem.nlg +173 -30
- package/frameworks/node/psm_server.nlg +95 -32
- package/frameworks/project/build.nlg +40 -23
- package/frameworks/project/projects.nlg +47 -26
- package/frameworks/running/debugnub.nlg +2 -5
- package/frameworks/storage/csv.nlg +120 -0
- package/frameworks/storage/dbt_pouch.nlg +41 -25
- package/frameworks/storage/psm.nlg +108 -28
- package/frameworks/storage/psm_dbtables.nlg +7 -3
- package/frameworks/storage/resources.nlg +30 -2
- package/lib/browser/env_web.js +147 -18
- package/lib/browser/env_webworker.js +1 -1
- package/lib/browser/require.js +1 -1
- package/lib/core/naanlib.js +5 -5
- package/package.json +1 -1
- package/plugins/serviceAws/aws_cloudwatchlogs.nlg +1 -1
- package/plugins/serviceAws/aws_dynamo.nlg +625 -141
- package/plugins/serviceAws/aws_dynextra.nlg +294 -0
- package/plugins/serviceAws/dbt_aws.nlg +31 -11
- package/plugins/serviceAws/psm_aws.nlg +42 -26
- package/plugins/serviceAws/serviceAws.nlg +1 -0
- package/plugins/serviceGitHub/psm_github.nlg +41 -25
- package/plugins/serviceGitLab/psm_gitlab.nlg +41 -25
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* aws_dynextra.nlg
|
|
3
|
+
* serviceAws
|
|
4
|
+
*
|
|
5
|
+
* Extra DynamoDB services for AWS.
|
|
6
|
+
*
|
|
7
|
+
* column positioning: // // !
|
|
8
|
+
*
|
|
9
|
+
* Copyright (c) 2022 by Richard C. Zulch
|
|
10
|
+
*
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* dynexBatchGetRecords
|
|
16
|
+
*
|
|
17
|
+
* Retrieve records from one or more tables using arrays of keys. The results are put back in a
|
|
18
|
+
* dictionary with the same shape as the source. The result is a standard `(error, data) tuple.
|
|
19
|
+
* The request/response layout is as follows:
|
|
20
|
+
* [
|
|
21
|
+
* {
|
|
22
|
+
* table: <DynaTable object> // table to act upon
|
|
23
|
+
* keys: [<hash-range tuple>] // records to get
|
|
24
|
+
* data: [<retrieved records>] // received records in reply
|
|
25
|
+
* },
|
|
26
|
+
* { <next-table> }
|
|
27
|
+
* ]
|
|
28
|
+
* The operations are processed in parallel batches, and the final result is a standard result
|
|
29
|
+
* tuple `(error, result). An error is only reported if we are unable to complete all operations.
|
|
30
|
+
*
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
function dynexBatchGetRecords(dyna, request,
|
|
34
|
+
local index, titem, tname, error, batches, activity, params, batchsize, primary) {
|
|
35
|
+
if !dyna.aws
|
|
36
|
+
return (list(Error("DynaTable.batchGetRecords: not logged into AWS")))
|
|
37
|
+
request = new(request) // allow local modifications
|
|
38
|
+
index = { }
|
|
39
|
+
for titem in request { // preflight all tables
|
|
40
|
+
tname = titem.table.tablename
|
|
41
|
+
if index[tname]
|
|
42
|
+
return (Error("DynaTable.batchGetRecords: duplicate table in request:", tname))
|
|
43
|
+
`(error) = titem.table.info()
|
|
44
|
+
if error
|
|
45
|
+
return (Error("DynaTable.batchGetRecords: table", tname, ":", error))
|
|
46
|
+
titem.data = []
|
|
47
|
+
index[tname] = titem
|
|
48
|
+
}
|
|
49
|
+
batches = []
|
|
50
|
+
activity = true
|
|
51
|
+
while activity { // loop until requests empty
|
|
52
|
+
params = {
|
|
53
|
+
RequestItems: { }
|
|
54
|
+
}
|
|
55
|
+
batchsize = 0
|
|
56
|
+
while :fillbatch activity { // loop until batch full
|
|
57
|
+
activity = false
|
|
58
|
+
for `(tname, titem) in index {
|
|
59
|
+
if titem.keys.length > 0 {
|
|
60
|
+
primary = titem.table.makeDynKey(titem.keys.unshift())
|
|
61
|
+
if params.RequestItems[tname].Keys
|
|
62
|
+
params.RequestItems[tname].Keys.push(primary)
|
|
63
|
+
else
|
|
64
|
+
params.RequestItems[tname] = {
|
|
65
|
+
Keys: [primary]
|
|
66
|
+
}
|
|
67
|
+
activity = true
|
|
68
|
+
if ++batchsize >= 100
|
|
69
|
+
break :fillbatch // our batch is full
|
|
70
|
+
} else
|
|
71
|
+
item.keys = undefined // clear for return result
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if batchsize > 0
|
|
75
|
+
batches.push(params)
|
|
76
|
+
}
|
|
77
|
+
asyncArray(batches, 10, closure bgr(params, local delayer, pending, err, response, table) {
|
|
78
|
+
delayer = dyna.conv.genBackoffDelayer()
|
|
79
|
+
loop {
|
|
80
|
+
pending = new(nonce)
|
|
81
|
+
dyna.aws.batchGetItem(params, function(err, data) {
|
|
82
|
+
if err
|
|
83
|
+
debuglog("aws.batchGetItem failed", err)
|
|
84
|
+
pending.signal(err, data)
|
|
85
|
+
})
|
|
86
|
+
`(err, response) = pending.wait()
|
|
87
|
+
for `(tname, table) in response.Responses
|
|
88
|
+
for titem in table
|
|
89
|
+
index[tname].data.push(dyna.conv.recordDynToNaan(titem))
|
|
90
|
+
if response.UnprocessedKeys
|
|
91
|
+
params.RequestItems = response.UnprocessedKeys // still more to do
|
|
92
|
+
else
|
|
93
|
+
break // total success or failure
|
|
94
|
+
if delayer() > dyna.timeout {
|
|
95
|
+
if !error // too slow
|
|
96
|
+
error = err
|
|
97
|
+
break
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}).wait()
|
|
101
|
+
if error
|
|
102
|
+
list(error)
|
|
103
|
+
else
|
|
104
|
+
list(false, request)
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
/*
|
|
109
|
+
* dynexBatchWriteRecords
|
|
110
|
+
*
|
|
111
|
+
* Put arrays of records into one or more tables, replacing or deleting any existing records with
|
|
112
|
+
* the same keys. This returns arrays of results coresponding to the input records, where each
|
|
113
|
+
* is a standard `(error, data) tuple. The request layout is as follows:
|
|
114
|
+
* [
|
|
115
|
+
* {
|
|
116
|
+
* table: <DynaTable object> // table to act upon
|
|
117
|
+
* delete: [<key tuples/records>] // records to delete (needing only keys)
|
|
118
|
+
* put: [<records>] // records to put
|
|
119
|
+
* },
|
|
120
|
+
* { <next-table> }
|
|
121
|
+
* ]
|
|
122
|
+
* The operations are processed in parallel batches, and the final result is a standard result
|
|
123
|
+
* tuple `(error, result). An error is only reported if we are unable to complete all operations.
|
|
124
|
+
*
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
closure dynexBatchWriteRecords(dyna, request, local batches, error, activity) {
|
|
128
|
+
if !dyna.aws
|
|
129
|
+
return (list(Error("DynaTable.batchWriteRecords: not logged into AWS")))
|
|
130
|
+
|
|
131
|
+
//
|
|
132
|
+
// addOp
|
|
133
|
+
// Add a put or delete operation to the parameters
|
|
134
|
+
//
|
|
135
|
+
function addOp(params, table, del, record, local request) {
|
|
136
|
+
if del { // deleting a record
|
|
137
|
+
if tuple(record)
|
|
138
|
+
record = table.makeDynKey(record) // caller provided hash/range key pair
|
|
139
|
+
else
|
|
140
|
+
request = { DeleteRequest: { Key: dyna.conv.recordNaanToDyn(record) } }
|
|
141
|
+
}
|
|
142
|
+
else // putting a record
|
|
143
|
+
request = { PutRequest: { Item: dyna.conv.recordNaanToDyn(record) } }
|
|
144
|
+
if params.RequestItems[table.tablename]
|
|
145
|
+
params.RequestItems[table.tablename].push(request)
|
|
146
|
+
else
|
|
147
|
+
params.RequestItems[table.tablename] = [request]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// transform into batches
|
|
151
|
+
//
|
|
152
|
+
// This is tricky because it wants each batch to use multiple tables for maximum parallelism,
|
|
153
|
+
// just in case AWS isn't already optimizing this. It takes a request from each table and
|
|
154
|
+
// combines them into a single batch repeatedly, until the batch is full or the tables in the
|
|
155
|
+
// request have no more entries.
|
|
156
|
+
//
|
|
157
|
+
request = new(request) // allow local modifications
|
|
158
|
+
batches = []
|
|
159
|
+
activity = true
|
|
160
|
+
while activity
|
|
161
|
+
let (params, batchsize, table) {
|
|
162
|
+
params = {
|
|
163
|
+
RequestItems: { }
|
|
164
|
+
}
|
|
165
|
+
batchsize = 0
|
|
166
|
+
while :fillbatch activity { // loop until no activity or batch full
|
|
167
|
+
activity = false
|
|
168
|
+
for table in request {
|
|
169
|
+
if table.delete.length > 0
|
|
170
|
+
addOp(params, table, true, table.delete.unshift())
|
|
171
|
+
else if table.put.length > 0
|
|
172
|
+
addOp(params, table, false, table.put.unshift())
|
|
173
|
+
else
|
|
174
|
+
continue
|
|
175
|
+
activity = true
|
|
176
|
+
if ++batchsize >= 25
|
|
177
|
+
break :fillbatch // our batch is full
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if batchsize > 0
|
|
181
|
+
batches.push(params)
|
|
182
|
+
} ()
|
|
183
|
+
asyncArray(batches, 10, closure bwr(params, local delayer, pending, err, response) {
|
|
184
|
+
delayer = dyna.conv.genBackoffDelayer()
|
|
185
|
+
loop {
|
|
186
|
+
pending = new(nonce)
|
|
187
|
+
dyna.aws.batchWriteItem(params, function(err, data) {
|
|
188
|
+
if err
|
|
189
|
+
debuglog("aws.batchWriteItem failed", err)
|
|
190
|
+
pending.signal(err, data)
|
|
191
|
+
})
|
|
192
|
+
`(err, response) = pending.wait()
|
|
193
|
+
if response.UnprocessedKeys
|
|
194
|
+
params.RequestItems = response.UnprocessedKeys // still more to do
|
|
195
|
+
else
|
|
196
|
+
break // total success or failure
|
|
197
|
+
if delayer() > dyna.timeout {
|
|
198
|
+
if !error // too slow
|
|
199
|
+
error = err
|
|
200
|
+
break
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}).wait()
|
|
204
|
+
if error
|
|
205
|
+
list(error)
|
|
206
|
+
else
|
|
207
|
+
list(false, { ok: true })
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
/*
|
|
213
|
+
* dynexTransactGetRecords
|
|
214
|
+
*
|
|
215
|
+
* Retrieve records from one or more tables using arrays of keys as a single transaction. The
|
|
216
|
+
* results are put back in an array with the same shape as the source. The request/response
|
|
217
|
+
* layout is as follows:
|
|
218
|
+
* [
|
|
219
|
+
* {
|
|
220
|
+
* table: <DynaTable object> // table to act upon
|
|
221
|
+
* keys: [<hash-range tuple>] // records to get
|
|
222
|
+
* data: [<retrieved records>] // received records in reply
|
|
223
|
+
* },
|
|
224
|
+
* { <next-table> }
|
|
225
|
+
* ]
|
|
226
|
+
* This returns a standard result tuple `(error, result). An error is only reported if we are
|
|
227
|
+
* unable to complete all operations.
|
|
228
|
+
*
|
|
229
|
+
*/
|
|
230
|
+
|
|
231
|
+
closure dynexTransactGetRecords(dyna, request,
|
|
232
|
+
local index, titem, tname, error, params, pending, response) {
|
|
233
|
+
if !dyna.aws
|
|
234
|
+
return (list(Error("DynaTable.transactGetRecords: not logged into AWS")))
|
|
235
|
+
request = new(request) // allow local modifications
|
|
236
|
+
index = { }
|
|
237
|
+
for titem in request { // preflight all tables
|
|
238
|
+
tname = titem.table.tablename
|
|
239
|
+
if index[tname]
|
|
240
|
+
return (Error("DynaTable.batchGetRecords: duplicate table in request:", tname))
|
|
241
|
+
`(error) = titem.table.info()
|
|
242
|
+
if error
|
|
243
|
+
return (Error("DynaTable.batchGetRecords: table", tname, ":", error))
|
|
244
|
+
titem.data = []
|
|
245
|
+
index[tname] = titem
|
|
246
|
+
}
|
|
247
|
+
params = {
|
|
248
|
+
TransactItems: []
|
|
249
|
+
}
|
|
250
|
+
for `(tname, titem) in index {
|
|
251
|
+
while titem.keys.length > 0 {
|
|
252
|
+
params.TransactItems.push({
|
|
253
|
+
Get: {
|
|
254
|
+
TableName: tname
|
|
255
|
+
Key: makeDynKey(titem.keys.unshift())
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
titem.keys = undefined
|
|
260
|
+
}
|
|
261
|
+
pending = new(nonce)
|
|
262
|
+
dyna.aws.transactGetItems(params, function(err, data) {
|
|
263
|
+
if err
|
|
264
|
+
debuglog("aws.transactGetItems failed", err)
|
|
265
|
+
pending.signal(err, data)
|
|
266
|
+
})
|
|
267
|
+
`(error, response) = pending.wait()
|
|
268
|
+
if error
|
|
269
|
+
list(error)
|
|
270
|
+
else {
|
|
271
|
+
for titem in response.Responses {
|
|
272
|
+
tname = titem.Item.*.0
|
|
273
|
+
index[tname].data.push(dyna.conv.recordDynToNaan(titem.Item[tname]))
|
|
274
|
+
}
|
|
275
|
+
list(false, request)
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
/*
|
|
281
|
+
* dynexInit
|
|
282
|
+
*
|
|
283
|
+
* Initialize the component.
|
|
284
|
+
*
|
|
285
|
+
*/
|
|
286
|
+
|
|
287
|
+
function dynexInit(local manifest) {
|
|
288
|
+
manifest = `(dynexBatchGetRecords, dynexBatchWriteRecords, dynexTransactGetRecords, dynexInit)
|
|
289
|
+
|
|
290
|
+
Naan.module.build(module.id, "aws_dynextra", function(modobj, compobj) {
|
|
291
|
+
compobj.manifest = manifest
|
|
292
|
+
require("./serviceAws.nlg")
|
|
293
|
+
})
|
|
294
|
+
} ();
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* column positioning: // // !
|
|
8
8
|
*
|
|
9
|
-
* Copyright (c) 2020-
|
|
9
|
+
* Copyright (c) 2020-2022 by Richard C. Zulch
|
|
10
10
|
*
|
|
11
11
|
*/
|
|
12
12
|
|
|
@@ -259,6 +259,8 @@ closure dawsBucket(s3, local bucket) {
|
|
|
259
259
|
listOne()
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
// finis
|
|
263
|
+
|
|
262
264
|
list(false, bucket)
|
|
263
265
|
|
|
264
266
|
};
|
|
@@ -502,7 +504,10 @@ closure dawsTable(database, path, local table) {
|
|
|
502
504
|
|
|
503
505
|
// head
|
|
504
506
|
//
|
|
505
|
-
// Return the header of the specified record, i.e. everything except the content.
|
|
507
|
+
// Return the header of the specified record, i.e. everything except the content. This operates
|
|
508
|
+
// on directories (with names ending in slash) differently because AWS does not support calling
|
|
509
|
+
// headObject upon them. We simply want to confirm that the directory exists, so we list objects
|
|
510
|
+
// that have the name without the slash and we get a common prefix if it exists.
|
|
506
511
|
|
|
507
512
|
table.head = closure head(key, callback, local id) {
|
|
508
513
|
if !callback
|
|
@@ -510,15 +515,30 @@ closure dawsTable(database, path, local table) {
|
|
|
510
515
|
id = makeID(key)
|
|
511
516
|
if !string(id)
|
|
512
517
|
return (asyncResult(callback, id)) // error from makeID
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
resp.
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
518
|
+
if id.slice(-1) == "/"
|
|
519
|
+
database.bucket.list(id.slice(0,-1), true, function blcb(error, resp) {
|
|
520
|
+
if error
|
|
521
|
+
error = Error("head failed", error, id, { status: error.statusCode })
|
|
522
|
+
else if id != resp.CommonPrefixes.0.Prefix {
|
|
523
|
+
error = Error("head not found", id, resp.CommonPrefixes.0.Prefix, { status: 404 })
|
|
524
|
+
resp = false
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
resp.key = key
|
|
528
|
+
resp = s3resp2doc(resp)
|
|
529
|
+
resp._id = id }
|
|
530
|
+
callback(error, resp)
|
|
531
|
+
})
|
|
532
|
+
else
|
|
533
|
+
database.bucket.get(id, true, function bgcb(error, resp) {
|
|
534
|
+
if error
|
|
535
|
+
error = Error("head failed", error, id, { status: error.statusCode })
|
|
536
|
+
else {
|
|
537
|
+
resp.key = key
|
|
538
|
+
resp = s3resp2doc(resp)
|
|
539
|
+
resp._id = id }
|
|
540
|
+
callback(error, resp)
|
|
541
|
+
})
|
|
522
542
|
}
|
|
523
543
|
|
|
524
544
|
// copy
|
|
@@ -50,36 +50,26 @@ closure psmaConnector(psm, local connClassID, connector, watch) {
|
|
|
50
50
|
key: "bucketName" }
|
|
51
51
|
]
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
// hashResourceID
|
|
55
|
-
//
|
|
56
|
-
// Make a stable hash based on the resource credentials that we can persist or share between
|
|
57
|
-
// different domains accessing the same resource.
|
|
58
|
-
connector.hashResourceID = function hashResourceID(resource, local ident) {
|
|
59
|
-
ident = resource.region.concat(resource.region, resource.bucketName)
|
|
60
|
-
if resource.label
|
|
61
|
-
ident = ident.concat(resource.label)
|
|
62
|
-
HashSHA256(ident)
|
|
63
|
-
}
|
|
64
53
|
|
|
65
54
|
// findResource
|
|
66
55
|
//
|
|
67
|
-
// Find a resource with the specified contents, returning the resID or false.
|
|
68
|
-
// is: if added, would the specified contents be redundant to an existing resource?
|
|
56
|
+
// Find a resource with the specified contents, returning the resID or false.
|
|
69
57
|
connector.findResource = function findResource(resource, local resID, creds) {
|
|
70
58
|
for resID in listResources().1 {
|
|
71
59
|
creds = access(resID).1
|
|
72
|
-
if resource.region
|
|
73
|
-
&& (!resource.
|
|
60
|
+
if ((!resource.region || resource.region == creds.region)
|
|
61
|
+
&& (!resource.bucketName || resource.bucketName == creds.bucketName)
|
|
62
|
+
&& (!resource.label || resource.label == creds.name))
|
|
74
63
|
return (resID)
|
|
75
64
|
}
|
|
76
65
|
false
|
|
77
66
|
}
|
|
78
67
|
|
|
79
|
-
//
|
|
68
|
+
// writeResource
|
|
80
69
|
//
|
|
81
|
-
// Add credentials for a named resource.
|
|
82
|
-
// comprising these
|
|
70
|
+
// Add or update credentials for a named resource. If no resID is specified this adds a new
|
|
71
|
+
// resource; otherwise it updates an existing one. The resource is a dictionary comprising these
|
|
72
|
+
// keys:
|
|
83
73
|
// label - [optional] label we use for this resource
|
|
84
74
|
// keyID - AWS IAM keyID
|
|
85
75
|
// keySecret - AWS IAM keySecret
|
|
@@ -87,10 +77,13 @@ closure psmaConnector(psm, local connClassID, connector, watch) {
|
|
|
87
77
|
// bucketName - AWS bucketName
|
|
88
78
|
// hidden - [optional] don't enumerate to user
|
|
89
79
|
// locked - [optional] don't allow user delete
|
|
90
|
-
// The return value is (error,
|
|
91
|
-
// keys and error diagnostic strings.
|
|
92
|
-
connector.
|
|
93
|
-
|
|
80
|
+
// The return value is an (error, resID) tuple. Error, if non-false, is a dictionary of field
|
|
81
|
+
// keys and error diagnostic strings.
|
|
82
|
+
connector.writeResource = function writeResource(resource, resID,
|
|
83
|
+
local creds, errors, error, data, change) {
|
|
84
|
+
// badField - test for a bad string
|
|
85
|
+
function badField(str) { !string(str) || str.trim().length == 0 }
|
|
86
|
+
|
|
94
87
|
creds = {
|
|
95
88
|
keyID: resource.keyID,
|
|
96
89
|
keySecret: resource.keySecret,
|
|
@@ -104,6 +97,8 @@ closure psmaConnector(psm, local connClassID, connector, watch) {
|
|
|
104
97
|
creds.label = resource.region.concat("-", resource.bucketName)
|
|
105
98
|
if badField(creds.label)
|
|
106
99
|
errors.label = "invalid label"
|
|
100
|
+
else if !resID && findResource({ label: creds.label })
|
|
101
|
+
errors.label = "duplicate name"
|
|
107
102
|
if badField(resource.keyID)
|
|
108
103
|
errors.keyID = "required field"
|
|
109
104
|
if badField(resource.keySecret)
|
|
@@ -119,14 +114,35 @@ closure psmaConnector(psm, local connClassID, connector, watch) {
|
|
|
119
114
|
creds.hidden = resource.hidden
|
|
120
115
|
if resource.locked
|
|
121
116
|
creds.locked = resource.locked
|
|
122
|
-
resID
|
|
123
|
-
|
|
117
|
+
if resID {
|
|
118
|
+
`(error, data) = connector.vault.updateResource(resID, creds)
|
|
119
|
+
change = { changed: [resID] }
|
|
120
|
+
} else {
|
|
121
|
+
resID = UUID()
|
|
122
|
+
`(error, data) = connector.vault.addResource(resID, creds)
|
|
123
|
+
change = { added: [resID] }
|
|
124
|
+
}
|
|
124
125
|
if data {
|
|
125
|
-
watch.notify(connClassID,
|
|
126
|
-
data = resID
|
|
126
|
+
watch.notify(connClassID, change)
|
|
127
|
+
data = resID
|
|
128
|
+
}
|
|
127
129
|
}
|
|
128
130
|
list(error, data)
|
|
129
131
|
}
|
|
132
|
+
|
|
133
|
+
// addResource
|
|
134
|
+
//
|
|
135
|
+
// Add a new resource, returning a standard error tuple per writeResource.
|
|
136
|
+
connector.addResource = function addResource(resource) {
|
|
137
|
+
writeResource(resource)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// updateResource
|
|
141
|
+
//
|
|
142
|
+
// Update an existing resource, returning a standard error tuple per writeResource.
|
|
143
|
+
connector.updateResource = function updateResource(resID, resource) {
|
|
144
|
+
writeResource(resource, resID)
|
|
145
|
+
}
|
|
130
146
|
|
|
131
147
|
// getResource
|
|
132
148
|
//
|
|
@@ -24,6 +24,7 @@ function sawsInit(local manifest) {
|
|
|
24
24
|
Naan.module.build(module.id, "serviceAws", function(modobj, compobj) {
|
|
25
25
|
compobj.manifest = manifest
|
|
26
26
|
require("frameworks/common").LiveImport()
|
|
27
|
+
require("./aws_utils.nlg")
|
|
27
28
|
function sawsPostload() {
|
|
28
29
|
if js.g
|
|
29
30
|
awsSDK = js.r("aws-sdk")
|
|
@@ -45,41 +45,34 @@ closure phubConnector(psm, local connClassID, connector, watch) {
|
|
|
45
45
|
|
|
46
46
|
// findResource
|
|
47
47
|
//
|
|
48
|
-
// Find a resource with the specified contents, returning the resID or false.
|
|
49
|
-
// is: if added, would the specified contents be redundant to an existing resource?
|
|
48
|
+
// Find a resource with the specified contents, returning the resID or false.
|
|
50
49
|
connector.findResource = function findResource(resource, local resID, creds) {
|
|
51
50
|
for resID in listResources().1 {
|
|
52
51
|
creds = access(resID).1
|
|
53
|
-
if resource.
|
|
52
|
+
if ((!resource.label || resource.label == creds.label)
|
|
53
|
+
&& (!resource.userName || resource.userName == creds.userName))
|
|
54
54
|
return (resID)
|
|
55
55
|
}
|
|
56
56
|
false
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
// hashResourceID
|
|
60
|
-
//
|
|
61
|
-
// Make a stable hash based on the resource credentials that we can persist or share between
|
|
62
|
-
// different domains accessing the same resource.
|
|
63
|
-
connector.hashResourceID = function hashResourceID(resource, local ident) {
|
|
64
|
-
ident = resource.userName
|
|
65
|
-
if resource.label
|
|
66
|
-
ident = ident.concat(resource.label)
|
|
67
|
-
HashSHA256(ident)
|
|
68
|
-
}
|
|
69
58
|
|
|
70
|
-
//
|
|
59
|
+
// writeResource
|
|
71
60
|
//
|
|
72
|
-
// Add credentials for a named resource.
|
|
73
|
-
// comprising these
|
|
61
|
+
// Add or update credentials for a named resource. If no resID is specified this adds a new
|
|
62
|
+
// resource; otherwise it updates an existing one. The resource is a dictionary comprising these
|
|
63
|
+
// keys:
|
|
74
64
|
// label - [optional] label we use for this resource
|
|
75
65
|
// userName - GitHub user name
|
|
76
66
|
// userPat - GitHUB Personal Access Token
|
|
77
67
|
// hidden - [optional] don't enumerate to user
|
|
78
68
|
// locked - [optional] don't allow user delete
|
|
79
|
-
// The return value is (error,
|
|
80
|
-
// keys and error diagnostic strings.
|
|
81
|
-
connector.
|
|
82
|
-
|
|
69
|
+
// The return value is an (error, resID) tuple. Error, if non-false, is a dictionary of field
|
|
70
|
+
// keys and error diagnostic strings.
|
|
71
|
+
connector.writeResource = function writeResource(resource, resID,
|
|
72
|
+
local label, errors, creds, error, data) {
|
|
73
|
+
// badField - test for a bad string
|
|
74
|
+
function badField(str) { !string(str) || str.trim().length == 0 }
|
|
75
|
+
|
|
83
76
|
creds = {
|
|
84
77
|
userName: resource.userName,
|
|
85
78
|
userPat: resource.userPat
|
|
@@ -91,6 +84,8 @@ closure phubConnector(psm, local connClassID, connector, watch) {
|
|
|
91
84
|
creds.label = resource.region.concat("-", resource.bucketName)
|
|
92
85
|
if badField(creds.label)
|
|
93
86
|
errors.label = "invalid label"
|
|
87
|
+
else if !resID && findResource({ label: creds.label })
|
|
88
|
+
errors.label = "duplicate name"
|
|
94
89
|
if badField(resource.userName)
|
|
95
90
|
errors.userName = "required field"
|
|
96
91
|
if badField(resource.userPat)
|
|
@@ -102,15 +97,36 @@ closure phubConnector(psm, local connClassID, connector, watch) {
|
|
|
102
97
|
creds.hidden = resource.hidden
|
|
103
98
|
if resource.locked
|
|
104
99
|
creds.locked = resource.locked
|
|
105
|
-
resID
|
|
106
|
-
|
|
100
|
+
if resID {
|
|
101
|
+
`(error, data) = connector.vault.updateResource(resID, creds)
|
|
102
|
+
change = { changed: [resID] }
|
|
103
|
+
} else {
|
|
104
|
+
resID = UUID()
|
|
105
|
+
`(error, data) = connector.vault.addResource(resID, creds)
|
|
106
|
+
change = { added: [resID] }
|
|
107
|
+
}
|
|
107
108
|
if data {
|
|
108
|
-
watch.notify(connClassID,
|
|
109
|
-
data = resID
|
|
109
|
+
watch.notify(connClassID, change)
|
|
110
|
+
data = resID
|
|
111
|
+
}
|
|
110
112
|
}
|
|
111
113
|
list(error, data)
|
|
112
114
|
}
|
|
113
115
|
|
|
116
|
+
// addResource
|
|
117
|
+
//
|
|
118
|
+
// Add a new resource, returning a standard error tuple per writeResource.
|
|
119
|
+
connector.addResource = function addResource(resource) {
|
|
120
|
+
writeResource(resource)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// updateResource
|
|
124
|
+
//
|
|
125
|
+
// Update an existing resource, returning a standard error tuple per writeResource.
|
|
126
|
+
connector.updateResource = function updateResource(resID, resource) {
|
|
127
|
+
writeResource(resource, resID)
|
|
128
|
+
}
|
|
129
|
+
|
|
114
130
|
// deleteResource
|
|
115
131
|
//
|
|
116
132
|
// Delete credentials for a resource
|