@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.
- package/LICENSE.md +21 -0
- package/README.md +46 -0
- package/dist/env_web.js +431 -0
- package/dist/naan.min.js +73 -0
- package/frameworks/browser/browser.nlg +30 -0
- package/frameworks/browser/pouchdb/pouchdb.min.js +7 -0
- package/frameworks/browser/spark-md5/spark-md5.min.js +1 -0
- package/frameworks/browser/sworker.js +192 -0
- package/frameworks/browser/terminals.nlg +787 -0
- package/frameworks/browser/workers.nlg +386 -0
- package/frameworks/client/apiclient.nlg +183 -0
- package/frameworks/client/client.nlg +29 -0
- package/frameworks/client/psm_client.nlg +572 -0
- package/frameworks/common/common.nlg +464 -0
- package/frameworks/common/preferences.nlg +131 -0
- package/frameworks/common/repltools.nlg +100 -0
- package/frameworks/common/watching.nlg +97 -0
- package/frameworks/node/apiserver.nlg +399 -0
- package/frameworks/node/filesystem.nlg +1012 -0
- package/frameworks/node/gitter.nlg +234 -0
- package/frameworks/node/node.nlg +81 -0
- package/frameworks/node/psm_server.nlg +465 -0
- package/frameworks/node/worker.nlg +560 -0
- package/frameworks/project/build.nlg +1229 -0
- package/frameworks/project/jszip.js +30 -0
- package/frameworks/project/jszip.nlg +87 -0
- package/frameworks/project/proj_cliser.nlg +139 -0
- package/frameworks/project/proj_console.nlg +137 -0
- package/frameworks/project/proj_folder.nlg +143 -0
- package/frameworks/project/proj_lambda.nlg +137 -0
- package/frameworks/project/proj_static.nlg +138 -0
- package/frameworks/project/project.nlg +29 -0
- package/frameworks/project/projects.nlg +587 -0
- package/frameworks/project/uglifyjs.js +1 -0
- package/frameworks/project/uglifyjs.nlg +79 -0
- package/frameworks/running/debugcom.nlg +163 -0
- package/frameworks/running/debugnub.nlg +565 -0
- package/frameworks/running/debugutil.nlg +510 -0
- package/frameworks/running/executors.nlg +767 -0
- package/frameworks/running/running.nlg +151 -0
- package/frameworks/running/sourcecode.nlg +346 -0
- package/frameworks/running/taskexec.nlg +196 -0
- package/frameworks/storage/dbt_pouch.nlg +773 -0
- package/frameworks/storage/file_manager.nlg +772 -0
- package/frameworks/storage/psm.nlg +326 -0
- package/frameworks/storage/psm_dbtables.nlg +814 -0
- package/frameworks/storage/resources.nlg +445 -0
- package/frameworks/storage/storage.nlg +52 -0
- package/lib/browser/env_web.js +431 -0
- package/lib/browser/env_webworker.js +146 -0
- package/lib/browser/require.js +1653 -0
- package/lib/core/naanlib.js +73 -0
- package/lib/env_node.js +282 -0
- package/lib/env_nodeworker.js +165 -0
- package/lib/node_repl.js +25 -0
- package/lib/node_repl_init.nlg +52 -0
- package/package.json +13 -0
- package/plugins/gitClient/clientops.nlg +317 -0
- package/plugins/gitClient/gitClient.nlg +63 -0
- package/plugins/gitClient/gitutils.nlg +1331 -0
- package/plugins/gitClient/minimatch.js +945 -0
- package/plugins/gitClient/minimatch_license.txt +15 -0
- package/plugins/gitClient/minimatch_readme.md +208 -0
- package/plugins/nodeLib/nodeLib.nlg +26 -0
- package/plugins/nodeLib/node_cp.nlg +65 -0
- package/plugins/nodeLib/node_fs.nlg +137 -0
- package/plugins/nodeLib/node_https.nlg +172 -0
- package/plugins/nodeLib/node_util.nlg +180 -0
- package/plugins/serviceAws/aws/aws-sdk.min.js +88 -0
- package/plugins/serviceAws/aws/lambda_rest_index.js +122 -0
- package/plugins/serviceAws/aws/lambda_rest_init.nlg +58 -0
- package/plugins/serviceAws/aws/lambda_ws_index.js +498 -0
- package/plugins/serviceAws/aws/lambda_ws_init.nlg +49 -0
- package/plugins/serviceAws/aws_cloudwatchlogs.nlg +122 -0
- package/plugins/serviceAws/aws_cognito.nlg +76 -0
- package/plugins/serviceAws/aws_dynamo.nlg +509 -0
- package/plugins/serviceAws/aws_lambda.nlg +95 -0
- package/plugins/serviceAws/aws_s3.nlg +134 -0
- package/plugins/serviceAws/aws_utils.nlg +86 -0
- package/plugins/serviceAws/dbt_aws.nlg +769 -0
- package/plugins/serviceAws/lambda_naan_control.nlg +175 -0
- package/plugins/serviceAws/psm_aws.nlg +258 -0
- package/plugins/serviceAws/serviceAws.nlg +38 -0
- package/plugins/serviceGitHub/gitops.nlg +522 -0
- package/plugins/serviceGitHub/psm_github.nlg +205 -0
- package/plugins/serviceGitHub/serviceGitHub.nlg +65 -0
- package/plugins/serviceGitLab/gitops.nlg +528 -0
- package/plugins/serviceGitLab/psm_gitlab.nlg +205 -0
- package/plugins/serviceGitLab/serviceGitLab.nlg +65 -0
- package/test/harness.nlg +312 -0
- package/test/node_test.js +33 -0
- package/test/test_01_core.nlg +768 -0
- package/test/test_02_context.nlg +269 -0
- package/test/test_03_jsinterop.nlg +137 -0
- package/test/test_04_numerics.nlg +450 -0
- package/test/test_05_strings.nlg +63 -0
- package/test/test_06_lingoparse.nlg +500 -0
- 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
|
+
}();
|