* Converts import script to use public API endpoints The endpoints don't exist yet, but this is a good way to see how the implementation of the data structures differ. * Exposes public API endpoint to fetch user's sketches * Implements public API delete endpoint * Adds helper to create custom ApplicationError classes * Adds create project endpoint that understand API's data structure This transforms the nested tree of file data into a mongoose Project model * Returns '201 Created' to match API spec * Removes 'CustomError' variable assignment as it shows up in test output * transformFiles will return file validation errors * Tests API project controller * Tests toModel() * Creates default files if no root-level .html file is provided * Do not auto-generate a slug if it is provided Fixes a bug where the slug was auto-generated using the sketch name, even if a slug property had been provided. * Validates uniqueness of slugs for projects created by the public API * Adds tests for slug uniqueness * Configures node's Promise implementation for mongoose (fixes warnings) * Moves createProject tests to match controller location * Adds support for code to ApplicationErrors * deleteProject controller tests * getProjectsForUser controller tests - implements tests - update apiKey tests to use new User mocks * Ensure error objects have consistent property names `message` is used as a high-level description of the errors `detail` is optional and has an plain language explanation of the individual errors `errors` is an array of each individual problem from `detail` in a machine-readable format * Assert environment variables are provided at script start * Version public API * Expect "files" property to always be provided * Fixes linting error * Converts import script to use public API endpoints The endpoints don't exist yet, but this is a good way to see how the implementation of the data structures differ. * Exposes public API endpoint to fetch user's sketches * Implements public API delete endpoint * Adds helper to create custom ApplicationError classes * Adds create project endpoint that understand API's data structure This transforms the nested tree of file data into a mongoose Project model * Returns '201 Created' to match API spec * Removes 'CustomError' variable assignment as it shows up in test output * transformFiles will return file validation errors * Tests API project controller * Tests toModel() * Creates default files if no root-level .html file is provided * Do not auto-generate a slug if it is provided Fixes a bug where the slug was auto-generated using the sketch name, even if a slug property had been provided. * Validates uniqueness of slugs for projects created by the public API * Adds tests for slug uniqueness * Configures node's Promise implementation for mongoose (fixes warnings) * Moves createProject tests to match controller location * deleteProject controller tests * Adds support for code to ApplicationErrors * getProjectsForUser controller tests - implements tests - update apiKey tests to use new User mocks * Ensure error objects have consistent property names `message` is used as a high-level description of the errors `detail` is optional and has an plain language explanation of the individual errors `errors` is an array of each individual problem from `detail` in a machine-readable format * Assert environment variables are provided at script start * Version public API * Expect "files" property to always be provided * Fixes linting error * Checks that authenticated user has permission to create under this namespace Previously, the project was always created under the authenticated user's namespace, but this not obvious behaviour.
This commit is contained in:
parent
7ea4ae5637
commit
37fcf46972
24 changed files with 1905 additions and 552 deletions
|
@ -1,5 +1,8 @@
|
||||||
import { configure } from 'enzyme'
|
import { configure } from 'enzyme'
|
||||||
import Adapter from 'enzyme-adapter-react-16'
|
import Adapter from 'enzyme-adapter-react-16'
|
||||||
import '@babel/polyfill'
|
import '@babel/polyfill'
|
||||||
|
import mongoose from 'mongoose'
|
||||||
|
|
||||||
|
mongoose.Promise = global.Promise;
|
||||||
|
|
||||||
configure({ adapter: new Adapter() })
|
configure({ adapter: new Adapter() })
|
||||||
|
|
5
server/controllers/__mocks__/aws.controller.js
Normal file
5
server/controllers/__mocks__/aws.controller.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export const getObjectKey = jest.mock();
|
||||||
|
export const deleteObjectsFromS3 = jest.fn();
|
||||||
|
export const signS3 = jest.fn();
|
||||||
|
export const copyObjectInS3 = jest.fn();
|
||||||
|
export const listObjectsInS3ForUser = jest.fn();
|
|
@ -1,155 +0,0 @@
|
||||||
/**
|
|
||||||
* @jest-environment node
|
|
||||||
*/
|
|
||||||
import { Response } from 'jest-express';
|
|
||||||
|
|
||||||
import { createMock } from '../../models/project';
|
|
||||||
import createProject from '../project.controller/createProject';
|
|
||||||
|
|
||||||
jest.mock('../../models/project');
|
|
||||||
|
|
||||||
describe('project.controller', () => {
|
|
||||||
describe('createProject()', () => {
|
|
||||||
let ProjectMock;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
ProjectMock = createMock();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
ProjectMock.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fails if create fails', (done) => {
|
|
||||||
const error = new Error('An error');
|
|
||||||
|
|
||||||
ProjectMock
|
|
||||||
.expects('create')
|
|
||||||
.rejects(error);
|
|
||||||
|
|
||||||
const request = { user: {} };
|
|
||||||
const response = new Response();
|
|
||||||
|
|
||||||
const promise = createProject(request, response);
|
|
||||||
|
|
||||||
function expectations() {
|
|
||||||
expect(response.json).toHaveBeenCalledWith({ success: false });
|
|
||||||
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
|
|
||||||
promise.then(expectations, expectations).catch(expectations);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('extracts parameters from request body', (done) => {
|
|
||||||
const request = {
|
|
||||||
user: { _id: 'abc123' },
|
|
||||||
body: {
|
|
||||||
name: 'Wriggly worm',
|
|
||||||
files: [{ name: 'file.js', content: 'var hello = true;' }]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const response = new Response();
|
|
||||||
|
|
||||||
|
|
||||||
ProjectMock
|
|
||||||
.expects('create')
|
|
||||||
.withArgs({
|
|
||||||
user: 'abc123',
|
|
||||||
name: 'Wriggly worm',
|
|
||||||
files: [{ name: 'file.js', content: 'var hello = true;' }]
|
|
||||||
})
|
|
||||||
.resolves();
|
|
||||||
|
|
||||||
const promise = createProject(request, response);
|
|
||||||
|
|
||||||
function expectations() {
|
|
||||||
expect(response.json).toHaveBeenCalled();
|
|
||||||
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
|
|
||||||
promise.then(expectations, expectations).catch(expectations);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: This should be extracted to a new model object
|
|
||||||
// so the controllers just have to call a single
|
|
||||||
// method for this operation
|
|
||||||
it('populates referenced user on project creation', (done) => {
|
|
||||||
const request = { user: { _id: 'abc123' } };
|
|
||||||
const response = new Response();
|
|
||||||
|
|
||||||
const result = {
|
|
||||||
_id: 'abc123',
|
|
||||||
id: 'abc123',
|
|
||||||
name: 'Project name',
|
|
||||||
serveSecure: false,
|
|
||||||
files: []
|
|
||||||
};
|
|
||||||
|
|
||||||
const resultWithUser = {
|
|
||||||
...result,
|
|
||||||
user: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
ProjectMock
|
|
||||||
.expects('create')
|
|
||||||
.withArgs({ user: 'abc123' })
|
|
||||||
.resolves(result);
|
|
||||||
|
|
||||||
ProjectMock
|
|
||||||
.expects('populate')
|
|
||||||
.withArgs(result)
|
|
||||||
.yields(null, resultWithUser)
|
|
||||||
.resolves(resultWithUser);
|
|
||||||
|
|
||||||
const promise = createProject(request, response);
|
|
||||||
|
|
||||||
function expectations() {
|
|
||||||
const doc = response.json.mock.calls[0][0];
|
|
||||||
|
|
||||||
expect(response.json).toHaveBeenCalled();
|
|
||||||
|
|
||||||
expect(JSON.parse(JSON.stringify(doc))).toMatchObject(resultWithUser);
|
|
||||||
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
|
|
||||||
promise.then(expectations, expectations).catch(expectations);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fails if referenced user population fails', (done) => {
|
|
||||||
const request = { user: { _id: 'abc123' } };
|
|
||||||
const response = new Response();
|
|
||||||
|
|
||||||
const result = {
|
|
||||||
_id: 'abc123',
|
|
||||||
id: 'abc123',
|
|
||||||
name: 'Project name',
|
|
||||||
serveSecure: false,
|
|
||||||
files: []
|
|
||||||
};
|
|
||||||
|
|
||||||
const error = new Error('An error');
|
|
||||||
|
|
||||||
ProjectMock
|
|
||||||
.expects('create')
|
|
||||||
.resolves(result);
|
|
||||||
|
|
||||||
ProjectMock
|
|
||||||
.expects('populate')
|
|
||||||
.yields(error)
|
|
||||||
.resolves(error);
|
|
||||||
|
|
||||||
const promise = createProject(request, response);
|
|
||||||
|
|
||||||
function expectations() {
|
|
||||||
expect(response.json).toHaveBeenCalledWith({ success: false });
|
|
||||||
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
|
|
||||||
promise.then(expectations, expectations).catch(expectations);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -2,7 +2,6 @@ import archiver from 'archiver';
|
||||||
import format from 'date-fns/format';
|
import format from 'date-fns/format';
|
||||||
import isUrl from 'is-url';
|
import isUrl from 'is-url';
|
||||||
import jsdom, { serializeDocument } from 'jsdom';
|
import jsdom, { serializeDocument } from 'jsdom';
|
||||||
import isBefore from 'date-fns/is_before';
|
|
||||||
import isAfter from 'date-fns/is_after';
|
import isAfter from 'date-fns/is_after';
|
||||||
import request from 'request';
|
import request from 'request';
|
||||||
import slugify from 'slugify';
|
import slugify from 'slugify';
|
||||||
|
@ -10,9 +9,10 @@ import Project from '../models/project';
|
||||||
import User from '../models/user';
|
import User from '../models/user';
|
||||||
import { resolvePathToFile } from '../utils/filePath';
|
import { resolvePathToFile } from '../utils/filePath';
|
||||||
import generateFileSystemSafeName from '../utils/generateFileSystemSafeName';
|
import generateFileSystemSafeName from '../utils/generateFileSystemSafeName';
|
||||||
import { deleteObjectsFromS3, getObjectKey } from './aws.controller';
|
|
||||||
|
|
||||||
export { default as createProject } from './project.controller/createProject';
|
export { default as createProject, apiCreateProject } from './project.controller/createProject';
|
||||||
|
export { default as deleteProject } from './project.controller/deleteProject';
|
||||||
|
export { default as getProjectsForUser, apiGetProjectsForUser } from './project.controller/getProjectsForUser';
|
||||||
|
|
||||||
export function updateProject(req, res) {
|
export function updateProject(req, res) {
|
||||||
Project.findById(req.params.project_id, (findProjectErr, project) => {
|
Project.findById(req.params.project_id, (findProjectErr, project) => {
|
||||||
|
@ -84,37 +84,6 @@ export function getProject(req, res) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteFilesFromS3(files) {
|
|
||||||
deleteObjectsFromS3(files.filter((file) => {
|
|
||||||
if (file.url) {
|
|
||||||
if (!process.env.S3_DATE || (
|
|
||||||
process.env.S3_DATE &&
|
|
||||||
isBefore(new Date(process.env.S3_DATE), new Date(file.createdAt)))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
.map(file => getObjectKey(file.url)));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deleteProject(req, res) {
|
|
||||||
Project.findById(req.params.project_id, (findProjectErr, project) => {
|
|
||||||
if (!project.user.equals(req.user._id)) {
|
|
||||||
res.status(403).json({ success: false, message: 'Session does not match owner of project.' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
deleteFilesFromS3(project.files);
|
|
||||||
Project.remove({ _id: req.params.project_id }, (removeProjectError) => {
|
|
||||||
if (removeProjectError) {
|
|
||||||
res.status(404).send({ message: 'Project with that id does not exist' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
res.json({ success: true });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getProjectsForUserId(userId) {
|
export function getProjectsForUserId(userId) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
Project.find({ user: userId })
|
Project.find({ user: userId })
|
||||||
|
@ -157,10 +126,6 @@ export function getProjectAsset(req, res) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProjectsForUserName(username) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getProjects(req, res) {
|
export function getProjects(req, res) {
|
||||||
if (req.user) {
|
if (req.user) {
|
||||||
getProjectsForUserId(req.user._id)
|
getProjectsForUserId(req.user._id)
|
||||||
|
@ -173,24 +138,6 @@ export function getProjects(req, res) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProjectsForUser(req, res) {
|
|
||||||
if (req.params.username) {
|
|
||||||
User.findOne({ username: req.params.username }, (err, user) => {
|
|
||||||
if (!user) {
|
|
||||||
res.status(404).json({ message: 'User with that username does not exist.' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Project.find({ user: user._id })
|
|
||||||
.sort('-createdAt')
|
|
||||||
.select('name files id createdAt updatedAt')
|
|
||||||
.exec((innerErr, projects) => res.json(projects));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// could just move this to client side
|
|
||||||
res.json([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function projectExists(projectId, callback) {
|
export function projectExists(projectId, callback) {
|
||||||
Project.findById(projectId, (err, project) => (
|
Project.findById(projectId, (err, project) => (
|
||||||
project ? callback(true) : callback(false)
|
project ? callback(true) : callback(false)
|
||||||
|
|
|
@ -0,0 +1,391 @@
|
||||||
|
/**
|
||||||
|
* @jest-environment node
|
||||||
|
*/
|
||||||
|
import { Response } from 'jest-express';
|
||||||
|
|
||||||
|
import Project, { createMock, createInstanceMock } from '../../../models/project';
|
||||||
|
import createProject, { apiCreateProject } from '../createProject';
|
||||||
|
|
||||||
|
jest.mock('../../../models/project');
|
||||||
|
|
||||||
|
describe('project.controller', () => {
|
||||||
|
describe('createProject()', () => {
|
||||||
|
let ProjectMock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ProjectMock = createMock();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
ProjectMock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails if create fails', (done) => {
|
||||||
|
const error = new Error('An error');
|
||||||
|
|
||||||
|
ProjectMock
|
||||||
|
.expects('create')
|
||||||
|
.rejects(error);
|
||||||
|
|
||||||
|
const request = { user: {} };
|
||||||
|
const response = new Response();
|
||||||
|
|
||||||
|
const promise = createProject(request, response);
|
||||||
|
|
||||||
|
function expectations() {
|
||||||
|
expect(response.json).toHaveBeenCalledWith({ success: false });
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(expectations, expectations).catch(expectations);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extracts parameters from request body', (done) => {
|
||||||
|
const request = {
|
||||||
|
user: { _id: 'abc123' },
|
||||||
|
body: {
|
||||||
|
name: 'Wriggly worm',
|
||||||
|
files: [{ name: 'file.js', content: 'var hello = true;' }]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const response = new Response();
|
||||||
|
|
||||||
|
|
||||||
|
ProjectMock
|
||||||
|
.expects('create')
|
||||||
|
.withArgs({
|
||||||
|
user: 'abc123',
|
||||||
|
name: 'Wriggly worm',
|
||||||
|
files: [{ name: 'file.js', content: 'var hello = true;' }]
|
||||||
|
})
|
||||||
|
.resolves();
|
||||||
|
|
||||||
|
const promise = createProject(request, response);
|
||||||
|
|
||||||
|
function expectations() {
|
||||||
|
expect(response.json).toHaveBeenCalled();
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(expectations, expectations).catch(expectations);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: This should be extracted to a new model object
|
||||||
|
// so the controllers just have to call a single
|
||||||
|
// method for this operation
|
||||||
|
it('populates referenced user on project creation', (done) => {
|
||||||
|
const request = { user: { _id: 'abc123' } };
|
||||||
|
const response = new Response();
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
_id: 'abc123',
|
||||||
|
id: 'abc123',
|
||||||
|
name: 'Project name',
|
||||||
|
serveSecure: false,
|
||||||
|
files: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const resultWithUser = {
|
||||||
|
...result,
|
||||||
|
user: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
ProjectMock
|
||||||
|
.expects('create')
|
||||||
|
.withArgs({ user: 'abc123' })
|
||||||
|
.resolves(result);
|
||||||
|
|
||||||
|
ProjectMock
|
||||||
|
.expects('populate')
|
||||||
|
.withArgs(result)
|
||||||
|
.yields(null, resultWithUser)
|
||||||
|
.resolves(resultWithUser);
|
||||||
|
|
||||||
|
const promise = createProject(request, response);
|
||||||
|
|
||||||
|
function expectations() {
|
||||||
|
const doc = response.json.mock.calls[0][0];
|
||||||
|
|
||||||
|
expect(response.json).toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(JSON.parse(JSON.stringify(doc))).toMatchObject(resultWithUser);
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(expectations, expectations).catch(expectations);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails if referenced user population fails', (done) => {
|
||||||
|
const request = { user: { _id: 'abc123' } };
|
||||||
|
const response = new Response();
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
_id: 'abc123',
|
||||||
|
id: 'abc123',
|
||||||
|
name: 'Project name',
|
||||||
|
serveSecure: false,
|
||||||
|
files: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const error = new Error('An error');
|
||||||
|
|
||||||
|
ProjectMock
|
||||||
|
.expects('create')
|
||||||
|
.resolves(result);
|
||||||
|
|
||||||
|
ProjectMock
|
||||||
|
.expects('populate')
|
||||||
|
.yields(error)
|
||||||
|
.resolves(error);
|
||||||
|
|
||||||
|
const promise = createProject(request, response);
|
||||||
|
|
||||||
|
function expectations() {
|
||||||
|
expect(response.json).toHaveBeenCalledWith({ success: false });
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(expectations, expectations).catch(expectations);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('apiCreateProject()', () => {
|
||||||
|
let ProjectMock;
|
||||||
|
let ProjectInstanceMock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ProjectMock = createMock();
|
||||||
|
ProjectInstanceMock = createInstanceMock();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
ProjectMock.restore();
|
||||||
|
ProjectInstanceMock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 201 with id of created sketch', (done) => {
|
||||||
|
const request = {
|
||||||
|
user: { _id: 'abc123', username: 'alice' },
|
||||||
|
params: { username: 'alice' },
|
||||||
|
body: {
|
||||||
|
name: 'My sketch',
|
||||||
|
files: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const response = new Response();
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
_id: 'abc123',
|
||||||
|
id: 'abc123',
|
||||||
|
name: 'Project name',
|
||||||
|
serveSecure: false,
|
||||||
|
files: []
|
||||||
|
};
|
||||||
|
|
||||||
|
ProjectInstanceMock.expects('isSlugUnique')
|
||||||
|
.resolves({ isUnique: true, conflictingIds: [] });
|
||||||
|
|
||||||
|
ProjectInstanceMock.expects('save')
|
||||||
|
.resolves(new Project(result));
|
||||||
|
|
||||||
|
const promise = apiCreateProject(request, response);
|
||||||
|
|
||||||
|
function expectations() {
|
||||||
|
const doc = response.json.mock.calls[0][0];
|
||||||
|
|
||||||
|
expect(response.status).toHaveBeenCalledWith(201);
|
||||||
|
expect(response.json).toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(JSON.parse(JSON.stringify(doc))).toMatchObject({
|
||||||
|
id: 'abc123'
|
||||||
|
});
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(expectations, expectations).catch(expectations);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails if slug is not unique', (done) => {
|
||||||
|
const request = {
|
||||||
|
user: { _id: 'abc123', username: 'alice' },
|
||||||
|
params: { username: 'alice' },
|
||||||
|
body: {
|
||||||
|
name: 'My sketch',
|
||||||
|
slug: 'a-slug',
|
||||||
|
files: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const response = new Response();
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
_id: 'abc123',
|
||||||
|
id: 'abc123',
|
||||||
|
name: 'Project name',
|
||||||
|
// slug: 'a-slug',
|
||||||
|
serveSecure: false,
|
||||||
|
files: []
|
||||||
|
};
|
||||||
|
|
||||||
|
ProjectInstanceMock.expects('isSlugUnique')
|
||||||
|
.resolves({ isUnique: false, conflictingIds: ['cde123'] });
|
||||||
|
|
||||||
|
ProjectInstanceMock.expects('save')
|
||||||
|
.resolves(new Project(result));
|
||||||
|
|
||||||
|
const promise = apiCreateProject(request, response);
|
||||||
|
|
||||||
|
function expectations() {
|
||||||
|
const doc = response.json.mock.calls[0][0];
|
||||||
|
|
||||||
|
expect(response.status).toHaveBeenCalledWith(409);
|
||||||
|
expect(response.json).toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(JSON.parse(JSON.stringify(doc))).toMatchObject({
|
||||||
|
message: 'Sketch Validation Failed',
|
||||||
|
detail: 'Slug "a-slug" is not unique. Check cde123'
|
||||||
|
});
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(expectations, expectations).catch(expectations);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails if user does not have permission', (done) => {
|
||||||
|
const request = {
|
||||||
|
user: { _id: 'abc123', username: 'alice' },
|
||||||
|
params: {
|
||||||
|
username: 'dana',
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
name: 'My sketch',
|
||||||
|
slug: 'a-slug',
|
||||||
|
files: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const response = new Response();
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
_id: 'abc123',
|
||||||
|
id: 'abc123',
|
||||||
|
name: 'Project name',
|
||||||
|
serveSecure: false,
|
||||||
|
files: []
|
||||||
|
};
|
||||||
|
|
||||||
|
ProjectInstanceMock.expects('isSlugUnique')
|
||||||
|
.resolves({ isUnique: true, conflictingIds: [] });
|
||||||
|
|
||||||
|
ProjectInstanceMock.expects('save')
|
||||||
|
.resolves(new Project(result));
|
||||||
|
|
||||||
|
const promise = apiCreateProject(request, response);
|
||||||
|
|
||||||
|
function expectations() {
|
||||||
|
expect(response.status).toHaveBeenCalledWith(401);
|
||||||
|
expect(response.json).toHaveBeenCalled();
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(expectations, expectations).catch(expectations);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns validation errors on files input', (done) => {
|
||||||
|
const request = {
|
||||||
|
user: { username: 'alice' },
|
||||||
|
params: { username: 'alice' },
|
||||||
|
body: {
|
||||||
|
name: 'My sketch',
|
||||||
|
files: {
|
||||||
|
'index.html': {
|
||||||
|
// missing content or url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const response = new Response();
|
||||||
|
|
||||||
|
const promise = apiCreateProject(request, response);
|
||||||
|
|
||||||
|
function expectations() {
|
||||||
|
const doc = response.json.mock.calls[0][0];
|
||||||
|
|
||||||
|
const responseBody = JSON.parse(JSON.stringify(doc));
|
||||||
|
|
||||||
|
expect(response.status).toHaveBeenCalledWith(422);
|
||||||
|
expect(responseBody.message).toBe('File Validation Failed');
|
||||||
|
expect(responseBody.detail).not.toBeNull();
|
||||||
|
expect(responseBody.errors.length).toBe(1);
|
||||||
|
expect(responseBody.errors).toEqual([
|
||||||
|
{ name: 'index.html', message: 'missing \'url\' or \'content\'' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(expectations, expectations).catch(expectations);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects file parameters not in object format', (done) => {
|
||||||
|
const request = {
|
||||||
|
user: { _id: 'abc123', username: 'alice' },
|
||||||
|
params: { username: 'alice' },
|
||||||
|
body: {
|
||||||
|
name: 'Wriggly worm',
|
||||||
|
files: [{ name: 'file.js', content: 'var hello = true;' }]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const response = new Response();
|
||||||
|
|
||||||
|
const promise = apiCreateProject(request, response);
|
||||||
|
|
||||||
|
function expectations() {
|
||||||
|
const doc = response.json.mock.calls[0][0];
|
||||||
|
|
||||||
|
const responseBody = JSON.parse(JSON.stringify(doc));
|
||||||
|
|
||||||
|
expect(response.status).toHaveBeenCalledWith(422);
|
||||||
|
expect(responseBody.message).toBe('File Validation Failed');
|
||||||
|
expect(responseBody.detail).toBe('\'files\' must be an object');
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(expectations, expectations).catch(expectations);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects if files object in not provided', (done) => {
|
||||||
|
const request = {
|
||||||
|
user: { _id: 'abc123', username: 'alice' },
|
||||||
|
params: { username: 'alice' },
|
||||||
|
body: {
|
||||||
|
name: 'Wriggly worm',
|
||||||
|
// files: {} is missing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const response = new Response();
|
||||||
|
|
||||||
|
const promise = apiCreateProject(request, response);
|
||||||
|
|
||||||
|
function expectations() {
|
||||||
|
const doc = response.json.mock.calls[0][0];
|
||||||
|
|
||||||
|
const responseBody = JSON.parse(JSON.stringify(doc));
|
||||||
|
|
||||||
|
expect(response.status).toHaveBeenCalledWith(422);
|
||||||
|
expect(responseBody.message).toBe('File Validation Failed');
|
||||||
|
expect(responseBody.detail).toBe('\'files\' must be an object');
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(expectations, expectations).catch(expectations);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,119 @@
|
||||||
|
/**
|
||||||
|
* @jest-environment node
|
||||||
|
*/
|
||||||
|
import { Request, Response } from 'jest-express';
|
||||||
|
|
||||||
|
import Project, { createMock, createInstanceMock } from '../../../models/project';
|
||||||
|
import User from '../../../models/user';
|
||||||
|
import deleteProject from '../../project.controller/deleteProject';
|
||||||
|
import { deleteObjectsFromS3 } from '../../aws.controller';
|
||||||
|
|
||||||
|
|
||||||
|
jest.mock('../../../models/project');
|
||||||
|
jest.mock('../../aws.controller');
|
||||||
|
|
||||||
|
describe('project.controller', () => {
|
||||||
|
describe('deleteProject()', () => {
|
||||||
|
let ProjectMock;
|
||||||
|
let ProjectInstanceMock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ProjectMock = createMock();
|
||||||
|
ProjectInstanceMock = createInstanceMock();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
ProjectMock.restore();
|
||||||
|
ProjectInstanceMock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 403 if project is not owned by authenticated user', (done) => {
|
||||||
|
const user = new User();
|
||||||
|
const project = new Project();
|
||||||
|
project.user = user;
|
||||||
|
|
||||||
|
const request = new Request();
|
||||||
|
request.setParams({ project_id: project._id });
|
||||||
|
request.user = { _id: 'abc123' };
|
||||||
|
|
||||||
|
const response = new Response();
|
||||||
|
|
||||||
|
ProjectMock
|
||||||
|
.expects('findById')
|
||||||
|
.resolves(project);
|
||||||
|
|
||||||
|
const promise = deleteProject(request, response);
|
||||||
|
|
||||||
|
function expectations() {
|
||||||
|
expect(response.status).toHaveBeenCalledWith(403);
|
||||||
|
expect(response.json).toHaveBeenCalledWith({
|
||||||
|
message: 'Authenticated user does not match owner of project'
|
||||||
|
});
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(expectations, expectations).catch(expectations);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 404 if project does not exist', (done) => {
|
||||||
|
const user = new User();
|
||||||
|
const project = new Project();
|
||||||
|
project.user = user;
|
||||||
|
|
||||||
|
const request = new Request();
|
||||||
|
request.setParams({ project_id: project._id });
|
||||||
|
request.user = { _id: 'abc123' };
|
||||||
|
|
||||||
|
const response = new Response();
|
||||||
|
|
||||||
|
ProjectMock
|
||||||
|
.expects('findById')
|
||||||
|
.resolves(null);
|
||||||
|
|
||||||
|
const promise = deleteProject(request, response);
|
||||||
|
|
||||||
|
function expectations() {
|
||||||
|
expect(response.status).toHaveBeenCalledWith(404);
|
||||||
|
expect(response.json).toHaveBeenCalledWith({
|
||||||
|
message: 'Project with that id does not exist'
|
||||||
|
});
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(expectations, expectations).catch(expectations);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes project and dependent files from S3 ', (done) => {
|
||||||
|
const user = new User();
|
||||||
|
const project = new Project();
|
||||||
|
project.user = user;
|
||||||
|
|
||||||
|
const request = new Request();
|
||||||
|
request.setParams({ project_id: project._id });
|
||||||
|
request.user = { _id: user._id };
|
||||||
|
|
||||||
|
const response = new Response();
|
||||||
|
|
||||||
|
ProjectMock
|
||||||
|
.expects('findById')
|
||||||
|
.resolves(project);
|
||||||
|
|
||||||
|
ProjectInstanceMock.expects('remove')
|
||||||
|
.yields();
|
||||||
|
|
||||||
|
const promise = deleteProject(request, response);
|
||||||
|
|
||||||
|
function expectations() {
|
||||||
|
expect(response.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(response.json).not.toHaveBeenCalled();
|
||||||
|
expect(deleteObjectsFromS3).toHaveBeenCalled();
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(expectations, expectations).catch(expectations);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,151 @@
|
||||||
|
/**
|
||||||
|
* @jest-environment node
|
||||||
|
*/
|
||||||
|
import { Request, Response } from 'jest-express';
|
||||||
|
|
||||||
|
import { createMock } from '../../../models/user';
|
||||||
|
import getProjectsForUser, { apiGetProjectsForUser } from '../../project.controller/getProjectsForUser';
|
||||||
|
|
||||||
|
jest.mock('../../../models/user');
|
||||||
|
jest.mock('../../aws.controller');
|
||||||
|
|
||||||
|
describe('project.controller', () => {
|
||||||
|
let UserMock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
UserMock = createMock();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
UserMock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getProjectsForUser()', () => {
|
||||||
|
it('returns empty array user not supplied as parameter', (done) => {
|
||||||
|
const request = new Request();
|
||||||
|
request.setParams({});
|
||||||
|
const response = new Response();
|
||||||
|
|
||||||
|
const promise = getProjectsForUser(request, response);
|
||||||
|
|
||||||
|
function expectations() {
|
||||||
|
expect(response.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(response.json).toHaveBeenCalledWith([]);
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(expectations, expectations).catch(expectations);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 404 if user does not exist', (done) => {
|
||||||
|
const request = new Request();
|
||||||
|
request.setParams({ username: 'abc123' });
|
||||||
|
const response = new Response();
|
||||||
|
|
||||||
|
UserMock
|
||||||
|
.expects('findOne')
|
||||||
|
.withArgs({ username: 'abc123' })
|
||||||
|
.yields(null, null);
|
||||||
|
|
||||||
|
const promise = getProjectsForUser(request, response);
|
||||||
|
|
||||||
|
function expectations() {
|
||||||
|
expect(response.status).toHaveBeenCalledWith(404);
|
||||||
|
expect(response.json).toHaveBeenCalledWith({ message: 'User with that username does not exist.' });
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(expectations, expectations).catch(expectations);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 500 on other errors', (done) => {
|
||||||
|
const request = new Request();
|
||||||
|
request.setParams({ username: 'abc123' });
|
||||||
|
const response = new Response();
|
||||||
|
|
||||||
|
UserMock
|
||||||
|
.expects('findOne')
|
||||||
|
.withArgs({ username: 'abc123' })
|
||||||
|
.yields(new Error(), null);
|
||||||
|
|
||||||
|
const promise = getProjectsForUser(request, response);
|
||||||
|
|
||||||
|
function expectations() {
|
||||||
|
expect(response.status).toHaveBeenCalledWith(500);
|
||||||
|
expect(response.json).toHaveBeenCalledWith({ message: 'Error fetching projects' });
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(expectations, expectations).catch(expectations);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('apiGetProjectsForUser()', () => {
|
||||||
|
it('returns validation error if user id not provided', (done) => {
|
||||||
|
const request = new Request();
|
||||||
|
request.setParams({});
|
||||||
|
const response = new Response();
|
||||||
|
|
||||||
|
const promise = apiGetProjectsForUser(request, response);
|
||||||
|
|
||||||
|
function expectations() {
|
||||||
|
expect(response.status).toHaveBeenCalledWith(422);
|
||||||
|
expect(response.json).toHaveBeenCalledWith({
|
||||||
|
message: 'Username not provided'
|
||||||
|
});
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(expectations, expectations).catch(expectations);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('returns 404 if user does not exist', (done) => {
|
||||||
|
const request = new Request();
|
||||||
|
request.setParams({ username: 'abc123' });
|
||||||
|
const response = new Response();
|
||||||
|
|
||||||
|
UserMock
|
||||||
|
.expects('findOne')
|
||||||
|
.withArgs({ username: 'abc123' })
|
||||||
|
.yields(null, null);
|
||||||
|
|
||||||
|
const promise = apiGetProjectsForUser(request, response);
|
||||||
|
|
||||||
|
function expectations() {
|
||||||
|
expect(response.status).toHaveBeenCalledWith(404);
|
||||||
|
expect(response.json).toHaveBeenCalledWith({ message: 'User with that username does not exist.' });
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(expectations, expectations).catch(expectations);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 500 on other errors', (done) => {
|
||||||
|
const request = new Request();
|
||||||
|
request.setParams({ username: 'abc123' });
|
||||||
|
const response = new Response();
|
||||||
|
|
||||||
|
UserMock
|
||||||
|
.expects('findOne')
|
||||||
|
.withArgs({ username: 'abc123' })
|
||||||
|
.yields(new Error(), null);
|
||||||
|
|
||||||
|
const promise = apiGetProjectsForUser(request, response);
|
||||||
|
|
||||||
|
function expectations() {
|
||||||
|
expect(response.status).toHaveBeenCalledWith(500);
|
||||||
|
expect(response.json).toHaveBeenCalledWith({ message: 'Error fetching projects' });
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(expectations, expectations).catch(expectations);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,4 +1,5 @@
|
||||||
import Project from '../../models/project';
|
import Project from '../../models/project';
|
||||||
|
import { toModel, FileValidationError, ProjectValidationError } from '../../domain-objects/Project';
|
||||||
|
|
||||||
export default function createProject(req, res) {
|
export default function createProject(req, res) {
|
||||||
let projectValues = {
|
let projectValues = {
|
||||||
|
@ -30,3 +31,67 @@ export default function createProject(req, res) {
|
||||||
.then(populateUserData)
|
.then(populateUserData)
|
||||||
.catch(sendFailure);
|
.catch(sendFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: What happens if you don't supply any files?
|
||||||
|
export function apiCreateProject(req, res) {
|
||||||
|
const params = Object.assign({ user: req.user._id }, req.body);
|
||||||
|
|
||||||
|
function sendValidationErrors(err, type, code = 422) {
|
||||||
|
res.status(code).json({
|
||||||
|
message: `${type} Validation Failed`,
|
||||||
|
detail: err.message,
|
||||||
|
errors: err.files,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Error handling to match spec
|
||||||
|
function sendFailure(err) {
|
||||||
|
res.status(500).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleErrors(err) {
|
||||||
|
if (err instanceof FileValidationError) {
|
||||||
|
sendValidationErrors(err, 'File', err.code);
|
||||||
|
} else if (err instanceof ProjectValidationError) {
|
||||||
|
sendValidationErrors(err, 'Sketch', err.code);
|
||||||
|
} else {
|
||||||
|
sendFailure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkUserHasPermission() {
|
||||||
|
if (req.user.username !== req.params.username) {
|
||||||
|
console.log('no permission');
|
||||||
|
const error = new ProjectValidationError(`'${req.user.username}' does not have permission to create for '${req.params.username}'`);
|
||||||
|
error.code = 401;
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
checkUserHasPermission();
|
||||||
|
|
||||||
|
const model = toModel(params);
|
||||||
|
|
||||||
|
return model.isSlugUnique()
|
||||||
|
.then(({ isUnique, conflictingIds }) => {
|
||||||
|
if (isUnique) {
|
||||||
|
return model.save()
|
||||||
|
.then((newProject) => {
|
||||||
|
res.status(201).json({ id: newProject.id });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = new ProjectValidationError(`Slug "${model.slug}" is not unique. Check ${conflictingIds.join(', ')}`);
|
||||||
|
error.code = 409;
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
})
|
||||||
|
.then(checkUserHasPermission)
|
||||||
|
.catch(handleErrors);
|
||||||
|
} catch (err) {
|
||||||
|
handleErrors(err);
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
57
server/controllers/project.controller/deleteProject.js
Normal file
57
server/controllers/project.controller/deleteProject.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import isBefore from 'date-fns/is_before';
|
||||||
|
import Project from '../../models/project';
|
||||||
|
import { deleteObjectsFromS3, getObjectKey } from '../aws.controller';
|
||||||
|
import createApplicationErrorClass from '../../utils/createApplicationErrorClass';
|
||||||
|
|
||||||
|
const ProjectDeletionError = createApplicationErrorClass('ProjectDeletionError');
|
||||||
|
|
||||||
|
function deleteFilesFromS3(files) {
|
||||||
|
deleteObjectsFromS3(files.filter((file) => {
|
||||||
|
if (file.url) {
|
||||||
|
if (!process.env.S3_DATE || (
|
||||||
|
process.env.S3_DATE &&
|
||||||
|
isBefore(new Date(process.env.S3_DATE), new Date(file.createdAt)))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.map(file => getObjectKey(file.url)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function deleteProject(req, res) {
|
||||||
|
function sendFailure(error) {
|
||||||
|
res.status(error.code).json({ message: error.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendProjectNotFound() {
|
||||||
|
sendFailure(new ProjectDeletionError('Project with that id does not exist', { code: 404 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleProjectDeletion(project) {
|
||||||
|
if (project == null) {
|
||||||
|
sendProjectNotFound();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!project.user.equals(req.user._id)) {
|
||||||
|
sendFailure(new ProjectDeletionError('Authenticated user does not match owner of project', { code: 403 }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteFilesFromS3(project.files);
|
||||||
|
|
||||||
|
project.remove((removeProjectError) => {
|
||||||
|
if (removeProjectError) {
|
||||||
|
sendProjectNotFound();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Project.findById(req.params.project_id)
|
||||||
|
.then(handleProjectDeletion)
|
||||||
|
.catch(sendFailure);
|
||||||
|
}
|
72
server/controllers/project.controller/getProjectsForUser.js
Normal file
72
server/controllers/project.controller/getProjectsForUser.js
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import Project from '../../models/project';
|
||||||
|
import User from '../../models/user';
|
||||||
|
import { toApi as toApiProjectObject } from '../../domain-objects/Project';
|
||||||
|
import createApplicationErrorClass from '../../utils/createApplicationErrorClass';
|
||||||
|
|
||||||
|
const UserNotFoundError = createApplicationErrorClass('UserNotFoundError');
|
||||||
|
|
||||||
|
function getProjectsForUserName(username) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
User.findOne({ username }, (err, user) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
reject(new UserNotFoundError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Project.find({ user: user._id })
|
||||||
|
.sort('-createdAt')
|
||||||
|
.select('name files id createdAt updatedAt')
|
||||||
|
.exec((innerErr, projects) => {
|
||||||
|
if (innerErr) {
|
||||||
|
reject(innerErr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(projects);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function getProjectsForUser(req, res) {
|
||||||
|
if (req.params.username) {
|
||||||
|
return getProjectsForUserName(req.params.username)
|
||||||
|
.then(projects => res.json(projects))
|
||||||
|
.catch((err) => {
|
||||||
|
if (err instanceof UserNotFoundError) {
|
||||||
|
res.status(404).json({ message: 'User with that username does not exist.' });
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ message: 'Error fetching projects' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// could just move this to client side
|
||||||
|
res.status(200).json([]);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function apiGetProjectsForUser(req, res) {
|
||||||
|
if (req.params.username) {
|
||||||
|
return getProjectsForUserName(req.params.username)
|
||||||
|
.then((projects) => {
|
||||||
|
const asApiObjects = projects.map(p => toApiProjectObject(p));
|
||||||
|
res.json({ sketches: asApiObjects });
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err instanceof UserNotFoundError) {
|
||||||
|
res.status(404).json({ message: 'User with that username does not exist.' });
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ message: 'Error fetching projects' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(422).json({ message: 'Username not provided' });
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
|
@ -1,73 +1,40 @@
|
||||||
/* @jest-environment node */
|
/* @jest-environment node */
|
||||||
|
|
||||||
import last from 'lodash/last';
|
import last from 'lodash/last';
|
||||||
|
import { Request, Response } from 'jest-express';
|
||||||
|
|
||||||
|
import User, { createMock, createInstanceMock } from '../../../models/user';
|
||||||
import { createApiKey, removeApiKey } from '../../user.controller/apiKey';
|
import { createApiKey, removeApiKey } from '../../user.controller/apiKey';
|
||||||
|
|
||||||
jest.mock('../../../models/user');
|
jest.mock('../../../models/user');
|
||||||
|
|
||||||
/*
|
|
||||||
Create a mock object representing an express Response
|
|
||||||
*/
|
|
||||||
const createResponseMock = function createResponseMock(done) {
|
|
||||||
const json = jest.fn(() => {
|
|
||||||
if (done) { done(); }
|
|
||||||
});
|
|
||||||
|
|
||||||
const status = jest.fn(() => ({ json }));
|
|
||||||
|
|
||||||
return {
|
|
||||||
status,
|
|
||||||
json
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
Create a mock of the mongoose User model
|
|
||||||
*/
|
|
||||||
const createUserMock = function createUserMock() {
|
|
||||||
const apiKeys = [];
|
|
||||||
let nextId = 0;
|
|
||||||
|
|
||||||
apiKeys.push = ({ label, hashedKey }) => {
|
|
||||||
const id = nextId;
|
|
||||||
nextId += 1;
|
|
||||||
const publicFields = { id, label };
|
|
||||||
const allFields = { ...publicFields, hashedKey };
|
|
||||||
|
|
||||||
Object.defineProperty(allFields, 'toObject', {
|
|
||||||
value: () => publicFields,
|
|
||||||
enumerable: false
|
|
||||||
});
|
|
||||||
|
|
||||||
return Array.prototype.push.call(apiKeys, allFields);
|
|
||||||
};
|
|
||||||
|
|
||||||
apiKeys.pull = ({ _id }) => {
|
|
||||||
const index = apiKeys.findIndex(({ id }) => id === _id);
|
|
||||||
return apiKeys.splice(index, 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
apiKeys,
|
|
||||||
save: jest.fn(callback => callback())
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const User = require('../../../models/user').default;
|
|
||||||
|
|
||||||
describe('user.controller', () => {
|
describe('user.controller', () => {
|
||||||
|
let UserMock;
|
||||||
|
let UserInstanceMock;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
User.__setFindById(null, null);
|
UserMock = createMock();
|
||||||
|
UserInstanceMock = createInstanceMock();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
UserMock.restore();
|
||||||
|
UserInstanceMock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('createApiKey', () => {
|
describe('createApiKey', () => {
|
||||||
it('returns an error if user doesn\'t exist', () => {
|
it('returns an error if user doesn\'t exist', () => {
|
||||||
const request = { user: { id: '1234' } };
|
const request = { user: { id: '1234' } };
|
||||||
const response = createResponseMock();
|
const response = new Response();
|
||||||
|
|
||||||
|
UserMock
|
||||||
|
.expects('findById')
|
||||||
|
.withArgs('1234')
|
||||||
|
.yields(null, null);
|
||||||
|
|
||||||
createApiKey(request, response);
|
createApiKey(request, response);
|
||||||
|
|
||||||
expect(User.findById.mock.calls[0][0]).toBe('1234');
|
|
||||||
expect(response.status).toHaveBeenCalledWith(404);
|
expect(response.status).toHaveBeenCalledWith(404);
|
||||||
expect(response.json).toHaveBeenCalledWith({
|
expect(response.json).toHaveBeenCalledWith({
|
||||||
error: 'User not found'
|
error: 'User not found'
|
||||||
|
@ -75,10 +42,13 @@ describe('user.controller', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns an error if label not provided', () => {
|
it('returns an error if label not provided', () => {
|
||||||
User.__setFindById(undefined, createUserMock());
|
|
||||||
|
|
||||||
const request = { user: { id: '1234' }, body: {} };
|
const request = { user: { id: '1234' }, body: {} };
|
||||||
const response = createResponseMock();
|
const response = new Response();
|
||||||
|
|
||||||
|
UserMock
|
||||||
|
.expects('findById')
|
||||||
|
.withArgs('1234')
|
||||||
|
.yields(null, new User());
|
||||||
|
|
||||||
createApiKey(request, response);
|
createApiKey(request, response);
|
||||||
|
|
||||||
|
@ -89,46 +59,59 @@ describe('user.controller', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns generated API key to the user', (done) => {
|
it('returns generated API key to the user', (done) => {
|
||||||
let response;
|
const request = new Request();
|
||||||
|
request.setBody({ label: 'my key' });
|
||||||
|
request.user = { id: '1234' };
|
||||||
|
|
||||||
const request = {
|
const response = new Response();
|
||||||
user: { id: '1234' },
|
|
||||||
body: { label: 'my key' }
|
|
||||||
};
|
|
||||||
|
|
||||||
const user = createUserMock();
|
const user = new User();
|
||||||
|
|
||||||
const checkExpecations = () => {
|
UserMock
|
||||||
|
.expects('findById')
|
||||||
|
.withArgs('1234')
|
||||||
|
.yields(null, user);
|
||||||
|
|
||||||
|
UserInstanceMock.expects('save')
|
||||||
|
.yields();
|
||||||
|
|
||||||
|
function expectations() {
|
||||||
const lastKey = last(user.apiKeys);
|
const lastKey = last(user.apiKeys);
|
||||||
|
|
||||||
expect(lastKey.label).toBe('my key');
|
expect(lastKey.label).toBe('my key');
|
||||||
expect(typeof lastKey.hashedKey).toBe('string');
|
expect(typeof lastKey.hashedKey).toBe('string');
|
||||||
|
|
||||||
expect(response.json).toHaveBeenCalledWith({
|
const responseData = response.json.mock.calls[0][0];
|
||||||
apiKeys: [
|
|
||||||
{ id: 0, label: 'my key', token: lastKey.hashedKey }
|
expect(responseData.apiKeys.length).toBe(1);
|
||||||
]
|
expect(responseData.apiKeys[0]).toMatchObject({
|
||||||
|
label: 'my key',
|
||||||
|
token: lastKey.hashedKey,
|
||||||
|
lastUsedAt: undefined,
|
||||||
|
createdAt: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
done();
|
done();
|
||||||
};
|
}
|
||||||
|
|
||||||
response = createResponseMock(checkExpecations);
|
const promise = createApiKey(request, response);
|
||||||
|
|
||||||
User.__setFindById(undefined, user);
|
promise.then(expectations, expectations).catch(expectations);
|
||||||
|
|
||||||
createApiKey(request, response);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('removeApiKey', () => {
|
describe('removeApiKey', () => {
|
||||||
it('returns an error if user doesn\'t exist', () => {
|
it('returns an error if user doesn\'t exist', () => {
|
||||||
const request = { user: { id: '1234' } };
|
const request = { user: { id: '1234' } };
|
||||||
const response = createResponseMock();
|
const response = new Response();
|
||||||
|
|
||||||
|
UserMock
|
||||||
|
.expects('findById')
|
||||||
|
.withArgs('1234')
|
||||||
|
.yields(null, null);
|
||||||
|
|
||||||
removeApiKey(request, response);
|
removeApiKey(request, response);
|
||||||
|
|
||||||
expect(User.findById.mock.calls[0][0]).toBe('1234');
|
|
||||||
expect(response.status).toHaveBeenCalledWith(404);
|
expect(response.status).toHaveBeenCalledWith(404);
|
||||||
expect(response.json).toHaveBeenCalledWith({
|
expect(response.json).toHaveBeenCalledWith({
|
||||||
error: 'User not found'
|
error: 'User not found'
|
||||||
|
@ -140,10 +123,13 @@ describe('user.controller', () => {
|
||||||
user: { id: '1234' },
|
user: { id: '1234' },
|
||||||
params: { keyId: 'not-a-real-key' }
|
params: { keyId: 'not-a-real-key' }
|
||||||
};
|
};
|
||||||
const response = createResponseMock();
|
const response = new Response();
|
||||||
|
const user = new User();
|
||||||
|
|
||||||
const user = createUserMock();
|
UserMock
|
||||||
User.__setFindById(undefined, user);
|
.expects('findById')
|
||||||
|
.withArgs('1234')
|
||||||
|
.yields(null, user);
|
||||||
|
|
||||||
removeApiKey(request, response);
|
removeApiKey(request, response);
|
||||||
|
|
||||||
|
@ -153,27 +139,41 @@ describe('user.controller', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip('removes key if it exists', () => {
|
it('removes key if it exists', () => {
|
||||||
const request = {
|
const user = new User();
|
||||||
user: { id: '1234' },
|
|
||||||
params: { keyId: 0 }
|
|
||||||
};
|
|
||||||
const response = createResponseMock();
|
|
||||||
|
|
||||||
const user = createUserMock();
|
|
||||||
|
|
||||||
user.apiKeys.push({ label: 'first key' }); // id 0
|
user.apiKeys.push({ label: 'first key' }); // id 0
|
||||||
user.apiKeys.push({ label: 'second key' }); // id 1
|
user.apiKeys.push({ label: 'second key' }); // id 1
|
||||||
|
|
||||||
User.__setFindById(undefined, user);
|
const firstKeyId = user.apiKeys[0]._id.toString();
|
||||||
|
const secondKeyId = user.apiKeys[1]._id.toString();
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
user: { id: '1234' },
|
||||||
|
params: { keyId: firstKeyId }
|
||||||
|
};
|
||||||
|
const response = new Response();
|
||||||
|
|
||||||
|
UserMock
|
||||||
|
.expects('findById')
|
||||||
|
.withArgs('1234')
|
||||||
|
.yields(null, user);
|
||||||
|
|
||||||
|
UserInstanceMock
|
||||||
|
.expects('save')
|
||||||
|
.yields();
|
||||||
|
|
||||||
removeApiKey(request, response);
|
removeApiKey(request, response);
|
||||||
|
|
||||||
expect(response.status).toHaveBeenCalledWith(200);
|
expect(response.status).toHaveBeenCalledWith(200);
|
||||||
expect(response.json).toHaveBeenCalledWith({
|
|
||||||
apiKeys: [
|
const responseData = response.json.mock.calls[0][0];
|
||||||
{ id: 1, label: 'second key' }
|
|
||||||
]
|
expect(responseData.apiKeys.length).toBe(1);
|
||||||
|
expect(responseData.apiKeys[0]).toMatchObject({
|
||||||
|
id: secondKeyId,
|
||||||
|
label: 'second key',
|
||||||
|
lastUsedAt: undefined,
|
||||||
|
createdAt: undefined
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,67 +19,85 @@ function generateApiKey() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createApiKey(req, res) {
|
export function createApiKey(req, res) {
|
||||||
User.findById(req.user.id, async (err, user) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!user) {
|
function sendFailure(code, error) {
|
||||||
res.status(404).json({ error: 'User not found' });
|
res.status(code).json({ error });
|
||||||
return;
|
resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!req.body.label) {
|
User.findById(req.user.id, async (err, user) => {
|
||||||
res.status(400).json({ error: 'Expected field \'label\' was not present in request body' });
|
if (!user) {
|
||||||
return;
|
sendFailure(404, 'User not found');
|
||||||
}
|
|
||||||
|
|
||||||
const keyToBeHashed = await generateApiKey();
|
|
||||||
|
|
||||||
const addedApiKeyIndex = user.apiKeys.push({ label: req.body.label, hashedKey: keyToBeHashed });
|
|
||||||
|
|
||||||
user.save((saveErr) => {
|
|
||||||
if (saveErr) {
|
|
||||||
res.status(500).json({ error: saveErr });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiKeys = user.apiKeys
|
if (!req.body.label) {
|
||||||
.map((apiKey, index) => {
|
sendFailure(400, 'Expected field \'label\' was not present in request body');
|
||||||
const fields = apiKey.toObject();
|
return;
|
||||||
const shouldIncludeToken = index === addedApiKeyIndex - 1;
|
}
|
||||||
|
|
||||||
return shouldIncludeToken ?
|
const keyToBeHashed = await generateApiKey();
|
||||||
{ ...fields, token: keyToBeHashed } :
|
|
||||||
fields;
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({ apiKeys });
|
const addedApiKeyIndex = user.apiKeys.push({ label: req.body.label, hashedKey: keyToBeHashed });
|
||||||
|
|
||||||
|
user.save((saveErr) => {
|
||||||
|
if (saveErr) {
|
||||||
|
sendFailure(500, saveErr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKeys = user.apiKeys
|
||||||
|
.map((apiKey, index) => {
|
||||||
|
const fields = apiKey.toObject();
|
||||||
|
const shouldIncludeToken = index === addedApiKeyIndex - 1;
|
||||||
|
|
||||||
|
return shouldIncludeToken ?
|
||||||
|
{ ...fields, token: keyToBeHashed } :
|
||||||
|
fields;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ apiKeys });
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeApiKey(req, res) {
|
export function removeApiKey(req, res) {
|
||||||
User.findById(req.user.id, (err, user) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (err) {
|
function sendFailure(code, error) {
|
||||||
res.status(500).json({ error: err });
|
res.status(code).json({ error });
|
||||||
return;
|
resolve();
|
||||||
}
|
|
||||||
if (!user) {
|
|
||||||
res.status(404).json({ error: 'User not found' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const keyToDelete = user.apiKeys.find(key => key.id === req.params.keyId);
|
|
||||||
if (!keyToDelete) {
|
|
||||||
res.status(404).json({ error: 'Key does not exist for user' });
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user.apiKeys.pull({ _id: req.params.keyId });
|
User.findById(req.user.id, (err, user) => {
|
||||||
|
if (err) {
|
||||||
user.save((saveErr) => {
|
sendFailure(500, err);
|
||||||
if (saveErr) {
|
|
||||||
res.status(500).json({ error: saveErr });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({ apiKeys: user.apiKeys });
|
if (!user) {
|
||||||
|
sendFailure(404, 'User not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyToDelete = user.apiKeys.find(key => key.id === req.params.keyId);
|
||||||
|
if (!keyToDelete) {
|
||||||
|
sendFailure(404, 'Key does not exist for user');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.apiKeys.pull({ _id: req.params.keyId });
|
||||||
|
|
||||||
|
user.save((saveErr) => {
|
||||||
|
if (saveErr) {
|
||||||
|
sendFailure(500, saveErr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({ apiKeys: user.apiKeys });
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
133
server/domain-objects/Project.js
Normal file
133
server/domain-objects/Project.js
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
import isPlainObject from 'lodash/isPlainObject';
|
||||||
|
import pick from 'lodash/pick';
|
||||||
|
import Project from '../models/project';
|
||||||
|
import createId from '../utils/createId';
|
||||||
|
import createApplicationErrorClass from '../utils/createApplicationErrorClass';
|
||||||
|
import createDefaultFiles from './createDefaultFiles';
|
||||||
|
|
||||||
|
export const FileValidationError = createApplicationErrorClass('FileValidationError');
|
||||||
|
export const ProjectValidationError = createApplicationErrorClass('ProjectValidationError');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This converts between a mongoose Project model
|
||||||
|
* and the public API Project object properties
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export function toApi(model) {
|
||||||
|
return {
|
||||||
|
id: model.id,
|
||||||
|
name: model.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms a tree of files matching the APIs DirectoryContents
|
||||||
|
* format into the data structure stored in mongodb
|
||||||
|
*
|
||||||
|
* - flattens the tree into an array of file/folders
|
||||||
|
* - each file/folder gets a generated BSON-ID
|
||||||
|
* - each folder has a `children` array containing the IDs of it's children
|
||||||
|
*/
|
||||||
|
function transformFilesInner(tree = {}, parentNode) {
|
||||||
|
const files = [];
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
Object.entries(tree).forEach(([name, params]) => {
|
||||||
|
const id = createId();
|
||||||
|
const isFolder = params.files != null;
|
||||||
|
|
||||||
|
if (isFolder) {
|
||||||
|
const folder = {
|
||||||
|
_id: id,
|
||||||
|
name,
|
||||||
|
fileType: 'folder',
|
||||||
|
children: [] // Initialise an empty folder
|
||||||
|
};
|
||||||
|
|
||||||
|
files.push(folder);
|
||||||
|
|
||||||
|
// The recursion will return a list of child files/folders
|
||||||
|
// It will also push the child's id into `folder.children`
|
||||||
|
const subFolder = transformFilesInner(params.files, folder);
|
||||||
|
files.push(...subFolder.files);
|
||||||
|
errors.push(...subFolder.errors);
|
||||||
|
} else {
|
||||||
|
const file = {
|
||||||
|
_id: id,
|
||||||
|
name,
|
||||||
|
fileType: 'file'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof params.url === 'string') {
|
||||||
|
file.url = params.url;
|
||||||
|
} else if (typeof params.content === 'string') {
|
||||||
|
file.content = params.content;
|
||||||
|
} else {
|
||||||
|
errors.push({ name, message: 'missing \'url\' or \'content\'' });
|
||||||
|
}
|
||||||
|
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push this child's ID onto it's parent's list
|
||||||
|
// of children
|
||||||
|
if (parentNode != null) {
|
||||||
|
parentNode.children.push(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { files, errors };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformFiles(tree = {}) {
|
||||||
|
const withRoot = {
|
||||||
|
root: {
|
||||||
|
files: tree
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { files, errors } = transformFilesInner(withRoot);
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
const message = `${errors.length} files failed validation. See error.files for individual errors.
|
||||||
|
|
||||||
|
Errors:
|
||||||
|
${errors.map(e => `* ${e.name}: ${e.message}`).join('\n')}
|
||||||
|
`;
|
||||||
|
const error = new FileValidationError(message);
|
||||||
|
error.files = errors;
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function containsRootHtmlFile(tree) {
|
||||||
|
return Object.keys(tree).find(name => /\.html$/.test(name)) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This converts between the public API's Project object
|
||||||
|
* properties and a mongoose Project model
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export function toModel(object) {
|
||||||
|
let files = [];
|
||||||
|
let tree = object.files;
|
||||||
|
|
||||||
|
if (isPlainObject(tree)) {
|
||||||
|
if (!containsRootHtmlFile(tree)) {
|
||||||
|
tree = Object.assign(createDefaultFiles(), tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
files = transformFiles(tree);
|
||||||
|
} else {
|
||||||
|
throw new FileValidationError('\'files\' must be an object');
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectValues = pick(object, ['user', 'name', 'slug']);
|
||||||
|
projectValues.files = files;
|
||||||
|
|
||||||
|
return new Project(projectValues);
|
||||||
|
}
|
385
server/domain-objects/__test__/Project.test.js
Normal file
385
server/domain-objects/__test__/Project.test.js
Normal file
|
@ -0,0 +1,385 @@
|
||||||
|
import find from 'lodash/find';
|
||||||
|
|
||||||
|
import { containsRootHtmlFile, toModel, transformFiles, FileValidationError } from '../Project';
|
||||||
|
|
||||||
|
jest.mock('../../utils/createId');
|
||||||
|
|
||||||
|
// TODO: File name validation
|
||||||
|
// TODO: File extension validation
|
||||||
|
//
|
||||||
|
describe('domain-objects/Project', () => {
|
||||||
|
describe('containsRootHtmlFile', () => {
|
||||||
|
it('returns true for at least one root .html', () => {
|
||||||
|
expect(containsRootHtmlFile({ 'index.html': {} })).toBe(true);
|
||||||
|
expect(containsRootHtmlFile({ 'another-one.html': {} })).toBe(true);
|
||||||
|
expect(containsRootHtmlFile({ 'one.html': {}, 'two.html': {} })).toBe(true);
|
||||||
|
expect(containsRootHtmlFile({ 'one.html': {}, 'sketch.js': {} })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false anything else', () => {
|
||||||
|
expect(containsRootHtmlFile({ 'sketch.js': {} })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores nested html', () => {
|
||||||
|
expect(containsRootHtmlFile({
|
||||||
|
examples: {
|
||||||
|
files: {
|
||||||
|
'index.html': {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toModel', () => {
|
||||||
|
it('filters extra properties', () => {
|
||||||
|
const params = {
|
||||||
|
name: 'My sketch',
|
||||||
|
extraThing: 'oopsie',
|
||||||
|
files: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const model = toModel(params);
|
||||||
|
|
||||||
|
expect(model.name).toBe('My sketch');
|
||||||
|
expect(model.extraThing).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws FileValidationError', () => {
|
||||||
|
const params = {
|
||||||
|
files: {
|
||||||
|
'index.html': {} // missing content or url
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => toModel(params)).toThrowError(FileValidationError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if files is not an object', () => {
|
||||||
|
const params = {
|
||||||
|
files: []
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => toModel(params)).toThrowError(FileValidationError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates default index.html and dependent files if no root .html is provided', () => {
|
||||||
|
const params = {
|
||||||
|
files: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { files } = toModel(params);
|
||||||
|
|
||||||
|
expect(files.length).toBe(4);
|
||||||
|
expect(find(files, { name: 'index.html' })).not.toBeUndefined();
|
||||||
|
expect(find(files, { name: 'sketch.js' })).not.toBeUndefined();
|
||||||
|
expect(find(files, { name: 'style.css' })).not.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not create default files if any root .html is provided', () => {
|
||||||
|
const params = {
|
||||||
|
files: {
|
||||||
|
'example.html': {
|
||||||
|
content: '<html><body>Hello!</body></html>'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { files } = toModel(params);
|
||||||
|
|
||||||
|
expect(files.length).toBe(2);
|
||||||
|
expect(find(files, { name: 'example.html' })).not.toBeUndefined();
|
||||||
|
expect(find(files, { name: 'index.html' })).toBeUndefined();
|
||||||
|
expect(find(files, { name: 'sketch.js' })).toBeUndefined();
|
||||||
|
expect(find(files, { name: 'style.css' })).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not overwrite default CSS and JS of the same name if provided', () => {
|
||||||
|
const params = {
|
||||||
|
files: {
|
||||||
|
'sketch.js': {
|
||||||
|
content: 'const sketch = true;'
|
||||||
|
},
|
||||||
|
'style.css': {
|
||||||
|
content: 'body { outline: 10px solid red; }'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { files } = toModel(params);
|
||||||
|
|
||||||
|
expect(files.length).toBe(4);
|
||||||
|
expect(find(files, { name: 'index.html' })).not.toBeUndefined();
|
||||||
|
|
||||||
|
const sketchFile = find(files, { name: 'sketch.js' });
|
||||||
|
expect(sketchFile.content).toBe('const sketch = true;');
|
||||||
|
|
||||||
|
const cssFile = find(files, { name: 'style.css' });
|
||||||
|
expect(cssFile.content).toBe('body { outline: 10px solid red; }');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('transformFiles', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// eslint-disable-next-line global-require
|
||||||
|
const { resetMockCreateId } = require('../../utils/createId');
|
||||||
|
|
||||||
|
resetMockCreateId();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates an empty root with no data', () => {
|
||||||
|
const tree = {};
|
||||||
|
|
||||||
|
expect(transformFiles(tree)).toEqual([{
|
||||||
|
_id: '0',
|
||||||
|
fileType: 'folder',
|
||||||
|
name: 'root',
|
||||||
|
children: []
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts tree-shaped files into list', () => {
|
||||||
|
const tree = {
|
||||||
|
'index.html': {
|
||||||
|
content: 'some contents',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(transformFiles(tree)).toEqual([
|
||||||
|
{
|
||||||
|
_id: '0',
|
||||||
|
fileType: 'folder',
|
||||||
|
name: 'root',
|
||||||
|
children: ['1']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: '1',
|
||||||
|
content: 'some contents',
|
||||||
|
fileType: 'file',
|
||||||
|
name: 'index.html'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses file url over content', () => {
|
||||||
|
const tree = {
|
||||||
|
'script.js': {
|
||||||
|
url: 'http://example.net/something.js'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(transformFiles(tree)).toEqual([
|
||||||
|
{
|
||||||
|
_id: '0',
|
||||||
|
fileType: 'folder',
|
||||||
|
name: 'root',
|
||||||
|
children: ['1']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: '1',
|
||||||
|
url: 'http://example.net/something.js',
|
||||||
|
fileType: 'file',
|
||||||
|
name: 'script.js'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates folders', () => {
|
||||||
|
const tree = {
|
||||||
|
'a-folder': {
|
||||||
|
files: {}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(transformFiles(tree)).toEqual([
|
||||||
|
{
|
||||||
|
_id: '0',
|
||||||
|
fileType: 'folder',
|
||||||
|
name: 'root',
|
||||||
|
children: ['1']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: '1',
|
||||||
|
children: [],
|
||||||
|
fileType: 'folder',
|
||||||
|
name: 'a-folder'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('walks the tree processing files', () => {
|
||||||
|
const tree = {
|
||||||
|
'index.html': {
|
||||||
|
content: 'some contents',
|
||||||
|
},
|
||||||
|
'a-folder': {
|
||||||
|
files: {
|
||||||
|
'data.csv': {
|
||||||
|
content: 'this,is,data'
|
||||||
|
},
|
||||||
|
'another.txt': {
|
||||||
|
content: 'blah blah'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(transformFiles(tree)).toEqual([
|
||||||
|
{
|
||||||
|
_id: '0',
|
||||||
|
fileType: 'folder',
|
||||||
|
name: 'root',
|
||||||
|
children: ['1', '2']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: '1',
|
||||||
|
name: 'index.html',
|
||||||
|
fileType: 'file',
|
||||||
|
content: 'some contents'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: '2',
|
||||||
|
name: 'a-folder',
|
||||||
|
fileType: 'folder',
|
||||||
|
children: ['3', '4']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: '3',
|
||||||
|
name: 'data.csv',
|
||||||
|
fileType: 'file',
|
||||||
|
content: 'this,is,data'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: '4',
|
||||||
|
name: 'another.txt',
|
||||||
|
fileType: 'file',
|
||||||
|
content: 'blah blah'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles deep nesting', () => {
|
||||||
|
const tree = {
|
||||||
|
first: {
|
||||||
|
files: {
|
||||||
|
second: {
|
||||||
|
files: {
|
||||||
|
third: {
|
||||||
|
files: {
|
||||||
|
'hello.js': {
|
||||||
|
content: 'world!'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(transformFiles(tree)).toEqual([
|
||||||
|
{
|
||||||
|
_id: '0',
|
||||||
|
fileType: 'folder',
|
||||||
|
name: 'root',
|
||||||
|
children: ['1']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: '1',
|
||||||
|
name: 'first',
|
||||||
|
fileType: 'folder',
|
||||||
|
children: ['2']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: '2',
|
||||||
|
name: 'second',
|
||||||
|
fileType: 'folder',
|
||||||
|
children: ['3']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: '3',
|
||||||
|
name: 'third',
|
||||||
|
fileType: 'folder',
|
||||||
|
children: ['4']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: '4',
|
||||||
|
name: 'hello.js',
|
||||||
|
fileType: 'file',
|
||||||
|
content: 'world!'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('allows duplicate names in different folder', () => {
|
||||||
|
const tree = {
|
||||||
|
'index.html': {
|
||||||
|
content: 'some contents',
|
||||||
|
},
|
||||||
|
'data': {
|
||||||
|
files: {
|
||||||
|
'index.html': {
|
||||||
|
content: 'different file'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(transformFiles(tree)).toEqual([
|
||||||
|
{
|
||||||
|
_id: '0',
|
||||||
|
fileType: 'folder',
|
||||||
|
name: 'root',
|
||||||
|
children: ['1', '2']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: '1',
|
||||||
|
name: 'index.html',
|
||||||
|
fileType: 'file',
|
||||||
|
content: 'some contents'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: '2',
|
||||||
|
name: 'data',
|
||||||
|
fileType: 'folder',
|
||||||
|
children: ['3']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: '3',
|
||||||
|
name: 'index.html',
|
||||||
|
fileType: 'file',
|
||||||
|
content: 'different file'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates files', () => {
|
||||||
|
const tree = {
|
||||||
|
'index.html': {} // missing `content`
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => transformFiles(tree)).toThrowError(FileValidationError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('collects all file validation errors', () => {
|
||||||
|
const tree = {
|
||||||
|
'index.html': {}, // missing `content`
|
||||||
|
'something.js': {} // also missing `content`
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
transformFiles(tree);
|
||||||
|
|
||||||
|
// Should not get here
|
||||||
|
throw new Error('should have thrown before this point');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(FileValidationError);
|
||||||
|
expect(err.files).toEqual([
|
||||||
|
{ name: 'index.html', message: 'missing \'url\' or \'content\'' },
|
||||||
|
{ name: 'something.js', message: 'missing \'url\' or \'content\'' }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
48
server/domain-objects/createDefaultFiles.js
Normal file
48
server/domain-objects/createDefaultFiles.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
const defaultSketch = `function setup() {
|
||||||
|
createCanvas(400, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
background(220);
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const defaultHTML =
|
||||||
|
`<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.8.0/p5.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.8.0/addons/p5.dom.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.8.0/addons/p5.sound.min.js"></script>
|
||||||
|
<link rel="stylesheet" type="text/css" href="style.css">
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="sketch.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const defaultCSS =
|
||||||
|
`html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function createDefaultFiles() {
|
||||||
|
return {
|
||||||
|
'index.html': {
|
||||||
|
content: defaultHTML
|
||||||
|
},
|
||||||
|
'style.css': {
|
||||||
|
content: defaultCSS
|
||||||
|
},
|
||||||
|
'sketch.js': {
|
||||||
|
content: defaultSketch
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -11,6 +11,24 @@ export function createMock() {
|
||||||
return sinon.mock(Project);
|
return sinon.mock(Project);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wraps the Project.prototype i.e. the
|
||||||
|
// instance methods in a mock so
|
||||||
|
// Project.save() can be mocked
|
||||||
|
export function createInstanceMock() {
|
||||||
|
// See: https://stackoverflow.com/questions/40962960/sinon-mock-of-mongoose-save-method-for-all-future-instances-of-a-model-with-pro
|
||||||
|
Object.defineProperty(Project.prototype, 'save', {
|
||||||
|
value: Project.prototype.save,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(Project.prototype, 'remove', {
|
||||||
|
value: Project.prototype.remove,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return sinon.mock(Project.prototype);
|
||||||
|
}
|
||||||
|
|
||||||
// Re-export the model, it will be
|
// Re-export the model, it will be
|
||||||
// altered by mockingoose whenever
|
// altered by mockingoose whenever
|
||||||
// we call methods on the MockConfig
|
// we call methods on the MockConfig
|
||||||
|
|
|
@ -1,12 +1,31 @@
|
||||||
let __err = null;
|
import sinon from 'sinon';
|
||||||
let __user = null;
|
import 'sinon-mongoose';
|
||||||
|
|
||||||
export default {
|
// Import the actual model to be mocked
|
||||||
__setFindById(err, user) {
|
const User = jest.requireActual('../user').default;
|
||||||
__err = err;
|
|
||||||
__user = user;
|
// Wrap User in a sinon mock
|
||||||
},
|
// The returned object is used to configure
|
||||||
findById: jest.fn(async (id, callback) => {
|
// the mocked model's behaviour
|
||||||
callback(__err, __user);
|
export function createMock() {
|
||||||
})
|
return sinon.mock(User);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
// Wraps the User.prototype i.e. the
|
||||||
|
// instance methods in a mock so
|
||||||
|
// User.save() can be mocked
|
||||||
|
export function createInstanceMock() {
|
||||||
|
// See: https://stackoverflow.com/questions/40962960/sinon-mock-of-mongoose-save-method-for-all-future-instances-of-a-model-with-pro
|
||||||
|
Object.defineProperty(User.prototype, 'save', {
|
||||||
|
value: User.prototype.save,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return sinon.mock(User.prototype);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Re-export the model, it will be
|
||||||
|
// altered by mockingoose whenever
|
||||||
|
// we call methods on the MockConfig
|
||||||
|
export default User;
|
||||||
|
|
|
@ -49,8 +49,43 @@ projectSchema.set('toJSON', {
|
||||||
|
|
||||||
projectSchema.pre('save', function generateSlug(next) {
|
projectSchema.pre('save', function generateSlug(next) {
|
||||||
const project = this;
|
const project = this;
|
||||||
project.slug = slugify(project.name, '_');
|
|
||||||
|
if (!project.slug) {
|
||||||
|
project.slug = slugify(project.name, '_');
|
||||||
|
}
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if slug is unique for this user's projects
|
||||||
|
*/
|
||||||
|
projectSchema.methods.isSlugUnique = async function isSlugUnique(cb) {
|
||||||
|
const project = this;
|
||||||
|
const hasCallback = typeof cb === 'function';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const docsWithSlug = await project.model('Project')
|
||||||
|
.find({ user: project.user, slug: project.slug }, '_id')
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
isUnique: docsWithSlug.length === 0,
|
||||||
|
conflictingIds: docsWithSlug.map(d => d._id) || []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (hasCallback) {
|
||||||
|
cb(null, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
if (hasCallback) {
|
||||||
|
cb(err, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default mongoose.model('Project', projectSchema);
|
export default mongoose.model('Project', projectSchema);
|
||||||
|
|
27
server/routes/api.routes.js
Normal file
27
server/routes/api.routes.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { Router } from 'express';
|
||||||
|
import passport from 'passport';
|
||||||
|
import * as ProjectController from '../controllers/project.controller';
|
||||||
|
|
||||||
|
const router = new Router();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:username/sketches',
|
||||||
|
passport.authenticate('basic', { session: false }),
|
||||||
|
ProjectController.apiGetProjectsForUser
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:username/sketches',
|
||||||
|
passport.authenticate('basic', { session: false }),
|
||||||
|
ProjectController.apiCreateProject
|
||||||
|
);
|
||||||
|
|
||||||
|
// NOTE: Currently :username will not be checked for ownership
|
||||||
|
// only the project's owner in the database.
|
||||||
|
router.delete(
|
||||||
|
'/:username/sketches/:project_id',
|
||||||
|
passport.authenticate('basic', { session: false }),
|
||||||
|
ProjectController.deleteProject
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
|
@ -1,11 +1,7 @@
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import rp from 'request-promise';
|
import rp from 'request-promise';
|
||||||
import Q from 'q';
|
import Q from 'q';
|
||||||
import mongoose from 'mongoose';
|
import { ok } from 'assert';
|
||||||
import objectID from 'bson-objectid';
|
|
||||||
import shortid from 'shortid';
|
|
||||||
import User from '../models/user';
|
|
||||||
import Project from '../models/project';
|
|
||||||
|
|
||||||
// TODO: Change branchName if necessary
|
// TODO: Change branchName if necessary
|
||||||
const branchName = 'release';
|
const branchName = 'release';
|
||||||
|
@ -14,11 +10,20 @@ const baseUrl = 'https://api.github.com/repos/ml5js/ml5-examples/contents';
|
||||||
const clientId = process.env.GITHUB_ID;
|
const clientId = process.env.GITHUB_ID;
|
||||||
const clientSecret = process.env.GITHUB_SECRET;
|
const clientSecret = process.env.GITHUB_SECRET;
|
||||||
const editorUsername = process.env.ML5_EXAMPLES_USERNAME;
|
const editorUsername = process.env.ML5_EXAMPLES_USERNAME;
|
||||||
|
const personalAccessToken = process.env.EDITOR_API_ACCESS_TOKEN;
|
||||||
|
const editorApiUrl = process.env.EDITOR_API_URL;
|
||||||
const headers = {
|
const headers = {
|
||||||
'User-Agent': 'p5js-web-editor/0.0.1'
|
'User-Agent': 'p5js-web-editor/0.0.1'
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestOptions = {
|
ok(clientId, 'GITHUB_ID is required');
|
||||||
|
ok(clientSecret, 'GITHUB_SECRET is required');
|
||||||
|
ok(editorUsername, 'ML5_EXAMPLES_USERNAME is required');
|
||||||
|
ok(personalAccessToken, 'EDITOR_API_ACCESS_TOKEN is required');
|
||||||
|
ok(editorApiUrl, 'EDITOR_API_URL is required');
|
||||||
|
|
||||||
|
//
|
||||||
|
const githubRequestOptions = {
|
||||||
url: baseUrl,
|
url: baseUrl,
|
||||||
qs: {
|
qs: {
|
||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
|
@ -29,14 +34,15 @@ const requestOptions = {
|
||||||
json: true
|
json: true
|
||||||
};
|
};
|
||||||
|
|
||||||
const mongoConnectionString = process.env.MONGO_URL;
|
const editorRequestOptions = {
|
||||||
mongoose.connect(mongoConnectionString, {
|
url: `${editorApiUrl}/${editorUsername}`,
|
||||||
useMongoClient: true
|
method: 'GET',
|
||||||
});
|
headers: {
|
||||||
mongoose.connection.on('error', () => {
|
...headers,
|
||||||
console.error('MongoDB Connection Error. Please make sure that MongoDB is running.');
|
Authorization: `Basic ${Buffer.from(`${editorUsername}:${personalAccessToken}`).toString('base64')}`
|
||||||
process.exit(1);
|
},
|
||||||
});
|
json: true
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ---------------------------------------------------------
|
* ---------------------------------------------------------
|
||||||
|
@ -51,12 +57,52 @@ function flatten(list) {
|
||||||
return list.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []);
|
return list.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch data for a single HTML/JS file, or return
|
||||||
|
* an url to the file's CDN location
|
||||||
|
*/
|
||||||
|
async function fetchFileContent(item) {
|
||||||
|
const { name } = item;
|
||||||
|
const file = { url: item.url };
|
||||||
|
|
||||||
|
// if it is an html or js file
|
||||||
|
if (
|
||||||
|
(file.url != null && name.endsWith('.html')) ||
|
||||||
|
name.endsWith('.js')
|
||||||
|
) {
|
||||||
|
const options = Object.assign({}, githubRequestOptions);
|
||||||
|
options.url = `${file.url}`;
|
||||||
|
|
||||||
|
if (
|
||||||
|
options.url !== undefined ||
|
||||||
|
options.url !== null ||
|
||||||
|
options.url !== ''
|
||||||
|
) {
|
||||||
|
file.content = await rp(options);
|
||||||
|
// NOTE: remove the URL property if there's content
|
||||||
|
// Otherwise the p5 editor will try to pull from that url
|
||||||
|
if (file.content !== null) delete file.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return file;
|
||||||
|
// if it is NOT an html or js file
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.url) {
|
||||||
|
const cdnRef = `https://cdn.jsdelivr.net/gh/ml5js/ml5-examples@${branchName}${file.url.split(branchName)[1]}`;
|
||||||
|
file.url = cdnRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* STEP 1: Get the top level cateogories
|
* STEP 1: Get the top level cateogories
|
||||||
*/
|
*/
|
||||||
async function getCategories() {
|
async function getCategories() {
|
||||||
try {
|
try {
|
||||||
const options = Object.assign({}, requestOptions);
|
const options = Object.assign({}, githubRequestOptions);
|
||||||
options.url = `${options.url}/p5js${branchRef}`;
|
options.url = `${options.url}/p5js${branchRef}`;
|
||||||
const results = await rp(options);
|
const results = await rp(options);
|
||||||
|
|
||||||
|
@ -76,13 +122,18 @@ async function getCategoryExamples(sketchRootList) {
|
||||||
const output = [];
|
const output = [];
|
||||||
const sketchRootCategories = sketchRootList.map(async (categories) => {
|
const sketchRootCategories = sketchRootList.map(async (categories) => {
|
||||||
// let options = Object.assign({url: `${requestOptions.url}/${categories.path}${branchRef}`}, requestOptions)
|
// let options = Object.assign({url: `${requestOptions.url}/${categories.path}${branchRef}`}, requestOptions)
|
||||||
const options = Object.assign({}, requestOptions);
|
const options = Object.assign({}, githubRequestOptions);
|
||||||
options.url = `${options.url}${categories.path}${branchRef}`;
|
options.url = `${options.url}${categories.path}${branchRef}`;
|
||||||
// console.log(options)
|
// console.log(options)
|
||||||
const sketchDirs = await rp(options);
|
const sketchDirs = await rp(options);
|
||||||
const result = flatten(sketchDirs);
|
|
||||||
|
|
||||||
return result;
|
try {
|
||||||
|
const result = flatten(sketchDirs);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const sketchList = await Q.all(sketchRootCategories);
|
const sketchList = await Q.all(sketchRootCategories);
|
||||||
|
@ -107,7 +158,7 @@ async function traverseSketchTree(parentObject) {
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
// let options = `https://api.github.com/repos/ml5js/ml5-examples/contents/${sketches.path}${branchRef}`
|
// let options = `https://api.github.com/repos/ml5js/ml5-examples/contents/${sketches.path}${branchRef}`
|
||||||
const options = Object.assign({}, requestOptions);
|
const options = Object.assign({}, githubRequestOptions);
|
||||||
options.url = `${options.url}${parentObject.path}${branchRef}`;
|
options.url = `${options.url}${parentObject.path}${branchRef}`;
|
||||||
|
|
||||||
output.tree = await rp(options);
|
output.tree = await rp(options);
|
||||||
|
@ -124,7 +175,6 @@ async function traverseSketchTree(parentObject) {
|
||||||
* @param {*} categoryExamples - all of the categories in an array
|
* @param {*} categoryExamples - all of the categories in an array
|
||||||
*/
|
*/
|
||||||
async function traverseSketchTreeAll(categoryExamples) {
|
async function traverseSketchTreeAll(categoryExamples) {
|
||||||
// const sketches = categoryExamples.map(async sketch => await traverseSketchTree(sketch));
|
|
||||||
const sketches = categoryExamples.map(async sketch => traverseSketchTree(sketch));
|
const sketches = categoryExamples.map(async sketch => traverseSketchTree(sketch));
|
||||||
|
|
||||||
const result = await Q.all(sketches);
|
const result = await Q.all(sketches);
|
||||||
|
@ -139,37 +189,24 @@ function traverseAndFormat(parentObject) {
|
||||||
const parent = Object.assign({}, parentObject);
|
const parent = Object.assign({}, parentObject);
|
||||||
|
|
||||||
if (!parentObject.tree) {
|
if (!parentObject.tree) {
|
||||||
const newid = objectID().toHexString();
|
|
||||||
// returns the files
|
// returns the files
|
||||||
return {
|
return {
|
||||||
name: parent.name,
|
name: parent.name,
|
||||||
url: parent.download_url,
|
url: parent.download_url
|
||||||
content: null,
|
|
||||||
id: newid,
|
|
||||||
_id: newid,
|
|
||||||
fileType: 'file'
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const subdir = parentObject.tree.map((item) => {
|
const subdir = parentObject.tree.map((item) => {
|
||||||
const newid = objectID().toHexString();
|
|
||||||
if (!item.tree) {
|
if (!item.tree) {
|
||||||
// returns the files
|
// returns the files
|
||||||
return {
|
return {
|
||||||
name: item.name,
|
name: item.name,
|
||||||
url: item.download_url,
|
url: item.download_url
|
||||||
content: null,
|
|
||||||
id: newid,
|
|
||||||
_id: newid,
|
|
||||||
fileType: 'file'
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const feat = {
|
const feat = {
|
||||||
name: item.name,
|
name: item.name,
|
||||||
id: newid,
|
|
||||||
_id: newid,
|
|
||||||
fileType: 'folder',
|
|
||||||
children: traverseAndFormat(item)
|
children: traverseAndFormat(item)
|
||||||
};
|
};
|
||||||
return feat;
|
return feat;
|
||||||
|
@ -178,69 +215,27 @@ function traverseAndFormat(parentObject) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Traverse the tree and flatten for project.files[]
|
* Traverse the tree and download all the content,
|
||||||
|
* transforming into an object keyed by file/directory name
|
||||||
* @param {*} projectFileTree
|
* @param {*} projectFileTree
|
||||||
*/
|
*/
|
||||||
function traverseAndFlatten(projectFileTree) {
|
async function traverseAndDownload(projectFileTree) {
|
||||||
const r = objectID().toHexString();
|
return projectFileTree.reduce(
|
||||||
|
async (previousPromise, item, idx) => {
|
||||||
|
const result = await previousPromise;
|
||||||
|
|
||||||
const projectRoot = {
|
if (Array.isArray(item.children)) {
|
||||||
name: 'root',
|
result[item.name] = {
|
||||||
id: r,
|
files: await traverseAndDownload(item.children)
|
||||||
_id: r,
|
};
|
||||||
children: [],
|
} else {
|
||||||
fileType: 'folder'
|
result[item.name] = await fetchFileContent(item);
|
||||||
};
|
|
||||||
|
|
||||||
let currentParent;
|
|
||||||
|
|
||||||
const output = projectFileTree.reduce(
|
|
||||||
(result, item, idx) => {
|
|
||||||
if (idx < projectFileTree.length) {
|
|
||||||
projectRoot.children.push(item.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.fileType === 'file') {
|
|
||||||
if (item.name === 'sketch.js') {
|
|
||||||
item.isSelectedFile = true;
|
|
||||||
}
|
|
||||||
result.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
// here's where the magic happens *twinkles*
|
|
||||||
if (item.fileType === 'folder') {
|
|
||||||
// recursively go down the tree of children
|
|
||||||
currentParent = traverseAndFlatten(item.children);
|
|
||||||
// the above will return an array of the children files
|
|
||||||
// concatenate that with the results
|
|
||||||
result = result.concat(currentParent); // eslint-disable-line no-param-reassign
|
|
||||||
// since we want to get the children ids,
|
|
||||||
// we can map the child ids to the current item
|
|
||||||
// then push that to our result array to get
|
|
||||||
// our flat files array.
|
|
||||||
item.children = item.children.map(child => child.id);
|
|
||||||
result.push(item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
[projectRoot]
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Kind of hacky way to remove all roots other than the starting one
|
|
||||||
let counter = 0;
|
|
||||||
output.forEach((item, idx) => {
|
|
||||||
if (item.name === 'root') {
|
|
||||||
if (counter === 0) {
|
|
||||||
counter += 1;
|
|
||||||
} else {
|
|
||||||
output.splice(idx, 1);
|
|
||||||
counter += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -249,16 +244,14 @@ function traverseAndFlatten(projectFileTree) {
|
||||||
* @param {*} sketch
|
* @param {*} sketch
|
||||||
* @param {*} user
|
* @param {*} user
|
||||||
*/
|
*/
|
||||||
function formatSketchForStorage(sketch, user) {
|
async function formatSketchForStorage(sketch, user) {
|
||||||
const newProject = new Project({
|
const newProject = {
|
||||||
_id: shortid.generate(),
|
|
||||||
name: sketch.name,
|
name: sketch.name,
|
||||||
user: user._id,
|
files: {} // <== add files to this object
|
||||||
files: [] // <== add files to this array as file objects and add _id reference to children of root
|
};
|
||||||
});
|
|
||||||
|
|
||||||
let projectFiles = traverseAndFormat(sketch);
|
let projectFiles = traverseAndFormat(sketch);
|
||||||
projectFiles = traverseAndFlatten(projectFiles);
|
projectFiles = await traverseAndDownload(projectFiles);
|
||||||
newProject.files = projectFiles;
|
newProject.files = projectFiles;
|
||||||
return newProject;
|
return newProject;
|
||||||
}
|
}
|
||||||
|
@ -271,68 +264,50 @@ function formatSketchForStorageAll(sketchWithItems, user) {
|
||||||
|
|
||||||
sketchList = sketchList.map(sketch => formatSketchForStorage(sketch, user));
|
sketchList = sketchList.map(sketch => formatSketchForStorage(sketch, user));
|
||||||
|
|
||||||
return sketchList;
|
return Promise.all(sketchList);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all the content for the relevant files in project.files[]
|
* Fetch a list of all projects from the API
|
||||||
* @param {*} projectObject
|
|
||||||
*/
|
*/
|
||||||
async function fetchSketchContent(projectObject) {
|
async function getProjectsList() {
|
||||||
const output = Object.assign({}, JSON.parse(JSON.stringify(projectObject)));
|
const options = Object.assign({}, editorRequestOptions);
|
||||||
|
options.url = `${options.url}/sketches`;
|
||||||
|
|
||||||
const newFiles = output.files.map(async (item, i) => {
|
const results = await rp(options);
|
||||||
// if it is an html or js file
|
|
||||||
if (
|
|
||||||
(item.fileType === 'file' && item.name.endsWith('.html')) ||
|
|
||||||
item.name.endsWith('.js')
|
|
||||||
) {
|
|
||||||
const options = Object.assign({}, requestOptions);
|
|
||||||
options.url = `${item.url}`;
|
|
||||||
|
|
||||||
if (
|
return results.sketches;
|
||||||
options.url !== undefined ||
|
|
||||||
options.url !== null ||
|
|
||||||
options.url !== ''
|
|
||||||
) {
|
|
||||||
item.content = await rp(options);
|
|
||||||
// NOTE: remove the URL property if there's content
|
|
||||||
// Otherwise the p5 editor will try to pull from that url
|
|
||||||
if (item.content !== null) delete item.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
return item;
|
|
||||||
// if it is NOT an html or js file
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.url) {
|
|
||||||
const cdnRef = `https://cdn.jsdelivr.net/gh/ml5js/ml5-examples@${branchName}${
|
|
||||||
item.url.split(branchName)[1]
|
|
||||||
}`;
|
|
||||||
item.content = cdnRef;
|
|
||||||
item.url = cdnRef;
|
|
||||||
}
|
|
||||||
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
|
|
||||||
output.files = await Q.all(newFiles);
|
|
||||||
return output;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* STEP 5
|
* Delete a project
|
||||||
* Get all the content for the relevant files in project.files[] for all sketches
|
|
||||||
* @param {*} formattedSketchList
|
|
||||||
*/
|
*/
|
||||||
async function fetchSketchContentAll(formattedSketchList) {
|
async function deleteProject(project) {
|
||||||
let output = formattedSketchList.slice(0);
|
const options = Object.assign({}, editorRequestOptions);
|
||||||
|
options.method = 'DELETE';
|
||||||
|
options.url = `${options.url}/sketches/${project.id}`;
|
||||||
|
|
||||||
output = output.map(async item => fetchSketchContent(item));
|
const results = await rp(options);
|
||||||
|
|
||||||
output = await Q.all(output);
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
return output;
|
/**
|
||||||
|
* Create a new project
|
||||||
|
*/
|
||||||
|
async function createProject(project) {
|
||||||
|
try {
|
||||||
|
const options = Object.assign({}, editorRequestOptions);
|
||||||
|
options.method = 'POST';
|
||||||
|
options.url = `${options.url}/sketches`;
|
||||||
|
options.body = project;
|
||||||
|
|
||||||
|
const results = await rp(options);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -342,43 +317,34 @@ async function fetchSketchContentAll(formattedSketchList) {
|
||||||
* @param {*} user
|
* @param {*} user
|
||||||
*/
|
*/
|
||||||
async function createProjectsInP5User(filledProjectList, user) {
|
async function createProjectsInP5User(filledProjectList, user) {
|
||||||
const userProjects = await Project.find({ user: user._id });
|
console.log('Finding existing projects...');
|
||||||
const removeProjects = userProjects.map(async project => Project.remove({ _id: project._id }));
|
|
||||||
await Q.all(removeProjects);
|
|
||||||
console.log('deleted old projects!');
|
|
||||||
|
|
||||||
const newProjects = filledProjectList.map(async (project) => {
|
const existingProjects = await getProjectsList();
|
||||||
const item = new Project(project);
|
|
||||||
console.log(`saving ${project.name}`);
|
|
||||||
await item.save();
|
|
||||||
});
|
|
||||||
await Q.all(newProjects);
|
|
||||||
console.log(`Projects saved to User: ${editorUsername}!`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
console.log(`Will delete ${existingProjects.length} projects`);
|
||||||
* STEP 0
|
|
||||||
* CHECK if user exists, ifnot create one
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async function checkP5User() {
|
|
||||||
const user = await User.findOne({ username: editorUsername });
|
|
||||||
|
|
||||||
if (!user) {
|
try {
|
||||||
const ml5user = new User({
|
await Q.all(existingProjects.map(deleteProject));
|
||||||
username: editorUsername,
|
console.log('deleted old projects!');
|
||||||
email: process.env.ML5_EXAMPLES_EMAIL,
|
} catch (error) {
|
||||||
password: process.env.ML5_EXAMPLES_PASS
|
console.log('Problem deleting projects');
|
||||||
});
|
console.log(error);
|
||||||
|
process.exit(1);
|
||||||
await ml5user.save((saveErr) => {
|
}
|
||||||
if (saveErr) throw saveErr;
|
|
||||||
console.log(`Created a user p5${ml5user}`);
|
try {
|
||||||
|
const newProjects = filledProjectList.map(async (project) => {
|
||||||
|
console.log(`saving ${project.name}`);
|
||||||
|
await createProject(project);
|
||||||
});
|
});
|
||||||
|
await Q.all(newProjects);
|
||||||
|
console.log(`Projects saved to User: ${editorUsername}!`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error saving projects');
|
||||||
|
console.log(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ---------------------------------------------------------
|
* ---------------------------------------------------------
|
||||||
* --------------------- main ------------------------------
|
* --------------------- main ------------------------------
|
||||||
|
@ -394,18 +360,14 @@ async function checkP5User() {
|
||||||
* Delete existing and save
|
* Delete existing and save
|
||||||
*/
|
*/
|
||||||
async function make() {
|
async function make() {
|
||||||
await checkP5User();
|
|
||||||
// Get the user
|
|
||||||
const user = await User.findOne({
|
|
||||||
username: editorUsername
|
|
||||||
});
|
|
||||||
// Get the categories and their examples
|
// Get the categories and their examples
|
||||||
const categories = await getCategories();
|
const categories = await getCategories();
|
||||||
const categoryExamples = await getCategoryExamples(categories);
|
const categoryExamples = await getCategoryExamples(categories);
|
||||||
|
|
||||||
const examplesWithResourceTree = await traverseSketchTreeAll(categoryExamples);
|
const examplesWithResourceTree = await traverseSketchTreeAll(categoryExamples);
|
||||||
const formattedSketchList = formatSketchForStorageAll(examplesWithResourceTree, user);
|
const formattedSketchList = await formatSketchForStorageAll(examplesWithResourceTree);
|
||||||
const filledProjectList = await fetchSketchContentAll(formattedSketchList);
|
|
||||||
await createProjectsInP5User(filledProjectList, user);
|
await createProjectsInP5User(formattedSketchList);
|
||||||
console.log('done!');
|
console.log('done!');
|
||||||
process.exit();
|
process.exit();
|
||||||
}
|
}
|
||||||
|
@ -418,20 +380,14 @@ async function make() {
|
||||||
* Format the sketch files to be save to the db
|
* Format the sketch files to be save to the db
|
||||||
* Delete existing and save
|
* Delete existing and save
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
async function test() {
|
async function test() {
|
||||||
await checkP5User();
|
|
||||||
// Get the user
|
|
||||||
const user = await User.findOne({
|
|
||||||
username: editorUsername
|
|
||||||
});
|
|
||||||
|
|
||||||
// read from file while testing
|
// read from file while testing
|
||||||
const examplesWithResourceTree = JSON.parse(fs.readFileSync('./ml5-examplesWithResourceTree.json'));
|
const examplesWithResourceTree = JSON.parse(fs.readFileSync('./ml5-examplesWithResourceTree.json'));
|
||||||
|
|
||||||
const formattedSketchList = formatSketchForStorageAll(examplesWithResourceTree, user);
|
const formattedSketchList = await formatSketchForStorageAll(examplesWithResourceTree);
|
||||||
|
|
||||||
const filledProjectList = await fetchSketchContentAll(formattedSketchList);
|
await createProjectsInP5User(formattedSketchList);
|
||||||
await createProjectsInP5User(filledProjectList, user);
|
|
||||||
console.log('done!');
|
console.log('done!');
|
||||||
process.exit();
|
process.exit();
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import webpackHotMiddleware from 'webpack-hot-middleware';
|
||||||
import config from '../webpack/config.dev';
|
import config from '../webpack/config.dev';
|
||||||
|
|
||||||
// Import all required modules
|
// Import all required modules
|
||||||
|
import api from './routes/api.routes';
|
||||||
import users from './routes/user.routes';
|
import users from './routes/user.routes';
|
||||||
import sessions from './routes/session.routes';
|
import sessions from './routes/session.routes';
|
||||||
import projects from './routes/project.routes';
|
import projects from './routes/project.routes';
|
||||||
|
@ -51,7 +52,7 @@ const corsOriginsWhitelist = [
|
||||||
// Run Webpack dev server in development mode
|
// Run Webpack dev server in development mode
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
const compiler = webpack(config);
|
const compiler = webpack(config);
|
||||||
app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config[0].output.publicPath }));
|
app.use(webpackDevMiddleware(compiler, { lazy: false, noInfo: true, publicPath: config[0].output.publicPath }));
|
||||||
app.use(webpackHotMiddleware(compiler));
|
app.use(webpackHotMiddleware(compiler));
|
||||||
|
|
||||||
corsOriginsWhitelist.push(/localhost/);
|
corsOriginsWhitelist.push(/localhost/);
|
||||||
|
@ -95,6 +96,7 @@ app.use(session({
|
||||||
|
|
||||||
app.use(passport.initialize());
|
app.use(passport.initialize());
|
||||||
app.use(passport.session());
|
app.use(passport.session());
|
||||||
|
app.use('/api/v1', requestsOfTypeJSON(), api);
|
||||||
app.use('/api', requestsOfTypeJSON(), users);
|
app.use('/api', requestsOfTypeJSON(), users);
|
||||||
app.use('/api', requestsOfTypeJSON(), sessions);
|
app.use('/api', requestsOfTypeJSON(), sessions);
|
||||||
app.use('/api', requestsOfTypeJSON(), files);
|
app.use('/api', requestsOfTypeJSON(), files);
|
||||||
|
|
16
server/utils/__mocks__/createId.js
Normal file
16
server/utils/__mocks__/createId.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/**
|
||||||
|
* Creates an increasing numeric ID
|
||||||
|
* for testing
|
||||||
|
*/
|
||||||
|
let nextId = 0;
|
||||||
|
|
||||||
|
export default function mockCreateId() {
|
||||||
|
const id = nextId;
|
||||||
|
nextId += 1;
|
||||||
|
|
||||||
|
return String(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetMockCreateId() {
|
||||||
|
nextId = 0;
|
||||||
|
}
|
33
server/utils/createApplicationErrorClass.js
Normal file
33
server/utils/createApplicationErrorClass.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
/**
|
||||||
|
* This is the base class for custom errors in
|
||||||
|
* the application.
|
||||||
|
*/
|
||||||
|
export class ApplicationError extends Error {
|
||||||
|
constructor(message, extra = {}) {
|
||||||
|
super();
|
||||||
|
if (Error.captureStackTrace) {
|
||||||
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
}
|
||||||
|
this.name = 'ApplicationError';
|
||||||
|
this.message = message;
|
||||||
|
this.code = extra.code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new custom error class e.g.
|
||||||
|
* const UserNotFoundError = createApplicationErrorClass('UserNotFoundError');
|
||||||
|
*
|
||||||
|
* // Later
|
||||||
|
* throw new UserNotFoundError(`user ${user.name} not found`);
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export default function createApplicationErrorClass(name) {
|
||||||
|
return class extends ApplicationError {
|
||||||
|
constructor(...params) {
|
||||||
|
super(...params);
|
||||||
|
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
8
server/utils/createId.js
Normal file
8
server/utils/createId.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import objectID from 'bson-objectid';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a mongo ID
|
||||||
|
*/
|
||||||
|
export default function createId() {
|
||||||
|
return objectID().toHexString();
|
||||||
|
}
|
Loading…
Reference in a new issue