p5.js-web-editor/server/domain-objects/Project.js

134 lines
3.4 KiB
JavaScript
Raw Permalink Normal View History

Public API: Create new project (fixes #1095) (#1106) * 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.
2019-08-30 18:26:57 +00:00
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);
}