diff --git a/.flowconfig b/.flowconfig index 9d26fab..32884c6 100644 --- a/.flowconfig +++ b/.flowconfig @@ -2,5 +2,8 @@ module.file_ext=.js module.file_ext=.jsx +[libs] +types/ + [ignore] /node_modules/fbjs diff --git a/src/lib/clone.js b/src/lib/clone.js index a747ae8..3fdec58 100644 --- a/src/lib/clone.js +++ b/src/lib/clone.js @@ -64,10 +64,6 @@ const cloneObject = ( }; const cloneModel = (model: {toJSON: () => Object}, i18n: i18nObject) => { - if (!model) { - return null; - } - const serialize = serializeValue(i18n); const result = model.toJSON(); diff --git a/src/logic/admin.js b/src/logic/admin.js index c8558f8..a5276a2 100644 --- a/src/logic/admin.js +++ b/src/logic/admin.js @@ -12,7 +12,7 @@ module.exports = function(app) { const ImageImport = models("ImageImport"); const RecordImport = models("RecordImport"); - const auth = require("./shared/auth"); + const {auth, canEdit} = require("./shared/auth"); const importRecords = ({i18n, lang, query, source}, res) => { const batchError = (err) => RecordImport.getError(i18n, err); @@ -145,7 +145,7 @@ module.exports = function(app) { const dataImport = results[1] .sort((a, b) => b.created - a.created); const title = i18n.format(i18n.gettext("%(name)s Admin Area"), { - name: source.getFullName, + name: source.getFullName(i18n), }); res.render("Admin", { @@ -266,24 +266,17 @@ module.exports = function(app) { routes() { const source = (req, res, next) => { - const {i18n, params} = req; + const {params: {source}} = req; const Source = models("Source"); - - try { - req.source = Source.getSource(params.source); - next(); - - } catch (e) { - return res.status(404).render("Error", { - title: i18n.gettext("Source not found."), - }); - } + req.source = Source.getSource(source); + next(); }; - app.get("/:type/source/:source/admin", auth, source, this.admin); - app.post("/:type/source/:source/upload-images", auth, source, - this.uploadImages); - app.post("/:type/source/:source/upload-data", auth, source, + app.get("/:type/source/:source/admin", auth, canEdit, source, + this.admin); + app.post("/:type/source/:source/upload-images", auth, canEdit, + source, this.uploadImages); + app.post("/:type/source/:source/upload-data", auth, canEdit, source, this.uploadData); }, }; diff --git a/src/logic/create.js b/src/logic/create.js new file mode 100644 index 0000000..2c5bc16 --- /dev/null +++ b/src/logic/create.js @@ -0,0 +1,223 @@ +// @flow + +const async = require("async"); +const formidable = require("formidable"); + +const db = require("../lib/db"); +const {cloneModel} = require("../lib/clone"); +const record = require("../lib/record"); +const models = require("../lib/models"); +const options = require("../lib/options"); +const metadata = require("../lib/metadata"); + +module.exports = function(app: express$Application) { + const Image = models("Image"); + + const {auth, canEdit} = require("./shared/auth"); + + const cloneView = ({ + i18n, + params: {type, source, recordName}, + }: express$Request, res) => { + const Record = record(type); + const model = metadata.model(type); + const id = `${source}/${recordName}`; + + Record.findById(id, (err, oldRecord) => { + if (err || !oldRecord) { + return res.status(404).render("Error", { + title: i18n.gettext("Not found."), + }); + } + + const recordTitle = oldRecord.getTitle(i18n); + const title = i18n.format( + i18n.gettext("Cloning '%(recordTitle)s'"), + {recordTitle}); + + const data = { + type, + source: oldRecord.source, + lang: oldRecord.lang, + }; + + const cloneFields = options.types[type].cloneFields || + Object.keys(model); + + for (const typeName of cloneFields) { + data[typeName] = oldRecord[typeName]; + } + + const record = new Record(data); + + record.loadImages(true, () => { + Record.getFacets(i18n, (err, globalFacets) => { + record.getDynamicValues(i18n, (err, dynamicValues) => { + res.render("EditRecord", { + title, + mode: "clone", + record: cloneModel(record, i18n), + globalFacets, + dynamicValues, + type, + }); + }); + }); + }); + }); + }; + + const createView = ({ + user, + params: {type}, + query: {source}, + i18n, + }: express$Request, res, next) => { + if (!user) { + return next(); + } + + const sources = user.getEditableSourcesByType()[type]; + + if (sources.length === 0) { + return res.status(403).render("Error", { + title: i18n.gettext("Authorization required."), + }); + } + + const Record = record(type); + const title = i18n.format( + i18n.gettext("%(recordName)s: Create New"), { + recordName: options.types[type].name(i18n), + }); + + Record.getFacets(i18n, (err, globalFacets) => { + res.render("EditRecord", { + title, + source, + sources, + mode: "create", + type, + globalFacets, + dynamicValues: {}, + }); + }); + }; + + const create = (req: express$Request, res, next) => { + const {params: {type, source}, i18n, lang} = req; + const props = {}; + const model = metadata.model(type); + const hasImageSearch = options.types[type].hasImageSearch(); + + const form = new formidable.IncomingForm(); + form.encoding = "utf-8"; + form.maxFieldsSize = options.maxUploadSize; + form.multiples = true; + + form.parse(req, (err, fields, files) => { + /* istanbul ignore if */ + if (err) { + return next(new Error( + i18n.gettext("Error processing upload."))); + } + + for (const prop in model) { + props[prop] = fields[prop]; + } + + if (options.types[type].autoID) { + props.id = db.mongoose.Types.ObjectId().toString(); + } else { + props.id = fields.id; + } + + Object.assign(props, { + lang, + source, + type, + }); + + const Record = record(type); + + const {data, error} = Record.lintData(props, i18n); + + if (error) { + return next(new Error(error)); + } + + const newRecord = new Record(data); + + const mockBatch = { + _id: db.mongoose.Types.ObjectId().toString(), + source: newRecord.source, + }; + + const images = Array.isArray(files.images) ? + files.images : + files.images ? + [files.images] : + []; + + async.mapSeries(images, (file, callback) => { + if (!file.path || file.size <= 0) { + return process.nextTick(callback); + } + + Image.fromFile(mockBatch, file, (err, image) => { + // TODO: Display better error message + if (err) { + return callback( + new Error( + i18n.gettext("Error processing image."))); + } + + image.save((err) => { + /* istanbul ignore if */ + if (err) { + return callback(err); + } + + callback(null, image); + }); + }); + }, (err, unfilteredImages) => { + if (err) { + return next(err); + } + + newRecord.images = unfilteredImages + .filter((image) => image) + .map((image) => image._id); + + newRecord.save((err) => { + if (err) { + return next(new Error( + i18n.gettext("Error saving record."))); + } + + const finish = () => + res.redirect(newRecord.getURL(lang)); + + if (newRecord.images.length === 0 || !hasImageSearch) { + return finish(); + } + + // If new images were added then we need to update + // their similarity and the similarity of all other + // images, as well. + Image.queueBatchSimilarityUpdate(mockBatch._id, finish); + }); + }); + }); + }; + + return { + routes() { + app.get("/:type/create", auth, canEdit, createView); + app.post("/:type/create", auth, canEdit, create); + app.get("/:type/:source/:recordName/clone", auth, canEdit, + cloneView); + }, + }; +}; diff --git a/src/logic/edit.js b/src/logic/edit.js new file mode 100644 index 0000000..61c37bd --- /dev/null +++ b/src/logic/edit.js @@ -0,0 +1,257 @@ +// @flow + +const async = require("async"); +const formidable = require("formidable"); + +const db = require("../lib/db"); +const {cloneModel} = require("../lib/clone"); +const record = require("../lib/record"); +const models = require("../lib/models"); +const options = require("../lib/options"); +const metadata = require("../lib/metadata"); +const urls = require("../lib/urls")(options); + +module.exports = function(app: express$Application) { + const Image = models("Image"); + + const {auth, canEdit} = require("./shared/auth"); + + const removeRecord = (req: express$Request, res, next) => { + const {params, i18n, lang} = req; + const {type} = params; + const Record = record(type); + const id = `${params.source}/${params.recordName}`; + + Record.findById(id, (err, record) => { + if (err || !record) { + return next(new Error(i18n.gettext("Not found."))); + } + + record.remove((err) => { + if (err) { + return next(new Error( + i18n.gettext("Error removing record."))); + } + + res.redirect(urls.gen(lang, "/")); + }); + }); + }; + + const editView = ({ + params: {type, source, recordName}, + i18n, + }: express$Request, res) => { + const Record = record(type); + const id = `${source}/${recordName}`; + + Record.findById(id, (err, record) => { + if (err || !record) { + return res.status(404).render("Error", { + title: i18n.gettext("Page not found."), + }); + } + + const recordTitle = record.getTitle(i18n); + const title = i18n.format( + i18n.gettext("Updating '%(recordTitle)s'"), + {recordTitle}); + + record.loadImages(true, () => { + Record.getFacets(i18n, (err, globalFacets) => { + record.getDynamicValues(i18n, (err, dynamicValues) => { + res.render("EditRecord", { + title, + mode: "edit", + record: cloneModel(record, i18n), + globalFacets, + dynamicValues, + type, + }); + }); + }); + }); + }); + }; + + const edit = (req: express$Request, res, next) => { + const {params: {type, recordName, source}, i18n, lang} = req; + const props = {}; + const model = metadata.model(type); + const hasImageSearch = options.types[type].hasImageSearch(); + const _id = `${source}/${recordName}`; + + const form = new formidable.IncomingForm(); + form.encoding = "utf-8"; + form.maxFieldsSize = options.maxUploadSize; + form.multiples = true; + + form.parse(req, (err, fields, files) => { + /* istanbul ignore if */ + if (err) { + return next(new Error( + i18n.gettext("Error processing upload."))); + } + + if (fields.removeRecord) { + return removeRecord(req, res, next); + } + + for (const prop in model) { + props[prop] = fields[prop]; + } + + Object.assign(props, { + id: recordName, + lang: lang, + source, + type, + }); + + const Record = record(type); + + const {data, error} = Record.lintData(props, i18n); + + if (error) { + return next(new Error(error)); + } + + const mockBatch = { + _id: db.mongoose.Types.ObjectId().toString(), + source, + }; + + const images = Array.isArray(files.images) ? + files.images : + files.images ? + [files.images] : + []; + + async.mapSeries(images, (file, callback) => { + if (!file.path || file.size <= 0) { + return process.nextTick(callback); + } + + Image.fromFile(mockBatch, file, (err, image) => { + // TODO: Display better error message + if (err) { + return callback( + new Error( + i18n.gettext("Error processing image."))); + } + + image.save((err) => { + /* istanbul ignore if */ + if (err) { + return callback(err); + } + + callback(null, image); + }); + }); + }, (err, unfilteredImages) => { + if (err) { + return next(err); + } + + Record.findById(_id, (err, record) => { + if (err || !record) { + return res.status(404).render("Error", { + title: i18n.gettext("Not found."), + }); + } + + record.set(data); + + for (const prop in model) { + if (!fields[prop] && !data[prop]) { + record[prop] = undefined; + } + } + + record.images = record.images.concat( + unfilteredImages + .filter((image) => image) + .map((image) => image._id)); + + record.save((err) => { + if (err) { + return next(new Error( + i18n.gettext("Error saving record."))); + } + + const finish = () => + res.redirect(record.getURL(lang)); + + if (record.images.length === 0 || !hasImageSearch) { + return finish(); + } + + // If new images were added then we need to update + // their similarity and the similarity of all other + // images, as well. + Image.queueBatchSimilarityUpdate(mockBatch._id, + finish); + }); + }); + }); + }); + }; + + const removeImage = (req: express$Request, res, next) => { + const {params: {type, source, recordName}, i18n, lang} = req; + const Record = record(type); + const hasImageSearch = options.types[type].hasImageSearch(); + const id = `${source}/${recordName}`; + + const form = new formidable.IncomingForm(); + form.encoding = "utf-8"; + + form.parse(req, (err, fields) => { + /* istanbul ignore if */ + if (err) { + return next(new Error( + i18n.gettext("Error processing request."))); + } + + const imageID = fields.image; + + Record.findById(id, (err, record) => { + if (err || !record) { + return next(new Error(i18n.gettext("Not found."))); + } + + record.images = record.images + .filter((image) => image !== imageID); + + record.save((err) => { + if (err) { + return next(new Error( + i18n.gettext("Error saving record."))); + } + + const finish = () => + res.redirect(record.getURL(lang)); + + if (!hasImageSearch) { + return finish(); + } + + record.updateSimilarity(finish); + }); + }); + }); + }; + + return { + routes() { + // Handle these last as they'll catch almost anything + app.get("/:type/:source/:recordName/edit", auth, canEdit, + editView); + app.post("/:type/:source/:recordName/edit", auth, canEdit, + edit); + app.post("/:type/:source/:recordName/remove-image", auth, canEdit, + removeImage); + }, + }; +}; diff --git a/src/logic/records.js b/src/logic/records.js deleted file mode 100644 index 4cdb74d..0000000 --- a/src/logic/records.js +++ /dev/null @@ -1,587 +0,0 @@ -const async = require("async"); -const formidable = require("formidable"); - -const db = require("../lib/db"); -const {cloneModel} = require("../lib/clone"); -const record = require("../lib/record"); -const models = require("../lib/models"); -const options = require("../lib/options"); -const metadata = require("../lib/metadata"); -const urls = require("../lib/urls")(options); - -module.exports = function(app) { - const Source = models("Source"); - const Image = models("Image"); - - const cache = require("../server/middlewares/cache"); - const search = require("./shared/search-page"); - const auth = require("./shared/auth"); - - const removeRecord = (req, res, next) => { - const {params, i18n, lang} = req; - const {type} = params; - const Record = record(type); - const id = `${params.source}/${params.recordName}`; - - Record.findById(id, (err, record) => { - if (err || !record) { - return next(new Error(i18n.gettext("Not found."))); - } - - record.remove((err) => { - if (err) { - return next(new Error( - i18n.gettext("Error removing record."))); - } - - res.redirect(urls.gen(lang, "/")); - }); - }); - }; - - return { - search(req, res, next) { - return search(req, res, next); - }, - - bySource(req, res, next) { - const {i18n, params} = req; - - try { - search(req, res, next, { - url: Source.getSource(params.source).url, - }); - - } catch (e) { - return res.status(404).render("Error", { - title: i18n.gettext("Source not found."), - }); - } - }, - - show({i18n, originalUrl, params, query}, res, next) { - const typeName = params.type; - - if (!options.types[typeName]) { - return res.status(404).render("Error", { - title: i18n.gettext("Page not found."), - }); - } - - if (options.types[typeName].alwaysEdit) { - return res.redirect(`${originalUrl}/edit`); - } - - const Record = record(typeName); - const compare = ("compare" in query); - const id = `${params.source}/${params.recordName}`; - - Record.findById(id, (err, record) => { - if (err || !record) { - // We don't return a 404 here to allow this to pass - // through to other handlers - return next(); - } - - record.loadImages(true, () => { - // TODO: Handle error loading images? - const title = record.getTitle(i18n); - const social = { - imgURL: record.getOriginalURL(), - title, - url: record.getURL(), - }; - - const clonedRecord = cloneModel(record, i18n); - - clonedRecord.imageModels = record.images - .map((image) => cloneModel(image)); - - // Sort the similar records by score - clonedRecord.similarRecords = record.similarRecords - .sort((a, b) => b.score - a.score); - - if (!compare) { - const similarRecords = record.similarRecords - .map((match) => ({ - _id: match._id, - score: match.score, - recordModel: - cloneModel(match.recordModel, i18n), - })); - - return res.render("Record", { - title, - social, - compare: false, - records: [clonedRecord], - similar: similarRecords, - sources: Source.getSourcesByType(typeName) - .map((source) => cloneModel(source, i18n)), - }); - } - - async.eachLimit(record.similarRecords, 4, - (similar, callback) => { - similar.recordModel.loadImages(false, callback); - }, () => { - const similarRecords = record.similarRecords - .map((similar) => { - const clonedRecord = - cloneModel(similar.recordModel, i18n); - clonedRecord.imageModels = record.images - .map((image) => cloneModel(image)); - return clonedRecord; - }); - res.render("Record", { - title, - social, - compare: true, - noIndex: true, - similar: [], - records: [clonedRecord] - .concat(similarRecords), - sources: Source.getSourcesByType(typeName) - .map((source) => cloneModel(source, i18n)), - }); - }); - }); - }); - }, - - editView({params, i18n}, res) { - const type = params.type; - const Record = record(type); - const id = `${params.source}/${params.recordName}`; - - Record.findById(id, (err, record) => { - if (err || !record) { - return res.status(404).render("Error", { - title: i18n.gettext("Not found."), - }); - } - - const recordTitle = record.getTitle(i18n); - const title = i18n.format( - i18n.gettext("Updating '%(recordTitle)s'"), - {recordTitle}); - - record.loadImages(true, () => { - Record.getFacets(i18n, (err, globalFacets) => { - record.getDynamicValues(i18n, (err, dynamicValues) => { - res.render("EditRecord", { - title, - mode: "edit", - record: cloneModel(record, i18n), - globalFacets, - dynamicValues, - type, - }); - }); - }); - }); - }); - }, - - edit(req, res, next) { - const {params, i18n, lang} = req; - const props = {}; - const {type} = params; - const model = metadata.model(type); - const hasImageSearch = options.types[type].hasImageSearch(); - const id = params.recordName; - const _id = `${params.source}/${id}`; - - const form = new formidable.IncomingForm(); - form.encoding = "utf-8"; - form.maxFieldsSize = options.maxUploadSize; - form.multiples = true; - - form.parse(req, (err, fields, files) => { - /* istanbul ignore if */ - if (err) { - return next(new Error( - i18n.gettext("Error processing upload."))); - } - - if (fields.removeRecord) { - return removeRecord(req, res, next); - } - - for (const prop in model) { - props[prop] = fields[prop]; - } - - Object.assign(props, { - id, - lang: lang, - source: params.source, - type, - }); - - const Record = record(type); - - const {data, error} = Record.lintData(props, i18n); - - if (error) { - return next(new Error(error)); - } - - const mockBatch = { - _id: db.mongoose.Types.ObjectId().toString(), - source: params.source, - }; - - const images = Array.isArray(files.images) ? - files.images : - files.images ? - [files.images] : - []; - - async.mapSeries(images, (file, callback) => { - if (!file.path || file.size <= 0) { - return process.nextTick(callback); - } - - Image.fromFile(mockBatch, file, (err, image) => { - // TODO: Display better error message - if (err) { - return callback( - new Error( - i18n.gettext("Error processing image."))); - } - - image.save((err) => { - /* istanbul ignore if */ - if (err) { - return callback(err); - } - - callback(null, image); - }); - }); - }, (err, unfilteredImages) => { - if (err) { - return next(err); - } - - Record.findById(_id, (err, record) => { - if (err || !record) { - return res.status(404).render("Error", { - title: i18n.gettext("Not found."), - }); - } - - record.set(data); - - for (const prop in model) { - if (!fields[prop] && !data[prop]) { - record[prop] = undefined; - } - } - - record.images = record.images.concat( - unfilteredImages - .filter((image) => image) - .map((image) => image._id)); - - record.save((err) => { - if (err) { - return next(new Error( - i18n.gettext("Error saving record."))); - } - - const finish = () => - res.redirect(record.getURL(lang)); - - if (record.images.length === 0 || !hasImageSearch) { - return finish(); - } - - // If new images were added then we need to update - // their similarity and the similarity of all other - // images, as well. - Image.queueBatchSimilarityUpdate(mockBatch._id, - finish); - }); - }); - }); - }); - }, - - removeImage(req, res, next) { - const {params, i18n, lang} = req; - const {type} = params; - const Record = record(type); - const hasImageSearch = options.types[type].hasImageSearch(); - const id = `${params.source}/${params.recordName}`; - - const form = new formidable.IncomingForm(); - form.encoding = "utf-8"; - - form.parse(req, (err, fields) => { - /* istanbul ignore if */ - if (err) { - return next(new Error( - i18n.gettext("Error processing request."))); - } - - const imageID = fields.image; - - Record.findById(id, (err, record) => { - if (err || !record) { - return next(new Error(i18n.gettext("Not found."))); - } - - record.images = record.images - .filter((image) => image !== imageID); - - record.save((err) => { - if (err) { - return next(new Error( - i18n.gettext("Error saving record."))); - } - - const finish = () => - res.redirect(record.getURL(lang)); - - if (!hasImageSearch) { - return finish(); - } - - record.updateSimilarity(finish); - }); - }); - }); - }, - - cloneView({i18n, params}, res) { - const {type} = params; - const Record = record(type); - const id = `${params.source}/${params.recordName}`; - - Record.findById(id, (err, oldRecord) => { - if (err || !oldRecord) { - return res.status(404).render("Error", { - title: i18n.gettext("Not found."), - }); - } - - const recordTitle = oldRecord.getTitle(i18n); - const title = i18n.format( - i18n.gettext("Cloning '%(recordTitle)s'"), - {recordTitle}); - - const data = { - type, - source: oldRecord.source, - lang: oldRecord.lang, - }; - - for (const typeName of options.types[type].cloneFields) { - data[typeName] = oldRecord[typeName]; - } - - const record = new Record(data); - - record.loadImages(true, () => { - Record.getFacets(i18n, (err, globalFacets) => { - record.getDynamicValues(i18n, (err, dynamicValues) => { - res.render("EditRecord", { - title, - mode: "clone", - record: cloneModel(record, i18n), - globalFacets, - dynamicValues, - type, - }); - }); - }); - }); - }); - }, - - createRedirect({user, params: {type}, lang, i18n}, res) { - const sources = user.getEditableSourcesByType()[type]; - - if (sources.length === 1) { - return res.redirect(urls.gen(lang, - `/${type}/${sources[0]}/create`)); - } - - // TODO(jeresig): Figure out a better way to handle multiple sources - res.status(404).render("Error", { - title: i18n.gettext("Page not found."), - }); - }, - - createView({params: {type}, i18n}, res) { - const Record = record(type); - - const title = i18n.format( - i18n.gettext("%(recordName)s: Create New"), { - recordName: options.types[type].name(i18n), - }); - - Record.getFacets(i18n, (err, globalFacets) => { - res.render("EditRecord", { - title, - mode: "create", - type, - globalFacets, - dynamicValues: {}, - }); - }); - }, - - create(req, res, next) { - const {params, i18n, lang} = req; - const props = {}; - const {type} = params; - const model = metadata.model(type); - const hasImageSearch = options.types[type].hasImageSearch(); - - const form = new formidable.IncomingForm(); - form.encoding = "utf-8"; - form.maxFieldsSize = options.maxUploadSize; - form.multiples = true; - - form.parse(req, (err, fields, files) => { - /* istanbul ignore if */ - if (err) { - return next(new Error( - i18n.gettext("Error processing upload."))); - } - - for (const prop in model) { - props[prop] = fields[prop]; - } - - if (options.types[type].autoID) { - props.id = db.mongoose.Types.ObjectId().toString(); - } else { - props.id = fields.id; - } - - Object.assign(props, { - lang, - source: params.source, - type, - }); - - const Record = record(type); - - const {data, error} = Record.lintData(props, i18n); - - if (error) { - return next(new Error(error)); - } - - const newRecord = new Record(data); - - const mockBatch = { - _id: db.mongoose.Types.ObjectId().toString(), - source: newRecord.source, - }; - - const images = Array.isArray(files.images) ? - files.images : - files.images ? - [files.images] : - []; - - async.mapSeries(images, (file, callback) => { - if (!file.path || file.size <= 0) { - return process.nextTick(callback); - } - - Image.fromFile(mockBatch, file, (err, image) => { - // TODO: Display better error message - if (err) { - return callback( - new Error( - i18n.gettext("Error processing image."))); - } - - image.save((err) => { - /* istanbul ignore if */ - if (err) { - return callback(err); - } - - callback(null, image); - }); - }); - }, (err, unfilteredImages) => { - if (err) { - return next(err); - } - - newRecord.images = unfilteredImages - .filter((image) => image) - .map((image) => image._id); - - newRecord.save((err) => { - if (err) { - return next(new Error( - i18n.gettext("Error saving record."))); - } - - const finish = () => - res.redirect(newRecord.getURL(lang)); - - if (newRecord.images.length === 0 || !hasImageSearch) { - return finish(); - } - - // If new images were added then we need to update - // their similarity and the similarity of all other - // images, as well. - Image.queueBatchSimilarityUpdate(mockBatch._id, finish); - }); - }); - }); - }, - - json({params, i18n}, res) { - const id = `${params.source}/${params.recordName}`; - const type = params.type; - const Record = record(type); - - Record.findById(id, (err, record) => { - if (record) { - return res.send(cloneModel(record, i18n)); - } - - res.status(404).send({ - error: i18n.gettext("Record not found."), - }); - }); - }, - - routes() { - app.get("/:type/search", cache(1), this.search); - app.get("/:type/create", auth, this.createRedirect); - app.get("/:type/source/:source", cache(1), this.bySource); - app.get("/:type/:source/create", auth, this.createView); - app.post("/:type/:source/create", auth, this.create); - - for (const typeName in options.types) { - const searchURLs = options.types[typeName].searchURLs; - for (const path in searchURLs) { - app.get(`/:type${path}`, cache(1), (req, res, next) => - searchURLs[path](req, res, next, search)); - } - } - - // Handle these last as they'll catch almost anything - app.get("/:type/:source/:recordName/edit", auth, this.editView); - app.post("/:type/:source/:recordName/edit", auth, this.edit); - app.get("/:type/:source/:recordName/clone", auth, this.cloneView); - app.post("/:type/:source/:recordName/remove-image", auth, - this.removeImage); - app.get("/:type/:source/:recordName/json", this.json); - app.get("/:type/:source/:recordName", this.show); - }, - }; -}; diff --git a/src/logic/search.js b/src/logic/search.js new file mode 100644 index 0000000..2e267e8 --- /dev/null +++ b/src/logic/search.js @@ -0,0 +1,47 @@ +// @flow + +const models = require("../lib/models"); +const options = require("../lib/options"); + +module.exports = function(app: express$Application) { + const Source = models("Source"); + + const cache = require("../server/middlewares/cache"); + const searchPage = require("./shared/search-page"); + + const search = (req: express$Request, res, next) => { + return searchPage(req, res, next); + }; + + const bySource = (req: express$Request, res, next) => { + const {i18n, params} = req; + + try { + searchPage(req, res, next, { + url: Source.getSource(params.source).url, + }); + + } catch (e) { + return res.status(404).render("Error", { + title: i18n.gettext("Source not found."), + }); + } + }; + + return { + routes() { + app.get("/:type/search", cache(1), search); + app.get("/:type/source/:source", cache(1), bySource); + + for (const typeName in options.types) { + const searchURLs = options.types[typeName].searchURLs; + for (const path in searchURLs) { + app.get(`/:type${path}`, cache(1), + (req: express$Request, res, next) => { + return searchURLs[path](req, res, next, search); + }); + } + } + }, + }; +}; diff --git a/src/logic/shared/auth.js b/src/logic/shared/auth.js index 1bd3aab..37f990f 100644 --- a/src/logic/shared/auth.js +++ b/src/logic/shared/auth.js @@ -2,19 +2,50 @@ const passport = require("passport"); const options = require("../../lib/options"); const urls = require("../../lib/urls")(options); +const models = require("../../lib/models"); // Only allow certain users to access these pages -module.exports = (req, res, next) => { - const {user, session, originalUrl, lang, i18n, params} = req; +const auth = (req, res, next) => { + const {user, session, originalUrl, lang} = req; passport.authenticate("local", () => { if (!user) { session.redirectTo = originalUrl; res.redirect(urls.gen(lang, "/login")); - } else if (!user.canEditSource(params.source)) { - next(new Error(i18n.gettext("Authorization required."))); } else { next(); } })(req, res, next); }; + +const canEdit = ({user, params: {type, source}, i18n}, res, next) => { + if (!options.types[type]) { + return res.status(404).render("Error", { + title: i18n.gettext("Page not found."), + }); + } + + if (source) { + try { + const Source = models("Source"); + Source.getSource(source); + + } catch (e) { + return res.status(404).render("Error", { + title: i18n.gettext("Source not found."), + }); + } + + const sources = user.getEditableSourcesByType()[type]; + + if (!sources.includes(source)) { + return res.status(403).render("Error", { + title: i18n.gettext("Authorization required."), + }); + } + } + + next(); +}; + +module.exports = {auth, canEdit}; diff --git a/src/logic/view.js b/src/logic/view.js new file mode 100644 index 0000000..fe5bacf --- /dev/null +++ b/src/logic/view.js @@ -0,0 +1,133 @@ +// @flow + +const async = require("async"); + +const {cloneModel} = require("../lib/clone"); +const record = require("../lib/record"); +const models = require("../lib/models"); +const options = require("../lib/options"); + +module.exports = function(app: express$Application) { + const Source = models("Source"); + + const show = ({ + i18n, + originalUrl, + params, + query, + }: express$Request, res, next) => { + const typeName = params.type; + + if (!options.types[typeName]) { + return res.status(404).render("Error", { + title: i18n.gettext("Page not found."), + }); + } + + if (options.types[typeName].alwaysEdit) { + return res.redirect(`${originalUrl}/edit`); + } + + const Record = record(typeName); + const compare = ("compare" in query); + const id = `${params.source}/${params.recordName}`; + + Record.findById(id, (err, record) => { + if (err || !record) { + // We don't return a 404 here to allow this to pass + // through to other handlers + return next(); + } + + record.loadImages(true, () => { + // TODO: Handle error loading images? + const title = record.getTitle(i18n); + const social = { + imgURL: record.getOriginalURL(), + title, + url: record.getURL(), + }; + + const clonedRecord = cloneModel(record, i18n); + + clonedRecord.imageModels = record.images + .map((image) => cloneModel(image, i18n)); + + // Sort the similar records by score + clonedRecord.similarRecords = record.similarRecords + .sort((a, b) => b.score - a.score); + + if (!compare) { + const similarRecords = record.similarRecords + .map((match) => ({ + _id: match._id, + score: match.score, + recordModel: + cloneModel(match.recordModel, i18n), + })); + + return res.render("Record", { + title, + social, + compare: false, + records: [clonedRecord], + similar: similarRecords, + sources: Source.getSourcesByType(typeName) + .map((source) => cloneModel(source, i18n)), + }); + } + + async.eachLimit(record.similarRecords, 4, + (similar, callback) => { + similar.recordModel.loadImages(false, callback); + }, () => { + const similarRecords = record.similarRecords + .map((similar) => { + const clonedRecord = + cloneModel(similar.recordModel, i18n); + clonedRecord.imageModels = record.images + .map((image) => cloneModel(image, i18n)); + return clonedRecord; + }); + res.render("Record", { + title, + social, + compare: true, + noIndex: true, + similar: [], + records: [clonedRecord] + .concat(similarRecords), + sources: Source.getSourcesByType(typeName) + .map((source) => cloneModel(source, i18n)), + }); + }); + }); + }); + }; + + const json = ({ + params: {type, source, recordName}, + i18n, + }: express$Request, res) => { + const id = `${source}/${recordName}`; + const Record = record(type); + + Record.findById(id, (err, record) => { + if (record) { + return res.send(cloneModel(record, i18n)); + } + + res.status(404).send({ + error: i18n.gettext("Record not found."), + }); + }); + }; + + return { + routes() { + // Handle these last as they'll catch almost anything + app.get("/:type/:source/:recordName/json", json); + app.get("/:type/:source/:recordName", show); + }, + }; +}; diff --git a/src/schemas/User.js b/src/schemas/User.js index 56fe9b2..aea473b 100644 --- a/src/schemas/User.js +++ b/src/schemas/User.js @@ -104,6 +104,10 @@ User.methods = { types[source.type] = []; } + if (!this.canEditSource(source)) { + continue; + } + types[source.type].push(source._id); } diff --git a/src/server/i18n.js b/src/server/i18n.js index 5d06fcd..a6d1f5a 100644 --- a/src/server/i18n.js +++ b/src/server/i18n.js @@ -1,14 +1,16 @@ +// @flow + const options = require("../lib/options"); const i18n = require("../lib/i18n"); const defaultLocale = Object.keys(options.locales)[0] || "en"; -module.exports = (app) => { - app.use((req, res, next) => { +module.exports = (app: express$Application) => { + app.use((req: express$Request, res, next) => { const {headers, query} = req; /* istanbul ignore next */ - const host = headers["x-forwarded-host"] || req.get("host"); + const host = headers["x-forwarded-host"] || req.get("host") || ""; let locale = options.usei18nSubdomain ? // Set the locale based upon the subdomain /^\w*/.exec(host)[0] : diff --git a/src/server/middlewares/cache.js b/src/server/middlewares/cache.js index 2ff6539..d9fe788 100644 --- a/src/server/middlewares/cache.js +++ b/src/server/middlewares/cache.js @@ -1,8 +1,14 @@ +// @flow + const config = require("../../lib/config"); // Utility method of setting the cache header on a request // Used as a piece of Express middleware -module.exports = (hours) => (req, res, next) => { +module.exports = (hours: number) => ( + req: express$Request, + res: express$Response, + next: express$NextFunction, +) => { /* istanbul ignore if */ if (config.NODE_ENV === "production") { res.setHeader("Cache-Control", `public, max-age=${hours * 3600}`); diff --git a/src/server/routes.js b/src/server/routes.js index 13efa72..1521209 100644 --- a/src/server/routes.js +++ b/src/server/routes.js @@ -1,15 +1,12 @@ -const fs = require("fs"); -const path = require("path"); +// @flow const options = require("../lib/options"); -const basePath = path.resolve(__dirname, "../logic/"); +module.exports = function(app: express$Application) { + const {auth} = require("../logic/shared/auth"); -module.exports = function(app) { if (options.authRequired) { - const auth = require(path.join(basePath, "shared", "auth.js")); - - app.use((req, res, next) => { + app.use((req: express$Request, res, next) => { const {path} = req; if (path === "/login" || path === "/logout") { @@ -21,16 +18,21 @@ module.exports = function(app) { } // Import all the logic routes - fs.readdirSync(basePath).forEach((file) => { - if (file.endsWith(".js")) { - const logic = require(path.resolve(basePath, file))(app); - logic.routes(); - } - }); + require("../logic/admin")(app).routes(); + require("../logic/create")(app).routes(); + require("../logic/edit")(app).routes(); + require("../logic/home")(app).routes(); + require("../logic/search")(app).routes(); + require("../logic/sitemaps")(app).routes(); + require("../logic/uploads")(app).routes(); + require("../logic/users")(app).routes(); + + // Keep at end as it has a catch-all route + require("../logic/view")(app).routes(); // Enable error handling and displaying of a 500 error page // when an exception is thrown - app.use((err, req, res, next) => { + app.use((err: ?Error, req: express$Request, res, next) => { /* istanbul ignore else */ if (err) { res.status(500).render("Error", { @@ -43,9 +45,10 @@ module.exports = function(app) { }); // Handle missing pages - app.use(({i18n}, res) => { + app.use(({i18n}: express$Request, res, next) => { res.status(404).render("Error", { title: i18n.gettext("Page Not Found"), }); + next(); }); }; diff --git a/src/server/server.js b/src/server/server.js index 9a39d28..96ecc48 100644 --- a/src/server/server.js +++ b/src/server/server.js @@ -1,3 +1,5 @@ +// @flow + const express = require("express"); const init = require("../lib/init"); @@ -10,11 +12,11 @@ const routes = require("./routes"); const tmplVars = require("./tmpl-vars"); const cron = require("./cron"); -module.exports = (callback) => { +module.exports = (callback: (err: ?Error, server: Server) => void) => { const port = config.PORT; const app = express(); - init((err) => { + init((err?: Error) => { /* istanbul ignore if */ if (err) { return callback(err); diff --git a/src/tests/init.js b/src/tests/init.js index a69bbcf..3890f53 100644 --- a/src/tests/init.js +++ b/src/tests/init.js @@ -72,6 +72,21 @@ let similarAdded; let user; let users; +const login = (request, email, callback) => { + request.post({ + url: "http://localhost:3000/login", + form: { + email, + password: "test", + }, + }, callback); +}; + +const adminLogin = (request, callback) => + login(request, "test@test.com", callback); +const normalLogin = (request, callback) => + login(request, "normal@test.com", callback); + // Sandbox the bound methods let sandbox; @@ -1029,6 +1044,8 @@ module.exports = { getUploads: () => uploads, getUploadImage: () => uploadImage, getUser: () => user, + adminLogin, + normalLogin, i18n, Image, Record, diff --git a/src/tests/logic/admin.js b/src/tests/logic/admin.js index aa5c7f2..f0c22c2 100644 --- a/src/tests/logic/admin.js +++ b/src/tests/logic/admin.js @@ -4,23 +4,10 @@ const path = require("path"); const tap = require("tap"); const request = require("request").defaults({jar: true}); -require("../init"); - -const login = (email, callback) => { - request.post({ - url: "http://localhost:3000/login", - form: { - email, - password: "test", - }, - }, callback); -}; - -const adminLogin = (callback) => login("test@test.com", callback); -const normalLogin = (callback) => login("normal@test.com", callback); +const {adminLogin, normalLogin} = require("../init"); tap.test("Admin Page", (t) => { - adminLogin(() => { + adminLogin(request, () => { const url = "http://localhost:3000/artworks/source/test/admin"; request.get(url, (err, res) => { t.error(err, "Error should be empty."); @@ -42,18 +29,18 @@ tap.test("Admin Page (Logged Out)", (t) => { }); tap.test("Admin Page (Unauthorized User)", (t) => { - normalLogin(() => { + normalLogin(request, () => { const url = "http://localhost:3000/artworks/source/test/admin"; request.get(url, (err, res) => { t.error(err, "Error should be empty."); - t.equal(res.statusCode, 500); + t.equal(res.statusCode, 403); t.end(); }); }); }); tap.test("Record Import Page", (t) => { - adminLogin(() => { + adminLogin(request, () => { const url = "http://localhost:3000/artworks/source/test/admin" + "?records=test/started"; request.get(url, (err, res) => { @@ -65,7 +52,7 @@ tap.test("Record Import Page", (t) => { }); tap.test("Record Import Page (Completed)", (t) => { - adminLogin(() => { + adminLogin(request, () => { const url = "http://localhost:3000/artworks/source/test/admin" + "?records=test/completed"; request.get(url, (err, res) => { @@ -77,7 +64,7 @@ tap.test("Record Import Page (Completed)", (t) => { }); tap.test("Record Import Page (Error)", (t) => { - adminLogin(() => { + adminLogin(request, () => { const url = "http://localhost:3000/artworks/source/test/admin" + "?records=test/error"; request.get(url, (err, res) => { @@ -89,7 +76,7 @@ tap.test("Record Import Page (Error)", (t) => { }); tap.test("Record Import Page (Missing)", (t) => { - adminLogin(() => { + adminLogin(request, () => { const url = "http://localhost:3000/artworks/source/test/admin" + "?records=test/foo"; request.get(url, (err, res) => { @@ -101,7 +88,7 @@ tap.test("Record Import Page (Missing)", (t) => { }); tap.test("Record Import Finalize", (t) => { - adminLogin(() => { + adminLogin(request, () => { const url = "http://localhost:3000/artworks/source/test/admin" + "?records=test/started&finalize=true"; request.get(url, (err, res) => { @@ -115,7 +102,7 @@ tap.test("Record Import Finalize", (t) => { }); tap.test("Record Import Abandon", (t) => { - adminLogin(() => { + adminLogin(request, () => { const url = "http://localhost:3000/artworks/source/test/admin" + "?records=test/started&abandon=true"; request.get(url, (err, res) => { @@ -129,7 +116,7 @@ tap.test("Record Import Abandon", (t) => { }); tap.test("Image Import Page", (t) => { - adminLogin(() => { + adminLogin(request, () => { const url = "http://localhost:3000/artworks/source/test/admin" + "?images=test/started"; request.get(url, (err, res) => { @@ -141,7 +128,7 @@ tap.test("Image Import Page", (t) => { }); tap.test("Image Import Page (Completed)", (t) => { - adminLogin(() => { + adminLogin(request, () => { const url = "http://localhost:3000/artworks/source/test/admin" + "?images=test/completed"; request.get(url, (err, res) => { @@ -153,7 +140,7 @@ tap.test("Image Import Page (Completed)", (t) => { }); tap.test("Image Import Page (Completed, Expanded)", (t) => { - adminLogin(() => { + adminLogin(request, () => { const url = "http://localhost:3000/artworks/source/test/admin" + "?images=test/completed&expanded=models"; request.get(url, (err, res) => { @@ -165,7 +152,7 @@ tap.test("Image Import Page (Completed, Expanded)", (t) => { }); tap.test("Image Import Page (Error)", (t) => { - adminLogin(() => { + adminLogin(request, () => { const url = "http://localhost:3000/artworks/source/test/admin" + "?images=test/error"; request.get(url, (err, res) => { @@ -177,7 +164,7 @@ tap.test("Image Import Page (Error)", (t) => { }); tap.test("Image Import Page (Missing)", (t) => { - adminLogin(() => { + adminLogin(request, () => { const url = "http://localhost:3000/artworks/source/test/admin" + "?images=test/foo"; request.get(url, (err, res) => { @@ -189,7 +176,7 @@ tap.test("Image Import Page (Missing)", (t) => { }); tap.test("uploadData: Source not found", (t) => { - adminLogin(() => { + adminLogin(request, () => { const url = "http://localhost:3000/artworks/source/foo/upload-data"; const formData = {}; request.post({url, formData}, (err, res) => { @@ -201,7 +188,7 @@ tap.test("uploadData: Source not found", (t) => { }); tap.test("uploadData: No files", (t) => { - adminLogin(() => { + adminLogin(request, () => { const url = "http://localhost:3000/artworks/source/test/upload-data"; const formData = {}; request.post({url, formData}, (err, res, body) => { @@ -214,7 +201,7 @@ tap.test("uploadData: No files", (t) => { }); tap.test("uploadData: File Error", (t) => { - adminLogin(() => { + adminLogin(request, () => { const url = "http://localhost:3000/artworks/source/test/upload-data"; const file = "default-error.json"; const formData = { @@ -238,7 +225,7 @@ tap.test("uploadData: File Error", (t) => { }); tap.test("uploadData: Default File", (t) => { - adminLogin(() => { + adminLogin(request, () => { const url = "http://localhost:3000/artworks/source/test/upload-data"; const file = "default.json"; const formData = { @@ -262,7 +249,7 @@ tap.test("uploadData: Default File", (t) => { }); tap.test("uploadImages: Source not found", (t) => { - adminLogin(() => { + adminLogin(request, () => { const url = "http://localhost:3000/artworks/source/foo/upload-images"; const formData = {}; request.post({url, formData}, (err, res) => { @@ -274,7 +261,7 @@ tap.test("uploadImages: Source not found", (t) => { }); tap.test("uploadImages: No files", (t) => { - adminLogin(() => { + adminLogin(request, () => { const url = "http://localhost:3000/artworks/source/test/upload-images"; const formData = {}; request.post({url, formData}, (err, res, body) => { @@ -287,7 +274,7 @@ tap.test("uploadImages: No files", (t) => { }); tap.test("uploadImages: Empty Zip", (t) => { - adminLogin(() => { + adminLogin(request, () => { const url = "http://localhost:3000/artworks/source/test/upload-images"; const file = "empty.zip"; const formData = { @@ -311,7 +298,7 @@ tap.test("uploadImages: Empty Zip", (t) => { }); tap.test("uploadImages: Corrupted Zip", (t) => { - adminLogin(() => { + adminLogin(request, () => { const url = "http://localhost:3000/artworks/source/test/upload-images"; const file = "corrupted.zip"; const formData = { @@ -335,7 +322,7 @@ tap.test("uploadImages: Corrupted Zip", (t) => { }); tap.test("uploadImages: Normal Zip", (t) => { - adminLogin(() => { + adminLogin(request, () => { const url = "http://localhost:3000/artworks/source/test/upload-images"; const file = "test.zip"; const formData = { diff --git a/src/tests/logic/create.js b/src/tests/logic/create.js new file mode 100644 index 0000000..d5efdc3 --- /dev/null +++ b/src/tests/logic/create.js @@ -0,0 +1,103 @@ +const tap = require("tap"); +const request = require("request").defaults({jar: true}); + +const {adminLogin, normalLogin} = require("../init"); + +tap.test("Clone Record", (t) => { + const url = "http://localhost:3000/artworks/test/1235/clone"; + adminLogin(request, () => { + request.get(url, (err, res) => { + t.error(err, "Error should be empty."); + t.equal(res.statusCode, 200); + t.end(); + }); + }); +}); + +tap.test("Clone Record (Missing)", (t) => { + const url = "http://localhost:3000/artworks/test/abcd/clone"; + adminLogin(request, () => { + request.get(url, (err, res) => { + t.error(err, "Error should be empty."); + t.equal(res.statusCode, 404); + t.end(); + }); + }); +}); + +tap.test("Clone Record (Wrong Type)", (t) => { + const url = "http://localhost:3000/abcd/test/1235/clone"; + adminLogin(request, () => { + request.get(url, (err, res) => { + t.error(err, "Error should be empty."); + t.equal(res.statusCode, 404); + t.end(); + }); + }); +}); + +tap.test("Clone Record (Logged Out)", (t) => { + const url = "http://localhost:3000/artworks/test/1235/clone"; + request.get(url, (err, res) => { + t.error(err, "Error should be empty."); + t.equal(res.statusCode, 200); + t.match(res.request.uri.href, + "http://localhost:3000/login"); + t.end(); + }); +}); + +tap.test("Clone Record (Unauthorized User)", (t) => { + const url = "http://localhost:3000/artworks/test/1235/clone"; + normalLogin(request, () => { + request.get(url, (err, res) => { + t.error(err, "Error should be empty."); + t.equal(res.statusCode, 403); + t.end(); + }); + }); +}); + +tap.test("Create Record", (t) => { + const url = "http://localhost:3000/artworks/create"; + adminLogin(request, () => { + request.get(url, (err, res) => { + t.error(err, "Error should be empty."); + t.equal(res.statusCode, 200); + t.end(); + }); + }); +}); + +tap.test("Create Record (Wrong Type)", (t) => { + const url = "http://localhost:3000/abcd/create"; + adminLogin(request, () => { + request.get(url, (err, res) => { + t.error(err, "Error should be empty."); + t.equal(res.statusCode, 404); + t.end(); + }); + }); +}); + +tap.test("Create Record (Logged Out)", (t) => { + const url = "http://localhost:3000/artworks/create"; + request.get(url, (err, res) => { + t.error(err, "Error should be empty."); + t.equal(res.statusCode, 200); + t.match(res.request.uri.href, + "http://localhost:3000/login"); + t.end(); + }); +}); + +tap.test("Create Record (Unauthorized User)", (t) => { + const url = "http://localhost:3000/artworks/create"; + normalLogin(request, () => { + request.get(url, (err, res) => { + t.error(err, "Error should be empty."); + t.equal(res.statusCode, 403); + t.end(); + }); + }); +}); diff --git a/src/tests/logic/edit.js b/src/tests/logic/edit.js new file mode 100644 index 0000000..6dd8a6d --- /dev/null +++ b/src/tests/logic/edit.js @@ -0,0 +1,59 @@ +const tap = require("tap"); +const request = require("request").defaults({jar: true}); + +const {adminLogin, normalLogin} = require("../init"); + +tap.test("Edit Record", (t) => { + const url = "http://localhost:3000/artworks/test/1235/edit"; + adminLogin(request, () => { + request.get(url, (err, res) => { + t.error(err, "Error should be empty."); + t.equal(res.statusCode, 200); + t.end(); + }); + }); +}); + +tap.test("Edit Record (Missing)", (t) => { + const url = "http://localhost:3000/artworks/test/abcd/edit"; + adminLogin(request, () => { + request.get(url, (err, res) => { + t.error(err, "Error should be empty."); + t.equal(res.statusCode, 404); + t.end(); + }); + }); +}); + +tap.test("Edit Record (Wrong Type)", (t) => { + const url = "http://localhost:3000/abcd/test/1235/edit"; + adminLogin(request, () => { + request.get(url, (err, res) => { + t.error(err, "Error should be empty."); + t.equal(res.statusCode, 404); + t.end(); + }); + }); +}); + +tap.test("Edit Record (Logged Out)", (t) => { + const url = "http://localhost:3000/artworks/test/1235/edit"; + request.get(url, (err, res) => { + t.error(err, "Error should be empty."); + t.equal(res.statusCode, 200); + t.match(res.request.uri.href, + "http://localhost:3000/login"); + t.end(); + }); +}); + +tap.test("Edit Record (Unauthorized User)", (t) => { + const url = "http://localhost:3000/artworks/test/1235/edit"; + normalLogin(request, () => { + request.get(url, (err, res) => { + t.error(err, "Error should be empty."); + t.equal(res.statusCode, 403); + t.end(); + }); + }); +}); diff --git a/src/tests/logic/records.js b/src/tests/logic/search.js similarity index 83% rename from src/tests/logic/records.js rename to src/tests/logic/search.js index 8709277..df19f6b 100644 --- a/src/tests/logic/records.js +++ b/src/tests/logic/search.js @@ -48,42 +48,6 @@ tap.test("By Source Missing", (t) => { }); }); -tap.test("Record", (t) => { - const url = "http://localhost:3000/artworks/test/1234"; - request.get(url, (err, res) => { - t.error(err, "Error should be empty."); - t.equal(res.statusCode, 200); - t.end(); - }); -}); - -tap.test("Record (Similar Images)", (t) => { - const url = "http://localhost:3000/artworks/test/1235"; - request.get(url, (err, res) => { - t.error(err, "Error should be empty."); - t.equal(res.statusCode, 200); - t.end(); - }); -}); - -tap.test("Record Compare", (t) => { - const url = "http://localhost:3000/artworks/test/1235?compare"; - request.get(url, (err, res) => { - t.error(err, "Error should be empty."); - t.equal(res.statusCode, 200); - t.end(); - }); -}); - -tap.test("Record Missing", (t) => { - const url = "http://localhost:3000/artworks/test/foo"; - request.get(url, (err, res) => { - t.error(err, "Error should be empty."); - t.equal(res.statusCode, 404); - t.end(); - }); -}); - tap.test("Search: Filter", (t) => { const url = "http://localhost:3000/artworks/search?filter=test"; request.get(url, (err, res) => { diff --git a/src/tests/logic/view.js b/src/tests/logic/view.js new file mode 100644 index 0000000..143c0cd --- /dev/null +++ b/src/tests/logic/view.js @@ -0,0 +1,40 @@ +const tap = require("tap"); +const request = require("request"); + +require("../init"); + +tap.test("Record", (t) => { + const url = "http://localhost:3000/artworks/test/1234"; + request.get(url, (err, res) => { + t.error(err, "Error should be empty."); + t.equal(res.statusCode, 200); + t.end(); + }); +}); + +tap.test("Record (Similar Images)", (t) => { + const url = "http://localhost:3000/artworks/test/1235"; + request.get(url, (err, res) => { + t.error(err, "Error should be empty."); + t.equal(res.statusCode, 200); + t.end(); + }); +}); + +tap.test("Record Compare", (t) => { + const url = "http://localhost:3000/artworks/test/1235?compare"; + request.get(url, (err, res) => { + t.error(err, "Error should be empty."); + t.equal(res.statusCode, 200); + t.end(); + }); +}); + +tap.test("Record Missing", (t) => { + const url = "http://localhost:3000/artworks/test/foo"; + request.get(url, (err, res) => { + t.error(err, "Error should be empty."); + t.equal(res.statusCode, 404); + t.end(); + }); +}); diff --git a/src/views/types.js b/src/views/types.js index 9001e95..53d132e 100644 --- a/src/views/types.js +++ b/src/views/types.js @@ -77,8 +77,8 @@ type YearRange = BaseModelType & { to?: number, }>, placeholder?: { - start?: number, - end?: number, + start?: number | string, + end?: number | string, }, }; diff --git a/src/views/types/edit/LinkedRecord.js b/src/views/types/edit/LinkedRecord.js index 82e1871..e47ddab 100644 --- a/src/views/types/edit/LinkedRecord.js +++ b/src/views/types/edit/LinkedRecord.js @@ -27,7 +27,12 @@ class LinkedRecordEdit extends React.Component { credentials: "same-origin", }) .then((res) => res.json()) - .then(({records}) => { + .then(({records}: { + records: Array<{ + _id: string, + getTitle: string, + }>, + }) => { return { options: records .filter((record) => record) diff --git a/types/express_v4.x.x.js b/types/express_v4.x.x.js new file mode 100644 index 0000000..583a197 --- /dev/null +++ b/types/express_v4.x.x.js @@ -0,0 +1,211 @@ +// flow-typed signature: 5800b9dee6b8969ab2b5fa338d02a89f +// flow-typed version: 473c121609/express_v4.x.x/flow_>=v0.32.x + +// NOTE(jeresig): This is a modified version of the Express flow types +// from flow-typed, modified to include the custom properties on the +// request object. Theoretically it should be possible to extend this +// according to this diff, but I can't figure it out: +// https://github.com/flowtype/flow-typed/pull/508 + +import type { Server } from 'http'; + +declare type express$RouterOptions = { + caseSensitive?: boolean, + mergeParams?: boolean, + strict?: boolean +}; + +declare class express$RequestResponseBase { + app: express$Application; + get(field: string): string | void; +} + +declare class express$Request extends http$IncomingMessage mixins express$RequestResponseBase { + baseUrl: string; + body: mixed; + cookies: {[cookie: string]: string}; + fresh: boolean; + hostname: string; + ip: string; + ips: Array; + method: string; + originalUrl: string; + params: {[param: string]: string}; + path: string; + protocol: 'https' | 'http'; + query: {[name: string]: string}; + route: string; + secure: boolean; + signedCookies: {[signedCookie: string]: string}; + stale: boolean; + subdomains: Array; + xhr: boolean; + accepts(types: string): string | false; + acceptsCharsets(...charsets: Array): string | false; + acceptsEncodings(...encoding: Array): string | false; + acceptsLanguages(...lang: Array): string | false; + header(field: string): string | void; + is(type: string): boolean; + param(name: string, defaultValue?: string): string | void; + + // Custom properties + lang: string; + i18n: { + lang: string; + gettext: (message: string) => string; + format: (msg: string, fields: {}) => string; + }; + user?: { + email: string, + sourceAdmin: Array, + siteAdmin: boolean, + getEditableSourcesByType: () => { + [type: string]: Array, + }, + }; +} + +declare type express$CookieOptions = { + domain?: string, + encode?: (value: string) => string, + expires?: Date, + httpOnly?: boolean, + maxAge?: number, + path?: string, + secure?: boolean, + signed?: boolean +}; + +declare type express$RenderCallback = (err: Error | null, html?: string) => mixed; + +declare type express$SendFileOptions = { + maxAge?: number, + root?: string, + lastModified?: boolean, + headers?: {[name: string]: string}, + dotfiles?: 'allow' | 'deny' | 'ignore' +}; + +declare class express$Response extends http$ServerResponse mixins express$RequestResponseBase { + headersSent: boolean; + locals: {[name: string]: mixed}; + append(field: string, value?: string): this; + attachment(filename?: string): this; + cookie(name: string, value: string, options?: express$CookieOptions): this; + clearCookie(name: string, options?: express$CookieOptions): this; + download(path: string, filename?: string, callback?: (err?: ?Error) => void): this; + format(typesObject: {[type: string]: Function}): this; + json(body?: mixed): this; + jsonp(body?: mixed): this; + links(links: {[name: string]: string}): this; + location(path: string): this; + redirect(url: string, ...args: Array): this; + redirect(status: number, url: string, ...args: Array): this; + render(view: string, locals?: {[name: string]: mixed}, callback?: express$RenderCallback): this; + send(body?: mixed): this; + sendFile(path: string, options?: express$SendFileOptions, callback?: (err?: ?Error) => mixed): this; + sendStatus(statusCode: number): this; + header(field: string, value?: string): this; + header(headers: {[name: string]: string}): this; + set(field: string, value?: string): this; + set(headers: {[name: string]: string}): this; + status(statusCode: number): this; + type(type: string): this; + vary(field: string): this; +} + +declare type express$NextFunction = (err?: ?Error) => mixed; +declare type express$Middleware = + ((req: express$Request, res: express$Response, next: express$NextFunction) => mixed) | + ((error: ?Error, req: express$Request, res: express$Response, next: express$NextFunction) => mixed); +declare interface express$RouteMethodType { + (middleware: express$Middleware): T; + (...middleware: Array): T; + (path: string|RegExp|string[], ...middleware: Array): T; +} +declare class express$Route { + all: express$RouteMethodType; + get: express$RouteMethodType; + post: express$RouteMethodType; + put: express$RouteMethodType; + head: express$RouteMethodType; + delete: express$RouteMethodType; + options: express$RouteMethodType; + trace: express$RouteMethodType; + copy: express$RouteMethodType; + lock: express$RouteMethodType; + mkcol: express$RouteMethodType; + move: express$RouteMethodType; + purge: express$RouteMethodType; + propfind: express$RouteMethodType; + proppatch: express$RouteMethodType; + unlock: express$RouteMethodType; + report: express$RouteMethodType; + mkactivity: express$RouteMethodType; + checkout: express$RouteMethodType; + merge: express$RouteMethodType; + + // @TODO Missing 'm-search' but get flow illegal name error. + + notify: express$RouteMethodType; + subscribe: express$RouteMethodType; + unsubscribe: express$RouteMethodType; + patch: express$RouteMethodType; + search: express$RouteMethodType; + connect: express$RouteMethodType; +} + +declare class express$Router extends express$Route { + constructor(options?: express$RouterOptions): void; + route(path: string): express$Route; + static (): express$Router; + use(middleware: express$Middleware): this; + use(...middleware: Array): this; + use(path: string|RegExp|string[], ...middleware: Array): this; + use(path: string, router: express$Router): this; + handle(req: http$IncomingMessage, res: http$ServerResponse, next: express$NextFunction): void; + + // Can't use regular callable signature syntax due to https://github.com/facebook/flow/issues/3084 + $call: (req: http$IncomingMessage, res: http$ServerResponse, next?: ?express$NextFunction) => void; +} + +declare class express$Application extends express$Router mixins events$EventEmitter { + constructor(): void; + locals: {[name: string]: mixed}; + mountpath: string; + listen(port: number, hostname?: string, backlog?: number, callback?: (err?: ?Error) => mixed): Server; + listen(port: number, hostname?: string, callback?: (err?: ?Error) => mixed): Server; + listen(port: number, callback?: (err?: ?Error) => mixed): Server; + listen(path: string, callback?: (err?: ?Error) => mixed): Server; + listen(handle: Object, callback?: (err?: ?Error) => mixed): Server; + disable(name: string): void; + disabled(name: string): boolean; + enable(name: string): void; + enabled(name: string): boolean; + engine(name: string, callback: Function): void; + /** + * Mixed will not be taken as a value option. Issue around using the GET http method name and the get for settings. + */ + // get(name: string): mixed; + set(name: string, value: mixed): mixed; + render(name: string, optionsOrFunction: {[name: string]: mixed}, callback: express$RenderCallback): void; + handle(req: http$IncomingMessage, res: http$ServerResponse, next?: ?express$NextFunction): void; +} + +declare module 'express' { + declare function serveStatic(root: string, options?: Object): express$Middleware; + + declare type RouterOptions = express$RouterOptions; + declare type CookieOptions = express$CookieOptions; + declare type Middleware = express$Middleware; + declare type NextFunction = express$NextFunction; + declare type $Response = express$Response; + declare type $Request = express$Request; + declare type $Application = express$Application; + + declare module.exports: { + (): express$Application, // If you try to call like a function, it will use this signature + static: serveStatic, // `static` property on the function + Router: typeof express$Router, // `Router` property on the function + }; +}