@damourlabs/euro-clinical-trials
Version:
A Nuxt 3 application for visualizing and managing clinical trials data in Europe, built with modern web technologies.
389 lines (344 loc) • 15.2 kB
text/typescript
// Drizzle ORM schema for GDPR Compliance tables
import {
pgTable,
uuid,
text,
timestamp,
integer,
date,
boolean,
jsonb,
check,
unique,
index,
pgEnum
} from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import { z } from 'zod';
import type { Expect, IsSameType } from '~/types/equality';
import { users, patients, trials } from '.';
import { pgConsentStatusEnum } from './patients';
import { AnonymizationMethodEnum, ConsentStatusEnum, ConsentTypeEnum, DataCategoriesEnum, DataSubjectRequestStatusEnum, DataSubjectRequestTypeEnum, DeletionTriggerEnum, LegalBasisEnum, PurposeEnum, TrialDataTypeEnum, WithdrawalMethodEnum, WithdrawalReasonEnum } from './enums';
// ---------------------
// --- GDPR Consents ---
// ---------------------
export const gdprFieldDefaultValues = {
consentGiven: false,
legalBasis: LegalBasisEnum.Enum.Consent,
consentType: ConsentTypeEnum.Enum.Data_processing,
purpose: PurposeEnum.Enum.Clinical_trial_management,
retentionPeriod: 5,
dataProcessingDetails: 'Data will be processed for the purpose of clinical trial management and regulatory compliance',
consentStatus: ConsentStatusEnum.Enum.NotConsented,
};
export const pgLegalBasisEnum = pgEnum('legal_basis_enum', LegalBasisEnum.options);
export const pgConsentTypeEnum = pgEnum('consent_type_enum', ConsentTypeEnum.options);
export const pgPurposeEnum = pgEnum('purpose_enum', PurposeEnum.options);
export const pgWithdrawalMethodEnum = pgEnum('withdrawal_method_enum', WithdrawalMethodEnum.options);
export const pgWithdrawalReasonEnum = pgEnum('withdrawal_reason_enum', WithdrawalReasonEnum.options);
export const gdprConsents = pgTable('gdpr_consents', {
uuid: uuid('uuid').primaryKey().defaultRandom(),
trialUuid: uuid('trial_uuid').notNull().references(() => trials.uuid, { onDelete: 'cascade' }),
patientUuid: uuid('patient_uuid').notNull().references(() => patients.uuid, { onDelete: 'cascade' }),
consentGiven: boolean('consent_given').notNull().default(gdprFieldDefaultValues.consentGiven),
consentDate: date('consent_date').notNull().default(sql`CURRENT_DATE`),
legalBasis: pgLegalBasisEnum("legal_basis").notNull().default(gdprFieldDefaultValues.legalBasis),
consentType: pgConsentTypeEnum("consent_type").notNull().default(gdprFieldDefaultValues.consentType),
purpose: pgPurposeEnum("purpose").notNull().default(gdprFieldDefaultValues.purpose),
retentionPeriod: integer('retention_period').notNull().default(gdprFieldDefaultValues.retentionPeriod),
dataProcessingDetails: text('data_processing_details').notNull().default(gdprFieldDefaultValues.dataProcessingDetails),
userAgent: text('user_agent'),
consentStatus: pgConsentStatusEnum("consent_status").notNull().default(gdprFieldDefaultValues.consentStatus),
withdrawalDate: date('withdrawal_date'),
withdrawalMethod: pgWithdrawalMethodEnum("withdrawal_method").notNull(),
withdrawalReason: pgWithdrawalReasonEnum("withdrawal_reason").notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
}, (table) => [
index('idx_gdpr_consents_patient').on(table.patientUuid),
index('idx_gdpr_consents_trial').on(table.trialUuid),
check('gdpr_consents_retention_positive', sql`${table.retentionPeriod} > 0`),
check('gdpr_consents_withdrawal_logic', sql`(${table.consentStatus} = 'Withdrawn' AND ${table.withdrawalDate} IS NOT NULL) OR (${table.consentStatus} != 'Withdrawn' AND ${table.withdrawalDate} IS NULL)`),
]);
// Zod schemas for validation
export const gdprConsentsSchema = z.object({
uuid: z.string()
.uuid()
.describe("Unique identifier for the GDPR consent, automatically generated if not provided")
,
trialUuid: z.string()
.uuid()
.describe("UUID of the trial this consent belongs to")
,
patientUuid: z.string()
.uuid()
.describe("UUID of the patient who gave consent")
,
consentGiven: z.boolean()
.describe("Whether consent was given")
.default(gdprFieldDefaultValues.consentGiven)
,
consentDate: z.string()
.describe("Date when consent was given")
,
legalBasis: LegalBasisEnum
.describe("Legal basis for data processing")
.default(gdprFieldDefaultValues.legalBasis)
,
consentType: ConsentTypeEnum
.describe("Type of consent")
.default(gdprFieldDefaultValues.consentType)
,
purpose: PurposeEnum
.describe("Purpose of data processing")
.default(gdprFieldDefaultValues.purpose)
,
retentionPeriod: z.number()
.int()
.positive()
.describe("Data retention period in years")
.default(gdprFieldDefaultValues.retentionPeriod)
,
dataProcessingDetails: z.string()
.describe("Details about data processing")
.default(gdprFieldDefaultValues.dataProcessingDetails)
,
userAgent: z.string()
.nullable()
.describe("User agent when consent was given")
,
consentStatus: ConsentStatusEnum
.describe("Current status of consent")
.default(gdprFieldDefaultValues.consentStatus)
,
withdrawalDate: z.string()
.nullable()
.describe("Date when consent was withdrawn")
,
withdrawalMethod: WithdrawalMethodEnum
.describe("Method used to withdraw consent")
,
withdrawalReason: WithdrawalReasonEnum
.describe("Reason for withdrawing consent")
,
createdAt: z.date()
.nullable()
.describe("Timestamp of when the consent record was created")
,
updatedAt: z.date()
.nullable()
.describe("Timestamp of when the consent record was last updated")
,
});
// ----------------------------
// --- GDPR Data Categories ---
// ----------------------------
export const pgCategoryEnum = pgEnum('gdpr_data_category_enum', DataCategoriesEnum.options);
export const gdprDataCategories = pgTable('gdpr_data_categories', {
uuid: uuid('uuid').primaryKey().defaultRandom(),
consentUuid: uuid('consent_uuid').notNull().references(() => gdprConsents.uuid, { onDelete: 'cascade' }),
category: pgCategoryEnum("category").notNull().default(DataCategoriesEnum.Enum.Health_data),
}, (table) => [
unique('gdpr_data_categories_unique_per_consent').on(table.consentUuid, table.category),
]);
export const gdprDataCategoriesSchema = z.object({
uuid: z.string()
.uuid()
.describe("Unique identifier for the data category, automatically generated if not provided")
,
consentUuid: z.string()
.uuid()
.describe("UUID of the consent this data category belongs to")
,
category: DataCategoriesEnum
.describe("Category of data being processed")
,
});
// -------------------------------
// --- Data Retention Policies ---
// -------------------------------
const dataRententionPoliciesFieldDefaultValues = {
deletionTrigger: DeletionTriggerEnum.Enum.PolicyBased,
retentionPeriodYears: 5,
dataType: TrialDataTypeEnum.Enum.PersonalData,
};
export const pgDeletionTriggerEnum = pgEnum('deletion_trigger_enum', DeletionTriggerEnum.options)
export const pgDataTypeEnum = pgEnum('data_type_enum', TrialDataTypeEnum.options);
export const dataRetentionPolicies = pgTable('data_retention_policies', {
uuid: uuid('uuid').primaryKey().defaultRandom(),
trialUuid: uuid('trial_uuid').notNull().references(() => trials.uuid, { onDelete: 'cascade' }),
dataType: pgDataTypeEnum("data_type").notNull().default(dataRententionPoliciesFieldDefaultValues.dataType),
retentionPeriodYears: integer('retention_period_years').notNull(),
deletionTrigger: pgDeletionTriggerEnum('deletion_trigger').notNull().default(dataRententionPoliciesFieldDefaultValues.deletionTrigger),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
}, (table) => [
check('data_retention_policies_retention_positive', sql`${table.retentionPeriodYears} > 0`),
unique('data_retention_policies_unique_per_trial').on(table.trialUuid, table.dataType),
]);
export const dataRetentionPoliciesSchema = z.object({
uuid: z.string()
.uuid()
.describe("Unique identifier for the retention policy, automatically generated if not provided")
,
trialUuid: z.string()
.uuid()
.describe("UUID of the trial this policy applies to")
,
dataType: TrialDataTypeEnum
.describe("Type of data the retention policy applies to")
.default(dataRententionPoliciesFieldDefaultValues.dataType)
,
retentionPeriodYears: z.number()
.int()
.positive()
.describe("Retention period in years")
,
deletionTrigger: DeletionTriggerEnum
.describe("What triggers the deletion of data")
.default(dataRententionPoliciesFieldDefaultValues.deletionTrigger)
,
createdAt: z.date()
.nullable()
.describe("Timestamp of when the policy was created")
,
});
// -------------------------------
// --- Data Subject Requests -----
// -------------------------------
const dataSubjectRequestsFieldDefaultValues = {
status: DataSubjectRequestStatusEnum.Enum.Pending,
requestType: DataSubjectRequestTypeEnum.Enum.Access,
}
export const pgDataSubjectRequestTypeEnum = pgEnum('data_subject_request_type_enum', DataSubjectRequestTypeEnum.options);
export const pgDataSubjectRequestStatusEnum = pgEnum('data_subject_request_status_enum', DataSubjectRequestStatusEnum.options);
const DataSubjectRequestResponseDataSchema = z.object({
requestId: z.string().uuid(),
patientUuid: z.string().uuid(),
processedAt: z.string().datetime(),
processedBy: z.string().uuid(),
requestType: DataSubjectRequestTypeEnum,
data: z.record(z.unknown()).optional(),
errors: z.array(z.object({
field: z.string(),
message: z.string(),
})),
status: DataSubjectRequestStatusEnum,
legalBasis: LegalBasisEnum.optional(),
retentionNotice: z.string().optional(),
});
export type DataSubjectRequestResponseData = z.infer<typeof DataSubjectRequestResponseDataSchema>
export const dataSubjectRequests = pgTable('data_subject_requests', {
uuid: uuid('uuid').primaryKey().defaultRandom(),
patientUuid: uuid('patient_uuid').notNull().references(() => patients.uuid, { onDelete: 'cascade' }),
requestType: pgDataSubjectRequestTypeEnum("request_type").notNull().default(dataSubjectRequestsFieldDefaultValues.requestType),
status: pgDataSubjectRequestStatusEnum("status").notNull().default(dataSubjectRequestsFieldDefaultValues.status),
requestedAt: timestamp('requested_at', { withTimezone: true }).defaultNow(),
processedAt: timestamp('processed_at', { withTimezone: true }),
processedBy: uuid('processed_by').references(() => users.uuid, { onDelete: 'set null' }),
responseData: jsonb('response_data').$type<DataSubjectRequestResponseData>().notNull(),
notes: text('notes'),
}, () => [
]);
export const dataSubjectRequestsSchema = z.object({
uuid: z.string()
.uuid()
.describe("Unique identifier for the data subject request, automatically generated if not provided")
,
patientUuid: z.string()
.uuid()
.describe("UUID of the patient making the request")
,
requestType: DataSubjectRequestTypeEnum
.describe("Type of data subject request")
.default(dataSubjectRequestsFieldDefaultValues.requestType)
,
status: DataSubjectRequestStatusEnum
.describe("Current status of the request")
.default(dataSubjectRequestsFieldDefaultValues.status)
,
requestedAt: z.date()
.nullable()
.describe("Timestamp when the request was made")
,
processedAt: z.date()
.nullable()
.describe("Timestamp when the request was processed")
,
processedBy: z.string()
.uuid()
.nullable()
.describe("UUID of the user who processed the request")
,
responseData: DataSubjectRequestResponseDataSchema
.describe("Response data for the request, including any errors or processed data")
,
notes: z.string()
.nullable()
.describe("Additional notes about the request")
,
});
// ------------------------------
// --- Data Anonymization Log ---
// ------------------------------
const gdprAnonymizationFieldDefaultValues = {
anonymizationMethod: AnonymizationMethodEnum.Enum.DataPseudonymization,
}
export const pgAnonymizationMethodEnum = pgEnum('anonymization_method_enum', AnonymizationMethodEnum.options);
const AnonymizedFieldsSchema = z.object({
fieldName: z.string(),
});
export type AnonymizedField = z.infer<typeof AnonymizedFieldsSchema>;
export const dataAnonymizationLog = pgTable('data_anonymization_log', {
uuid: uuid('uuid').primaryKey().defaultRandom(),
patientUuid: uuid('patient_uuid').notNull(),
trialUuid: uuid('trial_uuid').notNull().references(() => trials.uuid, { onDelete: 'cascade' }),
anonymizationDate: timestamp('anonymization_date', { withTimezone: true }).defaultNow(),
anonymizationMethod: pgAnonymizationMethodEnum("anonymization_method").notNull().default(gdprAnonymizationFieldDefaultValues.anonymizationMethod),
anonymizedFields: jsonb('anonymized_fields').$type<AnonymizedField>().notNull(),
performedBy: uuid('performed_by').notNull().references(() => users.uuid, { onDelete: 'restrict' }),
}, () => []);
export const dataAnonymizationLogSchema = z.object({
uuid: z.string()
.uuid()
.describe("Unique identifier for the anonymization log entry, automatically generated if not provided")
,
patientUuid: z.string()
.uuid()
.describe("UUID of the patient whose data was anonymized")
,
trialUuid: z.string()
.uuid()
.describe("UUID of the trial the data belongs to")
,
anonymizationDate: z.date()
.nullable()
.describe("Date when anonymization was performed")
,
anonymizationMethod: AnonymizationMethodEnum
.describe("Method used for anonymization")
.default(gdprAnonymizationFieldDefaultValues.anonymizationMethod)
,
anonymizedFields: AnonymizedFieldsSchema
.describe("Fields that were anonymized")
,
performedBy: z.string()
.uuid()
.describe("UUID of the user who performed the anonymization")
,
});
// -----------------------------------
// --- Exported Types & Validation ---
// -----------------------------------
export type GdprConsent = z.infer<typeof gdprConsentsSchema>;
export type GdprDataCategory = z.infer<typeof gdprDataCategoriesSchema>;
export type DataRetentionPolicy = z.infer<typeof dataRetentionPoliciesSchema>;
export type DataSubjectRequest = z.infer<typeof dataSubjectRequestsSchema>;
export type DataAnonymizationLog = z.infer<typeof dataAnonymizationLogSchema>;
// Validate to check if the schemas respect the table structures
// If these fail, it means the schemas and table structures are not in sync
// TODO: Fix type mismatches between Zod schemas and Drizzle table types
type _gdprConsentSchemaMatchesTable = Expect<IsSameType<GdprConsent, typeof gdprConsents.$inferSelect>>
type _gdprDataCategorySchemaMatchesTable = Expect<IsSameType<GdprDataCategory, typeof gdprDataCategories.$inferSelect>>
type _dataRetentionPolicySchemaMatchesTable = Expect<IsSameType<DataRetentionPolicy, typeof dataRetentionPolicies.$inferSelect>>
type _dataSubjectRequestSchemaMatchesTable = Expect<IsSameType<DataSubjectRequest, typeof dataSubjectRequests.$inferSelect>>
type _dataAnonymizationLogSchemaMatchesTable = Expect<IsSameType<DataAnonymizationLog, typeof dataAnonymizationLog.$inferSelect>>