mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 10:37:07 +00:00
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:
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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"])
|
||||
},
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,6 +4,7 @@ module.exports = {
|
||||
"leaflet-arrowheads"
|
||||
],
|
||||
devServer: {
|
||||
host: "0.0.0.0",
|
||||
proxy: {
|
||||
"^/api(/|$)": {
|
||||
target: "http://localhost:3000",
|
||||
|
||||
@@ -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)': {
|
||||
|
||||
0
lib/www/server/api/middleware/files/delete.js
Normal file
0
lib/www/server/api/middleware/files/delete.js
Normal file
29
lib/www/server/api/middleware/files/get.js
Normal file
29
lib/www/server/api/middleware/files/get.js
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
7
lib/www/server/api/middleware/files/index.js
Normal file
7
lib/www/server/api/middleware/files/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
module.exports = {
|
||||
get: require('./get'),
|
||||
post: require('./post'),
|
||||
put: require('./put'),
|
||||
delete: require('./delete')
|
||||
}
|
||||
0
lib/www/server/api/middleware/files/post.js
Normal file
0
lib/www/server/api/middleware/files/post.js
Normal file
0
lib/www/server/api/middleware/files/put.js
Normal file
0
lib/www/server/api/middleware/files/put.js
Normal file
@@ -1,5 +1,6 @@
|
||||
module.exports = {
|
||||
event: require('./event'),
|
||||
files: require('./files'),
|
||||
plan: require('./plan'),
|
||||
line: require('./line'),
|
||||
project: require('./project'),
|
||||
|
||||
13
lib/www/server/api/middleware/project/configuration/get.js
Normal file
13
lib/www/server/api/middleware/project/configuration/get.js
Normal 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);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
module.exports = {
|
||||
get: require('./get'),
|
||||
// post: require('./post'),
|
||||
// put: require('./put'),
|
||||
patch: require('./patch'),
|
||||
// delete: require('./delete'),
|
||||
};
|
||||
16
lib/www/server/api/middleware/project/configuration/patch.js
Normal file
16
lib/www/server/api/middleware/project/configuration/patch.js
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
15
lib/www/server/api/middleware/project/delete.js
Normal file
15
lib/www/server/api/middleware/project/delete.js
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
@@ -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'),
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
16
lib/www/server/api/middleware/project/post.js
Normal file
16
lib/www/server/api/middleware/project/post.js
Normal 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);
|
||||
}
|
||||
|
||||
};
|
||||
13
lib/www/server/api/middleware/project/summary/get.js
Normal file
13
lib/www/server/api/middleware/project/summary/get.js
Normal 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);
|
||||
}
|
||||
|
||||
};
|
||||
3
lib/www/server/api/middleware/project/summary/index.js
Normal file
3
lib/www/server/api/middleware/project/summary/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
get: require('./get'),
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
25
lib/www/server/lib/db/project/configuration/get.js
Normal file
25
lib/www/server/lib/db/project/configuration/get.js
Normal 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;
|
||||
8
lib/www/server/lib/db/project/configuration/index.js
Normal file
8
lib/www/server/lib/db/project/configuration/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
module.exports = {
|
||||
get: require('./get'),
|
||||
// post: require('./post'),
|
||||
// put: require('./put'),
|
||||
patch: require('./patch'),
|
||||
// delete: require('./delete'),
|
||||
};
|
||||
54
lib/www/server/lib/db/project/configuration/patch.js
Normal file
54
lib/www/server/lib/db/project/configuration/patch.js
Normal 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;
|
||||
193
lib/www/server/lib/db/project/create.js
Normal file
193
lib/www/server/lib/db/project/create.js
Normal 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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'),
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
24
lib/www/server/lib/db/project/summary/get.js
Normal file
24
lib/www/server/lib/db/project/summary/get.js
Normal 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;
|
||||
3
lib/www/server/lib/db/project/summary/index.js
Normal file
3
lib/www/server/lib/db/project/summary/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
get: require('./get'),
|
||||
};
|
||||
0
lib/www/server/lib/files/delete.js
Normal file
0
lib/www/server/lib/files/delete.js
Normal file
125
lib/www/server/lib/files/get.js
Normal file
125
lib/www/server/lib/files/get.js
Normal 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"};
|
||||
}
|
||||
}
|
||||
}*/
|
||||
7
lib/www/server/lib/files/index.js
Normal file
7
lib/www/server/lib/files/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
module.exports = {
|
||||
get: require('./get'),
|
||||
post: require('./post'),
|
||||
put: require('./put'),
|
||||
delete: require('./delete')
|
||||
}
|
||||
71
lib/www/server/lib/files/logical.js
Normal file
71
lib/www/server/lib/files/logical.js
Normal 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
|
||||
};
|
||||
22
lib/www/server/lib/files/mime-types.js
Normal file
22
lib/www/server/lib/files/mime-types.js
Normal 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;
|
||||
0
lib/www/server/lib/files/post.js
Normal file
0
lib/www/server/lib/files/post.js
Normal file
0
lib/www/server/lib/files/put.js
Normal file
0
lib/www/server/lib/files/put.js
Normal file
67
lib/www/server/lib/utils/deepMerge.js
Normal file
67
lib/www/server/lib/utils/deepMerge.js
Normal 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;
|
||||
@@ -3,5 +3,6 @@ module.exports = {
|
||||
geometryAsString: require('./geometryAsString'),
|
||||
dms: require('./dms'),
|
||||
replaceMarkers: require('./replaceMarkers'),
|
||||
flattenQCDefinitions: require('./flattenQCDefinitions')
|
||||
flattenQCDefinitions: require('./flattenQCDefinitions'),
|
||||
deepMerge: require('./deepMerge')
|
||||
};
|
||||
|
||||
1095
lib/www/server/package-lock.json
generated
1095
lib/www/server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user