@pgaubatz/npm-registry-couchapp
Version:
The CouchApp implementation of an npm registry
598 lines (504 loc) • 16.7 kB
JavaScript
var updates = exports
updates.distTags = function (doc, req) {
var dt = doc['dist-tags']
var versions = doc.versions
function error (message) {
return [ { _id: ".error.", forbidden: message },
JSON.stringify({ error: message }) ]
}
function ok () {
return [ doc, JSON.stringify({ ok: "dist-tags updated" }) ]
}
if (!dt || !versions)
return error("bad document: no dist-tags or no versions")
if (req.body)
var data = JSON.parse(req.body)
var tag = req.query.tag
switch (req.method) {
case "DELETE":
if (!tag)
return error("tag param required")
delete dt[tag]
return ok()
case "PUT":
case "POST":
if (typeof data === "string") {
if (!tag)
return error("tag param required when setting single dist-tag")
dt[tag] = data
} else if (data && typeof data === "object") {
if (tag)
return error("must not provide tag param when setting multiple dist-tags")
if (req.param === "PUT")
doc["dist-tags"] = data
else for (var tag in data)
dt[tag] = data[tag]
} else {
return error("unknown data type")
}
return ok()
default:
return error("unknown request method: " + req.method)
}
}
updates.delete = function (doc, req) {
if (req.method !== "DELETE")
return [ { _id: ".error.", forbidden: "Method not allowed" },
{ error: "method not allowed" } ]
require("monkeypatch").patch(Object, Date, Array, String)
var dt = doc['dist-tags']
var lv = dt && dt.latest
var latest = lv && doc.versions && doc.versions[lv]
var t = doc.time || {}
t.unpublished = {
name: req.userCtx.name,
time: new Date().toISOString(),
tags: dt || {},
maintainers: doc.maintainers,
description: latest && latest.description || doc.description,
versions: Object.keys(doc.versions || {})
}
return [ {
_id: doc._id,
_rev: doc._rev,
name: doc._id,
time: t
}, JSON.stringify({ ok: "deleted" }) ]
}
// We only want to update the metadata
updates.metadata = function (doc, req) {
var data = JSON.parse(req.body)
for (var i in data) {
if (i !== '_rev' &&
i !== 'maintainers' &&
i !== 'versions' &&
(typeof data[i] === 'string' || i === 'keywords'))
doc[i] = data[i]
}
doc.time.modified = new Date().toISOString()
return [doc, JSON.stringify({ok: "updated metadata" })]
}
updates.star = function (doc, req) {
var username = JSON.parse(req.body)
if (!doc.users) doc.users = {}
doc.users[username] = true
return [doc, JSON.stringify({ok: username + ' has starred ' + doc.name})]
}
updates.unstar = function (doc, req) {
var username = JSON.parse(req.body)
if (!doc.users) return [doc, JSON.stringify({ok: doc.name + ' has no users'})]
delete doc.users[username]
return [doc, JSON.stringify({ok: username + ' has unstarred ' + doc.name})]
}
// There are three types of things that will be PUT into here.
// 1. "root doc" with no versions
// 2. "root + version"
// 3. "root + versions + inline attachments"
// 4. query.version="some-tag", body="1.2.3"
// 5. query.version="1.2.3", body={"some":"doc"}
//
// For (4), we only need to update dist-tags.
// For (5), we only need to add the new version, and time.
//
// In the first three cases, what we want to do is:
// 1. Merge new versions into old.
// 2. Set the _npmUser on any new versions to req.userCtx.name
// 3. Set time.modified and time[newVersion] to now
// 4. If there's no time.created, set that to now
// 5. If there are no glaring problems, set the _rev on the return doc to
// the _rev of the existing doc (no 409 PUTGETPUT dance)
//
// In all cases, make sure that the "latest" version gets its junk copied
// onto the root doc.
updates.package = function (doc, req) {
require("monkeypatch").patch(Object, Date, Array, String)
var semver = require("semver")
var README_MAXLEN = 64 * 1024
var body = JSON.parse(req.body)
var deep = require("deep")
var deepEquals = deep.deepEquals
var now = (new Date()).toISOString()
// Sure would be nice if there was an easy way to toggle this in
// couchdb somehow.
var DEBUG = false
if (typeof process === 'object' &&
process &&
process.env &&
process.env === 'object')
DEBUG = true
var output = []
var d
if (typeof console === "object" &&
typeof process === "object" &&
typeof process.env === "object" &&
/\bvdu\b/.test(process.env.NODE_DEBUG)) {
d = console.error
} else if (DEBUG)
d = function() { output.push([].slice.apply(arguments)) }
else
d = function() {}
if (!doc) {
d('newDoc', body)
return newDoc(body)
} else if (req.query.version || req.query.tag) {
d('legacyupdate', req.query)
return legacyUpdate(doc, body, req.query)
} else {
d('updateDoc')
return updateDoc(body, doc)
}
// unreachable
return error("bug in update function. please report this.")
////////////
// methods
function legacyUpdate(doc, body, query) {
d('in legacyUpdate', body, query)
// we know that there's already a document to merge into.
// Figure out what we're trying to add into it.
//
// legacy npm clients would PUT the version to /:pkg/:version
// tagging is done by PUT /:pkg/:tag with a "version" string
if (typeof body === "string") {
var tag = query.version
var ver = body
return addTag(tag, ver)
}
// adding a new version.
return addNewVersion(body.version, body)
}
// return error(reason) to abort at any point.
// the vdu will not allow this _id, and will throw
// the "forbidden" value.
function error (reason) {
if (output.length) {
reason += "\n" + output.map(function(n) {
return n.map(function(a) {
return JSON.stringify(a)
}).join(" ")
}).join("\n")
}
return [{
_id: ".error.",
forbidden: reason
}, JSON.stringify({
forbidden: reason
})]
}
// Copy relevant properties from the "latest" published version to root
function latestCopy(doc) {
d('latestCopy', doc['dist-tags'], doc)
if (!doc['dist-tags'] || !doc.versions)
return
var copyFields = [
"description",
"homepage",
"keywords",
"repository",
"contributors",
"author",
"bugs",
"license"
]
var latest = doc.versions &&
doc['dist-tags'] &&
doc.versions[doc["dist-tags"].latest]
if (latest && typeof latest === "object") {
copyFields.forEach(function(k) {
if (!latest[k])
delete doc[k]
else
doc[k] = latest[k]
})
}
}
function descTrim(doc) {
if (doc.description && doc.description.length > 255) {
doc.description = doc.description.slice(0, 255)
}
if (doc.versions) {
for (var v in doc.versions) {
descTrim(doc.versions[v])
}
}
}
// Clean up excessive readmes and move to root of doc.
function readmeTrim(doc) {
var changed = false
var readme = doc.readme || ''
var readmeFilename = doc.readmeFilename || ''
if (doc['dist-tags'] && doc['dist-tags'].latest) {
var latest = doc.versions[doc['dist-tags'].latest]
if (latest && latest.readme) {
readme = latest.readme
readmeFilename = latest.readmeFilename || ''
}
}
for (var v in doc.versions) {
// If we still don't have one, just take the first one.
if (doc.versions[v].readme && !readme)
readme = doc.versions[v].readme
if (doc.versions[v].readmeFilename && !readmeFilename)
readmeFilename = doc.versions[v].readmeFilename
if (doc.versions[v].readme)
changed = true
delete doc.versions[v].readme
delete doc.versions[v].readmeFilename
}
if (readme && readme.length > README_MAXLEN) {
changed = true
readme = readme.slice(0, README_MAXLEN)
}
doc.readme = readme
doc.readmeFilename = readmeFilename
return changed
}
// return ok(result, message) to exit successfully at any point.
// Does some final data integrity cleanup stuff.
function ok (doc, message) {
// access is handled elsewhere, and should not be stored.
delete doc.access
delete doc.mtime
delete doc.ctime
var time = doc.time = doc.time || {}
time.modified = now
time.created = time.created || time.modified
for (var v in doc.versions) {
var ver = doc.versions[v]
delete ver.ctime
delete ver.mtime
time[v] = time[v] || now
}
delete doc.time.unpublished
findLatest(doc)
latestCopy(doc)
readmeTrim(doc)
descTrim(doc)
if (!doc.maintainers)
return error("no maintainers. Please upgrade your npm client.")
if (output.length) {
message += "\n" + output.map(function(n) {
return n.map(function(a) {
return JSON.stringify(a)
}).join(" ")
}).join("\n")
}
return [doc, JSON.stringify({ok:message})]
}
function findLatest(doc) {
var tags = doc['dist-tags'] = doc['dist-tags'] || {}
var versions = doc.versions = doc.versions || {}
var lv = tags.latest
var latest = versions[lv]
if (latest)
return
// figure out what the "latest" tag should be
var vers = Object.keys(versions)
if (!vers.length) return
vers = vers.sort(semver.compare)
tags.latest = vers.pop()
}
// Create new package doc
function newDoc (doc) {
if (!doc._id) doc._id = doc.name
if (!doc.versions) doc.versions = {}
var latest
for (var v in doc.versions) {
if (!semver.valid(v, true))
return error("Invalid version: "+JSON.stringify(v))
var p = doc.versions[v]
latest = semver.clean(v, true)
}
if (!doc['dist-tags']) doc['dist-tags'] = {}
if (latest && !doc['dist-tags'].latest) {
doc["dist-tags"].latest = latest
}
return ok(doc, "created new entry")
}
function addTag(tag, ver) {
// tag
if (!semver.valid(ver)) {
return error("setting tag "+tag+" to invalid version: "+ver)
}
if (!doc.versions || !doc.versions[ver]) {
return error("setting tag "+tag+" to unknown version: "+ver)
}
doc["dist-tags"][tag] = semver.clean(ver, true)
return ok(doc, "updated tag")
}
function addNewVersion(ver, body) {
d('addNewVersion ver=', ver)
if (typeof body !== "object" || !body) {
return error("putting invalid object to version "+req.query.version)
}
if (!semver.valid(ver, true)) {
return error("invalid version: "+ver)
}
if (doc.versions) {
if ((ver in doc.versions) ||
(semver.clean(ver, true) in doc.versions)) {
// attempting to overwrite an existing version.
// not allowed
return error("cannot modify existing version")
}
}
if (body.name !== doc.name || body.name !== doc._id) {
return error( "Invalid name: "+JSON.stringify(body.name))
}
body.version = semver.clean(body.version, true)
ver = semver.clean(ver, true)
if (body.version !== ver) {
return error( "version in doc doesn't match version in request: "
+ JSON.stringify(body.version)
+ " !== " + JSON.stringify(ver) )
}
body._id = body.name + "@" + body.version
d("set body.maintainers to doc.maintainers", doc.maintainers)
body.maintainers = doc.maintainers
body._npmUser = body._npmUser || { name: req.userCtx.name }
if (body.publishConfig && typeof body.publishConfig === 'object') {
Object.keys(body.publishConfig).filter(function (k) {
return k.match(/^_/)
}).forEach(function (k) {
delete body.publishConfig[k]
})
}
var tag = req.query.tag
|| (body.publishConfig && body.publishConfig.tag)
|| body.tag
|| "latest"
doc["dist-tags"] = doc["dist-tags"] || {}
doc.versions = doc.versions || {}
doc.time = doc.time || {}
if (!req.query.pre)
doc["dist-tags"][tag] = body.version
if (!doc["dist-tags"].latest)
doc["dist-tags"].latest = body.version
doc.versions[ver] = body
doc.time = doc.time || {}
doc.time[ver] = now
return ok(doc, "added version")
}
function isError(res) {
return res && res[0]._id === '.error.'
}
function mergeVersions(newdoc, doc) {
if (!newdoc.versions)
return
// If we are passing in the _rev, then that means that the client has
// fetched the current doc, and explicitly chosen to remove stuff
// If they aren't passing in a matching _rev, then just merge in
// new stuff, don't allow clobbering, and ignore missing versions.
var revMatch = newdoc._rev === doc._rev
if (!doc.versions) doc.versions = {}
for (var v in newdoc.versions) {
var nv = newdoc.versions[v]
var ov = doc.versions[v]
if (ov && !ov.directories &&
JSON.stringify(nv.directories) === '{}') {
delete nv.directories
}
if (!ov) {
var vc = semver.clean(v, true)
if (!vc || v !== vc)
return error('Invalid version: ' + v)
var res = addNewVersion(v, newdoc.versions[v])
if (isError(res))
return res
} else if (nv.deprecated) {
ov.deprecated = nv.deprecated
} else if (!deepEquals(nv, ov, [["deprecated"]])) {
d('old=%j', ov)
d('new=%j', nv)
// Trying to change an existing version! Shenanigans!
// XXX: we COULD just skip this version, and pretend
// it worked, without actually updating. The vdu would
// catch it anyway. Problem there is that then the user
// doesn't see their stuff update, and wonders why.
return error(
'cannot modify pre-existing version: ' + v + '\n' +
'old=' + JSON.stringify(ov) + '\n' +
'new=' + JSON.stringify(nv))
} else if (ov.deprecated && !nv.deprecated) {
delete ov.deprecated
}
}
if (revMatch) {
for (var v in doc.versions) {
if (!newdoc.versions[v])
delete doc.versions[v]
}
}
}
function mergeUsers(newdoc, doc) {
// Note: it IS actually legal to just PUT {_id,users:{..}}
// since it'll just merge it in.
if (!newdoc.users)
return
if (!doc.users) doc.users = {}
if (newdoc.users[req.userCtx.name])
doc.users[req.userCtx.name] = newdoc.users[req.userCtx.name]
else
delete doc.users[req.userCtx.name]
}
function mergeAttachments(newdoc, doc) {
if (!newdoc._attachments)
return
if (!doc._attachments) doc._attachments = {}
var inline = false
for(var k in newdoc._attachments) {
if(newdoc._attachments[k].data) {
doc._attachments[k] = newdoc._attachments[k]
inline = true
}
}
}
function updateDoc(newdoc, doc) {
if (doc.time && doc.time.unpublished) {
d("previously unpublished", doc.time.unpublished)
newdoc._rev = doc._rev
delete doc.time.unpublished
}
// Only allow maintainer update if the rev matches
if (newdoc.maintainers && newdoc._rev === doc._rev) {
d("set doc.maintainers to newdoc.maintainers", newdoc.maintainers)
doc.maintainers = newdoc.maintainers
}
// Don't copy over a dist-tags that is:
// a) empty
// b) not an object
if (newdoc["dist-tags"] && typeof newdoc["dist-tags"] === "object") {
var tags = Object.keys(newdoc["dist-tags"])
if (tags.length) {
doc["dist-tags"] = doc["dist-tags"] || {}
tags.forEach(function (t) {
doc["dist-tags"][t] = newdoc["dist-tags"][t]
})
}
// If the user sent us a single dist-tags entry, then treat it as
// the effective "?tag=foo" param, for the purporses of updating.
if (tags.length === 1) {
if (!req.query.tag)
req.query.tag = tags[0]
}
}
// Don't update the readme if we're publishing an alpha/pre-release
// only if it's a new default "latest" version
if (!req.query.pre && (!req.query.tag || req.query.tag === "latest")) {
for (var i in newdoc) {
if (typeof newdoc[i] === "string") {
doc[i] = newdoc[i]
}
}
}
var res = mergeVersions(newdoc, doc)
if (isError(res))
return res
var res = mergeUsers(newdoc, doc)
if (isError(res))
return res
var res = mergeAttachments(newdoc, doc)
if (isError(res))
return res
return ok(doc, "updated package")
}
}