blob: d73e6344fceeb3f91c90ccd0a6013f55a2e344d5 [file] [log] [blame]
// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
'use strict'
const u = require('url')
const assert = require('assert')
const querystring = require('querystring')
const request = require('request')
const errs = require('errs')
const follow = require('cloudant-follow')
const logger = require('./logger')
function isEmpty (val) {
return val == null || !(Object.keys(val) || val).length
}
function getCallback (opts, callback) {
if (typeof opts === 'function') {
callback = opts
opts = {}
}
opts = opts || {}
return {
opts,
callback
}
}
module.exports = exports = function dbScope (cfg) {
let serverScope = {}
if (typeof cfg === 'string') {
cfg = { url: cfg }
}
assert.strictEqual(typeof cfg, 'object',
'You must specify the endpoint url when invoking this module')
assert.ok(/^https?:/.test(cfg.url), 'url is not valid')
cfg = Object.assign({}, cfg)
serverScope.config = cfg
cfg.requestDefaults = cfg.requestDefaults || {}
const httpAgent = (typeof cfg.request === 'function') ? cfg.request
: request.defaults(cfg.requestDefaults)
const followAgent = (typeof cfg.follow === 'function') ? cfg.follow : follow
const log = typeof cfg.log === 'function' ? cfg.log : logger(cfg)
const parseUrl = 'parseUrl' in cfg ? cfg.parseUrl : true
function maybeExtractDatabaseComponent () {
if (!parseUrl) {
return
}
const path = u.parse(cfg.url)
let pathArray = path.pathname.split('/').filter(function (e) { return e })
const db = pathArray.pop()
const rootPath = path.pathname.replace(/\/?$/, '/..')
if (db) {
cfg.url = urlResolveFix(cfg.url, rootPath).replace(/\/?$/, '')
return db
}
}
function scrub (str) {
if (str) {
str = str.replace(/\/\/(.*)@/, '//XXXXXX:XXXXXX@')
}
return str
}
const responseHandler = function (req, opts, resolve, reject, callback) {
return function (err, response = { statusCode: 500 }, body = '') {
let parsed
const responseHeaders = Object.assign({
uri: req.uri,
statusCode: response.statusCode
}, response.headers)
if (err) {
log({ err: 'socket', body: body, headers: responseHeaders })
const returnError = errs.merge(err, {
message: 'error happened in your connection',
scope: 'socket',
errid: 'request'
})
if (reject) {
reject(returnError)
}
if (callback) {
callback(returnError)
}
return
}
delete responseHeaders.server
delete responseHeaders['content-length']
if (opts.dontParse) {
parsed = body
} else {
try { parsed = JSON.parse(body) } catch (err) { parsed = body }
}
if (responseHeaders.statusCode >= 200 && responseHeaders.statusCode < 400) {
log({ err: null, body: parsed, headers: responseHeaders })
if (resolve) {
resolve(parsed)
}
if (callback) {
callback(null, parsed, responseHeaders)
}
return
}
log({ err: 'couch', body: parsed, headers: responseHeaders })
// cloudant stacktrace
if (typeof parsed === 'string') {
parsed = { message: parsed }
}
if (!parsed.message && (parsed.reason || parsed.error)) {
parsed.message = (parsed.reason || parsed.error)
}
// fix cloudant issues where they give an erlang stacktrace as js
delete parsed.stack
// scrub credentials
req.uri = scrub(req.uri)
responseHeaders.uri = scrub(responseHeaders.uri)
if (req.headers.cookie) {
req.headers.cookie = 'XXXXXXX'
}
let errors = errs.merge({
message: 'couch returned ' + responseHeaders.statusCode,
scope: 'couch',
statusCode: responseHeaders.statusCode,
request: req,
headers: responseHeaders,
errid: 'non_200'
}, errs.create(parsed))
if (reject) {
reject(errors)
}
if (callback) {
callback(errors)
}
}
}
function relax (opts, callback) {
if (typeof opts === 'function') {
callback = opts
opts = { path: '' }
}
if (typeof opts === 'string') {
opts = { path: opts }
}
if (!opts) {
opts = { path: '' }
callback = null
}
const qs = Object.assign({}, opts.qs)
const headers = {
'content-type': 'application/json',
accept: 'application/json'
}
const req = {
method: (opts.method || 'GET'),
headers: headers,
uri: cfg.url
}
// https://github.com/mikeal/request#requestjar
const isJar = opts.jar || cfg.jar
if (isJar) {
req.jar = isJar
}
// http://wiki.apache.org/couchdb/HTTP_database_API#Naming_and_Addressing
if (opts.db) {
req.uri = urlResolveFix(req.uri, encodeURIComponent(opts.db))
}
if (opts.multipart) {
req.multipart = opts.multipart
}
req.headers = Object.assign(req.headers, opts.headers, cfg.defaultHeaders)
if (opts.path) {
req.uri += '/' + opts.path
} else if (opts.doc) {
if (!/^_design/.test(opts.doc)) {
// http://wiki.apache.org/couchdb/HTTP_Document_API#Naming.2FAddressing
req.uri += '/' + encodeURIComponent(opts.doc)
} else {
// http://wiki.apache.org/couchdb/HTTP_Document_API#Document_IDs
req.uri += '/' + opts.doc
}
// http://wiki.apache.org/couchdb/HTTP_Document_API#Attachments
if (opts.att) {
req.uri += '/' + opts.att
}
}
// prevent bugs where people set encoding when piping
if (opts.encoding !== undefined) {
req.encoding = opts.encoding
delete req.headers['content-type']
delete req.headers.accept
}
if (opts.contentType) {
req.headers['content-type'] = opts.contentType
delete req.headers.accept
}
if (opts.accept) {
req.headers['accept'] = opts.accept
}
// http://guide.couchdb.org/draft/security.html#cookies
if (cfg.cookie) {
req.headers['X-CouchDB-WWW-Authenticate'] = 'Cookie'
req.headers.cookie = cfg.cookie
}
// http://wiki.apache.org/couchdb/HTTP_view_API#Querying_Options
if (typeof opts.qs === 'object' && !isEmpty(opts.qs)) {
['startkey', 'endkey', 'key', 'keys', 'start_key', 'end_key'].forEach(function (key) {
if (key in opts.qs) {
qs[key] = JSON.stringify(opts.qs[key])
}
})
req.qs = qs
}
if (opts.body) {
if (Buffer.isBuffer(opts.body) || opts.dontStringify) {
req.body = opts.body
} else {
req.body = JSON.stringify(opts.body, function (key, value) {
// don't encode functions
if (typeof (value) === 'function') {
return value.toString()
} else {
return value
}
})
}
}
if (opts.form) {
req.headers['content-type'] =
'application/x-www-form-urlencoded; charset=utf-8'
req.body = querystring.stringify(opts.form).toString('utf8')
}
// ask request to render query string arrays as repeated values e.g.
// ?drilldown=["author","Dickens"]&drilldown=["publisher","Penguin"]
req.qsStringifyOptions = { arrayFormat: 'repeat' }
log(req)
if (opts.stream) {
// return the Request object for streaming
return httpAgent(req)
} else {
if (typeof callback === 'function') {
// return nothing - feedback via the callback function
httpAgent(req, responseHandler(req, opts, null, null, callback))
} else {
// return a Promise
return new Promise(function (resolve, reject) {
httpAgent(req, responseHandler(req, opts, resolve, reject))
})
}
}
}
// http://docs.couchdb.org/en/latest/api/server/authn.html#cookie-authentication
function auth (username, password, callback) {
return relax({
method: 'POST',
db: '_session',
form: {
name: username,
password: password
}
}, callback)
}
// http://docs.couchdb.org/en/latest/api/server/authn.html#post--_session
function session (callback) {
return relax({ db: '_session' }, callback)
}
// http://docs.couchdb.org/en/latest/api/server/common.html#get--_db_updates
function updates (qs0, callback0) {
const { opts, callback } = getCallback(qs0, callback0)
return relax({
db: '_db_updates',
qs: opts
}, callback)
}
function followUpdates (qs, callback) {
return followDb('_db_updates', qs, callback)
}
// http://docs.couchdb.org/en/latest/api/database/common.html#put--db
function createDb (dbName, qs0, callback0) {
const { opts, callback } = getCallback(qs0, callback0)
return relax({ db: dbName, method: 'PUT', qs: opts }, callback)
}
// http://docs.couchdb.org/en/latest/api/database/common.html#delete--db
function destroyDb (dbName, callback) {
return relax({ db: dbName, method: 'DELETE' }, callback)
}
// http://docs.couchdb.org/en/latest/api/database/common.html#get--db
function getDb (dbName, callback) {
return relax({ db: dbName }, callback)
}
// http://docs.couchdb.org/en/latest/api/server/common.html#get--_all_dbs
function listDbs (callback) {
return relax({ db: '_all_dbs' }, callback)
}
// http://docs.couchdb.org/en/latest/api/server/common.html#get--_all_dbs
function listDbsAsStream () {
return relax({ db: '_all_dbs', stream: true })
}
// http://docs.couchdb.org/en/latest/api/database/compact.html#post--db-_compact
function compactDb (dbName, ddoc, callback) {
if (typeof ddoc === 'function') {
callback = ddoc
ddoc = null
}
return relax({
db: dbName,
doc: '_compact',
att: ddoc,
method: 'POST'
}, callback)
}
// http://docs.couchdb.org/en/latest/api/database/changes.html#get--db-_changes
function changesDb (dbName, qs0, callback0) {
const { opts, callback } = getCallback(qs0, callback0)
return relax({ db: dbName, path: '_changes', qs: opts }, callback)
}
function changesDbAsStream (dbName, opts) {
return relax({ db: dbName, path: '_changes', stream: true, qs: opts })
}
function followDb (dbName, qs0, callback0) {
const { opts, callback } = getCallback(qs0, callback0)
opts.db = urlResolveFix(cfg.url, encodeURIComponent(dbName))
opts.httpAgent = httpAgent
if (typeof callback === 'function') {
return followAgent(opts, callback)
} else {
return new followAgent.Feed(opts)
}
}
function _serializeAsUrl (db) {
if (typeof db === 'object' && db.config && db.config.url && db.config.db) {
return urlResolveFix(db.config.url, encodeURIComponent(db.config.db))
} else {
const parsed = u.parse(db)
if (parsed.protocol) {
return db
} else {
return urlResolveFix(cfg.url, encodeURIComponent(db))
}
}
}
// http://docs.couchdb.org/en/latest/api/server/common.html#post--_replicate
function replicateDb (source, target, opts0, callback0) {
const { opts, callback } = getCallback(opts0, callback0)
// _replicate
opts.source = _serializeAsUrl(source)
opts.target = _serializeAsUrl(target)
return relax({ db: '_replicate', body: opts, method: 'POST' }, callback)
}
// http://docs.couchdb.org/en/latest/api/server/common.html#uuids
function uuids (count, callback) {
if (typeof count === 'function') {
callback = count
count = 1
}
return relax({ method: 'GET', path: '_uuids', qs: { count: count } }, callback)
}
// http://guide.couchdb.org/draft/replication.html
function enableReplication (source, target, opts0, callback0) {
const { opts, callback } = getCallback(opts0, callback0)
// _replicator
opts.source = _serializeAsUrl(source)
opts.target = _serializeAsUrl(target)
return relax({ db: '_replicator', body: opts, method: 'POST' }, callback)
}
// http://guide.couchdb.org/draft/replication.html
function queryReplication (id, opts0, callback0) {
const { opts, callback } = getCallback(opts0, callback0)
return relax({ db: '_replicator', method: 'GET', path: id, qs: opts }, callback)
}
// http://guide.couchdb.org/draft/replication.html
function disableReplication (id, rev, opts0, callback0) {
const { opts, callback } = getCallback(opts0, callback0)
const req = {
db: '_replicator',
method: 'DELETE',
path: id,
qs: Object.assign(opts, { rev: rev })
}
return relax(req, callback)
}
function docModule (dbName) {
let docScope = {}
dbName = decodeURIComponent(dbName)
// http://docs.couchdb.org/en/latest/api/document/common.html#put--db-docid
// http://docs.couchdb.org/en/latest/api/database/common.html#post--db
function insertDoc (doc, qs0, callback0) {
const req = { db: dbName, body: doc, method: 'POST' }
let { opts, callback } = getCallback(qs0, callback0)
if (typeof opts === 'string') {
opts = { docName: opts }
}
if (opts) {
if (opts.docName) {
req.doc = opts.docName
req.method = 'PUT'
delete opts.docName
}
req.qs = opts
}
return relax(req, callback)
}
// http://docs.couchdb.org/en/latest/api/document/common.html#delete--db-docid
function destroyDoc (docName, rev, callback) {
if (!docName) {
const msg = 'Invalid doc id'
if (callback) {
callback(msg, null)
} else {
return Promise.reject(msg)
}
} else {
return relax({
db: dbName,
doc: docName,
method: 'DELETE',
qs: { rev: rev }
}, callback)
}
}
// http://docs.couchdb.org/en/latest/api/document/common.html#get--db-docid
function getDoc (docName, qs0, callback0) {
const { opts, callback } = getCallback(qs0, callback0)
if (!docName) {
if (callback) { callback(new Error('Invalid doc id'), null) }
} else {
return relax({ db: dbName, doc: docName, qs: opts }, callback)
}
}
// http://docs.couchdb.org/en/latest/api/document/common.html#head--db-docid
function headDoc (docName, callback) {
if (callback) {
relax({
db: dbName,
doc: docName,
method: 'HEAD',
qs: {}
}, callback)
} else {
// this function doesn't pass on the Promise from relax because it needs
// to return the headers when resolving the Promise
return new Promise(function (resolve, reject) {
relax({
db: dbName,
doc: docName,
method: 'HEAD',
qs: {}
}, function (err, body, headers) {
if (err) {
reject(err)
} else {
resolve(headers)
}
})
})
}
}
// http://docs.couchdb.org/en/latest/api/document/common.html#copy--db-docid
function copyDoc (docSrc, docDest, opts0, callback0) {
const { opts, callback } = getCallback(opts0, callback0)
const qs = {
db: dbName,
doc: docSrc,
method: 'COPY',
headers: { 'Destination': docDest }
}
if (opts.overwrite) {
const p = headDoc(docDest).then(
function (h) {
if (h.etag) {
qs.headers.Destination += '?rev=' +
h.etag.substring(1, h.etag.length - 1)
}
relax(qs, callback)
},
function (e) {
if (e && e.statusCode !== 404) {
if (callback) {
callback(e)
} else {
return Promise.reject(e)
}
} else {
relax(qs, callback)
}
}
)
if (!callback) {
return p
}
} else {
return relax(qs, callback)
}
}
// http://docs.couchdb.org/en/latest/api/database/bulk-api.html#get--db-_all_docs
function listDoc (qs0, callback0) {
const { opts, callback } = getCallback(qs0, callback0)
return relax({ db: dbName, path: '_all_docs', qs: opts }, callback)
}
// http://docs.couchdb.org/en/latest/api/database/bulk-api.html#get--db-_all_docs
function listDocAsStream (opts) {
return relax({ db: dbName, path: '_all_docs', qs: opts, stream: true })
}
// http://docs.couchdb.org/en/latest/api/database/bulk-api.html#post--db-_all_docs
function fetchDocs (docNames, qs0, callback0) {
const { opts, callback } = getCallback(qs0, callback0)
opts['include_docs'] = true
return relax({
db: dbName,
path: '_all_docs',
method: 'POST',
qs: opts,
body: docNames
}, callback)
}
function fetchRevs (docNames, qs0, callback0) {
const { opts, callback } = getCallback(qs0, callback0)
return relax({
db: dbName,
path: '_all_docs',
method: 'POST',
qs: opts,
body: docNames
}, callback)
}
function view (ddoc, viewName, meta, qs0, callback0) {
const { opts, callback } = getCallback(qs0, callback0)
if (typeof meta.stream !== 'boolean') {
meta.stream = false
}
// prevent mutation of the client qs object by using a clone
const qs1 = Object.assign({}, opts)
const viewPath = meta.viewPath || '_design/' + ddoc + '/_' + meta.type +
'/' + viewName
if (meta.type === 'search') {
return relax({
db: dbName,
path: viewPath,
method: 'POST',
body: qs1,
stream: meta.stream
}, callback)
} else if (qs1 && qs1.keys) {
const body = { keys: qs1.keys }
delete qs1.keys
return relax({
db: dbName,
path: viewPath,
method: 'POST',
qs: qs1,
body: body,
stream: meta.stream
}, callback)
} else if (qs1 && qs1.queries) {
const body = { queries: qs1.queries }
delete qs1.queries
return relax({
db: dbName,
path: viewPath,
method: 'POST',
qs: qs1,
body: body
}, callback)
} else {
const req = {
db: dbName,
method: meta.method || 'GET',
path: viewPath,
qs: qs1,
stream: meta.stream
}
if (meta.body) {
req.body = meta.body
}
return relax(req, callback)
}
}
// http://docs.couchdb.org/en/latest/api/ddoc/views.html#post--db-_design-ddoc-_view-view
function viewDocs (ddoc, viewName, qs, callback) {
return view(ddoc, viewName, { type: 'view' }, qs, callback)
}
// http://docs.couchdb.org/en/latest/api/ddoc/views.html#post--db-_design-ddoc-_view-view
function viewDocsAsStream (ddoc, viewName, qs) {
return view(ddoc, viewName, { type: 'view', stream: true }, qs)
}
// cloudant
function viewSearch (ddoc, viewName, qs, callback) {
return view(ddoc, viewName, { type: 'search' }, qs, callback)
}
// cloudant
function viewSearchAsStream (ddoc, viewName, qs) {
return view(ddoc, viewName, { type: 'search', stream: true }, qs)
}
// http://docs.couchdb.org/en/latest/api/ddoc/render.html#get--db-_design-ddoc-_show-func
function showDoc (ddoc, viewName, docName, qs, callback) {
return view(ddoc, viewName + '/' + docName, { type: 'show' }, qs, callback)
}
// http://docs.couchdb.org/en/latest/api/ddoc/render.html#put--db-_design-ddoc-_update-func-docid
function updateWithHandler (ddoc, viewName, docName, body, callback) {
if (typeof body === 'function') {
callback = body
body = undefined
}
return view(ddoc, viewName + '/' + encodeURIComponent(docName), {
type: 'update',
method: 'PUT',
body: body
}, callback)
}
function viewWithList (ddoc, viewName, listName, qs, callback) {
return view(ddoc, listName + '/' + viewName, {
type: 'list'
}, qs, callback)
}
// http://docs.couchdb.org/en/latest/api/database/bulk-api.html#post--db-_bulksDoc
function bulksDoc (docs, qs0, callback0) {
const { opts, callback } = getCallback(qs0, callback0)
return relax({
db: dbName,
path: '_bulk_docs',
body: docs,
method: 'POST',
qs: opts
}, callback)
}
// http://docs.couchdb.org/en/latest/api/document/common.html#creating-multiple-attachments
function insertMultipart (doc, attachments, qs, callback) {
if (typeof qs === 'string') {
qs = { docName: qs }
}
qs = qs || {}
const docName = qs.docName
delete qs.docName
doc = Object.assign({ _attachments: {} }, doc)
const multipart = []
attachments.forEach(function (att) {
doc._attachments[att.name] = {
follows: true,
length: Buffer.isBuffer(att.data)
? att.data.length : Buffer.byteLength(att.data),
/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
'content_type': att.content_type
}
multipart.push({ body: att.data })
})
multipart.unshift({
'content-type': 'application/json',
body: JSON.stringify(doc)
})
return relax({
db: dbName,
method: 'PUT',
contentType: 'multipart/related',
doc: docName,
qs: qs,
multipart: multipart
}, callback)
}
function getMultipart (docName, qs0, callback0) {
const { opts, callback } = getCallback(qs0, callback0)
opts.attachments = true
return relax({
db: dbName,
doc: docName,
encoding: null,
accept: 'multipart/related',
qs: opts
}, callback)
}
function insertAtt (docName, attName, att, contentType, qs0, callback0) {
const { opts, callback } = getCallback(qs0, callback0)
return relax({
db: dbName,
att: attName,
method: 'PUT',
contentType: contentType,
doc: docName,
qs: opts,
body: att,
dontStringify: true
}, callback)
}
function insertAttAsStream (docName, attName, att, contentType, opts) {
return relax({
db: dbName,
att: attName,
method: 'PUT',
contentType: contentType,
doc: docName,
qs: opts,
body: att,
stream: true,
dontStringify: true
})
}
function getAtt (docName, attName, qs0, callback0) {
const { opts, callback } = getCallback(qs0, callback0)
return relax({
db: dbName,
att: attName,
doc: docName,
qs: opts,
encoding: null,
dontParse: true
}, callback)
}
function getAttAsStream (docName, attName, opts) {
return relax({
db: dbName,
att: attName,
doc: docName,
qs: opts,
stream: true,
encoding: null,
dontParse: true
})
}
function destroyAtt (docName, attName, qs, callback) {
return relax({
db: dbName,
att: attName,
method: 'DELETE',
doc: docName,
qs: qs
}, callback)
}
function find (selector, callback) {
return relax({
db: dbName,
path: '_find',
method: 'POST',
body: selector
}, callback)
}
function findAsStream (selector) {
return relax({
db: dbName,
path: '_find',
method: 'POST',
body: selector,
stream: true
})
}
function createIndex (indexDef, callback) {
return relax({
db: dbName,
path: '_index',
method: 'POST',
body: indexDef
}, callback)
}
// db level exports
docScope = {
info: function (cb) {
return getDb(dbName, cb)
},
replicate: function (target, opts, cb) {
return replicateDb(dbName, target, opts, cb)
},
compact: function (cb) {
return compactDb(dbName, cb)
},
changes: function (qs, cb) {
return changesDb(dbName, qs, cb)
},
changesAsStream: function (qs) {
return changesDbAsStream(dbName, qs)
},
follow: function (qs, cb) {
return followDb(dbName, qs, cb)
},
auth: auth,
session: session,
insert: insertDoc,
get: getDoc,
head: headDoc,
copy: copyDoc,
destroy: destroyDoc,
bulk: bulksDoc,
list: listDoc,
listAsStream: listDocAsStream,
fetch: fetchDocs,
fetchRevs: fetchRevs,
config: { url: cfg.url, db: dbName },
multipart: {
insert: insertMultipart,
get: getMultipart
},
attachment: {
insert: insertAtt,
insertAsStream: insertAttAsStream,
get: getAtt,
getAsStream: getAttAsStream,
destroy: destroyAtt
},
show: showDoc,
atomic: updateWithHandler,
updateWithHandler: updateWithHandler,
baseView: view,
search: viewSearch,
searchAsStream: viewSearchAsStream,
view: viewDocs,
viewAsStream: viewDocsAsStream,
find: find,
findAsStream: findAsStream,
createIndex: createIndex,
viewWithList: viewWithList,
server: serverScope,
replication: {
enable: function (target, opts, cb) {
return enableReplication(dbName, target, opts, cb)
},
disable: function (id, revision, opts, cb) {
return disableReplication(id, revision, opts, cb)
},
query: function (id, opts, cb) {
return queryReplication(id, opts, cb)
}
}
}
docScope.view.compact = function (ddoc, cb) {
return compactDb(dbName, ddoc, cb)
}
return docScope
}
// server level exports
serverScope = Object.assign(serverScope, {
db: {
create: createDb,
get: getDb,
destroy: destroyDb,
list: listDbs,
listAsStream: listDbsAsStream,
use: docModule,
scope: docModule,
compact: compactDb,
replicate: replicateDb,
replication: {
enable: enableReplication,
disable: disableReplication,
query: queryReplication
},
changes: changesDb,
follow: followDb,
followUpdates: followUpdates,
updates: updates
},
use: docModule,
scope: docModule,
request: relax,
relax: relax,
dinosaur: relax,
auth: auth,
session: session,
updates: updates,
followUpdates: followUpdates,
uuids: uuids
})
const db = maybeExtractDatabaseComponent()
return db ? docModule(db) : serverScope
}
/*
* and now an ascii dinosaur
* _
* / _) ROAR! i'm a vegan!
* .-^^^-/ /
* __/ /
* /__.|_|-|_|
*
* thanks for visiting! come again!
*/
function urlResolveFix (couchUrl, dbName) {
if (/[^/]$/.test(couchUrl)) {
couchUrl += '/'
}
return u.resolve(couchUrl, dbName)
}