person ok

This commit is contained in:
xfarrow 2024-10-17 15:37:34 +02:00
parent 9fe5879554
commit 5835bfbc16
7 changed files with 117 additions and 360 deletions

View File

@ -1,110 +0,0 @@
/*
This code is part of Blink
licensed under GPLv3
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
*/
/******************************************************************************
* WARNING
*
*
* Posts are now scheduled to be developed at a later stage in the development
* process, with the possibility that it may not be developed at all.
* I am unsure whether it is a good thing or it'll only be used to
* flood timelines with low-effort content, like other competing platforms.
*
*
*
******************************************************************************/
const knex = require('../utils/knex_config');
/**
* Create OrganizationPost object
* @param {*} organizationId
* @param {*} content
* @param {*} originalAuthor
*/
function createOrganizationPost(organizationId, content, originalAuthor) {
const organizationPost = {
organization_id: organizationId,
content,
original_author: originalAuthor
};
return organizationPost;
}
/**
* Insert an OrganizationPost if and only if the author is
* one of the Organization's administrators.
* @param {*} organization
* @returns the inserted OrganizationPost
*/
async function insertOrganizationPost(organization) {
const isOrganizationAdmin = await knex('OrganizationAdministrator')
.where('id_person', organization.original_author)
.where('id_organization', organization.organization_id)
.select('*')
.first();
// Non-exploitable TOC/TOU weakness
// For more information https://softwareengineering.stackexchange.com/questions/451038/when-should-i-be-worried-of-time-of-check-time-of-use-vulnerabilities-during-dat
if (!isOrganizationAdmin) {
return false;
}
const organizationPost = await knex('OrganizationPost')
.insert({
organization_id: organization.organization_id,
content: organization.content,
original_author: organization.original_author
})
.returning('*');
return organizationPost[0];
}
/**
* Checks whether the specified Person is an Organization Administrator
* of the Organization the Post belongs to.
* @param {*} postId
* @param {*} personId
* @returns true or false
*/
async function isPersonPostAdministrator(postId, personId) {
return await knex('OrganizationPost')
.join('OrganizationAdministrator', 'OrganizationPost.organization_id', 'OrganizationAdministrator.id_organization')
.where('OrganizationPost.id', postId)
.where('OrganizationAdministrator.id_person', personId)
.select('*')
.first();
}
/**
* Deletes the specified OrganizationPost if the requester is one
* of the Administrators of the Organization the Post belongs to
* @param {*} postId Id of the Post to delete
* @param {*} requester Id of the Person requesting the deletion
*/
async function deleteOrganizationPost(postId, requester) {
if (await isPersonPostAdministrator(postId, requester)) {
return await knex('OrganizationPost')
.where('id', postId)
.del() == 1;
}
return false;
}
module.exports = {
createOrganizationPost,
insertOrganizationPost,
isPersonPostAdministrator,
deleteOrganizationPost
};

View File

