feat(platform): harden access scoping and delivery baseline

This commit is contained in:
2026-03-30 00:27:31 +02:00
parent 00b936fa1f
commit 819345acfa
109 changed files with 26142 additions and 8081 deletions
@@ -0,0 +1,8 @@
-- GIN indexes for JSONB dynamic field filtering
-- Run: psql $DATABASE_URL -f packages/db/prisma/migrations/20260310_jsonb_gin_indexes.sql
CREATE INDEX IF NOT EXISTS idx_resource_dynamic_fields_gin
ON resources USING gin ("dynamicFields" jsonb_path_ops);
CREATE INDEX IF NOT EXISTS idx_project_dynamic_fields_gin
ON projects USING gin ("dynamicFields" jsonb_path_ops);
@@ -0,0 +1,71 @@
-- Additive persistence split for planning demand vs assignment
-- Run: psql $DATABASE_URL -f packages/db/prisma/migrations/20260313_demand_assignment_additive.sql
CREATE TABLE IF NOT EXISTS demand_requirements (
id text PRIMARY KEY,
"legacyAllocationId" text UNIQUE,
"projectId" text NOT NULL REFERENCES projects(id),
"startDate" date NOT NULL,
"endDate" date NOT NULL,
"hoursPerDay" double precision NOT NULL,
percentage double precision NOT NULL,
role text,
"roleId" text REFERENCES roles(id),
headcount integer NOT NULL DEFAULT 1,
status "AllocationStatus" NOT NULL DEFAULT 'PROPOSED',
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
"createdAt" timestamptz NOT NULL DEFAULT now(),
"updatedAt" timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS assignments (
id text PRIMARY KEY,
"legacyAllocationId" text UNIQUE,
"demandRequirementId" text REFERENCES demand_requirements(id),
"resourceId" text NOT NULL REFERENCES resources(id),
"projectId" text NOT NULL REFERENCES projects(id),
"startDate" date NOT NULL,
"endDate" date NOT NULL,
"hoursPerDay" double precision NOT NULL,
percentage double precision NOT NULL,
role text,
"roleId" text REFERENCES roles(id),
"dailyCostCents" integer NOT NULL,
status "AllocationStatus" NOT NULL DEFAULT 'PROPOSED',
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
"createdAt" timestamptz NOT NULL DEFAULT now(),
"updatedAt" timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS unique_assignment
ON assignments ("resourceId", "projectId", "startDate", "endDate");
CREATE INDEX IF NOT EXISTS idx_demand_requirements_legacy_allocation_id
ON demand_requirements ("legacyAllocationId");
CREATE INDEX IF NOT EXISTS idx_demand_requirements_project_id
ON demand_requirements ("projectId");
CREATE INDEX IF NOT EXISTS idx_demand_requirements_start_end
ON demand_requirements ("startDate", "endDate");
CREATE INDEX IF NOT EXISTS idx_demand_requirements_status
ON demand_requirements (status);
CREATE INDEX IF NOT EXISTS idx_assignments_legacy_allocation_id
ON assignments ("legacyAllocationId");
CREATE INDEX IF NOT EXISTS idx_assignments_demand_requirement_id
ON assignments ("demandRequirementId");
CREATE INDEX IF NOT EXISTS idx_assignments_resource_id
ON assignments ("resourceId");
CREATE INDEX IF NOT EXISTS idx_assignments_project_id
ON assignments ("projectId");
CREATE INDEX IF NOT EXISTS idx_assignments_start_end
ON assignments ("startDate", "endDate");
CREATE INDEX IF NOT EXISTS idx_assignments_status
ON assignments (status);
@@ -0,0 +1 @@
ALTER TYPE "DispoImportSourceKind" ADD VALUE IF NOT EXISTS 'ROSTER';
@@ -0,0 +1,3 @@
ALTER TABLE "staged_resources"
ADD COLUMN IF NOT EXISTS "lcrCents" INTEGER,
ADD COLUMN IF NOT EXISTS "ucrCents" INTEGER;
@@ -0,0 +1,328 @@
-- Dispo import staging foundation
-- Run: psql $DATABASE_URL -f packages/db/prisma/migrations/20260314_dispo_import_staging.sql
DO $$
BEGIN
CREATE TYPE "ImportBatchStatus" AS ENUM (
'DRAFT',
'STAGING',
'STAGED',
'REVIEW_READY',
'APPROVED',
'COMMITTING',
'COMMITTED',
'FAILED',
'CANCELLED'
);
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
DO $$
BEGIN
CREATE TYPE "StagedRecordStatus" AS ENUM (
'PARSED',
'NORMALIZED',
'UNRESOLVED',
'APPROVED',
'REJECTED',
'COMMITTED',
'FAILED'
);
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
DO $$
BEGIN
CREATE TYPE "DispoImportSourceKind" AS ENUM (
'REFERENCE',
'PLANNING',
'CHARGEABILITY'
);
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
DO $$
BEGIN
CREATE TYPE "DispoStagedRecordType" AS ENUM (
'RESOURCE',
'CLIENT',
'PROJECT',
'ASSIGNMENT',
'VACATION',
'AVAILABILITY_RULE',
'UNRESOLVED'
);
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
CREATE TABLE IF NOT EXISTS import_batches (
id text PRIMARY KEY,
"sourceSystem" text NOT NULL DEFAULT 'DISPO_V2',
status "ImportBatchStatus" NOT NULL DEFAULT 'DRAFT',
"referenceSourceFile" text,
"planningSourceFile" text,
"chargeabilitySourceFile" text,
notes text,
summary jsonb NOT NULL DEFAULT '{}'::jsonb,
"startedAt" timestamptz,
"stagedAt" timestamptz,
"approvedAt" timestamptz,
"committedAt" timestamptz,
"failedAt" timestamptz,
"createdAt" timestamptz NOT NULL DEFAULT now(),
"updatedAt" timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS staged_resources (
id text PRIMARY KEY,
"importBatchId" text NOT NULL REFERENCES import_batches(id) ON DELETE CASCADE,
status "StagedRecordStatus" NOT NULL DEFAULT 'PARSED',
"sourceKind" "DispoImportSourceKind" NOT NULL,
"sourceWorkbook" text NOT NULL,
"sourceSheet" text NOT NULL,
"sourceRow" integer NOT NULL,
"sourceColumn" text,
"canonicalExternalId" text NOT NULL,
"enterpriseId" text,
eid text,
"displayName" text,
email text,
chapter text,
"chapterCode" text,
"managementLevelGroupName" text,
"managementLevelName" text,
"countryCode" text,
"metroCityName" text,
"clientUnitName" text,
"resourceType" "ResourceType",
"chargeabilityTarget" double precision,
fte double precision,
availability jsonb,
"roleTokens" text[] NOT NULL DEFAULT ARRAY[]::text[],
warnings text[] NOT NULL DEFAULT ARRAY[]::text[],
"errorMessage" text,
"rawPayload" jsonb NOT NULL,
"normalizedData" jsonb NOT NULL DEFAULT '{}'::jsonb,
"createdAt" timestamptz NOT NULL DEFAULT now(),
"updatedAt" timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS staged_clients (
id text PRIMARY KEY,
"importBatchId" text NOT NULL REFERENCES import_batches(id) ON DELETE CASCADE,
status "StagedRecordStatus" NOT NULL DEFAULT 'PARSED',
"sourceKind" "DispoImportSourceKind" NOT NULL,
"sourceWorkbook" text NOT NULL,
"sourceSheet" text NOT NULL,
"sourceRow" integer NOT NULL,
"sourceColumn" text,
"clientCode" text,
"parentClientCode" text,
name text NOT NULL,
"sortOrder" integer,
"isActive" boolean NOT NULL DEFAULT true,
warnings text[] NOT NULL DEFAULT ARRAY[]::text[],
"errorMessage" text,
"rawPayload" jsonb NOT NULL,
"normalizedData" jsonb NOT NULL DEFAULT '{}'::jsonb,
"createdAt" timestamptz NOT NULL DEFAULT now(),
"updatedAt" timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS staged_projects (
id text PRIMARY KEY,
"importBatchId" text NOT NULL REFERENCES import_batches(id) ON DELETE CASCADE,
status "StagedRecordStatus" NOT NULL DEFAULT 'PARSED',
"sourceKind" "DispoImportSourceKind" NOT NULL,
"sourceWorkbook" text NOT NULL,
"sourceSheet" text NOT NULL,
"sourceRow" integer NOT NULL,
"sourceColumn" text,
"projectKey" text NOT NULL,
"shortCode" text,
name text,
"clientCode" text,
"utilizationCategoryCode" text,
"orderType" "OrderType",
"allocationType" "AllocationType",
"winProbability" integer,
"isInternal" boolean NOT NULL DEFAULT false,
"isTbd" boolean NOT NULL DEFAULT false,
"startDate" date,
"endDate" date,
warnings text[] NOT NULL DEFAULT ARRAY[]::text[],
"errorMessage" text,
"rawPayload" jsonb NOT NULL,
"normalizedData" jsonb NOT NULL DEFAULT '{}'::jsonb,
"createdAt" timestamptz NOT NULL DEFAULT now(),
"updatedAt" timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS staged_assignments (
id text PRIMARY KEY,
"importBatchId" text NOT NULL REFERENCES import_batches(id) ON DELETE CASCADE,
status "StagedRecordStatus" NOT NULL DEFAULT 'PARSED',
"sourceKind" "DispoImportSourceKind" NOT NULL,
"sourceWorkbook" text NOT NULL,
"sourceSheet" text NOT NULL,
"sourceRow" integer NOT NULL,
"sourceColumn" text,
"resourceExternalId" text NOT NULL,
"projectKey" text,
"assignmentDate" date,
"startDate" date,
"endDate" date,
"hoursPerDay" double precision,
percentage double precision,
"slotFraction" double precision,
"roleToken" text,
"roleName" text,
"chapterToken" text,
"utilizationCategoryCode" text,
"winProbability" integer,
"isInternal" boolean NOT NULL DEFAULT false,
"isUnassigned" boolean NOT NULL DEFAULT false,
"isTbd" boolean NOT NULL DEFAULT false,
warnings text[] NOT NULL DEFAULT ARRAY[]::text[],
"errorMessage" text,
"rawPayload" jsonb NOT NULL,
"normalizedData" jsonb NOT NULL DEFAULT '{}'::jsonb,
"createdAt" timestamptz NOT NULL DEFAULT now(),
"updatedAt" timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS staged_vacations (
id text PRIMARY KEY,
"importBatchId" text NOT NULL REFERENCES import_batches(id) ON DELETE CASCADE,
status "StagedRecordStatus" NOT NULL DEFAULT 'PARSED',
"sourceKind" "DispoImportSourceKind" NOT NULL,
"sourceWorkbook" text NOT NULL,
"sourceSheet" text NOT NULL,
"sourceRow" integer NOT NULL,
"sourceColumn" text,
"resourceExternalId" text NOT NULL,
"vacationType" "VacationType" NOT NULL,
"startDate" date NOT NULL,
"endDate" date NOT NULL,
note text,
"holidayName" text,
"isHalfDay" boolean NOT NULL DEFAULT false,
"halfDayPart" text,
"isPublicHoliday" boolean NOT NULL DEFAULT false,
warnings text[] NOT NULL DEFAULT ARRAY[]::text[],
"errorMessage" text,
"rawPayload" jsonb NOT NULL,
"normalizedData" jsonb NOT NULL DEFAULT '{}'::jsonb,
"createdAt" timestamptz NOT NULL DEFAULT now(),
"updatedAt" timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS staged_availability_rules (
id text PRIMARY KEY,
"importBatchId" text NOT NULL REFERENCES import_batches(id) ON DELETE CASCADE,
status "StagedRecordStatus" NOT NULL DEFAULT 'PARSED',
"sourceKind" "DispoImportSourceKind" NOT NULL,
"sourceWorkbook" text NOT NULL,
"sourceSheet" text NOT NULL,
"sourceRow" integer NOT NULL,
"sourceColumn" text,
"resourceExternalId" text NOT NULL,
"ruleType" text NOT NULL,
weekday integer,
"effectiveStartDate" date,
"effectiveEndDate" date,
"availableHours" double precision,
percentage double precision,
"isResolved" boolean NOT NULL DEFAULT false,
warnings text[] NOT NULL DEFAULT ARRAY[]::text[],
"errorMessage" text,
"rawPayload" jsonb NOT NULL,
"normalizedData" jsonb NOT NULL DEFAULT '{}'::jsonb,
"createdAt" timestamptz NOT NULL DEFAULT now(),
"updatedAt" timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS staged_unresolved_records (
id text PRIMARY KEY,
"importBatchId" text NOT NULL REFERENCES import_batches(id) ON DELETE CASCADE,
status "StagedRecordStatus" NOT NULL DEFAULT 'UNRESOLVED',
"sourceKind" "DispoImportSourceKind" NOT NULL,
"sourceWorkbook" text NOT NULL,
"sourceSheet" text NOT NULL,
"sourceRow" integer NOT NULL,
"sourceColumn" text,
"recordType" "DispoStagedRecordType" NOT NULL,
"resourceExternalId" text,
"projectKey" text,
message text NOT NULL,
"resolutionHint" text,
warnings text[] NOT NULL DEFAULT ARRAY[]::text[],
"rawPayload" jsonb NOT NULL,
"normalizedData" jsonb NOT NULL DEFAULT '{}'::jsonb,
"createdAt" timestamptz NOT NULL DEFAULT now(),
"updatedAt" timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_import_batches_status
ON import_batches (status);
CREATE INDEX IF NOT EXISTS idx_staged_resources_batch_status
ON staged_resources ("importBatchId", status);
CREATE INDEX IF NOT EXISTS idx_staged_resources_canonical_external_id
ON staged_resources ("canonicalExternalId");
CREATE INDEX IF NOT EXISTS idx_staged_clients_batch_status
ON staged_clients ("importBatchId", status);
CREATE INDEX IF NOT EXISTS idx_staged_clients_client_code
ON staged_clients ("clientCode");
CREATE INDEX IF NOT EXISTS idx_staged_projects_batch_status
ON staged_projects ("importBatchId", status);
CREATE INDEX IF NOT EXISTS idx_staged_projects_project_key
ON staged_projects ("projectKey");
CREATE INDEX IF NOT EXISTS idx_staged_assignments_batch_status
ON staged_assignments ("importBatchId", status);
CREATE INDEX IF NOT EXISTS idx_staged_assignments_resource_external_id
ON staged_assignments ("resourceExternalId");
CREATE INDEX IF NOT EXISTS idx_staged_assignments_project_key
ON staged_assignments ("projectKey");
CREATE INDEX IF NOT EXISTS idx_staged_assignments_assignment_date
ON staged_assignments ("assignmentDate");
CREATE INDEX IF NOT EXISTS idx_staged_vacations_batch_status
ON staged_vacations ("importBatchId", status);
CREATE INDEX IF NOT EXISTS idx_staged_vacations_resource_external_id
ON staged_vacations ("resourceExternalId");
CREATE INDEX IF NOT EXISTS idx_staged_vacations_start_end
ON staged_vacations ("startDate", "endDate");
CREATE INDEX IF NOT EXISTS idx_staged_availability_rules_batch_status
ON staged_availability_rules ("importBatchId", status);
CREATE INDEX IF NOT EXISTS idx_staged_availability_rules_resource_external_id
ON staged_availability_rules ("resourceExternalId");
CREATE INDEX IF NOT EXISTS idx_staged_unresolved_records_batch_status
ON staged_unresolved_records ("importBatchId", status);
CREATE INDEX IF NOT EXISTS idx_staged_unresolved_records_record_type
ON staged_unresolved_records ("recordType");
CREATE INDEX IF NOT EXISTS idx_staged_unresolved_records_resource_external_id
ON staged_unresolved_records ("resourceExternalId");
CREATE INDEX IF NOT EXISTS idx_staged_unresolved_records_project_key
ON staged_unresolved_records ("projectKey");
@@ -0,0 +1,29 @@
CREATE TYPE "AssistantApprovalStatus" AS ENUM ('PENDING', 'APPROVED', 'CANCELLED', 'EXPIRED');
CREATE TABLE "assistant_approvals" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"conversationId" TEXT NOT NULL,
"toolName" TEXT NOT NULL,
"toolArguments" TEXT NOT NULL,
"summary" TEXT NOT NULL,
"status" "AssistantApprovalStatus" NOT NULL DEFAULT 'PENDING',
"approvedAt" TIMESTAMP(3),
"cancelledAt" TIMESTAMP(3),
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "assistant_approvals_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "assistant_approvals_userId_conversationId_status_expiresAt_idx"
ON "assistant_approvals"("userId", "conversationId", "status", "expiresAt");
CREATE INDEX "assistant_approvals_status_expiresAt_idx"
ON "assistant_approvals"("status", "expiresAt");
ALTER TABLE "assistant_approvals"
ADD CONSTRAINT "assistant_approvals_userId_fkey"
FOREIGN KEY ("userId") REFERENCES "users"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,143 @@
BEGIN;
UPDATE "holiday_calendars"
SET "stateCode" = UPPER("stateCode")
WHERE "stateCode" IS NOT NULL;
UPDATE "holiday_calendars"
SET "stateCode" = NULL,
"metroCityId" = NULL
WHERE "scopeType" = 'COUNTRY';
UPDATE "holiday_calendars"
SET "metroCityId" = NULL
WHERE "scopeType" = 'STATE';
CREATE TEMP TABLE "tmp_holiday_calendar_merge_map" (
"duplicate_id" TEXT PRIMARY KEY,
"keeper_id" TEXT NOT NULL
) ON COMMIT DROP;
WITH ranked AS (
SELECT
"id",
FIRST_VALUE("id") OVER (
PARTITION BY "countryId"
ORDER BY "priority" DESC, "createdAt" DESC, "id" DESC
) AS "keeper_id",
ROW_NUMBER() OVER (
PARTITION BY "countryId"
ORDER BY "priority" DESC, "createdAt" DESC, "id" DESC
) AS "rn"
FROM "holiday_calendars"
WHERE "scopeType" = 'COUNTRY'
)
INSERT INTO "tmp_holiday_calendar_merge_map" ("duplicate_id", "keeper_id")
SELECT "id", "keeper_id"
FROM ranked
WHERE "rn" > 1;
WITH ranked AS (
SELECT
"id",
FIRST_VALUE("id") OVER (
PARTITION BY "countryId", "stateCode"
ORDER BY "priority" DESC, "createdAt" DESC, "id" DESC
) AS "keeper_id",
ROW_NUMBER() OVER (
PARTITION BY "countryId", "stateCode"
ORDER BY "priority" DESC, "createdAt" DESC, "id" DESC
) AS "rn"
FROM "holiday_calendars"
WHERE "scopeType" = 'STATE'
AND "stateCode" IS NOT NULL
)
INSERT INTO "tmp_holiday_calendar_merge_map" ("duplicate_id", "keeper_id")
SELECT "id", "keeper_id"
FROM ranked
WHERE "rn" > 1
ON CONFLICT ("duplicate_id") DO NOTHING;
WITH ranked AS (
SELECT
"id",
FIRST_VALUE("id") OVER (
PARTITION BY "countryId", "metroCityId"
ORDER BY "priority" DESC, "createdAt" DESC, "id" DESC
) AS "keeper_id",
ROW_NUMBER() OVER (
PARTITION BY "countryId", "metroCityId"
ORDER BY "priority" DESC, "createdAt" DESC, "id" DESC
) AS "rn"
FROM "holiday_calendars"
WHERE "scopeType" = 'CITY'
AND "metroCityId" IS NOT NULL
)
INSERT INTO "tmp_holiday_calendar_merge_map" ("duplicate_id", "keeper_id")
SELECT "id", "keeper_id"
FROM ranked
WHERE "rn" > 1
ON CONFLICT ("duplicate_id") DO NOTHING;
UPDATE "holiday_calendar_entries" AS "entry"
SET "holidayCalendarId" = "map"."keeper_id"
FROM "tmp_holiday_calendar_merge_map" AS "map"
WHERE "entry"."holidayCalendarId" = "map"."duplicate_id"
AND NOT EXISTS (
SELECT 1
FROM "holiday_calendar_entries" AS "existing"
WHERE "existing"."holidayCalendarId" = "map"."keeper_id"
AND "existing"."date" = "entry"."date"
);
WITH ranked_entries AS (
SELECT
"id",
ROW_NUMBER() OVER (
PARTITION BY "holidayCalendarId", "date"
ORDER BY "isRecurringAnnual" DESC, "updatedAt" DESC, "createdAt" DESC, "id" DESC
) AS "rn"
FROM "holiday_calendar_entries"
)
DELETE FROM "holiday_calendar_entries" AS "entry"
USING ranked_entries
WHERE "entry"."id" = ranked_entries."id"
AND ranked_entries."rn" > 1;
DELETE FROM "holiday_calendars" AS "calendar"
USING "tmp_holiday_calendar_merge_map" AS "map"
WHERE "calendar"."id" = "map"."duplicate_id";
DROP INDEX IF EXISTS "holiday_calendar_entries_holidayCalendarId_date_name_key";
DROP INDEX IF EXISTS "holiday_calendar_entries_holidayCalendarId_date_key";
CREATE UNIQUE INDEX "holiday_calendar_entries_holidayCalendarId_date_key"
ON "holiday_calendar_entries" ("holidayCalendarId", "date");
ALTER TABLE "holiday_calendars"
DROP CONSTRAINT IF EXISTS "holiday_calendars_scope_fields_check";
ALTER TABLE "holiday_calendars"
ADD CONSTRAINT "holiday_calendars_scope_fields_check"
CHECK (
("scopeType" = 'COUNTRY' AND "stateCode" IS NULL AND "metroCityId" IS NULL)
OR ("scopeType" = 'STATE' AND "stateCode" IS NOT NULL AND "metroCityId" IS NULL)
OR ("scopeType" = 'CITY' AND "metroCityId" IS NOT NULL)
);
DROP INDEX IF EXISTS "holiday_calendars_country_scope_unique";
DROP INDEX IF EXISTS "holiday_calendars_state_scope_unique";
DROP INDEX IF EXISTS "holiday_calendars_city_scope_unique";
CREATE UNIQUE INDEX "holiday_calendars_country_scope_unique"
ON "holiday_calendars" ("countryId")
WHERE "scopeType" = 'COUNTRY';
CREATE UNIQUE INDEX "holiday_calendars_state_scope_unique"
ON "holiday_calendars" ("countryId", "stateCode")
WHERE "scopeType" = 'STATE' AND "stateCode" IS NOT NULL;
CREATE UNIQUE INDEX "holiday_calendars_city_scope_unique"
ON "holiday_calendars" ("countryId", "metroCityId")
WHERE "scopeType" = 'CITY' AND "metroCityId" IS NOT NULL;
COMMIT;
@@ -0,0 +1,52 @@
CREATE TYPE "HolidayCalendarScope" AS ENUM ('COUNTRY', 'STATE', 'CITY');
CREATE TABLE "holiday_calendars" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"scopeType" "HolidayCalendarScope" NOT NULL,
"countryId" TEXT NOT NULL,
"stateCode" TEXT,
"metroCityId" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"priority" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "holiday_calendars_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "holiday_calendar_entries" (
"id" TEXT NOT NULL,
"holidayCalendarId" TEXT NOT NULL,
"date" DATE NOT NULL,
"name" TEXT NOT NULL,
"isRecurringAnnual" BOOLEAN NOT NULL DEFAULT false,
"source" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "holiday_calendar_entries_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "holiday_calendars_countryId_scopeType_idx" ON "holiday_calendars"("countryId", "scopeType");
CREATE INDEX "holiday_calendars_countryId_stateCode_idx" ON "holiday_calendars"("countryId", "stateCode");
CREATE INDEX "holiday_calendars_metroCityId_idx" ON "holiday_calendars"("metroCityId");
CREATE INDEX "holiday_calendar_entries_date_idx" ON "holiday_calendar_entries"("date");
CREATE UNIQUE INDEX "holiday_calendar_entries_holidayCalendarId_date_name_key"
ON "holiday_calendar_entries"("holidayCalendarId", "date", "name");
ALTER TABLE "holiday_calendars"
ADD CONSTRAINT "holiday_calendars_countryId_fkey"
FOREIGN KEY ("countryId") REFERENCES "countries"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "holiday_calendars"
ADD CONSTRAINT "holiday_calendars_metroCityId_fkey"
FOREIGN KEY ("metroCityId") REFERENCES "metro_cities"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "holiday_calendar_entries"
ADD CONSTRAINT "holiday_calendar_entries_holidayCalendarId_fkey"
FOREIGN KEY ("holidayCalendarId") REFERENCES "holiday_calendars"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,26 @@
CREATE TYPE "ReportTemplateEntity" AS ENUM ('RESOURCE', 'PROJECT', 'ASSIGNMENT', 'RESOURCE_MONTH');
CREATE TABLE "report_templates" (
"id" TEXT NOT NULL,
"ownerId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"entity" "ReportTemplateEntity" NOT NULL,
"config" JSONB NOT NULL,
"isShared" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "report_templates_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "report_templates_ownerId_updatedAt_idx"
ON "report_templates"("ownerId", "updatedAt");
CREATE UNIQUE INDEX "report_templates_ownerId_name_key"
ON "report_templates"("ownerId", "name");
ALTER TABLE "report_templates"
ADD CONSTRAINT "report_templates_ownerId_fkey"
FOREIGN KEY ("ownerId") REFERENCES "users"("id")
ON DELETE CASCADE ON UPDATE CASCADE;