-
Notifications
You must be signed in to change notification settings - Fork 9
/
app.js
309 lines (280 loc) · 7.88 KB
/
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
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
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
const fs = require('fs')
const ccurllib = require('ccurllib')
const app = require('./package.json')
const syntax =
`Syntax:
--url/-u (COUCH_URL) CouchDB URL (required)
--database/--db (COUCH_DATABASE) CouchDB Datbase name (required)
--designdoc/--dd Design document filename (required)
`
const URL = process.env.COUCH_URL
const DB = process.env.COUCH_DATABASE
const { parseArgs } = require('node:util')
const argv = process.argv.slice(2)
const options = {
url: {
type: 'string',
short: 'u',
default: URL
},
database: {
type: 'string',
short: 'd',
default: DB
},
db: {
type: 'string',
default: DB
},
designdoc: {
type: 'string'
},
dd: {
type: 'string'
},
help: {
type: 'boolean',
short: 'h',
default: false
}
}
const getReqObj = (method) => {
const obj = {
method: method || 'get',
url: `${values.url}/${values.database}`,
headers: {
'content-type': 'application/json',
'User-Agent': `${app.name}/${app.version}`
}
}
return obj
}
// parse command-line options
const { values } = parseArgs({ argv, options })
if (values.db) {
values.database = values.db
delete values.db
}
if (values.dd) {
values.designdoc = values.dd
delete values.dd
}
// help mode
if (values.help) {
console.log(syntax)
process.exit(0)
}
// mandatory parameters
if (!values.url || !values.designdoc || !values.database) {
console.log('You must supply a URL, database and design doc')
process.exit(1)
}
const debug = (status, data) => {
console.log(' status = ', status)
console.log(' data = ', JSON.stringify(data))
console.log('-------------------------------')
}
const copydoc = async (fromId, toId) => {
let fromDoc = null
let toDoc = null
let req, requestDefaults
// fetch the document we are copying
console.log('## copydoc - Fetching from', fromId)
req = getReqObj()
req.url += `/${fromId}`
res = await ccurllib.request(req) //await db.get(fromId)
if (res.status >= 400){
console.log(`docId ${fromId} does not exist - nothing to do`)
return
}
fromDoc = res.result
// fetch the document we are copying to (if it is there)
req = getReqObj()
req.url += `/${toId}`
res = await ccurllib.request(req) // await db.get(toId)
if (res.status < 300) {
toDoc = res.result
}
// overwrite the destination
console.log('## copydoc - Writing new to', toId)
fromDoc._id = toId
if (toDoc) {
fromDoc._rev = toDoc._rev
} else {
delete fromDoc._rev
}
req = getReqObj()
req.data = fromDoc
req.method = 'post'
res = await ccurllib.request(req) // await db.insert(fromDoc)
if (res.status >= 300) {
throw new Error(`Could not copy from ${fromId} to ${toId} - HTTP ${res.status} ${res.result}`)
}
}
const writedoc = async function (obj, docid) {
let preexistingdoc = null
let data
console.log('## writedoc - Looking for pre-existing', docid)
req = getReqObj()
req.url += `/${docid}`
res = await ccurllib.request(req) // await db.get(docid)
if (res.status < 300) {
preexistingdoc = res.result
}
obj._id = docid
if (preexistingdoc) {
obj._rev = preexistingdoc._rev
}
console.log('## writedoc - Writing doc', obj)
req = getReqObj()
req.data = obj
req.method = 'post'
res = await ccurllib.request(req) // data = await db.insert(obj)
debug(res.status, res.result)
if (res.status >= 300) {
throw new Error(`Could not write docId from ${docid} - HTTP ${res.status} ${res.result}`)
}
}
const deletedoc = async function (docid) {
let data
console.log('## deletedoc - Looking for docid', docid)
req = getReqObj()
req.url += `/${docid}`
res = await ccurllib.request(req) // await db.get(docid)
debug(res.status, res.result)
if (res.status >= 400) {
debug(null, `Document ${docid} does not exist`)
return
}
console.log('## deletedoc - Deleting ', docid, res.result._rev)
req = getReqObj()
req.url += `/${docid}`
req.qs = {}
req.qs.rev = res.result._rev
req.method = 'delete'
res = await ccurllib.request(req) // data = await db.insert(obj)
debug(res.status, res.result)
if (res.status >= 300) {
throw new Error(`Could not delete docId ${docId} - HTTP ${res.status} ${res.result}`)
}
// await db.destroy(docid, data._rev)
}
const clone = function (x) {
return JSON.parse(JSON.stringify(x))
}
// promisey-sleep
const sleep = async (ms) => {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms)
})
}
const migrate = async function (ddDocString) {
// this is the whole design document
let dd, data, req, res
try {
dd = JSON.parse(ddDocString)
} catch (e) {
console.log('FAILED to parse file contents as JSON - cannot continue')
process.exit(1)
}
const ddName = dd._id
delete dd._rev
const ddOldName = ddName + '_OLD'
const ddNewName = ddName + '_NEW'
// check that the database exists
console.log('## check db exists')
req = getReqObj()
data = await ccurllib.request(req) // await nano.db.get(values.database)
debug(data.status, data.result)
if (data.status >= 400) {
console.error('databases does not exist')
process.exit(1)
}
// check that the existing view isn't the same as the incoming view
console.log('## check existing view is not the same as the incoming view')
req = getReqObj()
req.url += `/${ddName}`
data = await ccurllib.request(req)
if (data.result > 300) {
res = ''
} else {
res = data.result
}
// data = await db.get(ddName)
const a = clone(res)
const b = clone(dd)
delete a._rev
delete a._id
delete b._rev
delete b._id
if (JSON.stringify(a) === JSON.stringify(b)) {
console.error('** The design document is the same, no need to migrate! **')
process.exit(2)
}
// copy original design document to _OLD
console.log('## copy original design document to _OLD')
await copydoc(ddName, ddOldName)
// write new design document to _NEW
console.log('## write new design document to _NEW')
await writedoc(dd, ddNewName)
// wait for the view build to complete, by polling
let hasData = false
do {
hasDate = false
if (typeof dd.views === 'object' && Object.keys(dd.views).length > 0) {
const path = `${ddNewName}/_view/${Object.keys(dd.views)[0]}`
await sleep(3000)
console.log('## query ',path, 'to validate freshness.')
req = getReqObj()
req.url += '/' + path
req.qs = { limit: 1}
res = await ccurllib.request(req)
if (res.status < 300) {
hasData = true
}
}
if (!hasData) {
// get progress from active tasks
req = getReqObj()
req.url = `${values.url}/_active_tasks`
res = await ccurllib.request(req)
data = res.result
let progress = 0
let shards = 0
for (const i in data) {
const task = data[i]
if (task.type === 'indexer' && task.design_document === ddNewName) {
shards++
progress = progress + parseInt(task.progress, 10)
}
}
const overallProgress = Math.floor(progress / shards)
console.log('## indexing progress:', overallProgress, '%')
}
} while (!hasData)
// copy _NEW to live
console.log('## copy _NEW to live', ddNewName, ddName)
await copydoc(ddNewName, ddName)
// delete the _OLD view
console.log('## delete the _OLD view', ddOldName)
await deletedoc(ddOldName)
// delete the _NEW view
console.log('## delete the _NEW view', ddNewName)
await deletedoc(ddNewName)
console.log('FINISHED!!!')
}
const main = async () => {
// load the design document
const ddFilename = values.designdoc
if (/\.js$/.test(ddFilename)) {
// use require to load js design doc
const path = require('path')
const dataAbs = path.join(process.cwd(), ddFilename.replace(/([^.]+)\.js$/, '$1'))
await migrate(JSON.stringify(require(dataAbs)))
} else {
// read json
const str = fs.readFileSync(ddFilename, { encoding: 'utf8' })
await migrate(str)
}
}
main()