Add database functions for project creation.

Instead of storing the project configuration in a YAML file
under `etc/surveys/`, this is now stored in public.projects.meta.

NOTE: as of this commit, the runner scripts (`bin/*.py`) are not
aware of this change and they will keep looking for project info
under `etc/surveys`. This means that projects created directly
in the database will be invisible to Dougal until the runner
scripts are changed accordingly.
This commit is contained in:
D. Berge
2023-08-21 14:39:45 +02:00
parent f2edd2bec5
commit 63b9cc5b16
2 changed files with 178 additions and 0 deletions

View File

@@ -0,0 +1,149 @@
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 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 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 {
console.log("create");
const survey_id = await nextSurveyId();
console.log("survey id", survey_id);
projectDefinition.schema = `survey_${survey_id}`;
projectDefinition.archived = projectDefinition.archived ?? false;
await createProjectSchema(projectDefinition)
await addProjectToList(projectDefinition);
} catch (err) {
DEBUG(err);
throw { status: 500, message: err.message ?? "Failed to create database for new project", detail: err.detail }
}
}
}
module.exports = {
checkSyntax,
create
};

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;