Merge branch '246-add-endpoint-for-creating-a-new-survey' into 'devel'

Resolve "Add endpoint for creating a new survey"

Closes #179, #174, and #246

See merge request wgp/dougal/software!29
This commit is contained in:
D. Berge
2023-09-02 13:10:56 +00:00
58 changed files with 1831 additions and 522 deletions

View File

@@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"scripts": {
"serve": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve",
"serve": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve --host=0.0.0.0",
"build": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service build"
},
"dependencies": {

View File

@@ -8,6 +8,10 @@ async function getProject ({commit, dispatch}, projectId) {
const recentProjects = JSON.parse(localStorage.getItem("recentProjects") || "[]")
recentProjects.unshift(res);
localStorage.setItem("recentProjects", JSON.stringify(recentProjects.slice(0, 3)));
} else {
commit('setProjectName', null);
commit('setProjectId', null);
commit('setProjectSchema', null);
}
}

View File

@@ -0,0 +1,14 @@
function projectId (state) {
return state.projectId;
}
function projectName (state) {
return state.projectName;
}
function projectSchema (state) {
return state.projectSchema;
}
export default { projectId, projectName, projectSchema };

View File

@@ -1,18 +1,25 @@
<template>
<v-container fluid fill-height class="ma-0 pa-0">
<v-row no-gutters align="stretch" class="fill-height">
<v-col cols="12">
<v-col cols="12" v-if="projectFound">
<!-- Show component here according to selected route -->
<keep-alive>
<router-view :key="$route.path"></router-view>
</keep-alive>
</v-col>
<v-col cols="12" v-else>
<v-card>
<v-card-text>
Project does not exist.
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script>
import { mapActions } from 'vuex'
import { mapActions, mapGetters } from 'vuex'
export default {
name: 'Project',
@@ -24,6 +31,24 @@ export default {
}
},
computed: {
projectFound () {
return this.loading || this.projectId;
},
...mapGetters(["loading", "projectId", "serverEvent"])
},
watch: {
async serverEvent (event) {
if (event.channel == "project" && event.payload?.operation == "DELETE" && event.payload?.schema == "public") {
// Project potentially deleted
await this.getProject(this.$route.params.project);
}
}
},
methods: {
...mapActions(["getProject"])
},

View File

