p5.js-web-editor/server/domain-objects/Project.js
Andrew Nicolaou 37fcf46972 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-09-08 16:45:58 +02:00

133 lines
3.4 KiB
JavaScript

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);
}