Initial content

This commit is contained in:
Adam Freeman
2024-05-29 18:54:49 +01:00
parent 2dbc1a8088
commit 5215a1d919
1944 changed files with 203378 additions and 0 deletions
@@ -0,0 +1,3 @@
document.addEventListener('DOMContentLoaded', () => {
// do nothing
});
@@ -0,0 +1,25 @@
import validator from "validator";
export const validate = (propName, formdata) => {
const val = formdata.get(propName);
const results = { };
const validationChain = {
get propertyName() { return propName},
get results () { return results }
};
validationChain.required = () => {
results.required = !validator.isEmpty(val, { ignore_whitespace: true});
return validationChain;
}
validationChain.minLength = (min) => {
results.minLength = validator.isLength(val, { min});
return validationChain;
};
validationChain.isInteger = () => {
results.isInteger = validator.isInt(val);
return validationChain;
}
return validationChain;
}
@@ -0,0 +1,13 @@
import { select } from "@inquirer/prompts";
import { ops } from "./operations.mjs";
(async function run() {
let loop = true;
while (loop) {
const selection = await select({
message: "Select an operation",
choices: [...Object.keys(ops).map(k => {return { value: k }})]
});
await ops[selection]();
}
})();
@@ -0,0 +1,73 @@
import { input } from "@inquirer/prompts";
const baseUrl = "http://localhost:5000";
export const ops = {
"Get All": () => sendRequest("GET", "/api/results"),
"Get Name": async () => {
const name = await input({ message: "Name?"});
await sendRequest("GET", `/api/results?name=${name}`);
},
"Get ID": async () => {
const id = await input({ message: "ID?"});
await sendRequest("GET", `/api/results/${id}`);
},
"Store": async () => {
const values = {
name: await input({message: "Name?"}),
age: await input({message: "Age?"}),
years: await input({message: "Years?"})
};
await sendRequest("POST", "/api/results", values);
},
"Delete": async () => {
const id = await input({ message: "ID?"});
await sendRequest("DELETE", `/api/results/${id}`);
},
"Replace": async () => {
const id = await input({ message: "ID?"});
const values = {
name: await input({message: "Name?"}),
age: await input({message: "Age?"}),
years: await input({message: "Years?"}),
nextage: await input({message: "Next Age?"})
};
await sendRequest("PUT", `/api/results/${id}`, values);
},
"Modify": async () => {
const id = await input({ message: "ID?"});
const values = {
name: await input({message: "Name?"}),
age: await input({message: "Age?"}),
years: await input({message: "Years?"}),
nextage: await input({message: "Next Age?"})
};
await sendRequest("PATCH", `/api/results/${id}`,
Object.entries(values).filter(([p, v]) => v !== "")
.map(([p, v]) => ({ op: "replace", path: "/" + p, value: v})),
"application/json-patch+json");
},
"Exit": () => process.exit()
}
const sendRequest = async (method, url, body, contentType) => {
const response = await fetch(baseUrl + url, {
method, headers: { "Content-Type": contentType ?? "application/json"},
body: JSON.stringify(body)
});
if (response.status == 200) {
const data = await response.json();
(Array.isArray(data) ? data : [data])
.forEach(elem => console.log(JSON.stringify(elem)));
} else {
console.log(response.status + " " + response.statusText);
}
}
@@ -0,0 +1,31 @@
import { Id, NullableId, Params } from "@feathersjs/feathers";
import { WebService } from "./http_adapter";
export class FeathersWrapper<T> {
constructor(private ws: WebService<T>) {}
get(id: Id) {
return this.ws.getOne(id);
}
find(params: Params) {
return this.ws.getMany(params.query);
}
create(data: any, params: Params) {
return this.ws.store(data);
}
remove(id: NullableId, params: Params) {
return this.ws.delete(id);
}
update(id: NullableId, data: any, params: Params) {
return this.ws.replace(id, data);
}
patch(id: NullableId, data: any, params: Params) {
return this.ws.modify(id, data);
}
}
@@ -0,0 +1,68 @@
import { Express, Response } from "express";
import { ValidationError } from "./validation_types";
export interface WebService<T> {
getOne(id: any) : Promise<T | undefined>;
getMany(query: any) : Promise<T[]>;
store(data: any) : Promise<T | undefined>;
delete(id: any): Promise<boolean>;
replace(id: any, data: any): Promise<T | undefined>;
modify(id: any, data: any): Promise<T | undefined>;
}
export function createAdapter<T>(app: Express, ws: WebService<T>, baseUrl: string) {
app.get(baseUrl, async (req, resp) => {
try {
resp.json(await ws.getMany(req.query));
resp.end();
} catch (err) { writeErrorResponse(err, resp) }
});
app.get(`${baseUrl}/:id`, async (req, resp) => {
try {
const data = await ws.getOne((req.params.id));
if (data == undefined) {
resp.writeHead(404);
} else {
resp.json(data);
}
resp.end();
} catch (err) { writeErrorResponse(err, resp) }
});
app.post(baseUrl, async (req, resp) => {
try {
const data = await ws.store(req.body);
resp.json(data);
resp.end();
} catch (err) { writeErrorResponse(err, resp) }
});
app.delete(`${baseUrl}/:id`, async (req, resp) => {
try {
resp.json(await ws.delete(req.params.id));
resp.end();
} catch (err) { writeErrorResponse(err, resp) }
});
app.put(`${baseUrl}/:id`, async (req, resp) => {
try {
resp.json(await ws.replace(req.params.id, req.body));
resp.end();
} catch (err) { writeErrorResponse(err, resp) }
});
app.patch(`${baseUrl}/:id`, async (req, resp) => {
try {
resp.json(await ws.modify(req.params.id, req.body));
resp.end();
} catch (err) { writeErrorResponse(err, resp) }
});
const writeErrorResponse = (err: any, resp: Response) => {
console.error(err);
resp.writeHead(err instanceof ValidationError ? 400 : 500);
resp.end();
}
}
@@ -0,0 +1,33 @@
import { Express } from "express";
import { createAdapter } from "./http_adapter";
import { ResultWebService } from "./results_api";
import { Validator } from "./validation_adapter";
import { ResultWebServiceValidation } from "./results_api_validation";
import { FeathersWrapper } from "./feathers_adapter";
import { feathers } from "@feathersjs/feathers";
import feathersExpress, { rest } from "@feathersjs/express";
import { ValidationError } from "./validation_types";
export const createApi = (app: Express) => {
// createAdapter(app, new Validator(new ResultWebService(),
// ResultWebServiceValidation), "/api/results");
const feathersApp = feathersExpress(feathers(), app).configure(rest());
const service = new Validator(new ResultWebService(),
ResultWebServiceValidation);
feathersApp.use('/api/results', new FeathersWrapper(service));
feathersApp.hooks({
error: {
all: [(ctx) => {
if (ctx.error instanceof ValidationError) {
ctx.http = { status: 400};
ctx.error = undefined;
}
}]
}
});
}
@@ -0,0 +1,48 @@
import { WebService } from "./http_adapter";
import { Result } from "../data/repository";
import repository from "../data";
import * as jsonpatch from "fast-json-patch";
import { validateModel } from "./validation_functions";
import { ResultModelValidation } from "./results_api_validation";
export class ResultWebService implements WebService<Result> {
getOne(id: any): Promise<Result | undefined> {
return repository.getResultById(id);
}
getMany(query: any): Promise<Result[]> {
if (query.name) {
return repository.getResultsByName(query.name, 10);
} else {
return repository.getAllResults(10);
}
}
async store(data: any): Promise<Result | undefined> {
const { name, age, years} = data;
const nextage = age + years;
const id = await repository.saveResult({ id: 0, name, age,
years, nextage});
return await repository.getResultById(id);
}
delete(id: any): Promise<boolean> {
return repository.delete(Number.parseInt(id));
}
replace(id: any, data: any): Promise<Result | undefined> {
const { name, age, years, nextage } = data;
const validated = validateModel({ name, age, years, nextage },
ResultModelValidation)
return repository.update({ id, ...validated });
}
async modify(id: any, data: any): Promise<Result | undefined> {
const dbData = await this.getOne(id);
if (dbData !== undefined) {
return await this.replace(id,
jsonpatch.applyPatch(dbData, data).newDocument);
}
}
}
@@ -0,0 +1,31 @@
import { ModelValidation, ValidationRequirements, ValidationRule,
WebServiceValidation } from "./validation_types";
import validator from "validator";
const intValidator : ValidationRule = {
validation: [val => validator.isInt(val.toString())],
converter: (val) => Number.parseInt(val)
}
const partialResultValidator: ValidationRequirements = {
name: [(val) => !validator.isEmpty(val)],
age: intValidator,
years: intValidator
}
export const ResultWebServiceValidation: WebServiceValidation = {
keyValidator: intValidator,
store: partialResultValidator,
replace: {
...partialResultValidator,
nextage: intValidator
}
}
export const ResultModelValidation : ModelValidation = {
propertyRules: { ...partialResultValidator, nextage: intValidator },
modelRule: [(m: any) => m.nextage === m.age + m.years]
}
@@ -0,0 +1,49 @@
import { WebService } from "./http_adapter";
import { validate, validateIdProperty } from "./validation_functions";
import { WebServiceValidation } from "./validation_types";
export class Validator<T> implements WebService<T> {
constructor(private ws: WebService<T>,
private validation: WebServiceValidation) {}
getOne(id: any): Promise<T | undefined> {
return this.ws.getOne(this.validateId(id));
}
getMany(query: any): Promise<T[]> {
if (this.validation.getMany) {
query = validate(query, this.validation.getMany);
}
return this.ws.getMany(query);
}
store(data: any): Promise<T | undefined> {
if (this.validation.store) {
data = validate(data, this.validation.store);
}
return this.ws.store(data);
}
delete(id: any): Promise<boolean> {
return this.ws.delete(this.validateId(id));
}
replace(id: any, data: any): Promise<T | undefined> {
if (this.validation.replace) {
data = validate(data, this.validation.replace);
}
return this.ws.replace(this.validateId(id), data);
}
modify(id: any, data: any): Promise<T | undefined> {
if (this.validation.modify) {
data = validate(data, this.validation.modify);
}
return this.ws.modify(this.validateId(id), data);
}
validateId(val: any) {
return validateIdProperty(val, this.validation);
}
}
@@ -0,0 +1,59 @@
import { ModelValidation, ValidationError, ValidationRequirements,
ValidationRule, WebServiceValidation } from "./validation_types";
export type ValidationResult = [valid: boolean, value: any];
export function validate(data: any, reqs: ValidationRequirements): any {
let validatedData: any = {};
Object.entries(reqs).forEach(([prop, rule]) => {
const [valid, value] = applyRule(data[prop], rule);
if (valid) {
validatedData[prop] = value;
} else {
throw new ValidationError(prop, "Validation Error");
}
});
return validatedData;
}
function applyRule(val: any,
rule: ValidationRule): ValidationResult {
const required = Array.isArray(rule) ? true : rule.required;
const checks = Array.isArray(rule) ? rule : rule.validation;
const convert = Array.isArray(rule) ? (v: any) => v : rule.converter;
if (val === null || val == undefined || val === "") {
return [required ? false : true, val];
}
let valid = true;
checks.forEach(check => {
if (!check(val)) {
valid = false;
}
});
return [valid, convert ? convert(val) : val];
}
export function validateIdProperty<T>(val: any,
v: WebServiceValidation) : any {
if (v.keyValidator) {
const [valid, value] = applyRule(val, v.keyValidator);
if (valid) {
return value;
}
throw new ValidationError("ID", "Validation Error");
}
return val;
}
export function validateModel(model: any, rules: ModelValidation) : any {
if (rules.propertyRules) {
model = validate(model, rules.propertyRules);
}
if (rules.modelRule) {
const [valid, data] = applyRule(model, rules.modelRule);
if (valid) {
return data;
}
throw new ValidationError("Model", "Validation Error");
}
}
@@ -0,0 +1,30 @@
export interface WebServiceValidation {
keyValidator?: ValidationRule;
getMany?: ValidationRequirements;
store?: ValidationRequirements;
replace?: ValidationRequirements;
modify?: ValidationRequirements;
}
export type ValidationRequirements = {
[key: string] : ValidationRule
}
export type ValidationRule =
((value: any) => boolean)[] |
{
required? : boolean,
validation: ((value: any) => boolean)[],
converter?: (value: any) => any,
}
export class ValidationError implements Error {
constructor(public name: string, public message: string) {}
stack?: string | undefined;
cause?: unknown;
}
export type ModelValidation = {
modelRule?: ValidationRule,
propertyRules?: ValidationRequirements
}
@@ -0,0 +1,29 @@
//import { IncomingMessage, ServerResponse } from "http";
//import { signCookie, validateCookie } from "./cookies_signed";
import { CookieOptions, Request, Response } from "express";
// const setheaderName = "Set-Cookie";
// const cookieSecret = "mysecret";
export const setCookie = (resp: Response, name: string, val: string,
opts?: CookieOptions) => {
resp.cookie(name, val, {
maxAge: 300 * 1000,
sameSite: "strict",
signed: true,
...opts
});
}
export const setJsonCookie = (resp: Response, name: string, val: any) => {;
setCookie(resp, name, JSON.stringify(val));
}
export const getCookie = (req: Request, key: string): string | undefined => {
return req.signedCookies[key];
}
export const getJsonCookie = (req: Request, key: string) : any => {
const cookie = getCookie(req, key);
return cookie ? JSON.parse(cookie) : undefined;
}
@@ -0,0 +1,17 @@
import { createHmac, timingSafeEqual } from "crypto";
export const signCookie = (value: string, secret: string) => {
return value + "." + createHmac("sha512", secret)
.update(value).digest("base64url");
}
export const validateCookie = (value: string, secret: string) => {
const cookieValue = value.split(".")[0];
const compareBuf = Buffer.from(signCookie(cookieValue, secret));
const candidateBuf = Buffer.from(value);
if (compareBuf.length == candidateBuf.length &&
timingSafeEqual(compareBuf, candidateBuf)) {
return cookieValue;
}
return undefined;
}
@@ -0,0 +1,5 @@
import { ApiRepository } from "./repository";
import { OrmRepository } from "./orm_repository";
const repository: ApiRepository = new OrmRepository();
export default repository;
@@ -0,0 +1,63 @@
import { DataTypes, Sequelize } from "sequelize";
import { Calculation, Person, ResultModel } from "./orm_models";
import { Result } from "./repository";
const primaryKey = {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
}
};
export const initializeModels = (sequelize: Sequelize) => {
Person.init({
...primaryKey,
name: { type: DataTypes.STRING }
}, { sequelize });
Calculation.init({
...primaryKey,
age: { type: DataTypes.INTEGER},
years: { type: DataTypes.INTEGER},
nextage: { type: DataTypes.INTEGER},
}, { sequelize });
ResultModel.init({
...primaryKey,
}, { sequelize });
}
export const defineRelationships = () => {
ResultModel.belongsTo(Person, { foreignKey: "personId" });
ResultModel.belongsTo(Calculation, { foreignKey: "calculationId"});
}
export const addSeedData = async (sequelize: Sequelize) => {
await sequelize.query(`
INSERT INTO Calculations
(id, age, years, nextage, createdAt, updatedAt) VALUES
(1, 35, 5, 40, date(), date()),
(2, 35, 10, 45, date(), date())`);
await sequelize.query(`
INSERT INTO People (id, name, createdAt, updatedAt) VALUES
(1, 'Alice', date(), date()), (2, "Bob", date(), date())`);
await sequelize.query(`
INSERT INTO ResultModels
(calculationId, personId, createdAt, updatedAt) VALUES
(1, 1, date(), date()), (2, 2, date(), date()),
(2, 1, date(), date());`);
}
export const fromOrmModel = (model: ResultModel | null) : Result => {
return {
id: model?.id || 0,
name: model?.Person?.name || "",
age: model?.Calculation?.age || 0,
years: model?.Calculation?.years || 0,
nextage: model?.Calculation?.nextage || 0
}
}
@@ -0,0 +1,29 @@
import { Model, CreationOptional, ForeignKey, InferAttributes,
InferCreationAttributes } from "sequelize";
export class Person extends Model<InferAttributes<Person>,
InferCreationAttributes<Person>> {
declare id?: CreationOptional<number>;
declare name: string
}
export class Calculation extends Model<InferAttributes<Calculation>,
InferCreationAttributes<Calculation>> {
declare id?: CreationOptional<number>;
declare age: number;
declare years: number;
declare nextage: number;
}
export class ResultModel extends Model<InferAttributes<ResultModel>,
InferCreationAttributes<ResultModel>> {
declare id: CreationOptional<number>;
declare personId: ForeignKey<Person["id"]>;
declare calculationId: ForeignKey<Calculation["id"]>;
declare Person?: InferAttributes<Person>;
declare Calculation?: InferAttributes<Calculation>;
}
@@ -0,0 +1,98 @@
import { Sequelize } from "sequelize";
import { ApiRepository, Result } from "./repository";
import { addSeedData, defineRelationships,
fromOrmModel, initializeModels } from "./orm_helpers";
import { Calculation, Person, ResultModel } from "./orm_models";
export class OrmRepository implements ApiRepository {
sequelize: Sequelize;
constructor() {
this.sequelize = new Sequelize({
dialect: "sqlite",
storage: "orm_age.db",
logging: console.log,
logQueryParameters: true
});
this.initModelAndDatabase();
}
async initModelAndDatabase() : Promise<void> {
initializeModels(this.sequelize);
defineRelationships();
await this.sequelize.drop();
await this.sequelize.sync();
await addSeedData(this.sequelize);
}
async saveResult(r: Result): Promise<number> {
return await this.sequelize.transaction(async (tx) => {
const [person] = await Person.findOrCreate({
where: { name : r.name},
transaction: tx
});
const [calculation] = await Calculation.findOrCreate({
where: {
age: r.age, years: r.years, nextage: r.nextage
},
transaction: tx
});
return (await ResultModel.create({
personId: person.id, calculationId: calculation.id},
{transaction: tx})).id;
});
}
async getAllResults(limit: number): Promise<Result[]> {
return (await ResultModel.findAll({
include: [Person, Calculation],
limit,
order: [["id", "DESC"]]
})).map(row => fromOrmModel(row));
}
async getResultsByName(name: string, limit: number): Promise<Result[]> {
return (await ResultModel.findAll({
include: [Person, Calculation],
where: {
"$Person.name$": name
},
limit, order: [["id", "DESC"]]
})).map(row => fromOrmModel(row));
}
async getResultById(id: number): Promise<Result | undefined> {
const model = await ResultModel.findByPk(id, {
include: [Person, Calculation ]
});
return model ? fromOrmModel(model): undefined;
}
async delete(id: number): Promise<boolean> {
const count = await ResultModel.destroy({ where: { id }});
return count == 1;
}
async update(r: Result) : Promise<Result | undefined > {
const mod = await this.sequelize.transaction(async (transaction) => {
const stored = await ResultModel.findByPk(r.id);
if (stored !== null) {
const [person] = await Person.findOrCreate({
where: { name : r.name}, transaction
});
const [calculation] = await Calculation.findOrCreate({
where: {
age: r.age, years: r.years, nextage: r.nextage
}, transaction
});
stored.personId = person.id;
stored.calculationId = calculation.id;
return await stored.save({transaction});
}
});
return mod ? this.getResultById(mod.id) : undefined;
}
}
@@ -0,0 +1,25 @@
export interface Result {
id: number,
name: string,
age: number,
years: number,
nextage: number
}
export interface Repository {
saveResult(r: Result): Promise<number>;
getAllResults(limit: number) : Promise<Result[]>;
getResultsByName(name: string, limit: number): Promise<Result[]>;
}
export interface ApiRepository extends Repository {
getResultById(id: number): Promise<Result | undefined>;
delete(id: number) : Promise<boolean>;
update(r: Result) : Promise<Result | undefined>
}
@@ -0,0 +1,37 @@
import { Database } from "sqlite3";
export class TransactionHelper {
steps: [sql: string, params: any][] = [];
add(sql: string, params: any): TransactionHelper {
this.steps.push([sql, params]);
return this;
}
run(db: Database): Promise<number> {
return new Promise((resolve, reject) => {
let index = 0;
let lastRow: number = NaN;
const cb = (err: any, rowID?: number) => {
if (err) {
db.run("ROLLBACK", () => reject());
} else {
lastRow = rowID ? rowID : lastRow;
if (++index === this.steps.length) {
db.run("COMMIT", () => resolve(lastRow));
} else {
this.runStep(index, db, cb);
}
}
}
db.run("BEGIN", () => this.runStep(0, db, cb));
});
}
runStep(idx: number, db: Database, cb: (err: any, row: number) => void) {
const [sql, params] = this.steps[idx];
db.run(sql, params, function (err: any) {
cb(err, this.lastID)
});
}
}
@@ -0,0 +1,31 @@
const baseSql = `
SELECT Results.*, name, age, years, nextage FROM Results
INNER JOIN People ON personId = People.id
INNER JOIN Calculations ON calculationId = Calculations.id`;
const endSql = `ORDER BY id DESC LIMIT $limit`;
export const queryAllSql = `${baseSql} ${endSql}`;
export const queryByNameSql = `${baseSql} WHERE name = $name ${endSql}`;
export const insertPerson = `
INSERT INTO People (name)
SELECT $name
WHERE NOT EXISTS (SELECT name FROM People WHERE name = $name)`;
export const insertCalculation = `
INSERT INTO Calculations (age, years, nextage)
SELECT $age, $years, $nextage
WHERE NOT EXISTS
(SELECT age, years, nextage FROM Calculations
WHERE age = $age AND years = $years AND nextage = $nextage)`;
export const insertResult = `
INSERT INTO Results (personId, calculationId)
SELECT People.id as personId, Calculations.id as calculationId from People
CROSS JOIN Calculations
WHERE People.name = $name
AND Calculations.age = $age
AND Calculations.years = $years
AND Calculations.nextage = $nextage`;
@@ -0,0 +1,50 @@
import { readFileSync } from "fs";
import { Database } from "sqlite3";
import { Repository, Result } from "./repository";
import { queryAllSql, queryByNameSql,
insertPerson, insertCalculation, insertResult } from "./sql_queries";
import { TransactionHelper } from "./sql_helpers";
export class SqlRepository implements Repository {
db: Database;
constructor() {
this.db = new Database("age.db");
this.db.exec(readFileSync("age.sql").toString(), err => {
if (err != undefined) throw err;
});
}
async saveResult(r: Result): Promise<number> {
return await new TransactionHelper()
.add(insertPerson, { $name: r.name })
.add(insertCalculation, {
$age: r.age, $years: r.years, $nextage: r.nextage
})
.add(insertResult, {
$name: r.name,
$age: r.age, $years: r.years, $nextage: r.nextage
})
.run(this.db);
}
getAllResults($limit: number): Promise<Result[]> {
return this.executeQuery(queryAllSql, { $limit });
}
getResultsByName($name: string, $limit: number): Promise<Result[]> {
return this.executeQuery(queryByNameSql, { $name, $limit });
}
executeQuery(sql: string, params: any) : Promise<Result[]> {
return new Promise<Result[]>((resolve, reject) => {
this.db.all<Result>(sql, params, (err, rows) => {
if (err == undefined) {
resolve(rows);
} else {
reject(err);
}
})
});
}
}
@@ -0,0 +1,44 @@
import express, { Express } from "express";
import repository from "./data";
import { getJsonCookie, setJsonCookie } from "./cookies";
import cookieMiddleware from "cookie-parser";
import { customSessionMiddleware } from "./sessions/middleware";
import { getSession, sessionMiddleware } from "./sessions/session_helpers";
const rowLimit = 10;
export const registerFormMiddleware = (app: Express) => {
app.use(express.urlencoded({extended: true}))
app.use(cookieMiddleware("mysecret"));
//app.use(customSessionMiddleware());
app.use(sessionMiddleware());
}
export const registerFormRoutes = (app: Express) => {
app.get("/form", async (req, resp) => {
resp.render("age", {
history: await repository.getAllResults(rowLimit),
personalHistory: getSession(req).personalHistory
});
});
app.post("/form", async (req, resp) => {
const nextage = Number.parseInt(req.body.age)
+ Number.parseInt(req.body.years);
await repository.saveResult({...req.body, nextage });
req.session.personalHistory = [{
id: 0, name: req.body.name, age: req.body.age,
years: req.body.years, nextage},
...(req.session.personalHistory || [])].splice(0, 5);
const context = {
...req.body, nextage,
history: await repository.getAllResults(rowLimit),
personalHistory: req.session.personalHistory
};
resp.render("age", context);
});
}
@@ -0,0 +1,14 @@
const matchPattern = /[&<>="'`]/g;
const characterMappings: Record<string, string> = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
"\"": "&quot;",
"=": "&#x3D;",
"'": "&#x27;",
"`": "&#x60;"
};
export const santizeValue = (value: string) =>
value?.replace(matchPattern, match => characterMappings[match]);
@@ -0,0 +1,42 @@
import { createServer } from "http";
import express, {Express } from "express";
import httpProxy from "http-proxy";
import helmet from "helmet";
import { engine } from "express-handlebars";
import { registerFormMiddleware, registerFormRoutes } from "./forms";
import { createApi } from "./api";
const port = 5000;
const expressApp: Express = express();
const proxy = httpProxy.createProxyServer({
target: "http://localhost:5100", ws: true
});
expressApp.set("views", "templates/server");
expressApp.engine("handlebars", engine());
expressApp.set("view engine", "handlebars");
expressApp.use(helmet());
expressApp.use(express.json({
type: ["application/json", "application/json-patch+json"]
}));
registerFormMiddleware(expressApp);
registerFormRoutes(expressApp);
createApi(expressApp);
expressApp.use("^/$", (req, resp) => resp.redirect("/form"));
expressApp.use(express.static("static"));
expressApp.use(express.static("node_modules/bootstrap/dist"));
expressApp.use((req, resp) => proxy.web(req, resp));
const server = createServer(expressApp);
server.on('upgrade', (req, socket, head) => proxy.ws(req, socket, head));
server.listen(port,
() => console.log(`HTTP Server listening on port ${port}`));
@@ -0,0 +1,33 @@
import { Session, SessionRepository } from "./repository";
import { randomUUID } from "crypto";
type SessionWrapper = {
session: Session,
expires: Date
}
export class MemoryRepository implements SessionRepository {
store = new Map<string, SessionWrapper>();
async createSession(): Promise<Session> {
return { id: randomUUID(), data: {} };
}
async getSession(id: string): Promise<Session | undefined> {
const wrapper = this.store.get(id);
if (wrapper && wrapper.expires > new Date(Date.now())) {
return structuredClone(wrapper.session)
}
}
async saveSession(session: Session, expires: Date): Promise<void> {
this.store.set(session.id, { session, expires });
}
async touchSession(session: Session, expires: Date): Promise<void> {
const wrapper = this.store.get(session.id);
if (wrapper) {
wrapper.expires = expires;
}
}
}
@@ -0,0 +1,41 @@
import { Request, Response, NextFunction } from "express";
import { SessionRepository, Session } from "./repository";
//import { MemoryRepository } from "./memory_repository";
import { setCookie, getCookie } from "../cookies";
import { OrmRepository } from "./orm_repository";
const session_cookie_name = "custom_session";
const expiry_seconds = 300;
const getExpiryDate = () => new Date(Date.now() + (expiry_seconds * 1_000));
export const customSessionMiddleware = () => {
//const repo: SessionRepository = new MemoryRepository();
const repo: SessionRepository = new OrmRepository();
return async (req: Request, resp: Response, next: NextFunction) => {
const id = getCookie(req, session_cookie_name);
const session = (id ? await repo.getSession(id) : undefined)
?? await repo.createSession();
(req as any).session = session;
setCookie(resp, session_cookie_name, session.id, {
maxAge: expiry_seconds * 1000
})
resp.once("finish", async () => {
if ( Object.keys(session.data).length > 0) {
if (req.method == "POST") {
await repo.saveSession(session, getExpiryDate());
} else {
await repo.touchSession(session, getExpiryDate());
}
}
})
next();
}
}
@@ -0,0 +1,19 @@
import { DataTypes, InferAttributes, InferCreationAttributes, Model,
Sequelize } from "sequelize";
export class SessionModel extends Model<InferAttributes<SessionModel>,
InferCreationAttributes<SessionModel>> {
declare id: string
declare data: any;
declare expires: Date
}
export const initializeModel = (sequelize: Sequelize) => {
SessionModel.init({
id: { type: DataTypes.STRING, primaryKey: true },
data: { type: DataTypes.JSON },
expires: { type: DataTypes.DATE }
}, { sequelize });
}
@@ -0,0 +1,49 @@
import { Op, Sequelize } from "sequelize";
import { Session, SessionRepository } from "./repository";
import { SessionModel, initializeModel } from "./orm_models";
import { randomUUID } from "crypto";
export class OrmRepository implements SessionRepository {
sequelize: Sequelize;
constructor() {
this.sequelize = new Sequelize({
dialect: "sqlite",
storage: "orm_sessions.db",
logging: console.log,
logQueryParameters: true
});
this.initModelAndDatabase();
}
async initModelAndDatabase() : Promise<void> {
initializeModel(this.sequelize);
await this.sequelize.drop();
await this.sequelize.sync();
}
async createSession(): Promise<Session> {
return { id: randomUUID(), data: {} };
}
async getSession(id: string): Promise<Session | undefined> {
const dbsession = await SessionModel.findOne({
where: { id, expires: { [Op.gt] : new Date(Date.now()) }}
});
if (dbsession) {
return { id, data: dbsession.data };
}
}
async saveSession(session: Session, expires: Date): Promise<void> {
await SessionModel.upsert({
id: session.id,
data: session.data,
expires
});
}
async touchSession(session: Session, expires: Date): Promise<void> {
await SessionModel.update({ expires }, { where: { id: session.id } });
}
}
@@ -0,0 +1,15 @@
export type Session = {
id: string,
data: { [key: string]: any }
}
export interface SessionRepository {
createSession() : Promise<Session>;
getSession(id: string): Promise<Session | undefined>;
saveSession(session: Session, expires: Date): Promise<void>;
touchSession(session: Session, expires: Date) : Promise<void>
}
@@ -0,0 +1,43 @@
import { Request } from "express";
//import { Session } from "./repository";
import session, { SessionData } from "express-session";
import sessionStore from "connect-session-sequelize";
import { Sequelize } from "sequelize";
import { Result } from "../data/repository";
export const getSession = (req: Request): SessionData => (req as any).session;
// declare global {
// module Express {
// interface Request {
// session: Session
// }
// }
// }
declare module "express-session" {
interface SessionData {
personalHistory: Result[];
}
}
export const sessionMiddleware = () => {
const sequelize = new Sequelize({
dialect: "sqlite",
storage: "pkg_sessions.db"
});
const store = new (sessionStore(session.Store))({
db: sequelize
});
store.sync();
return session({
secret: "mysecret",
store: store,
cookie: { maxAge: 300 * 1000, sameSite: "strict" },
resave: false, saveUninitialized: false
})
}
@@ -0,0 +1,15 @@
export const style = (stylesheet: any) => {
return `<link href="/css/${stylesheet}" rel="stylesheet" />`;
}
export const valueOrZero = (value: any) => {
return value !== undefined ? value : 0;
}
export const increment = (value: any) => {
return Number(valueOrZero(value)) + 1;
}
export const isOdd = (value: any) => {
return Number(valueOrZero(value)) % 2;
}
@@ -0,0 +1,7 @@
import { Request, Response } from "express";
export const testHandler = async (req: Request, resp: Response) => {
resp.setHeader("Content-Type", "application/json")
resp.json(req.body);
resp.end();
}
@@ -0,0 +1,52 @@
import { NextFunction, Request, Response } from "express";
import validator from "validator";
type ValidatedRequest = Request & {
validation: {
results: { [key: string]: {
[key: string]: boolean, valid: boolean
} },
valid: boolean
}
}
export const validate = (propName: string) => {
const tests: Record<string, (val: string) => boolean> = {};
const handler = (req: Request, resp: Response, next: NextFunction ) => {
const vreq = req as ValidatedRequest;
if (!vreq.validation) {
vreq.validation = { results: {}, valid: true };
}
vreq.validation.results[propName] = { valid: true };
Object.keys(tests).forEach(k => {
let valid = vreq.validation.results[propName][k]
= tests[k](req.body?.[propName]);
if (!valid) {
vreq.validation.results[propName].valid = false;
vreq.validation.valid = false;
}
});
next();
}
handler.required = () => {
tests.required = (val: string) =>
!validator.isEmpty(val, { ignore_whitespace: true});
return handler;
};
handler.minLength = (min: number) => {
tests.minLength = (val:string) => validator.isLength(val, { min});
return handler;
};
handler.isInteger = () => {
tests.isInteger = (val: string) => validator.isInt(val);
return handler;
}
return handler;
}
export const getValidationResults = (req: Request) => {
return (req as ValidatedRequest).validation || { valid : true }
}