Add Projects module database schema

Introduce the Projects domain model as Drizzle table definitions
(schema-only; no migration generated/run yet).

New tables in src/app/db/schema/projects/projects.ts: project,
project_type, project_property, contractor, workstream,
project_workstream, project_workstream_stage,
project_workstream_contractor, document_type,
project_workstream_evidence_requirement, work_order.

Also:
- uploaded_files: add "projects" file_source value and three nullable
  bigint FK columns linking evidence to the Projects module.
- relations.ts: wire the Projects relations.
- db.ts: register the new schema module.

Deviations from schema.md, decided during implementation:
- client_id is uuid -> organisation (Client maps to organisation).
- property_id FKs are bigint (Property has a bigint PK, not integer).
- TBC status/priority columns modelled as text for now.
- No new uploaded_by column; reuse the existing bigint FK -> user.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Daniel Roth 2026-07-17 14:14:30 +00:00
parent 558633c128
commit e452fc9ab2
5 changed files with 797 additions and 1 deletions

View file

@ -17,6 +17,7 @@ import * as UploadedFilesSchema from "@/app/db/schema/uploaded_files";
import * as PortfolioOrgSchema from "@/app/db/schema/portfolio_organisation";
import * as BulkAddressUploadsSchema from "@/app/db/schema/bulk_address_uploads";
import * as TagsSchema from "@/app/db/schema/tags";
import * as ProjectsSchema from "@/app/db/schema/projects/projects";
export const pool = new Pool({
host: process.env.DB_HOST,
@ -45,6 +46,7 @@ const schema = {
...PortfolioOrgSchema,
...BulkAddressUploadsSchema,
...TagsSchema,
...ProjectsSchema,
};
export const db = drizzle(pool, {

View file

@ -0,0 +1,219 @@
// Projects module schema.
//
// See ./schema.md for the domain-model proposal this implements. Design notes
// that differ from that document, decided during implementation:
// - "Client" maps to the existing `organisation` table (uuid PK), so
// `project.client_id` is a uuid FK, not the bigint the doc assumed.
// - Property has a bigint PK (see property.ts), so every `property_id` FK is
// bigint, not the integer the doc assumed.
// - status / priority columns whose enum values are still TBC are modelled as
// `text` for now; convert to pgEnum once the value sets are agreed.
import {
type AnyPgColumn,
bigint,
bigserial,
boolean,
date,
integer,
pgTable,
text,
timestamp,
unique,
uuid,
varchar,
} from "drizzle-orm/pg-core";
import { InferModel } from "drizzle-orm";
import { property } from "../property";
import { organisation } from "../organisation";
// Reference data: the available project types (e.g. Retrofit, New Build).
export const projectType = pgTable("project_type", {
id: bigserial("id", { mode: "bigint" }).primaryKey(),
name: varchar("name").notNull(),
});
// The aggregate root.
export const project = pgTable("project", {
id: bigserial("id", { mode: "bigint" }).primaryKey(),
name: varchar("name").notNull(),
// "Client" is the existing organisation table (uuid PK).
clientId: uuid("client_id")
.notNull()
.references(() => organisation.id),
projectTypeId: bigint("project_type_id", { mode: "bigint" })
.notNull()
.references(() => projectType.id),
description: text("description"),
startDate: date("start_date"),
endDate: date("end_date"),
domnaAdminAccess: boolean("domna_admin_access").notNull().default(false),
sharepointBaseUrl: varchar("sharepoint_base_url"),
sharepointSubFolderTemplate: varchar("sharepoint_sub_folder_template"),
});
// Bridging table for the many-to-many between Project and Property.
export const projectProperty = pgTable(
"project_property",
{
id: bigserial("id", { mode: "bigint" }).primaryKey(),
projectId: bigint("project_id", { mode: "bigint" })
.notNull()
.references(() => project.id),
// Property has a bigint PK (property.ts), not integer.
propertyId: bigint("property_id", { mode: "bigint" })
.notNull()
.references(() => property.id),
},
(t) => ({
projectPropertyUnique: unique().on(t.projectId, t.propertyId),
})
);
// An organisation responsible for completing work.
export const contractor = pgTable("contractor", {
id: bigserial("id", { mode: "bigint" }).primaryKey(),
name: varchar("name").notNull(),
phoneNumber: varchar("phone_number"),
emailAddress: varchar("email_address"),
});
// Reference data: the available workstream types (Windows, Doors, Roofs, ...).
export const workstream = pgTable("workstream", {
id: bigserial("id", { mode: "bigint" }).primaryKey(),
name: varchar("name").notNull(),
description: text("description").notNull(),
});
// A workstream configured for a specific project.
//
// `currentStageId` is a forward reference to projectWorkstreamStage (defined
// below); the `() => ...` thunk is evaluated lazily so declaration order is fine.
export const projectWorkstream = pgTable("project_workstream", {
id: bigserial("id", { mode: "bigint" }).primaryKey(),
projectId: bigint("project_id", { mode: "bigint" })
.notNull()
.references(() => project.id),
workstreamId: bigint("workstream_id", { mode: "bigint" })
.notNull()
.references(() => workstream.id),
// Return type annotated with AnyPgColumn to break the TS inference cycle
// created by the mutual FK with projectWorkstreamStage.projectWorkstreamId.
currentStageId: bigint("current_stage_id", { mode: "bigint" }).references(
(): AnyPgColumn => projectWorkstreamStage.id
),
});
// The workflow for an individual Project Workstream. Each Project Workstream
// owns its own stage sequence.
export const projectWorkstreamStage = pgTable("project_workstream_stage", {
id: bigserial("id", { mode: "bigint" }).primaryKey(),
projectWorkstreamId: bigint("project_workstream_id", { mode: "bigint" })
.notNull()
.references(() => projectWorkstream.id),
name: varchar("name").notNull(),
order: integer("order").notNull(),
startDate: date("start_date"),
dueDate: date("due_date"),
lastUpdated: timestamp("last_updated", { precision: 6, withTimezone: true })
.defaultNow()
.$onUpdate(() => new Date())
.notNull(),
// Enum values TBC — modelled as text until the value set is agreed.
status: text("status").notNull(),
statusDescription: text("status_description"),
});
// A contractor assigned to deliver a Project Workstream.
export const projectWorkstreamContractor = pgTable(
"project_workstream_contractor",
{
id: bigserial("id", { mode: "bigint" }).primaryKey(),
projectWorkstreamId: bigint("project_workstream_id", { mode: "bigint" })
.notNull()
.references(() => projectWorkstream.id),
contractorId: bigint("contractor_id", { mode: "bigint" })
.notNull()
.references(() => contractor.id),
updateStagesPermission: boolean("update_stages_permission")
.notNull()
.default(false),
uploadDocumentsPermission: boolean("upload_documents_permission")
.notNull()
.default(false),
assignedAt: timestamp("assigned_at", { precision: 6, withTimezone: true })
.defaultNow()
.notNull(),
}
);
// Reference data: the documents that may be requested.
export const documentType = pgTable("document_type", {
id: bigserial("id", { mode: "bigint" }).primaryKey(),
name: varchar("name").notNull(),
});
// Configuration describing which evidence is required.
export const projectWorkstreamEvidenceRequirement = pgTable(
"project_workstream_evidence_requirement",
{
id: bigserial("id", { mode: "bigint" }).primaryKey(),
projectWorkstreamId: bigint("project_workstream_id", { mode: "bigint" })
.notNull()
.references(() => projectWorkstream.id),
documentTypeId: bigint("document_type_id", { mode: "bigint" })
.notNull()
.references(() => documentType.id),
// Null when the requirement applies to the workstream as a whole.
projectWorkstreamStageId: bigint("project_workstream_stage_id", {
mode: "bigint",
}).references(() => projectWorkstreamStage.id),
required: boolean("required").notNull().default(true),
namingRule: varchar("naming_rule"),
}
);
// Work issued to a contractor.
export const workOrder = pgTable("work_order", {
id: bigserial("id", { mode: "bigint" }).primaryKey(),
reference: varchar("reference").notNull().unique(),
projectWorkstreamContractorId: bigint("project_workstream_contractor_id", {
mode: "bigint",
})
.notNull()
.references(() => projectWorkstreamContractor.id),
// Property has a bigint PK (property.ts), not integer.
propertyId: bigint("property_id", { mode: "bigint" })
.notNull()
.references(() => property.id),
projectWorkstreamStageId: bigint("project_workstream_stage_id", {
mode: "bigint",
})
.notNull()
.references(() => projectWorkstreamStage.id),
// Enum values TBC — modelled as text until the value sets are agreed.
status: text("status").notNull(),
forecastEnd: date("forecast_end"),
priority: text("priority"),
});
export type Project = InferModel<typeof project, "select">;
export type NewProject = InferModel<typeof project, "insert">;
export type ProjectType = InferModel<typeof projectType, "select">;
export type ProjectProperty = InferModel<typeof projectProperty, "select">;
export type Contractor = InferModel<typeof contractor, "select">;
export type Workstream = InferModel<typeof workstream, "select">;
export type ProjectWorkstream = InferModel<typeof projectWorkstream, "select">;
export type ProjectWorkstreamStage = InferModel<
typeof projectWorkstreamStage,
"select"
>;
export type ProjectWorkstreamContractor = InferModel<
typeof projectWorkstreamContractor,
"select"
>;
export type DocumentType = InferModel<typeof documentType, "select">;
export type ProjectWorkstreamEvidenceRequirement = InferModel<
typeof projectWorkstreamEvidenceRequirement,
"select"
>;
export type WorkOrder = InferModel<typeof workOrder, "select">;

View file

@ -0,0 +1,379 @@
# Project Domain Model Proposal
## Purpose
This document proposes the initial domain model and persistence design for the new **Projects** module.
The proposal is based on the approved UX flows and business requirements and aims to validate the proposed aggregate boundaries, entities, relationships and database schema before implementation.
The focus of this document is the domain model and its persistence, rather than application workflows or UI behaviour.
---
# Design Principles
The proposed model is based on the following principles:
- **Project** is the aggregate root.
- A Project covers one or more Properties, and a Property may belong to many Projects (bridged by Project Property).
- Workstreams are configured as part of project setup before contractors are assigned.
- Contractors are assigned to individual Project Workstreams rather than directly to a Project.
- Each Project Workstream owns its own stage progression, allowing different workstreams to follow different workflows.
- Evidence requirements are configuration data.
- Evidence submissions are operational data, stored as rows in the existing `uploaded_files` table (linked via nullable FK columns) rather than a new Projects-specific file store.
---
# Domain Model
```
Project
├── Properties (via Project Property)
├── Project Workstream (Windows)
│ ├── Stages
│ ├── Evidence Requirements
│ └── Contractor Assignment
│ ├── Evidence (rows in uploaded_files)
│ └── Work Orders
├── Project Workstream (Roofs)
│ ├── Stages
│ ├── Evidence Requirements
│ └── Contractor Assignment
└── Project Workstream (Doors)
```
---
# Proposed Schema
Conventions: bigint autoincrement primary keys for all new tables (and bigint FKs between them); FKs to pre-existing tables match the referenced PK's type (e.g. Property has an integer PK). `timestamp` columns default to `now()`, and status-like columns are Postgres enums (one enum type per column, values TBC).
## Project
The aggregate root.
| Column | Type | Nullable | Notes |
| ------------------------------ | ------- | -------- | -------------------------------- |
| id | bigint | No | Primary Key |
| name | varchar | No | |
| client_id | bigint | No | FK → Client |
| project_type_id | bigint | No | FK → Project Type |
| description | text | Yes | |
| start_date | date | Yes | May be unknown at creation |
| end_date | date | Yes | |
| domna_admin_access | boolean | No | Default `false` |
| sharepoint_base_url | varchar | Yes | Only set when SharePoint is used |
| sharepoint_sub_folder_template | varchar | Yes | |
---
## Project Property
Bridging table between Project and Property — the relationship is many-to-many.
| Column | Type | Nullable | Notes |
| ----------- | ------- | -------- | ------------------------------------------ |
| id | bigint | No | Primary Key |
| project_id | bigint | No | FK → Project |
| property_id | integer | No | FK → Property (existing table, integer PK) |
Unique constraint on `(project_id, property_id)`.
---
## Contractor
Represents an organisation responsible for completing work.
| Column | Type | Nullable | Notes |
| ------------- | ------- | -------- | ----------- |
| id | bigint | No | Primary Key |
| name | varchar | No | |
| phone_number | varchar | Yes | |
| email_address | varchar | Yes | |
---
## Workstream
Reference data defining available workstream types.
Examples:
- Windows
- Doors
- Roofs
- Electrical
- Heating
| Column | Type | Nullable | Notes |
| ----------- | ------- | -------- | ----------- |
| id | bigint | No | Primary Key |
| name | varchar | No | |
| description | text | No | |
---
## Project Workstream
Represents a workstream configured for a specific project.
| Column | Type | Nullable | Notes |
| ---------------- | ------ | -------- | --------------------------------------------------------------- |
| id | bigint | No | Primary Key |
| project_id | bigint | No | FK → Project |
| workstream_id | bigint | No | FK → Workstream |
| current_stage_id | bigint | Yes | FK → Project Workstream Stage; null until stages are configured |
---
## Project Workstream Stage
Defines the workflow for an individual Project Workstream.
Unlike a global Stage lookup, each Project Workstream owns its own stage sequence.
Example:
### Roofs
1. Ordered
2. In Progress
3. Completed
### Windows
1. Survey
2. Manufacturing
3. Installation
4. QA Complete
| Column | Type | Nullable | Notes |
| --------------------- | --------- | -------- | --------------------------------------------------------------------- |
| id | bigint | No | Primary Key |
| project_workstream_id | bigint | No | FK → Project Workstream |
| name | varchar | No | |
| order | integer | No | Position within the workstream's sequence |
| start_date | date | Yes | Set as work progresses |
| due_date | date | Yes | |
| last_updated | timestamp | No | Default `now()`, refreshed on change |
| status | enum | No | Postgres enum; values TBC (e.g. not_started / in_progress / complete) |
| status_description | text | Yes | Free-text commentary on the current status |
---
## Project Workstream Contractor
Represents a contractor assigned to deliver a Project Workstream.
| Column | Type | Nullable | Notes |
| --------------------------- | --------- | -------- | ----------------------- |
| id | bigint | No | Primary Key |
| project_workstream_id | bigint | No | FK → Project Workstream |
| contractor_id | bigint | No | FK → Contractor |
| update_stages_permission | boolean | No | Default `false` |
| upload_documents_permission | boolean | No | Default `false` |
| assigned_at | timestamp | No | Default `now()` |
---
## Document Type
Reference data describing the documents that may be requested.
Examples:
- Completion Photo
- RAMS
- Insurance Certificate
- Warranty
- Handover Pack
| Column | Type | Nullable | Notes |
| ------ | ------- | -------- | ----------- |
| id | bigint | No | Primary Key |
| name | varchar | No | |
---
## Project Workstream Evidence Requirement
Configuration describing which evidence is required.
| Column | Type | Nullable | Notes |
| --------------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------- |
| id | bigint | No | Primary Key |
| project_workstream_id | bigint | No | FK → Project Workstream |
| document_type_id | bigint | No | FK → Document Type |
| project_workstream_stage_id | bigint | Yes | FK → Project Workstream Stage; null when the requirement applies to the workstream as a whole rather than a specific stage |
| required | boolean | No | Default `true` |
| naming_rule | varchar | Yes | Optional regex/template |
---
## Uploaded File (existing table)
Evidence submitted by a contractor is stored in the existing `uploaded_files` table rather than a new Projects-specific table.
The table already provides the file storage columns (`s3_file_bucket`, `s3_file_key`, `s3_upload_timestamp`), `file_type` / `file_source` enums, and a set of nullable link columns (`uprn`, `hubspot_deal_id`, …). The Projects module adds the following columns, all nullable because pre-existing rows and non-Projects uploads will not populate them:
| Column | Type | Nullable | Notes |
| ------------------------------------------ | ------ | -------- | -------------------------------------------------------------------------------------------------- |
| project_workstream_id | bigint | Yes | FK → Project Workstream; nullable for now — aim to make it NOT NULL for Projects uploads in future |
| project_workstream_evidence_requirement_id | bigint | Yes | FK → Project Workstream Evidence Requirement; null for ad-hoc uploads not tied to a requirement |
| project_workstream_contractor_id | bigint | Yes | FK → Project Workstream Contractor |
| uploaded_by | text | Yes | Free text for now — undecided whether contractors get individual or shared company accounts |
The `file_source` enum will likely need a new value for uploads originating from the Projects module.
---
## Work Order
Represents work issued to a contractor.
| Column | Type | Nullable | Notes |
| -------------------------------- | ------- | -------- | ------------------------------------------ |
| id | bigint | No | Primary Key |
| reference | varchar | No | Unique |
| project_workstream_contractor_id | bigint | No | FK → Project Workstream Contractor |
| property_id | integer | No | FK → Property (existing table, integer PK) |
| project_workstream_stage_id | bigint | No | FK → Project Workstream Stage |
| status | enum | No | Postgres enum; values TBC |
| forecast_end | date | Yes | |
| priority | enum | Yes | Postgres enum; values TBC |
---
# Relationships
| Relationship | Cardinality |
| ----------------------------------------------- | ------------------------------------------------------------- |
| Project ↔ Property | Many : Many (via ProjectProperty) |
| Project → ProjectWorkstream | 1 : Many |
| Workstream → ProjectWorkstream | 1 : Many |
| ProjectWorkstream → ProjectWorkstreamStage | 1 : Many |
| ProjectWorkstream → EvidenceRequirement | 1 : Many |
| ProjectWorkstreamStage → EvidenceRequirement | 1 : Many (optional — a requirement may not be stage-specific) |
| ProjectWorkstream → ProjectWorkstreamContractor | 1 : Many |
| Contractor → ProjectWorkstreamContractor | 1 : Many |
| ProjectWorkstream → UploadedFile | 1 : Many (optional — nullable FK for now) |
| ProjectWorkstreamContractor → UploadedFile | 1 : Many (optional) |
| EvidenceRequirement → UploadedFile | 1 : Many (optional — null for ad-hoc uploads) |
| ProjectWorkstreamContractor → WorkOrder | 1 : Many |
| ProjectWorkstreamStage → WorkOrder | 1 : Many |
| Property → WorkOrder | 1 : Many |
---
# Design Rationale
## Aggregate
The aggregate root is **Project**.
Everything else exists within the context of a Project.
---
## Configuration vs Operational Data
The model intentionally separates project configuration from operational data.
### Configuration
- Project Workstream
- Project Workstream Stage
- Evidence Requirement
### Operational
- Contractor Assignment
- Evidence (rows in the existing `uploaded_files` table)
- Work Order
This allows a project to be fully configured before work begins.
---
## Project-Specific Workflow
Stages are owned by Project Workstreams rather than being globally defined.
This allows each workstream within a project to define its own workflow independently.
---
## Contractor Assignment
Contractors are assigned to Project Workstreams rather than Projects.
This more accurately reflects the business process and provides a natural location for assignment-specific permissions.
---
## Evidence Model
Evidence requirements define what documentation is expected.
Documents actually submitted are rows in the existing `uploaded_files` table, linked back to the requirement, workstream and contractor assignment via new nullable FK columns. This reuses the established upload pipeline instead of introducing a parallel file store.
Separating expectation from submission provides flexibility for future enhancements such as:
- approvals
- versioning
- expiry dates
- rejected submissions
- audit history
without requiring schema changes.
---
# Outstanding Design Questions
## Stage Templates
Should Projects be created from reusable templates that pre-populate Workstreams, Stages and Evidence Requirements?
Or should every Project define these independently?
---
## Evidence Versioning
Should multiple submissions be permitted against a single evidence requirement?
If so, should previous versions remain visible?
---
## Multiple Contractor Assignments
Can multiple contractors be assigned to the same Project Workstream simultaneously?
Or should assignments be exclusive?
---
## File Storage
Evidence reuses the existing `uploaded_files` table, which stores files in S3 (`s3_file_bucket` / `s3_file_key`) and records provenance via the `file_source` enum.
Open points: which `file_type` / `file_source` values Projects uploads should use (new enum values are likely needed), and whether the `project_workstream_id` column can eventually become NOT NULL for Projects-originated uploads.
---
# Summary
The proposed model aims to:
- represent business concepts rather than relational joins
- separate configuration from operational data
- support project-specific workflows
- minimise coupling to external systems
- provide a flexible foundation for future enhancements while keeping the initial implementation straightforward

View file

@ -22,6 +22,21 @@ import { material } from "./materials";
import { portfolio, portfolioUsers } from "./portfolio";
import { user } from "./users";
import { fundingPackage, fundingPackageMeasures } from "./funding";
import { organisation } from "./organisation";
import { uploadedFiles } from "./uploaded_files";
import {
project,
projectType,
projectProperty,
contractor,
workstream,
projectWorkstream,
projectWorkstreamStage,
projectWorkstreamContractor,
documentType,
projectWorkstreamEvidenceRequirement,
workOrder,
} from "./projects/projects";
// Define the other side of the one to many relation between a property and its recomendations
export const recommendationsRelations = relations(
@ -192,3 +207,164 @@ export const fundingPackageMeasuresRelations = relations(
}),
})
);
// ---------------------------------------------------------------------------
// Projects module relations (see ./projects/projects.ts).
//
// Inverses on pre-existing tables (property, organisation, user) are omitted
// so their existing relation blocks are left untouched; drizzle allows a
// one-directional relation where the pairing is unambiguous.
// ---------------------------------------------------------------------------
export const projectRelations = relations(project, ({ one, many }) => ({
client: one(organisation, {
fields: [project.clientId],
references: [organisation.id],
}),
projectType: one(projectType, {
fields: [project.projectTypeId],
references: [projectType.id],
}),
properties: many(projectProperty),
workstreams: many(projectWorkstream),
}));
export const projectTypeRelations = relations(projectType, ({ many }) => ({
projects: many(project),
}));
export const projectPropertyRelations = relations(
projectProperty,
({ one }) => ({
project: one(project, {
fields: [projectProperty.projectId],
references: [project.id],
}),
property: one(property, {
fields: [projectProperty.propertyId],
references: [property.id],
}),
})
);
export const contractorRelations = relations(contractor, ({ many }) => ({
assignments: many(projectWorkstreamContractor),
}));
export const workstreamRelations = relations(workstream, ({ many }) => ({
projectWorkstreams: many(projectWorkstream),
}));
export const projectWorkstreamRelations = relations(
projectWorkstream,
({ one, many }) => ({
project: one(project, {
fields: [projectWorkstream.projectId],
references: [project.id],
}),
workstream: one(workstream, {
fields: [projectWorkstream.workstreamId],
references: [workstream.id],
}),
// The workstream's currently-active stage.
currentStage: one(projectWorkstreamStage, {
fields: [projectWorkstream.currentStageId],
references: [projectWorkstreamStage.id],
relationName: "currentStage",
}),
// All stages owned by this workstream.
stages: many(projectWorkstreamStage, { relationName: "workstreamStages" }),
evidenceRequirements: many(projectWorkstreamEvidenceRequirement),
contractors: many(projectWorkstreamContractor),
uploadedFiles: many(uploadedFiles),
})
);
export const projectWorkstreamStageRelations = relations(
projectWorkstreamStage,
({ one, many }) => ({
projectWorkstream: one(projectWorkstream, {
fields: [projectWorkstreamStage.projectWorkstreamId],
references: [projectWorkstream.id],
relationName: "workstreamStages",
}),
// Inverse of projectWorkstream.currentStage.
currentOfWorkstreams: many(projectWorkstream, {
relationName: "currentStage",
}),
evidenceRequirements: many(projectWorkstreamEvidenceRequirement),
workOrders: many(workOrder),
})
);
export const projectWorkstreamContractorRelations = relations(
projectWorkstreamContractor,
({ one, many }) => ({
projectWorkstream: one(projectWorkstream, {
fields: [projectWorkstreamContractor.projectWorkstreamId],
references: [projectWorkstream.id],
}),
contractor: one(contractor, {
fields: [projectWorkstreamContractor.contractorId],
references: [contractor.id],
}),
workOrders: many(workOrder),
uploadedFiles: many(uploadedFiles),
})
);
export const documentTypeRelations = relations(documentType, ({ many }) => ({
evidenceRequirements: many(projectWorkstreamEvidenceRequirement),
}));
export const projectWorkstreamEvidenceRequirementRelations = relations(
projectWorkstreamEvidenceRequirement,
({ one, many }) => ({
projectWorkstream: one(projectWorkstream, {
fields: [projectWorkstreamEvidenceRequirement.projectWorkstreamId],
references: [projectWorkstream.id],
}),
documentType: one(documentType, {
fields: [projectWorkstreamEvidenceRequirement.documentTypeId],
references: [documentType.id],
}),
stage: one(projectWorkstreamStage, {
fields: [projectWorkstreamEvidenceRequirement.projectWorkstreamStageId],
references: [projectWorkstreamStage.id],
}),
uploadedFiles: many(uploadedFiles),
})
);
export const workOrderRelations = relations(workOrder, ({ one }) => ({
projectWorkstreamContractor: one(projectWorkstreamContractor, {
fields: [workOrder.projectWorkstreamContractorId],
references: [projectWorkstreamContractor.id],
}),
property: one(property, {
fields: [workOrder.propertyId],
references: [property.id],
}),
stage: one(projectWorkstreamStage, {
fields: [workOrder.projectWorkstreamStageId],
references: [projectWorkstreamStage.id],
}),
}));
export const uploadedFilesRelations = relations(uploadedFiles, ({ one }) => ({
projectWorkstream: one(projectWorkstream, {
fields: [uploadedFiles.projectWorkstreamId],
references: [projectWorkstream.id],
}),
projectWorkstreamEvidenceRequirement: one(
projectWorkstreamEvidenceRequirement,
{
fields: [uploadedFiles.projectWorkstreamEvidenceRequirementId],
references: [projectWorkstreamEvidenceRequirement.id],
}
),
projectWorkstreamContractor: one(projectWorkstreamContractor, {
fields: [uploadedFiles.projectWorkstreamContractorId],
references: [projectWorkstreamContractor.id],
}),
}));

View file

@ -1,5 +1,10 @@
import { bigint, bigserial, pgEnum, pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { user } from "./users";
import {
projectWorkstream,
projectWorkstreamContractor,
projectWorkstreamEvidenceRequirement,
} from "./projects/projects";
export const fileType = pgEnum("file_type", [
// Assessment
@ -70,7 +75,9 @@ export const fileSource = pgEnum("file_source", [
"contractor",
"magic_plan",
"coordination_hub",
"audit_generator"
"audit_generator",
// Uploads originating from the Projects module.
"projects"
]);
export const uploadedFiles = pgTable(
@ -90,5 +97,18 @@ export const uploadedFiles = pgTable(
source: fileSource("file_source"),
measureName: text("measure_name"),
uploadedBy: bigint("uploaded_by", { mode: "bigint" }).references(() => user.id),
// Projects module links. All nullable: pre-existing rows and non-Projects
// uploads don't populate them. Evidence submitted by a contractor is stored
// here rather than in a Projects-specific file store.
projectWorkstreamId: bigint("project_workstream_id", {
mode: "bigint"
}).references(() => projectWorkstream.id),
projectWorkstreamEvidenceRequirementId: bigint(
"project_workstream_evidence_requirement_id",
{ mode: "bigint" }
).references(() => projectWorkstreamEvidenceRequirement.id),
projectWorkstreamContractorId: bigint("project_workstream_contractor_id", {
mode: "bigint"
}).references(() => projectWorkstreamContractor.id),
}
);