@@ -83,7 +83,17 @@ export default {
},
computed: {
...mapGetters(['loading'])
...mapGetters(['loading', 'serverEvent'])
},
watch: {
async serverEvent (event) {
if (event.channel == "project" && event.payload?.schema == "public") {
if (event.payload?.operation == "DELETE" || event.payload?.operation == "INSERT") {
await this.load();
}
}
}
},
methods: {

View File

@@ -292,9 +292,13 @@
<v-list-item v-for="(path, index) in item.raw_files"
key="index"
link
title="View the shot log"
title="Download file"
:href="`/api/files${path}`"
>
{{ basename(path) }}
<v-list-item-action>
<v-icon right small>mdi-cloud-download</v-icon>
</v-list-item-action>
</v-list-item>
</v-list-group>
<v-list-group value="true" v-if="item.final_files">
@@ -308,10 +312,13 @@
</template>
<v-list-item v-for="(path, index) in item.final_files"
key="index"
link
title="View the shot log"
title="Download file"
:href="`/api/files${path}`"
>
{{ basename(path) }}
<v-list-item-action>
<v-icon right small>mdi-cloud-download</v-icon>
</v-list-item-action>
</v-list-item>
</v-list-group>
</v-list>

View File

@@ -4,6 +4,7 @@ module.exports = {
"leaflet-arrowheads"
],
devServer: {
host: "0.0.0.0",
proxy: {
"^/api(/|$)": {
target: "http://localhost:3000",

View File

@@ -1,6 +1,7 @@
const http = require('http');
const express = require('express');
express.yaml ??= require('body-parser').yaml; // NOTE: Use own customised body-parser
const cookieParser = require('cookie-parser')
const maybeSendAlert = require("../lib/alerts");
@@ -31,6 +32,7 @@ app.map = function(a, route){
};
app.use(express.json({type: "application/json", strict: false, limit: '10mb'}));
app.use(express.yaml({type: "application/yaml", limit: '10mb'}));
app.use(express.urlencoded({ type: "application/x-www-form-urlencoded", extended: true }));
app.use(express.text({type: "text/*", limit: '10mb'}));
app.use((req, res, next) => {
@@ -84,13 +86,19 @@ app.use(mw.auth.authentify);
// We must be authenticated before we can access these
app.map({
'/project': {
get: [ mw.project.list ], // Get list of projects
get: [ mw.project.get ], // Get list of projects
post: [ mw.auth.access.admin, mw.project.post ], // Create a new project
},
'/project/:project': {
get: [ mw.project.get ], // Get project data
get: [ mw.project.summary.get ], // Get project data
delete: [ mw.auth.access.admin, mw.project.delete ], // Delete a project (only if empty)
},
'/project/:project/summary': {
get: [ mw.project.get ],
get: [ mw.project.summary.get ],
},
'/project/:project/configuration': {
get: [ mw.auth.access.write, mw.project.configuration.get ], // Get project configuration
patch: [ mw.auth.access.write, mw.project.configuration.patch ], // Modify project configuration
},
/*
@@ -244,6 +252,12 @@ app.map({
// // post: [ mw.permissions.post ],
// // delete: [ mw.permissions.delete ]
// },
'/project/:project/files/:path(*)': {
get: [ mw.auth.access.write, mw.files.get ]
},
'/files/?:path(*)': {
get: [ mw.auth.access.write, mw.files.get ]
},
'/navdata/': {
get: [ mw.navdata.get ],
'gis/:featuretype(line|point)': {

View File

@@ -0,0 +1,29 @@
const files = require('../../../lib/files');
module.exports = async function (req, res, next) {
try {
const entity = await files.get(req.params.path, req.params.project, req.query);
if (entity) {
if (entity.download) {
res.download(...entity.download, (err) => next(err));
} else {
// Directory listing
res.status(203).json(entity);
next();
}
} else {
throw {
status: 404,
code: "ENOENT"
};
}
} catch (err) {
if (err.code == 'ENOENT') {
res.status(404).json({message: err.code});
} else {
next(err);
}
}
};

View File

@@ -0,0 +1,7 @@
module.exports = {
get: require('./get'),
post: require('./post'),
put: require('./put'),
delete: require('./delete')
}

View File

@@ -1,5 +1,6 @@
module.exports = {
event: require('./event'),
files: require('./files'),
plan: require('./plan'),
line: require('./line'),
project: require('./project'),

View File

@@ -0,0 +1,13 @@
const { project } = require('../../../../lib/db');
module.exports = async function (req, res, next) {
try {
res.status(200).send(await project.configuration.get(req.params.project));
next();
} catch (err) {
next(err);
}
};

View File

@@ -0,0 +1,8 @@
module.exports = {
get: require('./get'),
// post: require('./post'),
// put: require('./put'),
patch: require('./patch'),
// delete: require('./delete'),
};

View File

@@ -0,0 +1,16 @@
const { project } = require('../../../../lib/db');
module.exports = async function (req, res, next) {
try {
// TODO
// Implement If-Match header requirements
res.send(await project.configuration.patch(req.params.project, req.body));
next();
} catch (err) {
next(err);
}
};

View File

@@ -0,0 +1,15 @@
const { project } = require('../../../lib/db');
module.exports = async function (req, res, next) {
try {
await project.delete(req.params.project)
res.status(204).send();
next();
} catch (err) {
next(err);
}
};

View File

@@ -4,10 +4,11 @@ const { project} = require('../../../lib/db');
module.exports = async function (req, res, next) {
try {
res.status(200).send(await project.get(req.params.project));
res.status(200).send(await project.get());
next();
} catch (err) {
next(err);
}
};

View File

@@ -1,4 +1,7 @@
module.exports = {
list: require('./list'),
get: require('./get')
get: require('./get'),
post: require('./post'),
delete: require('./delete'),
summary: require('./summary'),
configuration: require('./configuration'),
};

View File

@@ -1,14 +0,0 @@
const { project} = require('../../../lib/db');
module.exports = async function (req, res, next) {
try {
res.status(200).send(await project.list());
next();
} catch (err) {
next(err);
}
};

View File

@@ -0,0 +1,16 @@
const { project } = require('../../../lib/db');
module.exports = async function (req, res, next) {
try {
const payload = req.body;
const projectDefinition = await project.post(payload);
res.status(201).send(projectDefinition);
next();
} catch (err) {
next(err);
}
};

View File

@@ -0,0 +1,13 @@
const { project } = require('../../../../lib/db');
module.exports = async function (req, res, next) {
try {
res.status(200).send(await project.summary.get(req.params.project));
next();
} catch (err) {
next(err);
}
};

View File

@@ -0,0 +1,3 @@
module.exports = {
get: require('./get'),
};

View File

@@ -1,17 +1,9 @@
const { setSurvey } = require('../connection');
const { setSurvey, pool } = require('../connection');
async function get (projectId, path, opts = {}) {
const client = await setSurvey(projectId);
const text = `
SELECT data
FROM file_data fd
INNER JOIN files f USING (hash)
WHERE f.path LIKE '%.yaml';
`;
const res = await client.query(text);
client.release();
const text = `SELECT meta FROM public.projects WHERE pid = $1;`;
const res = await pool.query(text, [projectId]);
const config = res.rows.length == 1
? res.rows[0].data

View File

@@ -49,6 +49,15 @@ async function schema2pid (schema, client) {
return res.rows[0] && res.rows[0].pid;;
}
async function pid2schema (pid, client) {
if (!client) {
client = await pool.connect();
}
const res = await client.query("SELECT schema FROM projects WHERE pid = $1", [pid]);
client.release();
return res.rows[0] && res.rows[0].schema;
}
/** Fetch one row from a database cursor.
*
* @a cursor A query cursor
@@ -75,5 +84,6 @@ module.exports = {
transaction,
setSurvey,
schema2pid,
pid2schema,
fetchRow
};

View File

@@ -0,0 +1,25 @@
const { setSurvey } = require('../../connection');
async function get (projectId, opts = {}) {
try {
const client = await setSurvey(); // Use public schema
const text = `
SELECT meta
FROM projects
WHERE pid = $1;
`;
const res = await client.query(text, [projectId]);
client.release();
return res.rows[0].meta;
} catch (err) {
if (err.code == "42P01") {
throw { status: 404, message: "Not found" };
} else {
throw err;
}
}
}
module.exports = get;

View File

@@ -0,0 +1,8 @@
module.exports = {
get: require('./get'),
// post: require('./post'),
// put: require('./put'),
patch: require('./patch'),
// delete: require('./delete'),
};

View File

@@ -0,0 +1,54 @@
const { setSurvey } = require('../../connection');
const { deepMerge } = require('../../../utils');
const { modify } = require('../create');
async function patch (projectId, payload, opts = {}) {
let client;
try {
client = await setSurvey(); // Use public schema
const text = `
SELECT meta
FROM projects
WHERE pid = $1;
`;
const res = await client.query(text, [projectId]);
const source = res.rows[0].meta;
if (!source) {
throw { status: 404, message: "Not found" };
}
if (("id" in payload) && (projectId != payload.id)) {
throw {
status: 422,
message: "Project ID cannot be changed in this Dougal version"
}
}
if (("name" in payload) && (source.name != payload.name)) {
throw {
status: 422,
message: "Project name cannot be changed in this Dougal version"
}
}
const dest = deepMerge(source, payload);
await modify(projectId, dest);
return dest;
} catch (err) {
if (err.code == "42P01") {
throw { status: 404, message: "Not found" };
} else {
throw err;
}
} finally {
client.release();
}
}
module.exports = patch;

View File

@@ -0,0 +1,193 @@
const path = require('path');
const fs = require('fs').promises;
const cfg = require('DOUGAL_ROOT/lib/config');
const { setSurvey, pool } = require('../connection');
const get = require('./get');
const { INFO, DEBUG, WARNING, ERROR } = require('DOUGAL_ROOT/debug')(__filename);
function checkSyntax (value, type = "project") {
switch (type) {
case "project":
var requiredFields = {
id: "string",
name: "string",
epsg: "number",
binning: function (value) { return checkSyntax (value, "binning"); }
};
break;
case "binning":
var requiredFields = {
theta: "number",
I_inc: "number",
J_inc: "number",
I_width: "number",
J_width: "number",
origin: function (value) { return checkSyntax (value, "origin"); }
}
break
case "origin":
var requiredFields = {
easting: "number",
northing: "number",
I: "number",
J: "number"
}
break;
default:
return typeof type == "function"
? type(value)
: typeof value == type;
}
// return Object.entries(requiredFields).every( ([field, test]) => {
// return value.hasOwnProperty(field) && checkSyntax(value[field], test);
// });
for (const [field, test] of Object.entries(requiredFields)) {
if (!value.hasOwnProperty(field)) {
return `Missing required property: ${field}`;
}
const res = checkSyntax(value[field], test);
if (res !== true) {
return res === false ? `Syntax error on "${field}"` : res;
}
}
return true;
}
async function applySchemaTemplate (projectDefinition) {
const templatePath = path.resolve(cfg.DOUGAL_ROOT, "etc/db/schema-template.sql");
const text = await fs.readFile(templatePath, "utf-8");
return text.replace(/_SURVEY__TEMPLATE_/g, projectDefinition.schema).replace(/_EPSG__CODE_/g, projectDefinition.epsg);
}
async function surveyIds () {
const res = await pool.query("SELECT schema FROM public.projects;");
if (res.rows?.length) {
return res.rows.map( s => Number(s.schema.replace(/^survey_/, "")) ).sort((a, b) => a-b);
} else {
return []
}
}
async function nextSurveyId () {
const ids = await surveyIds();
if (ids.length) {
return ids.pop() + 1;
} else {
return 1;
}
}
async function idExists (id) {
const surveys = await get();
return surveys.includes(s => s.pid.toLowerCase() == id.toLowerCase());
}
async function createProjectSchema (projectDefinition) {
const sql = await applySchemaTemplate(projectDefinition);
const client = await pool.connect();
try {
await client.query(sql);
} catch (err) {
console.error(err);
} finally {
client.release(true);
}
}
async function dropProjectSchema (projectDefinition) {
const sql = `
DROP SCHEMA ${projectDefinition.schema} CASCADE;
`;
try {
return await pool.query(sql);
} catch (err) {
console.error("dropProjectSchema", err);
}
}
async function addProjectToList (projectDefinition) {
const sql = `
INSERT INTO public.projects (pid, name, schema, meta) VALUES (LOWER($1), $2, $3, $4);
`;
const values = [ projectDefinition.id, projectDefinition.name, projectDefinition.schema, projectDefinition ];
try {
return await pool.query(sql, values);
} catch (err) {
if (err.code == "23505") {
if (err.constraint == "projects_name_key") {
throw { message: "A project with this name already exists" }
}
} else {
throw err;
}
}
}
async function updateProject (projectId, projectDefinition) {
const sql = `
UPDATE public.projects
SET
name = $2,
meta = $3
WHERE pid = $1;
`;
const values = [ projectId, projectDefinition.name, projectDefinition ];
try {
return await pool.query(sql, values);
} catch (err) {
throw err;
}
}
async function create (projectDefinition) {
const syntaxOk = checkSyntax(projectDefinition);
if (syntaxOk !== true) {
throw { status: 400, message: syntaxOk };
} else if (await idExists(projectDefinition.id)) {
throw { status: 409 }
} else {
try {
const survey_id = await nextSurveyId();
projectDefinition.schema = `survey_${survey_id}`;
projectDefinition.archived = projectDefinition.archived ?? false;
await createProjectSchema(projectDefinition)
await addProjectToList(projectDefinition);
} catch (err) {
DEBUG(err);
await dropProjectSchema(projectDefinition);
throw { status: 500, message: err.message ?? "Failed to create database for new project", detail: err.detail }
}
}
}
async function modify (projectId, projectDefinition) {
const syntaxOk = checkSyntax(projectDefinition);
if (syntaxOk !== true) {
throw { status: 400, message: syntaxOk };
} else {
try {
const res = await updateProject(projectId, projectDefinition);
} catch (err) {
DEBUG(err);
throw { status: 500, message: err.message ?? "Failed to update project definition", detail: err.detail }
}
}
}
module.exports = {
checkSyntax,
create,
modify
};

View File

@@ -0,0 +1,56 @@
const { setSurvey, pid2schema, pool } = require('../connection');
const event = require('../event');
const getSummary = require('./summary').get;
// Returns true if the project has no
// preplots, sequences or log entries,
// or if the project ID is not found
// in the database.
async function isDeletable (projectId) {
let summary;
try {
summary = await getSummary(projectId);
} catch (err) {
if (err.code == "42P01") {
// Project does not exist
return true;
} else {
throw err;
}
}
if (!summary) {
// projectId does not exist in the database
return true;
}
if (summary.total == 0 && summary.seq_raw == 0 && summary.seq_final == 0 && !summary.prod_duration) {
// Check for existing events (excluding deleted)
const events = await event.list(projectId, {limit: 1});
return events.length == 0;
}
return false;
};
async function del (projectId, opts = {}) {
if (await isDeletable(projectId)) {
const schema = await pid2schema(projectId);
if (schema) {
// NOTE: Should be reasonably safe as `schema` is not
// under user control.
const sql = `
DROP SCHEMA ${schema} CASCADE;
DELETE FROM public.projects WHERE schema = '${schema}';
`;
console.log(sql);
await pool.query(sql);
}
// We don't care if schema does not exist
} else {
throw { status: 405, message: "Project is not empty" }
}
}
module.exports = del;

View File

@@ -1,16 +1,8 @@
const { setSurvey } = require('../connection');
const { setSurvey, pool } = require('../connection');
async function get (projectId, opts = {}) {
const client = await setSurvey(projectId);
const text = `
SELECT *
FROM project_summary;
`;
const res = await client.query(text);
client.release();
return res.rows[0];
async function get () {
const res = await pool.query("SELECT pid, name, schema FROM public.projects;");
return res.rows;
}
module.exports = get;

View File

@@ -1,8 +1,9 @@
module.exports = {
list: require('./list'),
get: require('./get'),
post: require('./post'),
put: require('./put'),
delete: require('./delete')
delete: require('./delete'),
summary: require('./summary'),
configuration: require('./configuration'),
}

View File

@@ -1,8 +0,0 @@
const { setSurvey, pool } = require('../connection');
async function list () {
const res = await pool.query("SELECT * FROM public.projects;");
return res.rows;
}
module.exports = list;

View File

@@ -0,0 +1,29 @@
const { setSurvey, pool } = require('../connection');
const { create } = require('./create');
/*
* Creating a new project consists of these steps:
*
* - Check that the payload is well formed and includes all required items.
* If not, return 400.
* - Check if the id already exists. If it does, return 409.
* - Figure out what the next schema name is going to be (survey_XXX).
* - Read the SQL template from $DOUGAL_ROOT/etc/db/schema-template.sql and
* replace the `_SURVEY__TEMPLATE_` and `_EPSG__CODE_` placeholders with
* appropriate values.
* - Apply the resulting SQL.
* - Add the appropriate entry into public.projects
* - Add the survey definition details (the request's payload) into the
* database (or create a YAML file under $DOUGAL_ROOT/etc/surveys?)
* - Return a 201 with the survey definition as the payload.
*/
async function post (payload) {
try {
return await create(payload);
} catch (err) {
throw err;
}
}
module.exports = post;

View File

@@ -0,0 +1,24 @@
const { setSurvey } = require('../../connection');
async function get (projectId, opts = {}) {
try {
const client = await setSurvey(projectId);
const text = `
SELECT *
FROM project_summary;
`;
const res = await client.query(text);
client.release();
return res.rows[0];
} catch (err) {
if (err.code == "42P01") {
throw { status: 404, message: "Not found" };
} else {
throw err;
}
}
}
module.exports = get;

View File

@@ -0,0 +1,3 @@
module.exports = {
get: require('./get'),
};

View File

View File

@@ -0,0 +1,125 @@
const fs = require('fs/promises');
const Path = require('path');
const mime = require('./mime-types');
const { translatePath, logicalRoot } = require('./logical');
const systemCfg = require('../config');
const projectCfg = require('../db/configuration');
async function directoryListing (fullPath, root) {
const contents = await fs.readdir(fullPath, {withFileTypes: true});
const listing = [];
for (const entry of contents) {
const resolved = Path.resolve(fullPath, entry.name);
const relative = resolved.substring(fullPath.length).replace(/^\/+/, "");
const logical = Path.join(root, relative);
const stat = await fs.stat(resolved);
const mimetype = entry.isDirectory()
? "inode/directory"
: (mime.contentType(entry.name) || "application/octet-stream");
listing.push({
path: logical,
basename: Path.basename(relative),
"Content-Type": mimetype,
size: stat.size,
atime: stat.atime,
mtime: stat.mtime,
ctime: stat.ctime,
birthtime: stat.birthtime
});
}
return listing;
}
async function virtualDirectoryListing (logicalPaths) {
const listing = [];
for (const logical of logicalPaths) {
const fullPath = translatePath(logical);
const resolved = Path.resolve("/", logical);
const stat = await fs.stat(fullPath);
const mimetype = stat.isDirectory()
? "inode/directory"
: (mime.contentType(fullPath) || "application/octet-stream");
listing.push({
path: resolved,
basename: Path.basename(logical),
"Content-Type": mimetype,
size: stat.size,
atime: stat.atime,
mtime: stat.mtime,
ctime: stat.ctime,
birthtime: stat.birthtime
});
}
return listing;
}
async function projectRelativeGet (path, query) {
console.log("Not implemented yet");
throw {status: 404, message: "ENOENT"};
}
async function systemRelativeGet (path, query) {
try {
if (!path) {
return await systemRelativeGet("/", query);
} else if (Path.resolve(path) == "/") {
return await virtualDirectoryListing(logicalRoot())
} else {
const physicalPath = translatePath(path);
const stats = await fs.stat(physicalPath);
if (stats.isDirectory()) {
// Return directory listing, with types.
return await directoryListing(physicalPath, "/"+path.replace(/^\/+/, ""));
} else if (stats.isFile()) {
// Returns a list of arguments suitable for ExpressJS res.download
const headers = {
"Content-Type": mime.lookup(physicalPath) || "application/octet-stream"
};
return {
download: [ physicalPath, Path.basename(path), { headers } ]
};
} else {
throw {status: 403, message: "ENOACCESS"};
}
}
} catch (err) {
console.error(err);
throw err;
}
}
module.exports = async function get (path, projectId, query) {
if (projectId) {
return await projectRelativeGet(path, query);
} else {
return await systemRelativeGet(path, query);
}
};
/*
module.exports = async function get (path, projectId, query) {
const root = projectId
? Path.resolve(systemCfg.global.files.root, await projectCfg.get(projectId, "rootPath"))
: systemCfg.global.files.root;
const fullPath = Path.resolve(root, path);
// Check if there is an attempt to break out of root path
if (Path.relative(root, fullPath).includes("..")) {
// Throw something resolving to a 404
throw {status: 404, message: "ENOENT"};
} else {
const stats = await fs.stat(fullPath);
if (stats.isDirectory()) {
// Return directory listing, with types.
return await directoryListing(fullPath, root);
} else if (stats.isFile()) {
// Returns a list of arguments suitable for ExpressJS res.download
return {
download: [ fullPath, Path.basename(path) ]
};
} else {
throw {status: 403, message: "ENOACCESS"};
}
}
}*/

View File

@@ -0,0 +1,7 @@
module.exports = {
get: require('./get'),
post: require('./post'),
put: require('./put'),
delete: require('./delete')
}

View File

@@ -0,0 +1,71 @@
const Path = require('path');
const cfg = require('../config');
function translatePath (file) {
const root = Path.resolve(cfg.DOUGAL_ROOT);
const importPaths = cfg.global?.imports?.paths;
function validate (physicalPath, prefix) {
if (physicalPath.startsWith(prefix)) {
return physicalPath;
} else {
// An attempt to break out of the logical path?
throw {
status: 404,
message: "Not found"
};
}
}
if (Path.isAbsolute(file)) {
if (typeof importPaths === "string") {
// Substitute the root for the real physical path
// NOTE: `root` deals with import_paths not being absolute
const prefix = Path.resolve(Path.join(root, importPaths));
const suffix = Path.resolve(file).replace(/^\/+/, "");
const physicalPath = Path.resolve(Path.join(prefix, suffix));
return validate(physicalPath, prefix);
} else if (typeof importPaths === "object") {
const parts = Path.resolve(file).split("/").slice(1);
if (parts[0] in importPaths) {
const prefix = Path.join("/", importPaths[parts[0]])
const suffix = parts.slice(1).join("/");
const physicalPath = Path.resolve(Path.join(prefix, suffix));
return validate(physicalPath, prefix);
} else {
return validate(file, null); // Throws 404
}
} else {
// Most likely importPaths is undefined
return validate(file, null); // Throws 404
}
} else {
// A relative filepath is always resolved relative to the logical root
return translatePath(Path.resolve(Path.join("/", file)));
}
}
function untranslatePath (file) {
}
function logicalRoot () {
const root = Path.resolve(cfg.DOUGAL_ROOT);
const importPaths = cfg.global?.imports?.paths;
if (typeof importPaths === "string") {
return [ "/" ];
} else if (typeof importPaths === "object") {
return Object.keys(importPaths);
} else {
// Most likely importPaths is undefined
return [];
}
}
module.exports = {
translatePath,
untranslatePath,
logicalRoot
};

View File

@@ -0,0 +1,22 @@
const mime = require('mime-types');
const extraTypes = {
"text/plain": [
"sps", "SPS",
"p1", "P1",
"p190", "P190",
"p111", "P111",
"p2", "P2",
"p294", "P294",
"p211", "P211",
"hdr", "HDR"
]
};
for (let [mimeType, extensions] of Object.entries(extraTypes)) {
for (let extension of extensions) {
mime.types[extension] = mimeType;
}
}
module.exports = mime;

View File

View File

View File

@@ -0,0 +1,67 @@
// Copied from:
// https://gomakethings.com/how-to-deep-merge-arrays-and-objects-with-javascript/
/*!
* Deep merge two or more objects or arrays.
* (c) 2023 Chris Ferdinandi, MIT License, https://gomakethings.com
* @param {*} ...objs The arrays or objects to merge
* @returns {*} The merged arrays or objects
*/
function deepMerge (...objs) {
/**
* Get the object type
* @param {*} obj The object
* @return {String} The object type
*/
function getType (obj) {
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}
/**
* Deep merge two objects
* @return {Object}
*/
function mergeObj (clone, obj) {
for (let [key, value] of Object.entries(obj)) {
let type = getType(value);
if (clone[key] !== undefined && getType(clone[key]) === type && ['array', 'object'].includes(type)) {
clone[key] = deepMerge(clone[key], value);
} else {
clone[key] = structuredClone(value);
}
}
}
// Create a clone of the first item in the objs array
let clone = structuredClone(objs.shift());
// Loop through each item
for (let obj of objs) {
// Get the object type
let type = getType(obj);
// If the current item isn't the same type as the clone, replace it
if (getType(clone) !== type) {
clone = structuredClone(obj);
continue;
}
// Otherwise, merge
if (type === 'array') {
// Replace old array with new
clone = [...structuredClone(obj)];
} else if (type === 'object') {
mergeObj(clone, obj);
} else {
clone = obj;
}
}
return clone;
}
module.exports = deepMerge;

View File

@@ -3,5 +3,6 @@ module.exports = {
geometryAsString: require('./geometryAsString'),
dms: require('./dms'),
replaceMarkers: require('./replaceMarkers'),
flattenQCDefinitions: require('./flattenQCDefinitions')
flattenQCDefinitions: require('./flattenQCDefinitions'),
deepMerge: require('./deepMerge')
};

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@
"linux"
],
"dependencies": {
"body-parser": "gitlab:aaltronav/contrib/expressjs/body-parser",
"cookie-parser": "^1.4.5",
"debug": "^4.3.4",
"express": "^4.17.1",