@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,773 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* dbt_pouch.nlg
|
|
3
|
+
* frameworks/storage
|
|
4
|
+
*
|
|
5
|
+
* Naan database tables using PouchDB. These functions implement tables on top of PouchDB for use
|
|
6
|
+
* in both the browser and NodeJS.
|
|
7
|
+
*
|
|
8
|
+
* column positioning: // // !
|
|
9
|
+
*
|
|
10
|
+
* Copyright (c) 2017-2021 by Richard C. Zulch
|
|
11
|
+
*
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* dapoTable
|
|
17
|
+
*
|
|
18
|
+
* Create a database table access object.
|
|
19
|
+
*
|
|
20
|
+
* If a callback function is specified then it is called when the operation completes with the
|
|
21
|
+
* usual (error, result) arguments. If a callback function is not specified then the call blocks
|
|
22
|
+
* until the operation is complete, and an (error, result) tuple is returned.
|
|
23
|
+
* Records are presented as dictionaries to the client of this interface. The client's data is
|
|
24
|
+
* stored under <record>.content, while the record key is available under <record>.key.
|
|
25
|
+
*
|
|
26
|
+
* Internals
|
|
27
|
+
* Each PouchDB database offers a single linear list of records. The key structuring scheme here
|
|
28
|
+
* allows us to segregate each database into independent folders sharing a single bucket.
|
|
29
|
+
* Within each database, a table instance has a path starting with "" representing the root of
|
|
30
|
+
* the database. All records in the database having that path as a key prefix belong to this table
|
|
31
|
+
* instance. A pseudo-folder structure is implemented using "/" (slash) characters as delimiters.
|
|
32
|
+
* PouchDB does conflict detection, so the _rev key in a document must match the current value
|
|
33
|
+
* in the database or an error is generated on update.
|
|
34
|
+
*
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
closure dapoTable(database, path, local table, prefix) {
|
|
38
|
+
table = new(object, this)
|
|
39
|
+
table.type = database.type
|
|
40
|
+
table.db = database
|
|
41
|
+
table.path = path
|
|
42
|
+
table.prefix = makePrefix(path)
|
|
43
|
+
|
|
44
|
+
// makePrefix
|
|
45
|
+
//
|
|
46
|
+
// Make a key prefix from a path.
|
|
47
|
+
|
|
48
|
+
function makePrefix(path) {
|
|
49
|
+
if path == ""
|
|
50
|
+
path // in the root
|
|
51
|
+
else if path.slice(-1) == "/"
|
|
52
|
+
path // already complete prefix
|
|
53
|
+
else
|
|
54
|
+
path.concat("/") // non-root prefix always ends in slash
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// makeID
|
|
58
|
+
//
|
|
59
|
+
// Make an internal ID string from an external key, using the table's pre-determined prefix.
|
|
60
|
+
|
|
61
|
+
function makeID(key) {
|
|
62
|
+
if key eq `prefix
|
|
63
|
+
table.prefix
|
|
64
|
+
else if integer(key)
|
|
65
|
+
table.prefix.concat(tostring(key))
|
|
66
|
+
else if not string(key)
|
|
67
|
+
return (Error("invalid key type: ", typeof(key)))
|
|
68
|
+
else
|
|
69
|
+
table.prefix.concat(key)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// add
|
|
73
|
+
//
|
|
74
|
+
// Add a new record to the table with optional metadata. This will fail if the key already
|
|
75
|
+
// exists. Returns the new document record.
|
|
76
|
+
|
|
77
|
+
table.add = closure add(key, content, metadata, callback, local id, doc) {
|
|
78
|
+
if !callback
|
|
79
|
+
return (syncAdapter(add, key, content, metadata))
|
|
80
|
+
id = makeID(key)
|
|
81
|
+
if !string(id)
|
|
82
|
+
return (asyncResult(callback, id)) // error from makeID
|
|
83
|
+
if metadata && !dictionary(metadata)
|
|
84
|
+
return (asyncResult(callback, Error("invalid arguments")))
|
|
85
|
+
if !database.pouch
|
|
86
|
+
return (asyncResult(callback, Error("database closed")))
|
|
87
|
+
if metadata {
|
|
88
|
+
doc = new(metadata) // start with metadata
|
|
89
|
+
doc._rev = undefined } // _rev not allowed in metadata
|
|
90
|
+
else
|
|
91
|
+
doc = { }
|
|
92
|
+
if key eq `prefix
|
|
93
|
+
key = id
|
|
94
|
+
doc.key = key
|
|
95
|
+
doc._id = id
|
|
96
|
+
if content
|
|
97
|
+
doc.content = content
|
|
98
|
+
// ### encode large content to blob attachment here
|
|
99
|
+
database.pouch.put(doc, function(error, resp) {
|
|
100
|
+
if error
|
|
101
|
+
error = Error("add failed", error)
|
|
102
|
+
else {
|
|
103
|
+
doc._rev = resp.rev // update the original doc
|
|
104
|
+
if content {
|
|
105
|
+
doc._md5 = HashMD5(content)
|
|
106
|
+
doc._length = length(content) } }
|
|
107
|
+
callback(error, doc)
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// update
|
|
112
|
+
//
|
|
113
|
+
// Update a record in the table using the document previously returned by add or get.
|
|
114
|
+
// This will fail if the record has been modified by someone else in the meantime.
|
|
115
|
+
|
|
116
|
+
table.update = closure update(doc, callback) {
|
|
117
|
+
if !callback
|
|
118
|
+
return (syncAdapter(update, doc))
|
|
119
|
+
if !doc._id || !doc._id.startsWith(table.prefix) || !doc._rev
|
|
120
|
+
return (asyncResult(callback, Error("invalid argument")))
|
|
121
|
+
if makeID(doc.key) != doc._id
|
|
122
|
+
return (asyncResult(callback, Error("key corrupted")))
|
|
123
|
+
if !database.pouch
|
|
124
|
+
return (asyncResult(callback, Error("database closed")))
|
|
125
|
+
doc = new(doc)
|
|
126
|
+
doc._md5 = undefined
|
|
127
|
+
doc._length = undefined
|
|
128
|
+
database.pouch.put(doc, function(error, resp) {
|
|
129
|
+
if error
|
|
130
|
+
error = Error("update failed", error)
|
|
131
|
+
else {
|
|
132
|
+
doc._rev = resp.rev // update the original doc
|
|
133
|
+
if doc.content {
|
|
134
|
+
doc._md5 = HashMD5(doc.content)
|
|
135
|
+
doc._length = length(doc.content) } }
|
|
136
|
+
callback(error, doc)
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// delete
|
|
141
|
+
//
|
|
142
|
+
// Delete a record in the table using the document previously returned by add or get. This
|
|
143
|
+
// marks the record deleted but does not actually reclaim storage.
|
|
144
|
+
|
|
145
|
+
table.delete = closure delete(doc, callback) {
|
|
146
|
+
if !callback
|
|
147
|
+
return (syncAdapter(delete, doc))
|
|
148
|
+
if !doc._id || !doc._id.startsWith(table.prefix) || !doc._rev
|
|
149
|
+
return (asyncResult(callback, Error("invalid argument")))
|
|
150
|
+
if !database.pouch
|
|
151
|
+
return (asyncResult(callback, Error("database closed")))
|
|
152
|
+
doc = {
|
|
153
|
+
_id: doc._id
|
|
154
|
+
_rev: doc._rev
|
|
155
|
+
_deleted: true
|
|
156
|
+
}
|
|
157
|
+
database.pouch.put(doc, function(error, resp) {
|
|
158
|
+
if error
|
|
159
|
+
error = Error("delete failed", error)
|
|
160
|
+
else
|
|
161
|
+
resp = { ok: true }
|
|
162
|
+
callback(error, resp)
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// get
|
|
167
|
+
//
|
|
168
|
+
// Get the record in the table stored in the specified key.
|
|
169
|
+
|
|
170
|
+
table.get = closure get(key, callback, local id) {
|
|
171
|
+
if !callback
|
|
172
|
+
return (syncAdapter(get, key))
|
|
173
|
+
id = makeID(key)
|
|
174
|
+
if !string(id)
|
|
175
|
+
return (asyncResult(callback, id)) // error from makeID
|
|
176
|
+
if !database.pouch
|
|
177
|
+
return (asyncResult(callback, Error("database closed")))
|
|
178
|
+
database.pouch.get(id, function(error, resp) {
|
|
179
|
+
if error
|
|
180
|
+
error = Error("get failed", error)
|
|
181
|
+
else {
|
|
182
|
+
resp.key = key // ensure doc's key matches id
|
|
183
|
+
if resp.content {
|
|
184
|
+
resp._md5 = HashMD5(resp.content)
|
|
185
|
+
resp._length = length(resp.content) }
|
|
186
|
+
}
|
|
187
|
+
callback(error, resp)
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// md5
|
|
192
|
+
//
|
|
193
|
+
// Compute the MD5 hash of the content of the specified record if it exists. The result is "" if
|
|
194
|
+
// no content exists. The metadata is not part of the hash.
|
|
195
|
+
|
|
196
|
+
table.md5 = closure md5(key, callback) {
|
|
197
|
+
if !callback
|
|
198
|
+
return (syncAdapter(md5, key))
|
|
199
|
+
table.get(key, function(error, resp, local md5) {
|
|
200
|
+
if error
|
|
201
|
+
error = Error("get md5 failed", error, { status: error.status })
|
|
202
|
+
else if resp.content
|
|
203
|
+
md5 = HashMD5(resp.content)
|
|
204
|
+
else
|
|
205
|
+
md5 = ""
|
|
206
|
+
callback(error, md5)
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// head
|
|
211
|
+
//
|
|
212
|
+
// Retrieve the head record for a key (i.e. everything but the content.)
|
|
213
|
+
|
|
214
|
+
table.head = closure head(key, callback) {
|
|
215
|
+
if !callback
|
|
216
|
+
return (syncAdapter(head, key))
|
|
217
|
+
table.get(key, function(error, resp) {
|
|
218
|
+
if error
|
|
219
|
+
error = Error("head failed", error, { status: error.status })
|
|
220
|
+
else
|
|
221
|
+
resp.content = undefined
|
|
222
|
+
callback(error, resp)
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// copy
|
|
227
|
+
//
|
|
228
|
+
// Copy a record from a source key to a new record under a destination key.
|
|
229
|
+
|
|
230
|
+
table.copy = closure copy(srckey, destkey, callback, local srcid, destid) {
|
|
231
|
+
if !callback
|
|
232
|
+
return (syncAdapter(copy, srckey, destkey))
|
|
233
|
+
srcid = makeID(srckey)
|
|
234
|
+
if !string(srcid)
|
|
235
|
+
return (asyncResult(callback, srcid)) // error from makeID
|
|
236
|
+
destid = makeID(destkey)
|
|
237
|
+
if !string(destid)
|
|
238
|
+
return (asyncResult(callback, destid)) // error from makeID
|
|
239
|
+
if !database.pouch
|
|
240
|
+
return (asyncResult(callback, Error("database closed")))
|
|
241
|
+
database.pouch.get(srcid, function(error, resp, local doc) {
|
|
242
|
+
if error
|
|
243
|
+
return (callback(Error("copy read failed", {
|
|
244
|
+
source: srckey
|
|
245
|
+
destination: destkey
|
|
246
|
+
}, error)))
|
|
247
|
+
doc = resp
|
|
248
|
+
doc._id = destid
|
|
249
|
+
doc.key = destkey
|
|
250
|
+
doc._rev = undefined
|
|
251
|
+
database.pouch.put(resp, function(error, resp) {
|
|
252
|
+
if error
|
|
253
|
+
return (callback(Error("copy write failed", {
|
|
254
|
+
source: srckey
|
|
255
|
+
destination: destkey
|
|
256
|
+
}, error)))
|
|
257
|
+
doc._rev = resp.rev // return new doc
|
|
258
|
+
if doc.content {
|
|
259
|
+
doc._md5 = HashMD5(doc.content)
|
|
260
|
+
doc._length = length(doc.content) }
|
|
261
|
+
callback(false, doc)
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// records
|
|
267
|
+
//
|
|
268
|
+
// List all records in the table with the following options:
|
|
269
|
+
// prefix: <string> -- only records with the specified key prefix are included
|
|
270
|
+
// deep: true|false -- if true, include transitive children; default false
|
|
271
|
+
// head: true|false -- if true, include doc element with header for each record
|
|
272
|
+
// get: true|false -- if true, include doc element with content for each record
|
|
273
|
+
//
|
|
274
|
+
// The return structure has the following form:
|
|
275
|
+
//
|
|
276
|
+
// {prefix : "joe/bill/",
|
|
277
|
+
// rows : [
|
|
278
|
+
// {
|
|
279
|
+
// key : "john",
|
|
280
|
+
// _modified : [Date Sat Apr 04 2020 17:05:38 GMT-0700 (Pacific Daylight Time)]
|
|
281
|
+
// _md5 : "55ff07fb005494c4bd42322d4845e858"
|
|
282
|
+
// _length : 156043
|
|
283
|
+
// [ doc | error: <document from "get" or "head"> | <error from get|head> ] },
|
|
284
|
+
// {
|
|
285
|
+
// key : "jill",
|
|
286
|
+
// _modified : [Date Sat Apr 22 2020 17:05:38 GMT-0700 (Pacific Daylight Time)]
|
|
287
|
+
// _md5 : "f53f07fb00d42322dc4bd42322d4845e"
|
|
288
|
+
// _length : 155
|
|
289
|
+
// [ doc : <document from "get" or "head"> ] } ] }
|
|
290
|
+
//
|
|
291
|
+
// All transitive nested children are included with the options.deep flag non-false. Otherwise
|
|
292
|
+
// only immediate children are listed, with directories at the end--and no metadata for them.
|
|
293
|
+
//
|
|
294
|
+
|
|
295
|
+
table.records = closure records(options, callback, local prefix, pouchOpts, output) {
|
|
296
|
+
if !callback
|
|
297
|
+
return (syncAdapter(records, options))
|
|
298
|
+
output = {
|
|
299
|
+
rows: [] }
|
|
300
|
+
if options.prefix {
|
|
301
|
+
if !string(options.prefix)
|
|
302
|
+
return (asyncResult(callback, Error("prefix option must be string")))
|
|
303
|
+
output.prefix = options.prefix
|
|
304
|
+
prefix = table.prefix.concat(makePrefix(options.prefix)) }
|
|
305
|
+
else
|
|
306
|
+
prefix = table.prefix
|
|
307
|
+
if !database.pouch
|
|
308
|
+
return (asyncResult(callback, Error("database closed")))
|
|
309
|
+
pouchOpts = {
|
|
310
|
+
startkey: prefix
|
|
311
|
+
endkey: prefix.concat("\ufff0") // recommended last character
|
|
312
|
+
}
|
|
313
|
+
if options.get || options.head
|
|
314
|
+
pouchOpts.include_docs = true
|
|
315
|
+
database.pouch.allDocs(pouchOpts, function(error, rowlist, local firstkids, obj, key, offset, item) {
|
|
316
|
+
if error
|
|
317
|
+
error = Error("allDocs failed", error)
|
|
318
|
+
else {
|
|
319
|
+
firstkids = { }
|
|
320
|
+
for obj in rowlist.rows {
|
|
321
|
+
if obj.id.startsWith(table.prefix)
|
|
322
|
+
key = obj.id.substring(table.prefix.length)
|
|
323
|
+
else
|
|
324
|
+
key = "*key error*" // should not happen
|
|
325
|
+
if !options.deep {
|
|
326
|
+
offset = key.indexOf("/", prefix.length)
|
|
327
|
+
if offset >= 0 {
|
|
328
|
+
key = key.substring(0, offset+1) // child directory name
|
|
329
|
+
if !firstkids[key]
|
|
330
|
+
firstkids[key] = key // first time we've seen this child directory
|
|
331
|
+
else
|
|
332
|
+
continue } } // ignore deeper child
|
|
333
|
+
item = {
|
|
334
|
+
key: key
|
|
335
|
+
_id: obj.id
|
|
336
|
+
_rev: obj.value.rev
|
|
337
|
+
}
|
|
338
|
+
if obj.doc {
|
|
339
|
+
if obj.doc.content {
|
|
340
|
+
item._md5 = HashMD5(obj.doc.content)
|
|
341
|
+
item._length = length(obj.doc.content) }
|
|
342
|
+
item.doc = obj.doc
|
|
343
|
+
if !options.get
|
|
344
|
+
item.doc.content = undefined }
|
|
345
|
+
output.rows.push(item) }
|
|
346
|
+
callback(false, output)
|
|
347
|
+
}
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// finis
|
|
352
|
+
|
|
353
|
+
list(false, table)
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
/*
|
|
358
|
+
* PouchDB
|
|
359
|
+
*
|
|
360
|
+
* column positioning: // // !
|
|
361
|
+
* Make a database access object for a PouchDB database at the specified urlname. The database is
|
|
362
|
+
* created if it does not already exist. The urlname can be a string like:
|
|
363
|
+
* urlname - URL (e.g. "http://domain.com/dbname") - remote access
|
|
364
|
+
* - filepath (e.g. "/Users/richardz/dbname") - NodeJS filesystem access
|
|
365
|
+
* - string (e.g. "dbname") - local browser access
|
|
366
|
+
* or it can be a dictionary containing:
|
|
367
|
+
* urlname - as above
|
|
368
|
+
* authname - the authentication name for a URL
|
|
369
|
+
* authpw - the autentication password, required if authname is provided
|
|
370
|
+
* Return values from the various methods are typically a two-element tuple of (error, data), where
|
|
371
|
+
* at most one of the two values is non-false.
|
|
372
|
+
*
|
|
373
|
+
*/
|
|
374
|
+
|
|
375
|
+
closure PouchDB(urlname, local database, options) {
|
|
376
|
+
options = {
|
|
377
|
+
revs_limit: 1 // number of revs to maintain for replication
|
|
378
|
+
auto_compaction: false } // don't compcat on every change
|
|
379
|
+
if !string(urlname) {
|
|
380
|
+
if urlname.authname
|
|
381
|
+
options.auth = {
|
|
382
|
+
username: urlname.authName
|
|
383
|
+
password: urlname.authPW
|
|
384
|
+
}
|
|
385
|
+
urlname = urlname.urlName
|
|
386
|
+
}
|
|
387
|
+
if !string(urlname) || urlname == ""
|
|
388
|
+
return (list(Error("invalid database url/path/name")))
|
|
389
|
+
database = new(object, this)
|
|
390
|
+
database.type = "PouchDB"
|
|
391
|
+
if RegExp("https?://").test(urlname)
|
|
392
|
+
database.name = urlname.match(RegExp("https?://[^?]*?([^/?]+)([?].*)*$")).1
|
|
393
|
+
else if urlname.indexOf("/") >= 0 {
|
|
394
|
+
options.prefix = jspath.dirname(urlname).concat("/")
|
|
395
|
+
database.name = jspath.basename(urlname) }
|
|
396
|
+
else
|
|
397
|
+
database.name = urlname
|
|
398
|
+
if !xobject(pouchdb)
|
|
399
|
+
return (list(Error("PouchDB module not found")))
|
|
400
|
+
database.pouch = xnew(pouchdb, database.name, options) // don't compact after every change
|
|
401
|
+
if !database.pouch
|
|
402
|
+
return (list(Error("could not access database", urlname)))
|
|
403
|
+
|
|
404
|
+
// isopen
|
|
405
|
+
//
|
|
406
|
+
// False iff the database is not open.
|
|
407
|
+
|
|
408
|
+
database.isopen = function isopen() {
|
|
409
|
+
!!database.pouch
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// close
|
|
413
|
+
//
|
|
414
|
+
// Close the database.
|
|
415
|
+
|
|
416
|
+
database.close = function close(local pending) {
|
|
417
|
+
if !database.pouch
|
|
418
|
+
return (list(Error("database closed")))
|
|
419
|
+
pending = new(nonce)
|
|
420
|
+
database.pouch.close(function(error) {
|
|
421
|
+
database.pouch = false
|
|
422
|
+
pending.signal(list(error, { ok: true }))
|
|
423
|
+
})
|
|
424
|
+
pending.wait()
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// info
|
|
428
|
+
//
|
|
429
|
+
// Return PouchDB-specif database info.
|
|
430
|
+
|
|
431
|
+
database.info = function info(local pending) {
|
|
432
|
+
if !database.pouch
|
|
433
|
+
return (list(Error("database closed")))
|
|
434
|
+
pending = new(nonce)
|
|
435
|
+
database.pouch.info(function(error, resp) {
|
|
436
|
+
resp = new(resp)
|
|
437
|
+
resp.type = database.type
|
|
438
|
+
pending.signal(list(error, resp))
|
|
439
|
+
})
|
|
440
|
+
pending.wait()
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// destroy
|
|
444
|
+
//
|
|
445
|
+
// Delete the database and close it.
|
|
446
|
+
|
|
447
|
+
database.destroy = function destroy(local pending) {
|
|
448
|
+
if !database.pouch
|
|
449
|
+
return (list(Error("database closed")))
|
|
450
|
+
pending = new(nonce)
|
|
451
|
+
database.pouch.destroy(function(error, resp) {
|
|
452
|
+
database.pouch = false
|
|
453
|
+
pending.signal(list(error, resp))
|
|
454
|
+
})
|
|
455
|
+
pending.wait()
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// table
|
|
459
|
+
//
|
|
460
|
+
// Return a table-access object for the table.
|
|
461
|
+
|
|
462
|
+
database.table = function table(path) {
|
|
463
|
+
if !database.pouch
|
|
464
|
+
return (list(Error("database closed")))
|
|
465
|
+
if !string(path)
|
|
466
|
+
return (list(Error("invalid table path")))
|
|
467
|
+
dapoTable(database, path)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// finis
|
|
471
|
+
|
|
472
|
+
list(false, database)
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
/*
|
|
477
|
+
* ListBrowserDBs
|
|
478
|
+
*
|
|
479
|
+
* List PouchDB instances in the browser.
|
|
480
|
+
*
|
|
481
|
+
*/
|
|
482
|
+
|
|
483
|
+
closure ListBrowserDBs(local pending, output) {
|
|
484
|
+
if !indexedDB
|
|
485
|
+
return (list(Error("browser database unavailable")))
|
|
486
|
+
if !indexedDB.databases
|
|
487
|
+
return (list(Error("API for indexedDB.databases unavailable")))
|
|
488
|
+
pending = new(nonce)
|
|
489
|
+
output = []
|
|
490
|
+
indexedDB.databases().then(function(success) {
|
|
491
|
+
for item in success
|
|
492
|
+
if item.name.startsWith("_pouch_")
|
|
493
|
+
output.push(item.name.substring(7))
|
|
494
|
+
pending.signal(list(false, output))
|
|
495
|
+
}, function(failure) {
|
|
496
|
+
pending.signal(list(Error("can't list databases", failure)))
|
|
497
|
+
})
|
|
498
|
+
pending.wait()
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
/*
|
|
503
|
+
* DbConnector
|
|
504
|
+
*
|
|
505
|
+
* Make a PSM connector for PouchDB database tables.
|
|
506
|
+
*
|
|
507
|
+
*/
|
|
508
|
+
|
|
509
|
+
closure DbConnector(psm, local connClassID, connector, watch) {
|
|
510
|
+
connClassID = "PouchDB"
|
|
511
|
+
connector = new(object, this)
|
|
512
|
+
connector.psm = psm
|
|
513
|
+
connector.vault = psm.vault(connClassID)
|
|
514
|
+
connector.label = "PouchDB"
|
|
515
|
+
watch = psm.watchable(connector)
|
|
516
|
+
|
|
517
|
+
// localDBcred
|
|
518
|
+
//
|
|
519
|
+
// True iff the specified credential dictionary is for a local browser DB.
|
|
520
|
+
function localDBcred(creds) {
|
|
521
|
+
!creds.authName && creds.urlName.indexOf("://") < 0
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// uifields
|
|
525
|
+
//
|
|
526
|
+
// Report the UI fields we need to add a resource
|
|
527
|
+
connector.uifields = function uifields() {
|
|
528
|
+
[{
|
|
529
|
+
label: "URL | Path | Name"
|
|
530
|
+
placeholder: "http://domain.com/dbname | /Host-path/dbname | dbname"
|
|
531
|
+
key: "urlName" },
|
|
532
|
+
{
|
|
533
|
+
label: "Auth Name"
|
|
534
|
+
placeholder: "account name"
|
|
535
|
+
key: "authName" },
|
|
536
|
+
{
|
|
537
|
+
label: "Auth Password"
|
|
538
|
+
placeholder: "account password"
|
|
539
|
+
key: "authPW"
|
|
540
|
+
type: "password" },
|
|
541
|
+
]
|
|
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
|
+
|
|
555
|
+
// findResource
|
|
556
|
+
//
|
|
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?
|
|
559
|
+
connector.findResource = function findResource(resource, local resID, creds) {
|
|
560
|
+
for resID in listResources().1 {
|
|
561
|
+
creds = access(resID).1
|
|
562
|
+
if resource.urlName == creds.urlName
|
|
563
|
+
&& (!resource.label || resource.label == creds.label)
|
|
564
|
+
&& resource.authName == creds.authName
|
|
565
|
+
return (resID)
|
|
566
|
+
}
|
|
567
|
+
false
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// addResource
|
|
571
|
+
//
|
|
572
|
+
// Add credentials for a named resource. The resource is a dictionary comprising these keys:
|
|
573
|
+
// label - [optional] label we use for this resource
|
|
574
|
+
// urlName - URL (e.g. "http://domain.com/dbname")
|
|
575
|
+
// - filepath (e.g. "/Users/richardz/dbname") - NodeJS filesystem access
|
|
576
|
+
// - string (e.g. "dbname") - local browser access
|
|
577
|
+
// authName - [optional] the authentication name for a URL
|
|
578
|
+
// authPW - [optional] the autentication password, required if authname is provided
|
|
579
|
+
// hidden - [optional] don't enumerate to user
|
|
580
|
+
// locked - [optional] don't allow user delete
|
|
581
|
+
// 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) {
|
|
585
|
+
function badField(str) { !string(str) || str.trim().length == 0 }
|
|
586
|
+
creds = { urlName: resource.urlName }
|
|
587
|
+
errors = { }
|
|
588
|
+
if resource.label
|
|
589
|
+
creds.label = resource.label
|
|
590
|
+
else
|
|
591
|
+
creds.label = resource.urlName
|
|
592
|
+
if badField(creds.label)
|
|
593
|
+
errors.label = "invalid label"
|
|
594
|
+
if badField(resource.urlName)
|
|
595
|
+
errors.urlName = "required field"
|
|
596
|
+
else if !resource.existing {
|
|
597
|
+
original = ListBrowserDBs().1
|
|
598
|
+
if original && original.indexOf(resource.urlName) >= 0
|
|
599
|
+
errors.urlName = "database already exists" // database exists
|
|
600
|
+
}
|
|
601
|
+
if !badField(resource.authName) {
|
|
602
|
+
creds.authName = resource.authName
|
|
603
|
+
creds.authPW = resource.authPW
|
|
604
|
+
if badField(resource.authPW)
|
|
605
|
+
errors.authPW = "required if Auth Name specified" }
|
|
606
|
+
if length(errors) > 0
|
|
607
|
+
error = errors
|
|
608
|
+
else if localDBcred(creds)
|
|
609
|
+
`(error, data) = PouchDB(creds) // create the database now
|
|
610
|
+
if !error {
|
|
611
|
+
if resource.hidden
|
|
612
|
+
creds.hidden = resource.hidden
|
|
613
|
+
if resource.locked
|
|
614
|
+
creds.locked = resource.locked
|
|
615
|
+
resID = hashResourceID(resource)
|
|
616
|
+
`(error, data) = connector.vault.addResource(resID, creds)
|
|
617
|
+
if data {
|
|
618
|
+
watch.notify(connClassID, { added: [resID] })
|
|
619
|
+
data = resID }
|
|
620
|
+
}
|
|
621
|
+
list(error, data)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// getResource
|
|
625
|
+
//
|
|
626
|
+
// Get credentials for a named resource, matching the dictionary semantics of addResource. The
|
|
627
|
+
// result is a standard result tuple.
|
|
628
|
+
connector.getResource = function getResource(resID, local error, creds, resource) {
|
|
629
|
+
`(error, creds) = connector.vault.accessResource(resID)
|
|
630
|
+
if error
|
|
631
|
+
return (list(error))
|
|
632
|
+
resource = {
|
|
633
|
+
classID: connClassID
|
|
634
|
+
label: creds.label
|
|
635
|
+
}
|
|
636
|
+
if string(creds.urlName)
|
|
637
|
+
resource.urlName = creds.urlName
|
|
638
|
+
if string(creds.authName)
|
|
639
|
+
resource.authName = creds.authName
|
|
640
|
+
if string(creds.authPW)
|
|
641
|
+
resource.authPW = creds.authPW
|
|
642
|
+
list(false, resource)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// deleteResource
|
|
646
|
+
//
|
|
647
|
+
// Delete credentials for a resource, which includes the database itself if local.
|
|
648
|
+
connector.deleteResource = function deleteResource(resID, local error, creds, db) {
|
|
649
|
+
`(error, creds) = access(resID)
|
|
650
|
+
if error || creds.locked
|
|
651
|
+
return (list(Error("database protected", resID)))
|
|
652
|
+
if !error && localDBcred(creds) {
|
|
653
|
+
if !js.w.confirm("Really delete PouchDB ".concat(creds.urlName, " permanently?"))
|
|
654
|
+
return (list(Error("delete canceled"))) // ### temporary until debugged
|
|
655
|
+
`(error, db) = PouchDB(creds)
|
|
656
|
+
if db
|
|
657
|
+
db.destroy()
|
|
658
|
+
}
|
|
659
|
+
connector.vault.deleteResource(resID)
|
|
660
|
+
watch.notify(connClassID, { deleted: [resID] })
|
|
661
|
+
list(false, { ok: true})
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// listResources
|
|
665
|
+
//
|
|
666
|
+
// List saved resources in our database
|
|
667
|
+
connector.listResources = function listResources(local error, output, original, resID, creds, ddex) {
|
|
668
|
+
`(error, output) = connector.vault.listResources()
|
|
669
|
+
if error
|
|
670
|
+
return (list(error)) // can't access our saved resources
|
|
671
|
+
original = ListBrowserDBs().1
|
|
672
|
+
if original {
|
|
673
|
+
for resID in output {
|
|
674
|
+
`(error, creds) = connector.vault.accessResource(resID)
|
|
675
|
+
if !error && localDBcred(creds) && (ddex = original.indexOf(creds.urlName)) >= 0
|
|
676
|
+
original.splice(ddex, 1) // remove local databases we have credential for
|
|
677
|
+
}
|
|
678
|
+
output = original.concat(output) // add unreferenced local DBs
|
|
679
|
+
}
|
|
680
|
+
list(false, output)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// info
|
|
684
|
+
//
|
|
685
|
+
// Return info for a resource.
|
|
686
|
+
connector.info = function info(resID, local info, error, creds) {
|
|
687
|
+
info = {
|
|
688
|
+
classID: connClassID
|
|
689
|
+
type: "PouchDB"
|
|
690
|
+
services: ["NideDB", "NideFS"]
|
|
691
|
+
}
|
|
692
|
+
`(error, creds) = connector.vault.accessResource(resID)
|
|
693
|
+
if creds {
|
|
694
|
+
if creds.label
|
|
695
|
+
info.name = creds.label
|
|
696
|
+
if localDBcred(creds)
|
|
697
|
+
info.type = "browser storage"
|
|
698
|
+
if creds.locked
|
|
699
|
+
info.locked = true
|
|
700
|
+
if creds.hidden
|
|
701
|
+
info.hidden = true
|
|
702
|
+
info.where = creds.urlName }
|
|
703
|
+
else {
|
|
704
|
+
info.where = "internal"
|
|
705
|
+
info.hidden = true
|
|
706
|
+
info.locked = true }
|
|
707
|
+
list(false, info)
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// access
|
|
711
|
+
//
|
|
712
|
+
// Return an PouchDB access object, which is the credentials to open a database.
|
|
713
|
+
connector.access = function access(resID, local error, creds) {
|
|
714
|
+
`(error, creds) = connector.vault.accessResource(resID)
|
|
715
|
+
if error && ListBrowserDBs().1.indexOf(resID) >= 0 {
|
|
716
|
+
error = false
|
|
717
|
+
creds = resID
|
|
718
|
+
}
|
|
719
|
+
list(error, creds)
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// connect
|
|
723
|
+
//
|
|
724
|
+
// Connect to a database, returning a PSM view.
|
|
725
|
+
connector.connect = function connect(resID, service, args, local error, creds, db, rootpath, table) {
|
|
726
|
+
`(error, creds) = connector.access(resID)
|
|
727
|
+
if !error
|
|
728
|
+
`(error, db) = PouchDB(creds)
|
|
729
|
+
if error
|
|
730
|
+
list(error)
|
|
731
|
+
else if service == "NideDB"
|
|
732
|
+
list(false, db)
|
|
733
|
+
else if service == "NideFS" {
|
|
734
|
+
rootpath = args.0 // connect to a NideFS filesystem
|
|
735
|
+
if !rootpath
|
|
736
|
+
return (Error("mandatory rootpath missing"))
|
|
737
|
+
`(error, table) = db.table(rootpath)
|
|
738
|
+
if error
|
|
739
|
+
list(error)
|
|
740
|
+
else
|
|
741
|
+
FSview(table, rootpath)
|
|
742
|
+
} else
|
|
743
|
+
list(Error("unsupported service", service))
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// finis
|
|
747
|
+
|
|
748
|
+
psm.register(connClassID, connector)
|
|
749
|
+
connector
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
/*
|
|
754
|
+
* dapoInit
|
|
755
|
+
*
|
|
756
|
+
* Initialize the Database module.
|
|
757
|
+
*
|
|
758
|
+
*/
|
|
759
|
+
|
|
760
|
+
function dapoInit(local manifest) {
|
|
761
|
+
|
|
762
|
+
manifest = `(dapoTable, PouchDB, ListBrowserDBs, DbConnector, dapoInit)
|
|
763
|
+
|
|
764
|
+
Naan.module.build(module.id, "dbt_pouch", function(modobj, compobj) {
|
|
765
|
+
require("./storage.nlg")
|
|
766
|
+
require("./psm_dbtables.nlg")
|
|
767
|
+
compobj.manifest = manifest
|
|
768
|
+
modobj.exports.PouchDB = PouchDB
|
|
769
|
+
modobj.exports.ListBrowserDBs = ListBrowserDBs
|
|
770
|
+
modobj.exports.DbConnector = DbConnector
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
} ();
|