1 |
|
2 | const vm = require('vm')
|
3 | const os = require('os')
|
4 | const path = require('path')
|
5 | const fs = require('fs')
|
6 | const ini = require('ini')
|
7 | const {cli} = require('cli-ux')
|
8 | const faunadb = require('faunadb')
|
9 | const escodegen = require('escodegen')
|
10 | const Errors = require('@oclif/errors')
|
11 | var rp = require('request-promise')
|
12 |
|
13 | const FAUNA_CLOUD_DOMAIN = 'db.fauna.com'
|
14 | const ERROR_NO_DEFAULT_ENDPOINT = "You need to set a default endpoint. \nTry running 'fauna default-endpoint ENDPOINT_ALIAS'."
|
15 | const ERROR_WRONG_CLOUD_ENDPOINT = "You already have an endpoint 'cloud' defined and it doesn't point to 'db.fauna.com'.\nPlease fix your '~/.fauna-shell' file."
|
16 | const ERROR_SPECIFY_SECRET_KEY = 'You must specify a secret key to connect to FaunaDB'
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 | function saveEndpointOrError(newEndpoint, alias, secret) {
|
27 | return loadEndpoints()
|
28 | .then(function (endpoints) {
|
29 | if (endpointExists(endpoints, alias)) {
|
30 | return confirmEndpointOverwrite(alias)
|
31 | .then(function (overwrite) {
|
32 | if (overwrite) {
|
33 | return saveEndpoint(endpoints, newEndpoint, alias, secret)
|
34 | } else {
|
35 | throw new Error('Try entering a different endpoint alias.')
|
36 | }
|
37 | })
|
38 | } else {
|
39 | return saveEndpoint(endpoints, newEndpoint, alias, secret)
|
40 | }
|
41 | })
|
42 | }
|
43 |
|
44 | function deleteEndpointOrError(alias) {
|
45 | return loadEndpoints()
|
46 | .then(function (endpoints) {
|
47 | if (endpointExists(endpoints, alias)) {
|
48 | return confirmEndpointDelete(alias)
|
49 | .then(function (del) {
|
50 | if (del) {
|
51 | return deleteEndpoint(endpoints, alias)
|
52 | } else {
|
53 | throw new Error("Couldn't override endpoint")
|
54 | }
|
55 | })
|
56 | } else {
|
57 | throw new Error(`The endpoint '${alias}' doesn't exist`)
|
58 | }
|
59 | })
|
60 | .catch(function (err) {
|
61 | errorOut(err, 1)
|
62 | })
|
63 | }
|
64 |
|
65 |
|
66 |
|
67 |
|
68 | function validCloudEndpoint() {
|
69 | return loadEndpoints().then(function (config) {
|
70 | return new Promise(function (resolve, reject) {
|
71 | if (config.cloud && config.cloud.domain !== FAUNA_CLOUD_DOMAIN) {
|
72 | reject(new Error(ERROR_WRONG_CLOUD_ENDPOINT))
|
73 | } else {
|
74 | resolve(true)
|
75 | }
|
76 | })
|
77 | })
|
78 | }
|
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 | function setDefaultEndpoint(endpoint) {
|
85 | return loadEndpoints().then(function (endpoints) {
|
86 | return new Promise(function (resolve, reject) {
|
87 | if (endpoints[endpoint]) {
|
88 | endpoints.default = endpoint
|
89 | return saveConfig(endpoints)
|
90 | .then(function (_) {
|
91 | resolve(`Endpoint '${endpoint}' set as default endpoint.`)
|
92 | })
|
93 | .catch(function (err) {
|
94 | reject(err)
|
95 | })
|
96 | } else {
|
97 | reject(new Error(`Endpoint '${endpoint}' doesn't exist.`))
|
98 | }
|
99 | })
|
100 | })
|
101 | }
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 | function loadEndpoints() {
|
108 | return readFile(getConfigFile())
|
109 | .then(function (configData) {
|
110 | return ini.parse(configData)
|
111 | })
|
112 | .catch(function (err) {
|
113 | if (fileNotFound(err)) {
|
114 | return {}
|
115 | }
|
116 | throw err
|
117 | })
|
118 | }
|
119 |
|
120 | function endpointExists(endpoints, endpointAlias) {
|
121 | return endpointAlias in endpoints
|
122 | }
|
123 |
|
124 | function confirmEndpointOverwrite(alias) {
|
125 | return cli.confirm(`The '${alias}' endpoint already exists. Overwrite? [y/n]`)
|
126 | }
|
127 |
|
128 | function confirmEndpointDelete(alias) {
|
129 | return cli.confirm(`Are you sure you want to delete the '${alias}' endpoint? [y/n]`)
|
130 | }
|
131 |
|
132 | function saveEndpoint(config, endpoint, alias, secret) {
|
133 | var port = endpoint.port ? `:${endpoint.port}` : ''
|
134 | var uri = `${endpoint.protocol}//${endpoint.host}${port}`
|
135 | var options = {
|
136 | method: 'HEAD',
|
137 | uri: uri,
|
138 | resolveWithFullResponse: true,
|
139 | }
|
140 | return rp(options)
|
141 | .then(function (res) {
|
142 | if ('x-faunadb-build' in res.headers) {
|
143 | return saveConfig(addEndpoint(config, endpoint, alias, secret))
|
144 | } else {
|
145 | throw new Error(`'${alias}' is not a FaunaDB endopoint`)
|
146 | }
|
147 | })
|
148 | .catch(function (err) {
|
149 |
|
150 | if (err.response !== undefined) {
|
151 | if ('x-faunadb-build' in err.response.headers) {
|
152 | return saveConfig(addEndpoint(config, endpoint, alias, secret))
|
153 | } else {
|
154 | throw new Error(`'${alias}' is not a FaunaDB endopoint`)
|
155 | }
|
156 | } else {
|
157 | throw err
|
158 | }
|
159 | })
|
160 | }
|
161 |
|
162 | function addEndpoint(config, endpoint, alias, secret) {
|
163 | if (shouldSetAsDefaultEndpoint(config)) {
|
164 | config.default = alias
|
165 | }
|
166 | config[alias] = buildEndpointObject(endpoint, secret)
|
167 | return config
|
168 | }
|
169 |
|
170 | function deleteEndpoint(endpoints, alias) {
|
171 | if (endpoints.default === alias) {
|
172 | delete endpoints.default
|
173 | console.log(`Endpoint '${alias}' deleted. '${alias}' was the default endpoint.`)
|
174 | console.log(ERROR_NO_DEFAULT_ENDPOINT)
|
175 | }
|
176 | delete endpoints[alias]
|
177 | return saveConfig(endpoints)
|
178 | }
|
179 |
|
180 | function shouldSetAsDefaultEndpoint(config) {
|
181 | return 'default' in config === false
|
182 | }
|
183 |
|
184 | function buildEndpointObject(endpoint, secret) {
|
185 | var domain = endpoint.hostname
|
186 | var port = endpoint.port
|
187 | var scheme = endpoint.protocol.slice(0, -1)
|
188 |
|
189 | domain = domain === null ? null : {domain}
|
190 | port = port === null ? null : {port}
|
191 | scheme = scheme === null ? null : {scheme}
|
192 | return Object.assign({}, domain, port, scheme, {secret})
|
193 | }
|
194 |
|
195 |
|
196 |
|
197 |
|
198 |
|
199 | function saveConfig(config) {
|
200 | return writeFile(getConfigFile(), ini.stringify(config), 0o700)
|
201 | }
|
202 |
|
203 |
|
204 |
|
205 |
|
206 | function getConfigFile() {
|
207 | return path.join(os.homedir(), '.fauna-shell')
|
208 | }
|
209 |
|
210 |
|
211 |
|
212 |
|
213 | function readFile(fileName) {
|
214 | return new Promise(function (resolve, reject) {
|
215 | fs.readFile(fileName, 'utf8', (err, data) => {
|
216 | err ? reject(err) : resolve(data)
|
217 | })
|
218 | })
|
219 | }
|
220 |
|
221 |
|
222 |
|
223 |
|
224 | function writeFile(fileName, data, mode) {
|
225 | return new Promise(function (resolve, reject) {
|
226 | fs.writeFile(fileName, data, {mode: mode}, err => {
|
227 | err ? reject(err) : resolve(data)
|
228 | })
|
229 | })
|
230 | }
|
231 |
|
232 |
|
233 |
|
234 |
|
235 | function fileNotFound(err) {
|
236 | return err.code === 'ENOENT' && err.syscall === 'open'
|
237 | }
|
238 |
|
239 |
|
240 |
|
241 |
|
242 | function errorOut(msg, code) {
|
243 | code = code || 1
|
244 | console.error(`Error: ${msg}`)
|
245 | process.exit(code)
|
246 |
|
247 |
|
248 | }
|
249 |
|
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 |
|
256 |
|
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
262 |
|
263 |
|
264 |
|
265 |
|
266 |
|
267 |
|
268 |
|
269 |
|
270 |
|
271 |
|
272 | function buildConnectionOptions(cmdFlags, dbScope, role) {
|
273 | return new Promise(function (resolve, reject) {
|
274 | readFile(getConfigFile())
|
275 | .then(function (configData) {
|
276 | var endpoint = {}
|
277 | const config = ini.parse(configData)
|
278 |
|
279 | if (hasValidEndpoint(config, cmdFlags.endpoint)) {
|
280 | endpoint = getEndpoint(config, cmdFlags.endpoint)
|
281 | } else if (!cmdFlags.hasOwnProperty('secret')) {
|
282 | reject(ERROR_NO_DEFAULT_ENDPOINT)
|
283 | }
|
284 |
|
285 | const connectionOptions = Object.assign(endpoint, cmdFlags)
|
286 |
|
287 | if (connectionOptions.secret) {
|
288 | resolve(cleanUpConnectionOptions(maybeScopeKey(connectionOptions, dbScope, role)))
|
289 | } else {
|
290 | reject(ERROR_SPECIFY_SECRET_KEY)
|
291 | }
|
292 | })
|
293 | .catch(function (err) {
|
294 | if (fileNotFound(err)) {
|
295 | if (cmdFlags.secret) {
|
296 | resolve(cleanUpConnectionOptions(maybeScopeKey(cmdFlags, dbScope, role)))
|
297 | } else {
|
298 | reject(ERROR_SPECIFY_SECRET_KEY)
|
299 | }
|
300 | } else {
|
301 | reject(err)
|
302 | }
|
303 | })
|
304 | })
|
305 | }
|
306 |
|
307 | function getEndpoint(config, cmdEndpoint) {
|
308 | const alias = cmdEndpoint ? cmdEndpoint : config.default
|
309 | return config[alias]
|
310 | }
|
311 |
|
312 | function hasValidEndpoint(config, cmdEndpoint) {
|
313 | if (cmdEndpoint) {
|
314 | return config.hasOwnProperty(cmdEndpoint)
|
315 | } else {
|
316 | return config.hasOwnProperty('default') && config.hasOwnProperty(config.default)
|
317 | }
|
318 | }
|
319 |
|
320 |
|
321 |
|
322 |
|
323 |
|
324 | function cleanUpConnectionOptions(connectionOptions) {
|
325 | const accepted = ['domain', 'scheme', 'port', 'secret', 'timeout']
|
326 | const res = {}
|
327 | accepted.forEach(function (key) {
|
328 | if (connectionOptions[key]) {
|
329 | res[key] = connectionOptions[key]
|
330 | }
|
331 | })
|
332 | return res
|
333 | }
|
334 |
|
335 |
|
336 |
|
337 |
|
338 |
|
339 | function maybeScopeKey(config, dbScope, role) {
|
340 | var scopedSecret = config.secret
|
341 | if (dbScope !== undefined && role !== undefined) {
|
342 | scopedSecret = config.secret + ':' + dbScope + ':' + role
|
343 | }
|
344 | return Object.assign(config, {secret: scopedSecret})
|
345 | }
|
346 |
|
347 |
|
348 | function promiseSerial(fs) {
|
349 | return fs.reduce(function (promise, f) {
|
350 | return promise.then(function (result) {
|
351 | return f().then(Array.prototype.concat.bind(result))
|
352 | })
|
353 | }, Promise.resolve([]))
|
354 | }
|
355 |
|
356 | class QueryError extends Error {
|
357 | constructor(exp, faunaError, queryNumber, ...params) {
|
358 | super(params)
|
359 |
|
360 | if (Error.captureStackTrace) {
|
361 | Error.captureStackTrace(this, QueryError)
|
362 | }
|
363 |
|
364 | this.exp = exp
|
365 | this.faunaError = faunaError
|
366 | this.queryNumber = queryNumber
|
367 | }
|
368 | }
|
369 |
|
370 | function wrapQueries(expressions, client) {
|
371 | const q = faunadb.query
|
372 | vm.createContext(q)
|
373 | return expressions.map(function (exp, queryNumber) {
|
374 | return function () {
|
375 | return client.query(vm.runInContext(escodegen.generate(exp), q))
|
376 | .catch(function (err) {
|
377 | throw new QueryError(escodegen.generate(exp), err, queryNumber + 1)
|
378 | })
|
379 | }
|
380 | })
|
381 | }
|
382 |
|
383 | function runQueries(expressions, client) {
|
384 | if (expressions.length == 1) {
|
385 | var f = wrapQueries(expressions, client)[0]
|
386 | return f()
|
387 | } else {
|
388 | return promiseSerial(wrapQueries(expressions, client))
|
389 | }
|
390 | }
|
391 |
|
392 | module.exports = {
|
393 | saveEndpointOrError: saveEndpointOrError,
|
394 | deleteEndpointOrError: deleteEndpointOrError,
|
395 | setDefaultEndpoint: setDefaultEndpoint,
|
396 | validCloudEndpoint: validCloudEndpoint,
|
397 | loadEndpoints: loadEndpoints,
|
398 | buildConnectionOptions: buildConnectionOptions,
|
399 | errorOut: errorOut,
|
400 | readFile: readFile,
|
401 | writeFile: writeFile,
|
402 | runQueries: runQueries,
|
403 | }
|