Browse Source

Reworking how the validation and object cleaning system works so I can then also fix the Form to use FormField and the correct validation.

master
Zed A. Shaw 2 months ago
parent
commit
7f23646453
  1. 39
      api/admin/table.js
  2. 2
      api/register.js
  3. 2
      api/user/profile.js
  4. 74
      client/components/Form.svelte
  5. 32
      client/pages/admin/ReadUpdate.svelte
  6. 12
      lib/api.js
  7. 98
      lib/ormish.js

39
api/admin/table.js

@ -1,4 +1,4 @@
import { knex } from '../../lib/ormish.js';
import { knex, validation } from '../../lib/ormish.js';
import logging from "../../lib/logging.js";
import { developer_admin, API } from '../../lib/api.js';
@ -77,14 +77,23 @@ export const post = async (req, res) => {
if(!api.admin_authenticated) {
return api.error(401, "Admin rights required.");
} else if(name && row_id) {
let data = req.body;
// remove id so it's not updated too
delete data.id;
let res = await knex(name).where({id: row_id}).update(data);
return api.reply(200, {message: "OK", result: res});
// normally the validation rules come from the
// model class once and placed at the top of the
// module, but here we get it dynamically based
// on the name.
const rules = validation(name, {}, true);
const data = api.validate(rules);
if(!data._valid) {
return api.validation_error(res, data, rules);
} else {
// remove id so it's not updated too
api.clean_form(data, rules, ["id"]);
let res = await knex(name).
where({id: row_id}).update(data);
return api.reply(200, {message: "OK", result: res});
}
} else {
return api.error(500, "name and row_id required for delete");
}
@ -104,11 +113,17 @@ export const put = async (req, res) => {
if(!api.admin_authenticated) {
return api.error(401, "Admin rights required.");
} else if(name) {
delete req.body.id;
const rules = validation(name, {}, true);
const data = api.validate(rules);
let res = await knex(name).insert(req.body);
if(!data._valid) {
return api.validation_error(res, data, rules);
} else {
api.clean_form(data, rules, ["id"]);
let res = await knex(name).insert(data);
return api.reply(200, {message: "OK", id: res[0]});
return api.reply(200, {message: "OK", id: res[0]});
}
} else {
return api.error(500, "name required for delete");
}

2
api/register.js

@ -19,7 +19,7 @@ export const post = async (req, res) => {
if(!form._valid) {
return api.validation_error(res, form, rules);
} else {
api.clean_form(form);
api.clean_form(form, rules);
let good = await User.register({
email: form.email,

2
api/user/profile.js

@ -21,7 +21,7 @@ export const post = async (req, res) => {
if(!user._valid) {
return api.validation_error(res, user, rules);
} else {
api.clean_form(user, ["password_repeat"]);
api.clean_form(user, rules, ["password_repeat"]);
user.password = User.encrypt_password(user.password);

74
client/components/Form.svelte

@ -1,16 +1,18 @@
<script>
import { push, link } from 'svelte-spa-router';
import Icon from "./Icon.svelte";
import { link } from 'svelte-spa-router';
import Spinner from "./Spinner.svelte";
import FormField from "./FormField.svelte";
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import api from "$/client/api.js";
import { defer } from "$/client/helpers.js";
export let label_names = [];
let label_names = [];
export let data = {_errors: {}, _valid: false};
let schema = {};
export let data = {};
export let table = "";
export let notice = "";
let current_column = false;
let schema_promise = defer();
onMount(async () => {
let [status, tables] = await api.get('/api/admin/schema');
@ -24,8 +26,10 @@
}
label_names = Object.keys(schema);
schema_promise.resolve();
} else {
push('/');
notice = "Unable to load table schema.";
schema_promise.reject();
}
});
</script>
@ -48,53 +52,57 @@
color: var(--color-accent);
text-shadow: 2px 2px var(--color-shadow);
}
span#help-overlay {
font-size: 0.6em;
}
</style>
<section>
{#await schema_promise}
<Spinner />
{:then form}
<form>
<card>
<top>
<h1><a href="/admin/table/{ table }/" use:link>{ table }</a></h1>
{#if notice}<b in:fade class="notice">{ notice }</b>{/if}
{#if data._errors && data._errors.main}
<error data-testid="update-error">{ data._errors.main }</error>
{:else if notice}
<b in:fade|local class="notice">{ notice }</b>
{/if}
</top>
<middle>
{#each label_names as label}
<label for={ label }>
{ label }
<span on:mouseover={ () => current_column = label }>
<Icon name="help-circle" size="18" />
<FormField form={ data } field={ label }
label={ label }>
<span id="help-overlay">
{schema[label]["defaultValue"]}|
{schema[label]["maxLength"]}|
{schema[label]["nullable"]}|
{schema[label]["type"]}
</span>
</label>
<input disabled={ label == "id" } name={ label } id={ label } bind:value={ data[label] }>
<input disabled={ label == "id" } name={ label } id={ label } bind:value={ data[label] }>
</FormField>
{/each}
</middle>
<bottom>
{#if notice}
<callout in:fade|local>
{ notice }
</callout>
{/if}
<button-group>
<slot></slot>
</button-group>
{#if current_column}
<schemas in:fade>
<h3>{ current_column }</h3>
<table>
<thead>
<tr><th>default</th><th>max</th><th>null</th><th>type</th></tr>
</thead>
<tbody>
<tr>
<td>{schema[current_column]["defaultValue"]}</td>
<td>{schema[current_column]["maxLength"]}</td>
<td>{schema[current_column]["nullable"]}</td>
<td>{schema[current_column]["type"]}</td>
</tr>
</tbody>
</table>
</schemas>
{/if}
</bottom>
</card>
</form>
{:catch}
<callout class="error">
<p>{ notice }</p>
</callout>
{/await}
</section>

32
client/pages/admin/ReadUpdate.svelte

@ -7,21 +7,25 @@
import Form from "$/client/components/Form.svelte";
import api from "$/client/api.js";
import { log } from "$/client/logging.js";
import { defer } from "$/client/helpers.js";
let form_data = [];
let label_names = [];
let form_data = {_errors: {}, _valid: false};
let notice = "";
export let params = {};
let delete_confirm = false;
let data_promise = defer();
const delete_record = async () => {
let [status, data] = await api.del(`/api/admin/table?name=${params.table}&row_id=${params.row_id}`);
if(status === 200) {
form_data = data;
form_data._errors = {}; // setup for errors later
push(`/admin/table/${params.table}/`);
} else if(status == 401) {
push('/login/');
} else {
push('/');
notice = "Failed to delete.";
}
}
@ -32,8 +36,11 @@
if(status == 200) {
notice = "Update successful.";
} else if(status == 401) {
push('/login/');
} else {
push('/');
notice = "Update failed.";
form_data = Object.assign(form_data, data);
}
}
@ -52,9 +59,12 @@
if(status == 200) {
form_data = data;
label_names = Object.keys(data);
data_promise.resolve();
} else if(status == 401) {
push('/login/');
} else {
push('/');
notice = "Failed to load table data.";
data_promise.reject();
}
});
</script>
@ -66,7 +76,10 @@
</style>
<Layout authenticated={ true } testid="page-admin-readupdate">
<Form data={form_data} table={params.table} notice={ notice } label_names = {label_names}>
{#await data_promise}
<!-- form already has a spinner -->
{:then}
<Form data={form_data} table={params.table} notice={ notice }>
<a href="/admin/table/{ params.table }" data-testid="button-back" use:link>
<Icon name="arrow-left-circle" size="48" />
</a>
@ -83,6 +96,11 @@
<Icon name="trash" size="48" />
</span>
</Form>
{:catch}
<callout class="error">
<p>{ notice }</p>
</callout>
{/await}
</Layout>
{#if delete_confirm}

12
lib/api.js

@ -44,10 +44,14 @@ export class API {
return res.status(400).json({_errors: form._errors, _rules: rules, _valid: form._valid});
}
clean_form(form, extras=[]) {
delete form._errors;
delete form._valid;
delete form._rules;
clean_form(form, rules, extras=[]) {
// what I wouldn't give for set operations
for(let [key, rule] of Object.entries(form)) {
if(!(key in rules)) {
delete form[key];
}
}
for(let field of extras) {
delete form[field];
}

98
lib/ormish.js

@ -19,6 +19,64 @@ const load_schema = async () => {
await load_schema();
/**
* In some cases (like the generic admin) you need to get validation
* rules but you don't have a specific class to work with. This function
* is called by Model.validation and you can call it directly to get rules
* for a database table.
*
* @param name string - the table name.
* @param rules Object - default rules with empty "" for the rules you want filled in
* @param all boolean - set this to true if you want everything
* @return Object - the resulting rules to use with Validator
*/
export const validation = (name, rules, all=false) => {
assert(rules, "rules parameter is required and will be modified");
const schema = SCHEMA[name];
assert(schema, `There is no schema for table named ${name}`);
for(let [key, opts] of Object.entries(schema._columns)) {
if(all || rules[key] === "") {
let required = opts.nullable ? "nullable|" : "required|";
switch(opts.type) {
case "varchar": // fallthrough
case "text":
// some databases have an email type but we'll also look for ones named email
if(key === "email") {
rules[key] = required + "email";
} else {
rules[key] = required + "string";
}
if(opts.maxLength) {
rules[key] += `|min:0|max:${opts.maxLength}`;
}
break;
case "boolean":
rules[key] = required + "boolean";
break;
case "integer":
case "float":
rules[key] = required + "numeric";
break;
case "datetime":
rules[key] = required + "date";
break;
case "email":
// not all databases have this
rules[key] = required + "email";
break;
default:
rules[key] = required;
}
}
}
console.log("RULES", rules);
return rules;
}
export class Model {
constructor(attr) {
assert(attr, "Must give attributes.");
@ -62,45 +120,7 @@ export class Model {
* @param rules {Object} - rules specifier
*/
static validation(rules) {
assert(rules, "rules parameter is required and will be modified");
for(let [key, opts] of Object.entries(this.schema)) {
if(rules[key] === "") {
switch(opts.type) {
case "varchar": // fallthrough
case "text":
// some databases have an email type but we'll also look for ones named email
if(key === "email") {
rules[key] = "required|email";
} else {
rules[key] = "required|string";
}
if(opts.maxLength) {
rules[key] += `|min:0|max:${opts.maxLength}`;
}
break;
case "boolean":
rules[key] = "required|boolean";
break;
case "integer":
case "float":
rules[key] = "required|numeric";
break;
case "datetime":
rules[key] = "required|date";
break;
case "email":
// not all databases have this
rules[key] = "required|email";
break;
default:
rules[key] = "required";
}
}
}
return rules;
return validation(this.table_name, rules);
}
async destroy() {

Loading…
Cancel
Save