@ -25,17 +25,18 @@ const bcrypt = require('bcrypt');
* @param {*} placeOfLiving
* @returns
*/
function createPerson(email, password, displayName, dateOfBirth, available, enabled, placeOfLiving, aboutMe, qualification) {
function createPerson(email, password, displayName, dateOfBirth, placeOfLiving, aboutMe, qualification, openToWork, enabled) {
const person = {
email: email.toLowerCase(),
password,
display_name: displayName,
date_of_birth: dateOfBirth,
available,
enabled,
open_to_work: openToWork,
place_of_living: placeOfLiving,
about_me: aboutMe,
qualification
qualification,
enabled,
visibility: "EVERYONE"
};
return person;
}
@ -80,7 +81,7 @@ async function insert(person, activationLink) {
const insertedPerson = await tr('Person')
.insert(person)
.returning('*');
if(activationLink != null){
if (activationLink != null) {
await tr('ActivationLink')
.insert({
person_id: insertedPerson[0].id,

View File

@ -1,106 +0,0 @@
/*
This code is part of Blink
licensed under GPLv3
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
*/
/******************************************************************************
* WARNING
*
*
* Posts are now scheduled to be developed at a later stage in the development
* process, with the possibility that it may not be developed at all.
* I am unsure whether it is a good thing or it'll only be used to
* flood timelines with low-effort content, like other competing platforms.
*
*
*
******************************************************************************/
const organizationPostModel = require('../models/organization_post_model');
const express = require('express');
const jwtUtils = require('../utils/jwt_utils');
/**
* POST Request
*
* Creates a Post belonging to an organization
*
* Required field(s): organization_id, content
* @returns the inserted Post
*/
async function createOrganizationPost(req, res) {
// Ensure that the required fields are present before proceeding
if (!req.params.idOrganization || !req.body.content) {
return res.status(400).json({
error: 'Invalid request'
});
}
const organizationPost = organizationPostModel.createOrganizationPost(
req.params.idOrganization,
req.body.content,
req.jwt.person_id);
try {
const insertedOrganization = await organizationPostModel.insertOrganizationPost(organizationPost);
if(!!insertedOrganization){
return res.status(200).json(insertedOrganization);
}
return res.status(401).json({
error: 'Forbidden'
});
} catch (error) {
console.error(`Error in function ${createOrganizationPost.name}: ${error}`);
return res.status(500).json({
error: 'Internal server error'
});
}
}
/**
* DELETE Request
*
* Deletes a Post belonging to an Organization, only if
* the logged user is an administrator of that Organization.
*
* Required field(s): none.
*/
async function deleteOrganizationPost(req, res) {
try {
const success = await organizationPostModel.deleteOrganizationPost(req.params.id, req.jwt.person_id);
if (success) {
return res.status(200).json({
success: true
});
}
return res.status(401).json({
error: 'Forbidden'
});
} catch (error) {
console.error(`Error in function ${deleteOrganizationPost.name}: ${error}`);
res.status(500).json({
error: 'Internal server error'
});
}
}
const routes = express.Router();
routes.post('/:idOrganization/posts', jwtUtils.extractToken, createOrganizationPost);
routes.delete('/posts/:id', jwtUtils.extractToken, deleteOrganizationPost);
// Exporting a function
// means making a JavaScript function defined in one
// module available for use in another module.
module.exports = {
routes
};

View File

@ -16,7 +16,7 @@ const jwtUtils = require('../utils/jwt_utils');
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const Person = require('../models/person_model');
const activationModel = require('../models/activation_model');
const Activation = require('../models/activation_model');
const express = require('express');
const mailUtils = require('../utils/mail_utils');
@ -66,13 +66,14 @@ async function registerPerson(req, res) {
const personToInsert = Person.createPerson(
req.body.email,
await hashPasswordPromise,
req.body.display_name,
req.body.date_of_birth,
req.body.available,
req.body.displayName,
req.body.dateOfBirth,
req.body.placeOfLiving,
req.body.aboutMe,
req.body.qualification,
req.body.openToWork,
isEnabled,
req.body.place_of_living,
req.body.about_me,
req.body.qualification);
);
const insertedPerson = await Person.insert(personToInsert, activationCode);
delete insertedPerson.password;
@ -190,8 +191,8 @@ async function getMyself(req, res) {
* not present, they shall be ignored. An user can
* only update themselves
*
* Required field(s): At least one. Both old_password and
* new_password if updating the password.
* Required field(s): At least one. Both oldPassword and
* newPassword if updating the password.
*
*/
async function updatePerson(req, res) {
@ -205,46 +206,50 @@ async function updatePerson(req, res) {
const updatePerson = {};
if (req.body.display_name !== undefined) {
updatePerson.display_name = req.body.display_name;
if (req.body.displayName !== undefined) {
updatePerson.display_name = req.body.displayName;
}
if (req.body.date_of_birth !== undefined) {
updatePerson.date_of_birth = req.body.date_of_birth;
if (req.body.dateOfBirth !== undefined) {
updatePerson.date_of_birth = req.body.dateOfBirth;
}
if (req.body.available !== undefined) {
updatePerson.available = req.body.available;
if (req.body.openToWork !== undefined) {
updatePerson.open_to_work = req.body.openToWork;
}
if (req.body.place_of_living !== undefined) {
updatePerson.place_of_living = req.body.place_of_living;
if (req.body.placeOfLiving !== undefined) {
updatePerson.place_of_living = req.body.placeOfLiving;
}
if (req.body.about_me !== undefined) {
updatePerson.about_me = req.body.about_me;
if (req.body.aboutMe !== undefined) {
updatePerson.about_me = req.body.aboutMe;
}
if (req.body.qualification !== undefined) {
updatePerson.qualification = req.body.qualification;
}
if (req.body.visibility !== undefined) {
updatePerson.visibility = req.body.visibility;
}
// If we are tying to change password, the old password must be provided
if (req.body.old_password !== undefined || req.body.new_password !== undefined) {
if (req.body.old_password === undefined) {
if (req.body.oldPassword !== undefined || req.body.newPassword !== undefined) {
if (req.body.oldPassword === undefined) {
return res.status(401).json({
error: 'The old password must be specified'
});
}
if (req.body.new_password === undefined) {
if (req.body.newPassword === undefined) {
return res.status(401).json({
error: 'The new password must be specified'
});
}
const user = await Person.findById(req.jwt.person_id);
const passwordMatches = await bcrypt.compare(req.body.old_password, user.password);
const passwordMatches = await bcrypt.compare(req.body.oldPassword, user.password);
if (passwordMatches) {
updatePerson.password = await bcrypt.hash(req.body.new_password, 10);
updatePerson.password = await bcrypt.hash(req.body.newPassword, 10);
} else {
return res.status(401).json({
error: 'Password verification failed'
@ -306,7 +311,7 @@ async function confirmActivation(req, res) {
errors: errors.array()
});
}
const personId = await activationModel.getPersonIdByIdentifier(req.body.code);
const personId = await Activation.getPersonIdByIdentifier(req.body.code);
if (!personId) {
return res.status(401).json({
error: 'Activation Link either not valid or expired'

View File

@ -17,7 +17,7 @@ const {
} = require("express-validator"); // This is the destructuring part. It specifies which properties of the imported object (express-validator) you want to extract.
const registerValidator = [
check('display_name').trim().notEmpty().escape().isLength({
check('displayName').trim().notEmpty().escape().isLength({
max: 128
}),
check('email').isEmail().normalizeEmail().escape().isLength({
@ -26,12 +26,12 @@ const registerValidator = [
check('password').isLength({
min: 5
}).trim().escape().withMessage('Password must be at least 5 characters'),
check('date_of_birth').optional().isDate().withMessage('Invalid date format. Date must be YYYY-MM-DD'),
check('available').optional().isBoolean(),
check('place_of_living').isLength({
check('dateOfBirth').optional().isDate().withMessage('Invalid date format. Date must be YYYY-MM-DD'),
check('openToWork').optional().isBoolean(),
check('placeOfLiving').isLength({
max: 128
}).escape(),
check('about_me').isLength({
check('aboutMe').isLength({
max: 4096
}).escape(),
check('qualification').isLength({
@ -45,15 +45,15 @@ const getTokenValidator = [
];
const updatePersonValidator = [
check('display_name').trim().escape().isLength({
check('displayName').trim().escape().isLength({
max: 128
}),
check('date_of_birth').optional().isDate().withMessage('Invalid date format. Date must be YYYY-MM-DD'),
check('available').optional().isBoolean(),
check('place_of_living').isLength({
check('dateOfBirth').optional().isDate().withMessage('Invalid date format. Date must be YYYY-MM-DD'),
check('openToWork').optional().isBoolean(),
check('placeOfLiving').isLength({
max: 128
}).escape(),
check('about_me').isLength({
check('aboutMe').isLength({
max: 4096
}).escape(),
check('qualification').isLength({

View File

@ -2,12 +2,4 @@
-- DROP DATABASE IF EXISTS "Blink";
CREATE DATABASE "Blink"
WITH
OWNER = pg_database_owner
ENCODING = 'UTF8'
LC_COLLATE = 'Italian_Italy.1252'
LC_CTYPE = 'Italian_Italy.1252'
TABLESPACE = pg_default
CONNECTION LIMIT = -1
IS_TEMPLATE = False;
CREATE DATABASE "Blink";

View File

@ -1,140 +1,115 @@
CREATE TYPE Visibility AS ENUM ('NOBODY', 'THIS_INSTANCE', 'EVERYONE');
CREATE TYPE SalaryCurrency as ENUM ('EUR', 'USD', 'GBP');
CREATE TYPE RemotePosition as ENUM ('YES', 'NO', 'NOT_SPECIFIED', 'PARTIALLY');
CREATE TYPE ContractType as ENUM ('FULL-TIME','PART-TIME','INTERNSHIP','CONTRACT','FREELANCE','TEMPORARY','SEASONAL','APPRENTICESHIP','VOLUNTEER','ZERO-HOURS','FIXED-TERM','CASUAL','PROBATIONARY','SECONDMENT','JOB-SHARING');
CREATE TYPE ExperienceType as ENUM ('EDUCATION', 'WORK', 'VOLOUNTEER');
-- Table: Person
CREATE TABLE IF NOT EXISTS "Person" (
id SERIAL PRIMARY KEY,
email character varying(128) NOT NULL UNIQUE,
-- Primary e-mail
password character varying(128) NOT NULL,
display_name character varying(128) NOT NULL,
email CHARACTER VARYING(128) NOT NULL UNIQUE,
password CHARACTER VARYING(128) NOT NULL,
display_name CHARACTER VARYING(128) NOT NULL,
date_of_birth date,
available boolean,
-- Whether this person is available to be hired
enabled boolean NOT NULL DEFAULT false,
-- Whether this profile is active
place_of_living character varying(128),
about_me character varying(4096),
qualification character varying(64)
place_of_living CHARACTER VARYING(128),
about_me CHARACTER VARYING(4096),
qualification CHARACTER VARYING(64),
open_to_work BOOLEAN,
enabled BOOLEAN NOT NULL DEFAULT false,
visibility Visibility NOT NULL
);
-- Table: ActivationLink
CREATE TABLE IF NOT EXISTS "ActivationLink" (
identifier character varying PRIMARY KEY,
person_id integer NOT NULL,
identifier CHARACTER VARYING PRIMARY KEY,
person_id INTEGER NOT NULL,
CONSTRAINT "PersonActivationLinkFK" FOREIGN KEY (person_id) REFERENCES "Person" (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE
);
-- Table: Organization
CREATE TABLE IF NOT EXISTS "Organization" (
id SERIAL PRIMARY KEY,
name character varying(128) NOT NULL,
location character varying,
description text
);
-- Table: OrganizationPost
CREATE TABLE IF NOT EXISTS "OrganizationPost" (
id SERIAL PRIMARY KEY,
organization_id integer NOT NULL,
content text NOT NULL,
created_at timestamp without time zone DEFAULT now(),
original_author integer NOT NULL,
CONSTRAINT "AuthorIdFK" FOREIGN KEY (original_author) REFERENCES "Person" (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE NOT VALID,
CONSTRAINT "OrganizationIdFk" FOREIGN KEY (organization_id) REFERENCES "Organization" (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE NOT VALID
name CHARACTER VARYING(128) NOT NULL,
location CHARACTER VARYING,
description TEXT
);
-- Table: OrganizationAdministrator
CREATE TABLE IF NOT EXISTS "OrganizationAdministrator" (
id_person integer NOT NULL,
id_organization integer NOT NULL,
id_person INTEGER NOT NULL,
id_organization INTEGER NOT NULL,
CONSTRAINT "OrganizationAdministrator_pkey" PRIMARY KEY (id_organization, id_person),
CONSTRAINT "OrganizationAdministratorOrganizationId" FOREIGN KEY (id_organization) REFERENCES "Organization" (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE NOT VALID,
CONSTRAINT "OrganizationAdministratorUserId" FOREIGN KEY (id_person) REFERENCES "Person" (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE
);
-- Table: Message
CREATE TABLE IF NOT EXISTS "Message" (
id serial NOT NULL,
person_id integer NOT NULL,
organization_id integer NOT NULL,
author_on_behalf_of_organization integer,
"timestamp" timestamp without time zone NOT NULL,
content character varying(4096) NOT NULL,
sender_type character varying(12) NOT NULL,
CONSTRAINT "Message_pkey" PRIMARY KEY (id),
CONSTRAINT "Message_author_on_behalf_of_company_fkey" FOREIGN KEY (
author_on_behalf_of_organization
) REFERENCES "Person" (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE,
CONSTRAINT "Message_organization_id_fkey" FOREIGN KEY (organization_id) REFERENCES "Organization" (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE,
CONSTRAINT "Message_person_id_fkey" FOREIGN KEY (person_id) REFERENCES "Person" (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE,
CONSTRAINT "Message_sender_type_check" CHECK (
sender_type :: text = 'ORGANIZATION' :: text
OR sender_type :: text = 'PERSON' :: text
),
CONSTRAINT "Message_sender_constraint" CHECK (
author_on_behalf_of_organization IS NULL
AND sender_type :: text = 'PERSON' :: text
OR author_on_behalf_of_organization IS NOT NULL
AND sender_type :: text = 'ORGANIZATION' :: text
)
);
COMMENT ON COLUMN "Message".sender_type IS 'sender_type can be either be PERSON or ORGANIZATION, depending who''s sending the message. It is PERSON if and only if author_on_behalf_of_organization is NULL. It is ORGANIZATION if and only if author_on_behalf_of_organization is NOT NULL. This column may seem redundant if we already know how to identify a sender, but it does give more clarity to the API implementers';
COMMENT ON CONSTRAINT "Message_sender_type_check" ON "Message" IS 'We want the sender to be either PERSON or ORGANIZATION';
COMMENT ON CONSTRAINT "Message_sender_constraint" ON "Message" IS 'If ''author_on_behalf_of_organization'' is NULL, then the sender is a person, instead, if ''author_on_behalf_of_organization'' is not NULL, the sender is a organization';
-- Table: Tag
CREATE TABLE IF NOT EXISTS "Tag" (
id SERIAL,
tag character varying(256) NOT NULL,
CONSTRAINT "Tag_pkey" PRIMARY KEY (id)
id SERIAL PRIMARY KEY,
tag CHARACTER VARYING(256) NOT NULL
);
-- Table: JobOffer
CREATE TABLE IF NOT EXISTS "JobOffer" (
id SERIAL,
title character varying(2048) NOT NULL,
description character varying(4096),
requirements character varying(4096),
salary money NOT NULL,
salary_frequency character varying(64) NOT NULL,
salary_currency character varying(64) NOT NULL,
location character varying(256),
organization_id integer,
CONSTRAINT "JobOffer_pkey" PRIMARY KEY (id),
id SERIAL PRIMARY KEY,
organization_id INTEGER,
title CHARACTER VARYING(2048) NOT NULL,
description CHARACTER VARYING(4096),
salary int4range,
salary_currency SalaryCurrency,
location CHARACTER VARYING(256),
remote RemotePosition NOT NULL,
contract_type ContractType,
CONSTRAINT "OrganizationFK" FOREIGN KEY (organization_id) REFERENCES "Organization" (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE NOT VALID
);
-- Table: JobOfferTag
-- This table allows to create a N-to-N map between Tag and JobOffer
CREATE TABLE IF NOT EXISTS "JobOfferTag" (
id serial,
job_offer_id integer NOT NULL,
tag_id integer NOT NULL,
CONSTRAINT "JobOfferTag_pkey" PRIMARY KEY (id),
id SERIAL PRIMARY KEY,
job_offer_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
CONSTRAINT "JobOfferFk" FOREIGN KEY (job_offer_id) REFERENCES "JobOffer" (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT "TagFk" FOREIGN KEY (tag_id) REFERENCES "Tag" (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE
);
-- Table: Applicant
CREATE TABLE IF NOT EXISTS "Applicant" (
id SERIAL PRIMARY KEY,
person_id INTEGER NOT NULL,
job_offer_id INTEGER NOT NULL,
CONSTRAINT "PersonFk" FOREIGN KEY (person_id) REFERENCES "Person" (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT "JobOfferFk" FOREIGN KEY (job_offer_id) REFERENCES "JobOffer" (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE
);
-- Table: Skills
CREATE TABLE IF NOT EXISTS "Skill" (
id SERIAL PRIMARY KEY,
person_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
CONSTRAINT "PersonFk" FOREIGN KEY (person_id) REFERENCES "Person" (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT "TagFk" FOREIGN KEY (tag_id) REFERENCES "Tag" (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE
);
-- Table: RequestResetPassword
CREATE TABLE IF NOT EXISTS "RequestResetPassword" (
id serial,
email character varying(128) NOT NULL,
secret character varying NOT NULL,
id SERIAL,
email CHARACTER VARYING(128) NOT NULL,
secret CHARACTER VARYING NOT NULL,
time_of_request timestamp without time zone NOT NULL DEFAULT now(),
CONSTRAINT "RequestResetPassword_pkey" PRIMARY KEY (id)
);
-- Table: Experience
CREATE TABLE IF NOT EXISTS "Experience" (
id serial,
title character varying(128) NOT NULL,
description text NOT NULL,
organization character varying(128) NOT NULL,
id SERIAL PRIMARY KEY,
title CHARACTER VARYING(128) NOT NULL,
description TEXT NOT NULL,
organization CHARACTER VARYING(128) NOT NULL,
organization_id INTEGER,
date daterange NOT NULL,
organization_id integer,
person_id integer NOT NULL,
type character varying(32) NOT NULL,
CONSTRAINT "Experience_pkey" PRIMARY KEY (id),
CONSTRAINT "OrganizationFk" FOREIGN KEY (organization_id) REFERENCES "Organization" (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE
SET
NULL,
person_id INTEGER NOT NULL,
type ExperienceType NOT NULL,
CONSTRAINT "OrganizationFk" FOREIGN KEY (organization_id) REFERENCES "Organization" (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE SET NULL,
CONSTRAINT "PersonFk" FOREIGN KEY (person_id) REFERENCES "Person" (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE
);