mirror of
https://github.com/documenso/documenso.git
synced 2025-11-12 15:53:02 +10:00
feat: add organisations (#1820)
This commit is contained in:
@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright 2024 Viascom Ltd liab. Co
|
||||
*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
-- The `nanoid()` function generates a compact, URL-friendly unique identifier.
|
||||
-- Based on the given size and alphabet, it creates a randomized string that's ideal for
|
||||
-- use-cases requiring small, unpredictable IDs (e.g., URL shorteners, generated file names, etc.).
|
||||
-- While it comes with a default configuration, the function is designed to be flexible,
|
||||
-- allowing for customization to meet specific needs.
|
||||
DROP FUNCTION IF EXISTS nanoid(int, text, float);
|
||||
CREATE OR REPLACE FUNCTION nanoid(
|
||||
size int DEFAULT 21, -- The number of symbols in the NanoId String. Must be greater than 0.
|
||||
alphabet text DEFAULT '_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', -- The symbols used in the NanoId String. Must contain between 1 and 255 symbols.
|
||||
additionalBytesFactor float DEFAULT 1.6 -- The additional bytes factor used for calculating the step size. Must be equal or greater then 1.
|
||||
)
|
||||
RETURNS text -- A randomly generated NanoId String
|
||||
LANGUAGE plpgsql
|
||||
VOLATILE
|
||||
PARALLEL SAFE
|
||||
-- Uncomment the following line if you have superuser privileges
|
||||
-- LEAKPROOF
|
||||
AS
|
||||
$$
|
||||
DECLARE
|
||||
alphabetArray text[];
|
||||
alphabetLength int := 64;
|
||||
mask int := 63;
|
||||
step int := 34;
|
||||
BEGIN
|
||||
IF size IS NULL OR size < 1 THEN
|
||||
RAISE EXCEPTION 'The size must be defined and greater than 0!';
|
||||
END IF;
|
||||
|
||||
IF alphabet IS NULL OR length(alphabet) = 0 OR length(alphabet) > 255 THEN
|
||||
RAISE EXCEPTION 'The alphabet can''t be undefined, zero or bigger than 255 symbols!';
|
||||
END IF;
|
||||
|
||||
IF additionalBytesFactor IS NULL OR additionalBytesFactor < 1 THEN
|
||||
RAISE EXCEPTION 'The additional bytes factor can''t be less than 1!';
|
||||
END IF;
|
||||
|
||||
alphabetArray := regexp_split_to_array(alphabet, '');
|
||||
alphabetLength := array_length(alphabetArray, 1);
|
||||
mask := (2 << cast(floor(log(alphabetLength - 1) / log(2)) as int)) - 1;
|
||||
step := cast(ceil(additionalBytesFactor * mask * size / alphabetLength) AS int);
|
||||
|
||||
IF step > 1024 THEN
|
||||
step := 1024; -- The step size % can''t be bigger then 1024!
|
||||
END IF;
|
||||
|
||||
RETURN nanoid_optimized(size, alphabet, mask, step);
|
||||
END
|
||||
$$;
|
||||
|
||||
-- Generates an optimized random string of a specified size using the given alphabet, mask, and step.
|
||||
-- This optimized version is designed for higher performance and lower memory overhead.
|
||||
-- No checks are performed! Use it only if you really know what you are doing.
|
||||
DROP FUNCTION IF EXISTS nanoid_optimized(int, text, int, int);
|
||||
CREATE OR REPLACE FUNCTION nanoid_optimized(
|
||||
size int, -- The desired length of the generated string.
|
||||
alphabet text, -- The set of characters to choose from for generating the string.
|
||||
mask int, -- The mask used for mapping random bytes to alphabet indices. Should be `(2^n) - 1` where `n` is a power of 2 less than or equal to the alphabet size.
|
||||
step int -- The number of random bytes to generate in each iteration. A larger value may speed up the function but increase memory usage.
|
||||
)
|
||||
RETURNS text -- A randomly generated NanoId String
|
||||
LANGUAGE plpgsql
|
||||
VOLATILE
|
||||
PARALLEL SAFE
|
||||
-- Uncomment the following line if you have superuser privileges
|
||||
-- LEAKPROOF
|
||||
AS
|
||||
$$
|
||||
DECLARE
|
||||
idBuilder text := '';
|
||||
counter int := 0;
|
||||
bytes bytea;
|
||||
alphabetIndex int;
|
||||
alphabetArray text[];
|
||||
alphabetLength int := 64;
|
||||
BEGIN
|
||||
alphabetArray := regexp_split_to_array(alphabet, '');
|
||||
alphabetLength := array_length(alphabetArray, 1);
|
||||
|
||||
LOOP
|
||||
bytes := gen_random_bytes(step);
|
||||
FOR counter IN 0..step - 1
|
||||
LOOP
|
||||
alphabetIndex := (get_byte(bytes, counter) & mask) + 1;
|
||||
IF alphabetIndex <= alphabetLength THEN
|
||||
idBuilder := idBuilder || alphabetArray[alphabetIndex];
|
||||
IF length(idBuilder) = size THEN
|
||||
RETURN idBuilder;
|
||||
END IF;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- CUSTOM FUNCTION FOR GENERIC PREFIXED IDS
|
||||
CREATE OR REPLACE FUNCTION generate_prefix_id(prefix TEXT)
|
||||
RETURNS TEXT AS $$
|
||||
BEGIN
|
||||
RETURN prefix || '_' || nanoid(16, 'abcdefhiklmnorstuvwxyz');
|
||||
END;
|
||||
$$ LANGUAGE plpgsql VOLATILE;
|
||||
|
||||
-- CUSTOM FUNCTION FOR GENERIC IDS
|
||||
CREATE OR REPLACE FUNCTION generate_id()
|
||||
RETURNS TEXT AS $$
|
||||
BEGIN
|
||||
RETURN nanoid(16, 'abcdefhiklmnorstuvwxyz');
|
||||
END;
|
||||
$$ LANGUAGE plpgsql VOLATILE;
|
||||
@ -0,0 +1,894 @@
|
||||
/*
|
||||
* Organisation migration
|
||||
*
|
||||
* High level summary:
|
||||
* 1. Create a personal organisation for all users and move their personal entities (documents/templates/etc) into it
|
||||
* 2. Create an organisation for each user subscription, group teams with no subscriptions into these organisations
|
||||
* 3. Create an organisation for all teams with subscriptions
|
||||
*
|
||||
* Search "CUSTOM_CHANGE" to find areas where custom changes to the migration have occurred.
|
||||
*
|
||||
* POST MIGRATION REQUIREMENTS:
|
||||
* - Move individual subscriptions into personal organisations and delete the original organisation
|
||||
* - Set claims for all organisations
|
||||
* - Todo: orgs check for anything else.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Clean up subscriptions prior to full migration:
|
||||
* - Ensure each user has a maximum of 1 subscription tied to the "User" table
|
||||
* - Move the customerId from the teams/users into the subscription itself
|
||||
*/
|
||||
-- [CUSTOM_CHANGE_START]
|
||||
WITH subscriptions_to_delete AS (
|
||||
SELECT
|
||||
id,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY "userId"
|
||||
ORDER BY
|
||||
(status = 'ACTIVE') DESC, -- Prioritize active subscriptions
|
||||
"updatedAt" DESC -- Then by most recently updated
|
||||
) AS rn
|
||||
FROM "Subscription" s
|
||||
WHERE s."userId" IS NOT NULL
|
||||
),
|
||||
to_delete AS (
|
||||
SELECT id
|
||||
FROM subscriptions_to_delete
|
||||
WHERE rn > 1
|
||||
)
|
||||
DELETE FROM "Subscription"
|
||||
WHERE id IN (SELECT id FROM to_delete);
|
||||
|
||||
-- Add customerId to Subscription
|
||||
ALTER TABLE "Subscription" ADD COLUMN "customerId" TEXT;
|
||||
|
||||
-- Move customerId from User to Subscription
|
||||
UPDATE "Subscription" s
|
||||
SET "customerId" = u."customerId"
|
||||
FROM "User" u
|
||||
WHERE s."userId" = u."id";
|
||||
|
||||
-- Move customerId from Team to Subscription
|
||||
UPDATE "Subscription" s
|
||||
SET "customerId" = t."customerId"
|
||||
FROM "Team" t
|
||||
WHERE s."teamId" = t."id";
|
||||
|
||||
-- Remove any subscriptions with missing customerId
|
||||
DELETE FROM "Subscription"
|
||||
WHERE "customerId" IS NULL;
|
||||
|
||||
-- Make customerId not null
|
||||
ALTER TABLE "Subscription" ALTER COLUMN "customerId" SET NOT NULL;
|
||||
|
||||
-- [CUSTOM_CHANGE_END]
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Subscription" DROP CONSTRAINT "Subscription_teamId_fkey";
|
||||
ALTER TABLE "Subscription" DROP CONSTRAINT "Subscription_userId_fkey";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "Subscription_teamId_key";
|
||||
DROP INDEX "Subscription_userId_idx";
|
||||
|
||||
-- DropConstraints
|
||||
ALTER TABLE "Subscription" DROP CONSTRAINT "teamid_or_userid_check";
|
||||
|
||||
/*
|
||||
* Before starting the real migration, we want to do the following:
|
||||
* - Give every user a team with their personal entities (documents, templates, webhooks, profile, apiTokens)
|
||||
* - The team is temporary and tagged with a "isPersonal" boolean
|
||||
*/
|
||||
-- [CUSTOM_CHANGE_START]
|
||||
|
||||
-- 1. Ensure all users have a URL by setting a default CUID
|
||||
UPDATE "User"
|
||||
SET "url" = generate_id()
|
||||
WHERE "url" IS NULL;
|
||||
|
||||
-- 2. Make User URL required
|
||||
ALTER TABLE "User" ALTER COLUMN "url" SET NOT NULL;
|
||||
|
||||
-- 3. Add temp isPersonal boolean to Team table with default false
|
||||
ALTER TABLE "Team" ADD COLUMN "isPersonal" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- 4. Create a personal team for every user
|
||||
INSERT INTO "Team" ("name", "url", "createdAt", "ownerUserId", "avatarImageId", "isPersonal")
|
||||
SELECT
|
||||
'Personal Team',
|
||||
"url", -- Use the user's URL directly
|
||||
NOW(),
|
||||
"id",
|
||||
"avatarImageId",
|
||||
true -- Set isPersonal to true for these personal teams
|
||||
FROM "User" u;
|
||||
|
||||
-- 5. Add each user as an ADMIN member of their own team
|
||||
INSERT INTO "TeamMember" ("teamId", "userId", "role", "createdAt")
|
||||
SELECT t."id", u."id", 'ADMIN', NOW()
|
||||
FROM "User" u
|
||||
JOIN "Team" t ON t."ownerUserId" = u."id"
|
||||
WHERE t."isPersonal" = true;
|
||||
|
||||
-- 6. Migrate user's documents to their personal team
|
||||
UPDATE "Document"
|
||||
SET
|
||||
"teamId" = t."id"
|
||||
FROM "Team" t, "TeamMember" tm
|
||||
WHERE tm."teamId" = t."id"
|
||||
AND tm."userId" = "Document"."userId"
|
||||
AND "Document"."userId" = t."ownerUserId"
|
||||
AND "Document"."teamId" IS NULL
|
||||
AND t."isPersonal" = true;
|
||||
|
||||
-- 7. Migrate user's templates to their team
|
||||
UPDATE "Template"
|
||||
SET
|
||||
"teamId" = t."id"
|
||||
FROM "Team" t, "TeamMember" tm
|
||||
WHERE tm."teamId" = t."id"
|
||||
AND tm."userId" = "Template"."userId"
|
||||
AND "Template"."userId" = t."ownerUserId"
|
||||
AND "Template"."teamId" IS NULL
|
||||
AND t."isPersonal" = true;
|
||||
|
||||
-- 8. Migrate user's folders to their team
|
||||
UPDATE "Folder" f
|
||||
SET "teamId" = t."id"
|
||||
FROM "Team" t
|
||||
WHERE f."userId" = t."ownerUserId" AND f."teamId" IS NULL AND t."isPersonal" = true;
|
||||
|
||||
-- 9. Migrate user's webhooks to their team
|
||||
UPDATE "Webhook" w
|
||||
SET "teamId" = t."id"
|
||||
FROM "Team" t
|
||||
WHERE w."userId" = t."ownerUserId" AND w."teamId" IS NULL AND t."isPersonal" = true;
|
||||
|
||||
-- 10. Migrate user's API tokens to their team
|
||||
UPDATE "ApiToken" apiToken
|
||||
SET "teamId" = t."id"
|
||||
FROM "Team" t
|
||||
WHERE apiToken."userId" = t."ownerUserId" AND apiToken."teamId" IS NULL AND t."isPersonal" = true;
|
||||
|
||||
-- 11. Migrate user's team profiles to their team
|
||||
INSERT INTO "TeamProfile" ("id", "enabled", "bio", "teamId")
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
up."enabled",
|
||||
up."bio",
|
||||
t."id" AS teamId
|
||||
FROM "UserProfile" up
|
||||
JOIN "User" u ON u."id" = up."userId"
|
||||
JOIN "Team" t ON t."ownerUserId" = u."id" AND t."isPersonal" = TRUE;
|
||||
|
||||
-- [CUSTOM_CHANGE_END]
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "OrganisationGroupType" AS ENUM ('INTERNAL_ORGANISATION', 'INTERNAL_TEAM', 'CUSTOM');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "OrganisationMemberRole" AS ENUM ('ADMIN', 'MANAGER', 'MEMBER');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "OrganisationMemberInviteStatus" AS ENUM ('ACCEPTED', 'PENDING', 'DECLINED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "OrganisationType" AS ENUM ('PERSONAL', 'ORGANISATION');
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Document" DROP CONSTRAINT "Document_teamId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Team" DROP CONSTRAINT "Team_ownerUserId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "TeamGlobalSettings" DROP CONSTRAINT "TeamGlobalSettings_teamId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "TeamMemberInvite" DROP CONSTRAINT "TeamMemberInvite_teamId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "TeamPending" DROP CONSTRAINT "TeamPending_ownerUserId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "TeamTransferVerification" DROP CONSTRAINT "TeamTransferVerification_teamId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "UserProfile" DROP CONSTRAINT "UserProfile_userId_fkey";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "Team_customerId_key";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "TeamGlobalSettings_teamId_key";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "User_customerId_key";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "User_url_key";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "UserProfile";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Folder" ALTER COLUMN "teamId" SET NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Webhook" ALTER COLUMN "teamId" SET NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ApiToken" ALTER COLUMN "teamId" SET NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Document" ALTER COLUMN "teamId" SET NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Subscription" ADD COLUMN "organisationId" TEXT; -- [CUSTOM_CHANGE] This is supposed to be NOT NULL (we reapply it at the end)
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Team" DROP COLUMN "customerId",
|
||||
ADD COLUMN "organisationId" TEXT, -- [CUSTOM_CHANGE] This is supposed to be NOT NULL (we reapply it at the end)
|
||||
ADD COLUMN "teamGlobalSettingsId" TEXT; -- [CUSTOM_CHANGE] This is supposed to be NOT NULL (we reapply it at the end)
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "TeamGlobalSettings" DROP COLUMN "allowEmbeddedAuthoring",
|
||||
DROP COLUMN "brandingHidePoweredBy",
|
||||
ADD COLUMN "id" TEXT, -- [CUSTOM_CHANGE] Supposed to be NOT NULL but we apply it after generating default IDs
|
||||
ALTER COLUMN "documentVisibility" DROP NOT NULL,
|
||||
ALTER COLUMN "documentVisibility" DROP DEFAULT,
|
||||
ALTER COLUMN "includeSenderDetails" DROP NOT NULL,
|
||||
ALTER COLUMN "includeSenderDetails" DROP DEFAULT,
|
||||
ALTER COLUMN "brandingCompanyDetails" DROP NOT NULL,
|
||||
ALTER COLUMN "brandingCompanyDetails" DROP DEFAULT,
|
||||
ALTER COLUMN "brandingEnabled" DROP NOT NULL,
|
||||
ALTER COLUMN "brandingEnabled" DROP DEFAULT,
|
||||
ALTER COLUMN "brandingLogo" DROP NOT NULL,
|
||||
ALTER COLUMN "brandingLogo" DROP DEFAULT,
|
||||
ALTER COLUMN "brandingUrl" DROP NOT NULL,
|
||||
ALTER COLUMN "brandingUrl" DROP DEFAULT,
|
||||
ALTER COLUMN "documentLanguage" DROP NOT NULL,
|
||||
ALTER COLUMN "documentLanguage" DROP DEFAULT,
|
||||
ALTER COLUMN "typedSignatureEnabled" DROP NOT NULL,
|
||||
ALTER COLUMN "typedSignatureEnabled" DROP DEFAULT,
|
||||
ALTER COLUMN "includeSigningCertificate" DROP NOT NULL,
|
||||
ALTER COLUMN "includeSigningCertificate" DROP DEFAULT,
|
||||
ALTER COLUMN "drawSignatureEnabled" DROP NOT NULL,
|
||||
ALTER COLUMN "drawSignatureEnabled" DROP DEFAULT,
|
||||
ALTER COLUMN "uploadSignatureEnabled" DROP NOT NULL,
|
||||
ALTER COLUMN "uploadSignatureEnabled" DROP DEFAULT;
|
||||
|
||||
-- [CUSTOM_CHANGE] Generate IDs for existing TeamGlobalSettings records. We link it later.
|
||||
UPDATE "TeamGlobalSettings" SET "id" = generate_prefix_id('team_setting') WHERE "id" IS NULL;
|
||||
|
||||
-- [CUSTOM_CHANGE] Make the id column NOT NULL and add primary key
|
||||
ALTER TABLE "TeamGlobalSettings"
|
||||
ALTER COLUMN "id" SET NOT NULL,
|
||||
ADD CONSTRAINT "TeamGlobalSettings_pkey" PRIMARY KEY ("id");
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Template" ALTER COLUMN "teamId" SET NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" DROP COLUMN "customerId";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "TeamMemberInvite";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "TeamPending";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "TeamTransferVerification";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "TeamMemberInviteStatus";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SubscriptionClaim" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"locked" BOOLEAN NOT NULL DEFAULT false,
|
||||
"teamCount" INTEGER NOT NULL,
|
||||
"memberCount" INTEGER NOT NULL,
|
||||
"flags" JSONB NOT NULL,
|
||||
|
||||
CONSTRAINT "SubscriptionClaim_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- Todo: orgs validate prior to release
|
||||
-- [CUSTOM_CHANGE] Insert default subscription claims
|
||||
INSERT INTO "SubscriptionClaim" ("id", "name", "locked", "teamCount", "memberCount", "flags", "createdAt", "updatedAt")
|
||||
VALUES
|
||||
('free', 'Free', true, 1, 1, '{}'::jsonb, NOW(), NOW()),
|
||||
('individual', 'Individual', true, 1, 1, '{"unlimitedDocuments": true}'::jsonb, NOW(), NOW()),
|
||||
('team', 'Teams', true, 1, 5, '{"unlimitedDocuments": true, "allowCustomBranding": true, "embedSigning": true}'::jsonb, NOW(), NOW()),
|
||||
('platform', 'Platform', true, 1, 0, '{"unlimitedDocuments": true, "allowCustomBranding": true, "hidePoweredBy": true, "embedAuthoring": false, "embedAuthoringWhiteLabel": true, "embedSigning": false, "embedSigningWhiteLabel": true}'::jsonb, NOW(), NOW()),
|
||||
('enterprise', 'Enterprise', true, 0, 0, '{"unlimitedDocuments": true, "allowCustomBranding": true, "hidePoweredBy": true, "embedAuthoring": true, "embedAuthoringWhiteLabel": true, "embedSigning": true, "embedSigningWhiteLabel": true, "cfr21": true}'::jsonb, NOW(), NOW()),
|
||||
('earlyAdopter', 'Early Adopter', true, 0, 0, '{"unlimitedDocuments": true, "allowCustomBranding": true, "hidePoweredBy": true, "embedSigning": true, "embedSigningWhiteLabel": true}'::jsonb, NOW(), NOW());
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OrganisationClaim" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"originalSubscriptionClaimId" TEXT,
|
||||
"teamCount" INTEGER NOT NULL,
|
||||
"memberCount" INTEGER NOT NULL,
|
||||
"flags" JSONB NOT NULL,
|
||||
|
||||
CONSTRAINT "OrganisationClaim_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Organisation" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"type" "OrganisationType" NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"url" TEXT NOT NULL,
|
||||
"avatarImageId" TEXT,
|
||||
"customerId" TEXT,
|
||||
"ownerUserId" INTEGER NOT NULL,
|
||||
"organisationClaimId" TEXT, -- [CUSTOM_CHANGE] Is supposed to be NOT NULL (we reapply it at the end)
|
||||
"organisationGlobalSettingsId" TEXT, -- [CUSTOM_CHANGE] Is supposed to be NOT NULL (we reapply it at the end)
|
||||
"teamId" INTEGER, -- [CUSTOM_CHANGE] This is a temporary column for migration purposes.
|
||||
|
||||
CONSTRAINT "Organisation_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OrganisationMember" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"organisationId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "OrganisationMember_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OrganisationMemberInvite" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"email" TEXT NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
"status" "OrganisationMemberInviteStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"organisationId" TEXT NOT NULL,
|
||||
"organisationRole" "OrganisationMemberRole" NOT NULL,
|
||||
|
||||
CONSTRAINT "OrganisationMemberInvite_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OrganisationGroup" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"type" "OrganisationGroupType" NOT NULL,
|
||||
"organisationRole" "OrganisationMemberRole" NOT NULL,
|
||||
"organisationId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "OrganisationGroup_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OrganisationGroupMember" (
|
||||
"id" TEXT NOT NULL,
|
||||
"groupId" TEXT NOT NULL,
|
||||
"organisationMemberId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "OrganisationGroupMember_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TeamGroup" (
|
||||
"id" TEXT NOT NULL,
|
||||
"organisationGroupId" TEXT NOT NULL,
|
||||
"teamRole" "TeamMemberRole" NOT NULL,
|
||||
"teamId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "TeamGroup_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OrganisationGlobalSettings" (
|
||||
"id" TEXT NOT NULL,
|
||||
"documentVisibility" "DocumentVisibility" NOT NULL DEFAULT 'EVERYONE',
|
||||
"documentLanguage" TEXT NOT NULL DEFAULT 'en',
|
||||
"includeSenderDetails" BOOLEAN NOT NULL DEFAULT true,
|
||||
"includeSigningCertificate" BOOLEAN NOT NULL DEFAULT true,
|
||||
"typedSignatureEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"uploadSignatureEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"drawSignatureEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"brandingEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"brandingLogo" TEXT NOT NULL DEFAULT '',
|
||||
"brandingUrl" TEXT NOT NULL DEFAULT '',
|
||||
"brandingCompanyDetails" TEXT NOT NULL DEFAULT '',
|
||||
"organisationId" TEXT, -- [CUSTOM_CHANGE] This is a temporary column for migration purposes.
|
||||
|
||||
CONSTRAINT "OrganisationGlobalSettings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Organisation_url_key" ON "Organisation"("url");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Organisation_customerId_key" ON "Organisation"("customerId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Organisation_organisationClaimId_key" ON "Organisation"("organisationClaimId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Organisation_organisationGlobalSettingsId_key" ON "Organisation"("organisationGlobalSettingsId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "OrganisationMember_userId_organisationId_key" ON "OrganisationMember"("userId", "organisationId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "OrganisationMemberInvite_token_key" ON "OrganisationMemberInvite"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "OrganisationGroupMember_organisationMemberId_groupId_key" ON "OrganisationGroupMember"("organisationMemberId", "groupId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "TeamGroup_teamId_organisationGroupId_key" ON "TeamGroup"("teamId", "organisationGroupId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Subscription_organisationId_key" ON "Subscription"("organisationId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Subscription_organisationId_idx" ON "Subscription"("organisationId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Team_teamGlobalSettingsId_key" ON "Team"("teamGlobalSettingsId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Template_userId_idx" ON "Template"("userId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "Organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Document" ADD CONSTRAINT "Document_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Organisation" ADD CONSTRAINT "Organisation_organisationClaimId_fkey" FOREIGN KEY ("organisationClaimId") REFERENCES "OrganisationClaim"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Organisation" ADD CONSTRAINT "Organisation_avatarImageId_fkey" FOREIGN KEY ("avatarImageId") REFERENCES "AvatarImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Organisation" ADD CONSTRAINT "Organisation_ownerUserId_fkey" FOREIGN KEY ("ownerUserId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Organisation" ADD CONSTRAINT "Organisation_organisationGlobalSettingsId_fkey" FOREIGN KEY ("organisationGlobalSettingsId") REFERENCES "OrganisationGlobalSettings"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OrganisationMember" ADD CONSTRAINT "OrganisationMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OrganisationMember" ADD CONSTRAINT "OrganisationMember_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "Organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OrganisationMemberInvite" ADD CONSTRAINT "OrganisationMemberInvite_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "Organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OrganisationGroup" ADD CONSTRAINT "OrganisationGroup_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "Organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OrganisationGroupMember" ADD CONSTRAINT "OrganisationGroupMember_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "OrganisationGroup"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OrganisationGroupMember" ADD CONSTRAINT "OrganisationGroupMember_organisationMemberId_fkey" FOREIGN KEY ("organisationMemberId") REFERENCES "OrganisationMember"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TeamGroup" ADD CONSTRAINT "TeamGroup_organisationGroupId_fkey" FOREIGN KEY ("organisationGroupId") REFERENCES "OrganisationGroup"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TeamGroup" ADD CONSTRAINT "TeamGroup_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Team" ADD CONSTRAINT "Team_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "Organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Team" ADD CONSTRAINT "Team_teamGlobalSettingsId_fkey" FOREIGN KEY ("teamGlobalSettingsId") REFERENCES "TeamGlobalSettings"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- [CUSTOM_CHANGE] FROM HERE ON IT'S ALL CUSTOM
|
||||
|
||||
/*
|
||||
* The current state of the migration is that:
|
||||
* - All users have a team with their personal entities excluding subscriptions
|
||||
* - All entities should be tied into teams
|
||||
*
|
||||
* Our goal is now to migrate organisations
|
||||
*
|
||||
* If subscription is attached to user account, that means it is either:
|
||||
* - Individual
|
||||
* - Platform
|
||||
* - Enterprise
|
||||
* - Early Adopter
|
||||
*
|
||||
* If subscription is attached to a team, that means it is:
|
||||
* - Team
|
||||
*
|
||||
* Note: We will handle moving Individual plans into personal organisations as a part of a
|
||||
* secondary migration script since we need the Stripe price IDs
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* Handle creating free personal organisations
|
||||
*
|
||||
* Criteria for "personal team":
|
||||
* - Team is "isPersonal" is true
|
||||
*/
|
||||
WITH personal_organisations AS (
|
||||
INSERT INTO "Organisation" (
|
||||
"id", "createdAt", "updatedAt", "type", "name", "url", "avatarImageId", "ownerUserId", "teamId", "customerId"
|
||||
)
|
||||
SELECT
|
||||
generate_prefix_id('org'),
|
||||
t."createdAt",
|
||||
NOW(),
|
||||
'PERSONAL'::"OrganisationType",
|
||||
'Personal Organisation',
|
||||
u."url",
|
||||
t."avatarImageId",
|
||||
t."ownerUserId",
|
||||
t."id",
|
||||
s."customerId"
|
||||
FROM "Team" t
|
||||
JOIN "User" u ON u."id" = t."ownerUserId"
|
||||
LEFT JOIN "Subscription" s ON s."teamId" = t."id"
|
||||
WHERE t."isPersonal"
|
||||
RETURNING "id", "ownerUserId", "teamId", "customerId"
|
||||
)
|
||||
UPDATE "Team" t
|
||||
SET "organisationId" = o."id"
|
||||
FROM personal_organisations o
|
||||
WHERE o."teamId" = t."id";
|
||||
|
||||
/*
|
||||
* Handle creating organisations for teams with "teams plan" subscriptions
|
||||
*
|
||||
* Criteria for "teams plan" team:
|
||||
* - Team has a subscription
|
||||
*/
|
||||
WITH team_plan_organisations AS (
|
||||
INSERT INTO "Organisation" (
|
||||
"id", "createdAt", "updatedAt", "type", "name", "url", "avatarImageId", "ownerUserId", "teamId", "customerId"
|
||||
)
|
||||
SELECT
|
||||
generate_prefix_id('org'),
|
||||
t."createdAt",
|
||||
NOW(),
|
||||
'ORGANISATION'::"OrganisationType",
|
||||
t."name",
|
||||
generate_id(),
|
||||
t."avatarImageId",
|
||||
t."ownerUserId",
|
||||
t."id",
|
||||
s."customerId"
|
||||
FROM "Team" t
|
||||
LEFT JOIN "Subscription" s ON s."teamId" = t."id"
|
||||
WHERE s."teamId" IS NOT NULL
|
||||
RETURNING "id", "ownerUserId", "teamId", "customerId"
|
||||
)
|
||||
UPDATE "Team" t
|
||||
SET "organisationId" = o."id"
|
||||
FROM team_plan_organisations o
|
||||
WHERE o."teamId" = t."id";
|
||||
|
||||
/*
|
||||
* Handle account level subscriptions
|
||||
*
|
||||
* Goal:
|
||||
* - Create a single organisation for each user subscription
|
||||
* - Move all non personal teams without subscriptions into the newly created organisation
|
||||
* - Move user subscription to the organisation
|
||||
*
|
||||
* Plan:
|
||||
* 1. Create organisation for every user who still has a subscription attached to the user
|
||||
* 2. Find all teams that are not yet linked to an organisation
|
||||
* 3. Link the team into the organisation of the owner which is NOT personal
|
||||
*
|
||||
*/
|
||||
WITH users_to_migrate AS (
|
||||
SELECT u."id" AS user_id, s."customerId" AS customer_id
|
||||
FROM "User" u
|
||||
JOIN "Subscription" s ON s."userId" = u."id"
|
||||
),
|
||||
new_orgs AS (
|
||||
INSERT INTO "Organisation" (
|
||||
"id", "createdAt", "updatedAt", "type", "name", "url", "ownerUserId", "customerId"
|
||||
)
|
||||
SELECT
|
||||
generate_prefix_id('org'),
|
||||
NOW(),
|
||||
NOW(),
|
||||
'ORGANISATION'::"OrganisationType",
|
||||
'Organisation Name',
|
||||
generate_id(),
|
||||
u.user_id,
|
||||
u.customer_id
|
||||
FROM users_to_migrate u
|
||||
RETURNING "id", "ownerUserId", "customerId"
|
||||
),
|
||||
update_teams AS (
|
||||
UPDATE "Team" t
|
||||
SET "organisationId" = o."id"
|
||||
FROM new_orgs o
|
||||
WHERE
|
||||
t."ownerUserId" = o."ownerUserId"
|
||||
AND t."organisationId" IS NULL
|
||||
AND t."isPersonal" = FALSE
|
||||
)
|
||||
UPDATE "Subscription" s
|
||||
SET "organisationId" = o."id"
|
||||
FROM new_orgs o
|
||||
WHERE s."userId" = o."ownerUserId";
|
||||
|
||||
-- Create 3 internal groups for each organisation (ADMIN, MANAGER, MEMBER)
|
||||
WITH org_groups AS (
|
||||
SELECT
|
||||
o.id as org_id,
|
||||
unnest(ARRAY[
|
||||
'ADMIN'::"OrganisationMemberRole",
|
||||
'MANAGER'::"OrganisationMemberRole",
|
||||
'MEMBER'::"OrganisationMemberRole"
|
||||
]) as role
|
||||
FROM "Organisation" o
|
||||
)
|
||||
INSERT INTO "OrganisationGroup" ("id", "type", "organisationRole", "organisationId")
|
||||
SELECT
|
||||
generate_prefix_id('org_group'),
|
||||
'INTERNAL_ORGANISATION'::"OrganisationGroupType",
|
||||
og.role,
|
||||
og.org_id
|
||||
FROM org_groups og;
|
||||
|
||||
-- Create TeamGlobalSettings for all teams that do not have a teamGlobalSettingsId
|
||||
INSERT INTO "TeamGlobalSettings" ("id", "teamId")
|
||||
SELECT
|
||||
generate_prefix_id('team_setting'),
|
||||
t."id"
|
||||
FROM "Team" t
|
||||
WHERE t."teamGlobalSettingsId" IS NULL;
|
||||
|
||||
-- Update teams with their corresponding teamGlobalSettingsId
|
||||
UPDATE "Team" t
|
||||
SET "teamGlobalSettingsId" = tgs."id"
|
||||
FROM "TeamGlobalSettings" tgs
|
||||
WHERE tgs."teamId" = t."id" AND t."teamGlobalSettingsId" IS NULL;
|
||||
|
||||
-- Create default OrganisationGlobalSettings for all organisations
|
||||
INSERT INTO "OrganisationGlobalSettings" ("id", "organisationId")
|
||||
SELECT
|
||||
generate_prefix_id('org_setting'),
|
||||
o."id"
|
||||
FROM "Organisation" o
|
||||
WHERE o."organisationGlobalSettingsId" IS NULL;
|
||||
|
||||
-- Update organisations with their corresponding organisationGlobalSettingsId
|
||||
UPDATE "Organisation" o
|
||||
SET "organisationGlobalSettingsId" = ogs."id"
|
||||
FROM "OrganisationGlobalSettings" ogs
|
||||
WHERE ogs."organisationId" = o."id" AND o."organisationGlobalSettingsId" IS NULL;
|
||||
|
||||
-- Create TeamGlobalSettings for all teams missing it
|
||||
WITH teams_to_update AS (
|
||||
SELECT "id" AS team_id, generate_prefix_id('team_setting') AS settings_id
|
||||
FROM "Team"
|
||||
WHERE "teamGlobalSettingsId" IS NULL
|
||||
),
|
||||
new_team_settings AS (
|
||||
INSERT INTO "TeamGlobalSettings" ("id")
|
||||
SELECT settings_id FROM teams_to_update
|
||||
RETURNING "id"
|
||||
)
|
||||
UPDATE "Team" t
|
||||
SET "teamGlobalSettingsId" = ttu.settings_id
|
||||
FROM teams_to_update ttu
|
||||
WHERE t."id" = ttu.team_id;
|
||||
|
||||
-- Create OrganisationClaim for every organisation, use the default "FREE" claim
|
||||
WITH orgs_to_update AS (
|
||||
SELECT "id" AS org_id, generate_prefix_id('org_claim') AS claim_id
|
||||
FROM "Organisation"
|
||||
WHERE "organisationClaimId" IS NULL
|
||||
),
|
||||
new_claims AS (
|
||||
INSERT INTO "OrganisationClaim" (
|
||||
"id",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"originalSubscriptionClaimId",
|
||||
"teamCount",
|
||||
"memberCount",
|
||||
"flags"
|
||||
)
|
||||
SELECT
|
||||
claim_id,
|
||||
now(),
|
||||
now(),
|
||||
'free',
|
||||
1,
|
||||
1,
|
||||
'{}'::jsonb
|
||||
FROM orgs_to_update
|
||||
RETURNING "id" AS claim_id
|
||||
)
|
||||
UPDATE "Organisation" o
|
||||
SET "organisationClaimId" = otu.claim_id
|
||||
FROM orgs_to_update otu
|
||||
WHERE o."id" = otu.org_id;
|
||||
|
||||
-- Create 2 TeamGroups to assign the internal Organisation Admin/Manager groups to teams
|
||||
WITH org_internal_groups AS (
|
||||
SELECT
|
||||
og.id as group_id,
|
||||
og."organisationId",
|
||||
og."organisationRole",
|
||||
t.id as team_id
|
||||
FROM "OrganisationGroup" og
|
||||
JOIN "Team" t ON t."organisationId" = og."organisationId"
|
||||
WHERE og.type = 'INTERNAL_ORGANISATION'
|
||||
AND og."organisationRole" IN ('ADMIN', 'MANAGER')
|
||||
)
|
||||
INSERT INTO "TeamGroup" ("id", "teamId", "organisationGroupId", "teamRole")
|
||||
SELECT
|
||||
generate_prefix_id('team_group'),
|
||||
oig.team_id,
|
||||
oig.group_id,
|
||||
'ADMIN'::"TeamMemberRole" -- Org Admins/Managers will be Team ADMINS
|
||||
FROM org_internal_groups oig;
|
||||
|
||||
-- Temp columns for the following procedure
|
||||
ALTER TABLE "OrganisationGroup" ADD COLUMN temp_team_id INT;
|
||||
ALTER TABLE "OrganisationGroup" ADD COLUMN temp_team_role TEXT;
|
||||
|
||||
WITH team_internal_groups AS (
|
||||
-- Step 1: Create all team+role combinations
|
||||
SELECT
|
||||
t.id as team_id,
|
||||
t."organisationId",
|
||||
unnest(ARRAY[
|
||||
'ADMIN'::"TeamMemberRole",
|
||||
'MANAGER'::"TeamMemberRole",
|
||||
'MEMBER'::"TeamMemberRole"
|
||||
]) as team_role
|
||||
FROM "Team" t
|
||||
),
|
||||
created_org_groups AS (
|
||||
-- Step 2: Create OrganisationGroups with temp data
|
||||
INSERT INTO "OrganisationGroup" (
|
||||
"id",
|
||||
"type",
|
||||
"organisationRole",
|
||||
"organisationId",
|
||||
temp_team_id,
|
||||
temp_team_role
|
||||
)
|
||||
SELECT
|
||||
generate_prefix_id('org_group'),
|
||||
'INTERNAL_TEAM'::"OrganisationGroupType",
|
||||
'MEMBER'::"OrganisationMemberRole",
|
||||
tig."organisationId",
|
||||
tig.team_id,
|
||||
tig.team_role::TEXT
|
||||
FROM team_internal_groups tig
|
||||
RETURNING "id", temp_team_id, temp_team_role
|
||||
)
|
||||
-- Step 3: Create TeamGroups using the temp data
|
||||
INSERT INTO "TeamGroup" ("id", "organisationGroupId", "teamRole", "teamId")
|
||||
SELECT
|
||||
generate_prefix_id('team_group'),
|
||||
cog."id",
|
||||
cog.temp_team_role::"TeamMemberRole",
|
||||
cog.temp_team_id
|
||||
FROM created_org_groups cog;
|
||||
|
||||
-- Clean up temp columns
|
||||
ALTER TABLE "OrganisationGroup" DROP COLUMN temp_team_id;
|
||||
ALTER TABLE "OrganisationGroup" DROP COLUMN temp_team_role;
|
||||
|
||||
-- Create OrganisationMembers for each unique user-organisation combination
|
||||
-- This ensures only one OrganisationMember per user per organisation, even if they belong to multiple teams
|
||||
INSERT INTO "OrganisationMember" ("id", "createdAt", "updatedAt", "userId", "organisationId")
|
||||
SELECT DISTINCT
|
||||
generate_prefix_id('member'),
|
||||
MIN(tm."createdAt"),
|
||||
NOW(),
|
||||
tm."userId",
|
||||
t."organisationId"
|
||||
FROM "TeamMember" tm
|
||||
JOIN "Team" t ON t."id" = tm."teamId"
|
||||
GROUP BY tm."userId", t."organisationId";
|
||||
|
||||
-- Create OrganisationMembers for Organisations with 0 members
|
||||
-- This can only occur for platform/enterprise/earlyAdopter/individual plans where they have 0 teams
|
||||
-- So we create an OrganisationMember for the owner user
|
||||
INSERT INTO "OrganisationMember" ("id", "createdAt", "updatedAt", "userId", "organisationId")
|
||||
SELECT
|
||||
generate_prefix_id('member'),
|
||||
NOW(),
|
||||
NOW(),
|
||||
o."ownerUserId",
|
||||
o."id"
|
||||
FROM "Organisation" o
|
||||
WHERE o."id" NOT IN (SELECT "organisationId" FROM "OrganisationMember");
|
||||
|
||||
-- Add users to the appropriate INTERNAL_TEAM groups based on their team membership and role
|
||||
-- This creates OrganisationGroupMember records to link users to their team-specific groups
|
||||
-- Skip organisation owners as they are handled separately
|
||||
INSERT INTO "OrganisationGroupMember" ("id", "groupId", "organisationMemberId")
|
||||
SELECT
|
||||
generate_prefix_id('group_member'),
|
||||
og."id",
|
||||
om."id"
|
||||
FROM "TeamMember" tm
|
||||
JOIN "Team" t ON t."id" = tm."teamId"
|
||||
JOIN "Organisation" o ON o."id" = t."organisationId"
|
||||
JOIN "OrganisationMember" om ON om."userId" = tm."userId" AND om."organisationId" = t."organisationId"
|
||||
JOIN "TeamGroup" tg ON tg."teamId" = t."id" AND tg."teamRole" = tm."role"
|
||||
JOIN "OrganisationGroup" og ON og."id" = tg."organisationGroupId" AND og."type" = 'INTERNAL_TEAM'::"OrganisationGroupType"
|
||||
WHERE tm."userId" != o."ownerUserId";
|
||||
|
||||
-- Add organisation owners to the INTERNAL_ORGANISATION ADMIN group
|
||||
INSERT INTO "OrganisationGroupMember" ("id", "groupId", "organisationMemberId")
|
||||
SELECT
|
||||
generate_prefix_id('group_member'),
|
||||
og."id",
|
||||
om."id"
|
||||
FROM "Organisation" o
|
||||
JOIN "OrganisationMember" om ON om."organisationId" = o."id" AND om."userId" = o."ownerUserId"
|
||||
JOIN "OrganisationGroup" og ON og."organisationId" = o."id"
|
||||
AND og."type" = 'INTERNAL_ORGANISATION'::"OrganisationGroupType"
|
||||
AND og."organisationRole" = 'ADMIN'::"OrganisationMemberRole";
|
||||
|
||||
-- Add all other organisation members to the INTERNAL_ORGANISATION MEMBER group
|
||||
INSERT INTO "OrganisationGroupMember" ("id", "groupId", "organisationMemberId")
|
||||
SELECT
|
||||
generate_prefix_id('group_member'),
|
||||
og."id",
|
||||
om."id"
|
||||
FROM "Organisation" o
|
||||
JOIN "OrganisationMember" om ON om."organisationId" = o."id" AND om."userId" != o."ownerUserId"
|
||||
JOIN "OrganisationGroup" og ON og."organisationId" = o."id"
|
||||
AND og."type" = 'INTERNAL_ORGANISATION'::"OrganisationGroupType"
|
||||
AND og."organisationRole" = 'MEMBER'::"OrganisationMemberRole";
|
||||
|
||||
-- Migrate team subscriptions to the organisation level
|
||||
UPDATE "Subscription" s
|
||||
SET "organisationId" = t."organisationId"
|
||||
FROM "Team" t
|
||||
WHERE s."teamId" = t."id" AND s."teamId" IS NOT NULL;
|
||||
|
||||
-- Drop team member related entities (DropForeignKey/DropTable)
|
||||
ALTER TABLE "TeamMember" DROP CONSTRAINT "TeamMember_teamId_fkey";
|
||||
ALTER TABLE "TeamMember" DROP CONSTRAINT "TeamMember_userId_fkey";
|
||||
DROP TABLE "TeamMember";
|
||||
|
||||
-- Drop temp columns
|
||||
ALTER TABLE "Organisation" DROP COLUMN "teamId";
|
||||
ALTER TABLE "Team" DROP COLUMN "isPersonal";
|
||||
ALTER TABLE "TeamGlobalSettings" DROP COLUMN "teamId";
|
||||
ALTER TABLE "OrganisationGlobalSettings" DROP COLUMN "organisationId";
|
||||
|
||||
-- REAPPLY NOT NULL to any temporary nullable columns
|
||||
ALTER TABLE "Team" ALTER COLUMN "organisationId" SET NOT NULL;
|
||||
ALTER TABLE "Team" ALTER COLUMN "teamGlobalSettingsId" SET NOT NULL;
|
||||
ALTER TABLE "Subscription" ALTER COLUMN "organisationId" SET NOT NULL;
|
||||
ALTER TABLE "Organisation" ALTER COLUMN "organisationClaimId" SET NOT NULL;
|
||||
ALTER TABLE "Organisation" ALTER COLUMN "organisationGlobalSettingsId" SET NOT NULL;
|
||||
|
||||
-- Drop columns
|
||||
ALTER TABLE "Team" DROP COLUMN "ownerUserId";
|
||||
ALTER TABLE "User" DROP COLUMN "url";
|
||||
ALTER TABLE "Subscription" DROP COLUMN "teamId";
|
||||
ALTER TABLE "Subscription" DROP COLUMN "userId";
|
||||
@ -1,3 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
|
||||
@ -39,7 +39,6 @@ enum Role {
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
name String?
|
||||
customerId String? @unique
|
||||
email String @unique
|
||||
emailVerified DateTime?
|
||||
password String? // Todo: (RR7) Remove after RR7 migration.
|
||||
@ -53,24 +52,23 @@ model User {
|
||||
avatarImageId String?
|
||||
disabled Boolean @default(false)
|
||||
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
documents Document[]
|
||||
folders Folder[]
|
||||
subscriptions Subscription[]
|
||||
passwordResetTokens PasswordResetToken[]
|
||||
ownedTeams Team[]
|
||||
ownedPendingTeams TeamPending[]
|
||||
teamMembers TeamMember[]
|
||||
twoFactorSecret String?
|
||||
twoFactorEnabled Boolean @default(false)
|
||||
twoFactorBackupCodes String?
|
||||
url String? @unique
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
passwordResetTokens PasswordResetToken[]
|
||||
|
||||
ownedOrganisations Organisation[]
|
||||
organisationMember OrganisationMember[]
|
||||
|
||||
twoFactorSecret String?
|
||||
twoFactorEnabled Boolean @default(false)
|
||||
twoFactorBackupCodes String?
|
||||
|
||||
folders Folder[]
|
||||
documents Document[]
|
||||
templates Template[]
|
||||
|
||||
profile UserProfile?
|
||||
verificationTokens VerificationToken[]
|
||||
apiTokens ApiToken[]
|
||||
templates Template[]
|
||||
securityAuditLogs UserSecurityAuditLog[]
|
||||
webhooks Webhook[]
|
||||
siteSettings SiteSettings[]
|
||||
@ -80,15 +78,6 @@ model User {
|
||||
@@index([email])
|
||||
}
|
||||
|
||||
model UserProfile {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false)
|
||||
userId Int @unique
|
||||
bio String?
|
||||
|
||||
User User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model TeamProfile {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false)
|
||||
@ -191,8 +180,8 @@ model Webhook {
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
teamId Int?
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
teamId Int
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
webhookCalls WebhookCall[]
|
||||
}
|
||||
|
||||
@ -228,8 +217,8 @@ model ApiToken {
|
||||
createdAt DateTime @default(now())
|
||||
userId Int?
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
teamId Int?
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
teamId Int
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
enum SubscriptionStatus {
|
||||
@ -244,16 +233,46 @@ model Subscription {
|
||||
planId String @unique
|
||||
priceId String
|
||||
periodEnd DateTime?
|
||||
userId Int?
|
||||
teamId Int? @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
cancelAtPeriodEnd Boolean @default(false)
|
||||
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
customerId String
|
||||
|
||||
@@index([userId])
|
||||
organisationId String @unique
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([organisationId])
|
||||
}
|
||||
|
||||
/// @zod.import(["import { ZClaimFlagsSchema } from '@documenso/lib/types/subscription';"])
|
||||
model SubscriptionClaim {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
name String
|
||||
locked Boolean @default(false)
|
||||
|
||||
teamCount Int
|
||||
memberCount Int
|
||||
|
||||
flags Json /// [ClaimFlags] @zod.custom.use(ZClaimFlagsSchema)
|
||||
}
|
||||
|
||||
/// @zod.import(["import { ZClaimFlagsSchema } from '@documenso/lib/types/subscription';"])
|
||||
model OrganisationClaim {
|
||||
id String @id
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
originalSubscriptionClaimId String?
|
||||
organisation Organisation?
|
||||
|
||||
teamCount Int
|
||||
memberCount Int
|
||||
|
||||
flags Json /// [ClaimFlags] @zod.custom.use(ZClaimFlagsSchema)
|
||||
}
|
||||
|
||||
model Account {
|
||||
@ -323,8 +342,8 @@ model Folder {
|
||||
name String
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
teamId Int?
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
teamId Int
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
pinned Boolean @default(false)
|
||||
parentId String?
|
||||
parent Folder? @relation("FolderToFolder", fields: [parentId], references: [id], onDelete: Cascade)
|
||||
@ -344,11 +363,16 @@ model Folder {
|
||||
|
||||
/// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';", "import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';"])
|
||||
model Document {
|
||||
id Int @id @default(autoincrement())
|
||||
qrToken String? /// @zod.string.describe("The token for viewing the document using the QR code on the certificate.")
|
||||
externalId String? /// @zod.string.describe("A custom external ID you can use to identify the document.")
|
||||
userId Int /// @zod.number.describe("The ID of the user that created this document.")
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
id Int @id @default(autoincrement())
|
||||
qrToken String? /// @zod.string.describe("The token for viewing the document using the QR code on the certificate.")
|
||||
externalId String? /// @zod.string.describe("A custom external ID you can use to identify the document.")
|
||||
|
||||
userId Int /// @zod.number.describe("The ID of the user that created this document.")
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
teamId Int
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
|
||||
authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema)
|
||||
formValues Json? /// [DocumentFormValues] @zod.custom.use(ZDocumentFormValuesSchema)
|
||||
visibility DocumentVisibility @default(EVERYONE)
|
||||
@ -363,14 +387,12 @@ model Document {
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
completedAt DateTime?
|
||||
deletedAt DateTime?
|
||||
teamId Int?
|
||||
templateId Int?
|
||||
source DocumentSource
|
||||
|
||||
useLegacyFieldInsertion Boolean @default(false)
|
||||
|
||||
documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade)
|
||||
team Team? @relation(fields: [teamId], references: [id])
|
||||
template Template? @relation(fields: [templateId], references: [id], onDelete: SetNull)
|
||||
|
||||
auditLogs DocumentAuditLog[]
|
||||
@ -569,20 +591,141 @@ model DocumentShareLink {
|
||||
@@unique([documentId, email])
|
||||
}
|
||||
|
||||
enum OrganisationType {
|
||||
PERSONAL
|
||||
ORGANISATION
|
||||
}
|
||||
|
||||
model Organisation {
|
||||
id String @id
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
type OrganisationType
|
||||
name String
|
||||
url String @unique
|
||||
avatarImageId String?
|
||||
|
||||
customerId String? @unique
|
||||
subscription Subscription?
|
||||
|
||||
organisationClaimId String @unique
|
||||
organisationClaim OrganisationClaim @relation(fields: [organisationClaimId], references: [id])
|
||||
|
||||
members OrganisationMember[]
|
||||
invites OrganisationMemberInvite[]
|
||||
groups OrganisationGroup[]
|
||||
|
||||
teams Team[]
|
||||
|
||||
avatarImage AvatarImage? @relation(fields: [avatarImageId], references: [id], onDelete: SetNull)
|
||||
|
||||
ownerUserId Int
|
||||
owner User @relation(fields: [ownerUserId], references: [id], onDelete: Cascade)
|
||||
|
||||
organisationGlobalSettingsId String @unique
|
||||
organisationGlobalSettings OrganisationGlobalSettings @relation(fields: [organisationGlobalSettingsId], references: [id])
|
||||
}
|
||||
|
||||
model OrganisationMember {
|
||||
id String @id
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
organisationId String
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
|
||||
organisationGroupMembers OrganisationGroupMember[]
|
||||
|
||||
@@unique([userId, organisationId])
|
||||
}
|
||||
|
||||
model OrganisationMemberInvite {
|
||||
id String @id
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
email String
|
||||
token String @unique
|
||||
|
||||
status OrganisationMemberInviteStatus @default(PENDING)
|
||||
|
||||
organisationId String
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
organisationRole OrganisationMemberRole
|
||||
}
|
||||
|
||||
model OrganisationGroup {
|
||||
id String @id
|
||||
name String?
|
||||
|
||||
type OrganisationGroupType
|
||||
organisationRole OrganisationMemberRole
|
||||
|
||||
organisationId String
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
|
||||
organisationGroupMembers OrganisationGroupMember[]
|
||||
|
||||
teamGroups TeamGroup[]
|
||||
}
|
||||
|
||||
model OrganisationGroupMember {
|
||||
id String @id
|
||||
|
||||
groupId String
|
||||
group OrganisationGroup @relation(fields: [groupId], references: [id], onDelete: Cascade)
|
||||
|
||||
organisationMember OrganisationMember @relation(fields: [organisationMemberId], references: [id], onDelete: Cascade)
|
||||
organisationMemberId String
|
||||
|
||||
@@unique([organisationMemberId, groupId])
|
||||
}
|
||||
|
||||
model TeamGroup {
|
||||
id String @id
|
||||
|
||||
organisationGroupId String
|
||||
organisationGroup OrganisationGroup @relation(fields: [organisationGroupId], references: [id], onDelete: Cascade)
|
||||
|
||||
teamRole TeamMemberRole
|
||||
|
||||
teamId Int
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([teamId, organisationGroupId])
|
||||
}
|
||||
|
||||
enum OrganisationGroupType {
|
||||
INTERNAL_ORGANISATION
|
||||
INTERNAL_TEAM
|
||||
CUSTOM
|
||||
}
|
||||
|
||||
enum OrganisationMemberRole {
|
||||
ADMIN
|
||||
MANAGER
|
||||
MEMBER
|
||||
}
|
||||
|
||||
enum TeamMemberRole {
|
||||
ADMIN
|
||||
MANAGER
|
||||
MEMBER
|
||||
}
|
||||
|
||||
enum TeamMemberInviteStatus {
|
||||
enum OrganisationMemberInviteStatus {
|
||||
ACCEPTED
|
||||
PENDING
|
||||
DECLINED
|
||||
}
|
||||
|
||||
model TeamGlobalSettings {
|
||||
teamId Int @unique
|
||||
model OrganisationGlobalSettings {
|
||||
id String @id
|
||||
organisation Organisation?
|
||||
|
||||
documentVisibility DocumentVisibility @default(EVERYONE)
|
||||
documentLanguage String @default("en")
|
||||
includeSenderDetails Boolean @default(true)
|
||||
@ -596,11 +739,25 @@ model TeamGlobalSettings {
|
||||
brandingLogo String @default("")
|
||||
brandingUrl String @default("")
|
||||
brandingCompanyDetails String @default("")
|
||||
brandingHidePoweredBy Boolean @default(false)
|
||||
}
|
||||
|
||||
allowEmbeddedAuthoring Boolean @default(false)
|
||||
model TeamGlobalSettings {
|
||||
id String @id
|
||||
team Team?
|
||||
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
documentVisibility DocumentVisibility?
|
||||
documentLanguage String?
|
||||
includeSenderDetails Boolean?
|
||||
includeSigningCertificate Boolean?
|
||||
|
||||
typedSignatureEnabled Boolean?
|
||||
uploadSignatureEnabled Boolean?
|
||||
drawSignatureEnabled Boolean?
|
||||
|
||||
brandingEnabled Boolean?
|
||||
brandingLogo String?
|
||||
brandingUrl String?
|
||||
brandingCompanyDetails String?
|
||||
}
|
||||
|
||||
model Team {
|
||||
@ -609,49 +766,25 @@ model Team {
|
||||
url String @unique
|
||||
createdAt DateTime @default(now())
|
||||
avatarImageId String?
|
||||
customerId String? @unique
|
||||
ownerUserId Int
|
||||
|
||||
members TeamMember[]
|
||||
invites TeamMemberInvite[]
|
||||
teamEmail TeamEmail?
|
||||
emailVerification TeamEmailVerification?
|
||||
transferVerification TeamTransferVerification?
|
||||
teamGlobalSettings TeamGlobalSettings?
|
||||
avatarImage AvatarImage? @relation(fields: [avatarImageId], references: [id], onDelete: SetNull)
|
||||
teamEmail TeamEmail?
|
||||
emailVerification TeamEmailVerification?
|
||||
avatarImage AvatarImage? @relation(fields: [avatarImageId], references: [id], onDelete: SetNull)
|
||||
|
||||
profile TeamProfile?
|
||||
owner User @relation(fields: [ownerUserId], references: [id], onDelete: Cascade)
|
||||
subscription Subscription?
|
||||
profile TeamProfile?
|
||||
|
||||
documents Document[]
|
||||
templates Template[]
|
||||
folders Folder[]
|
||||
apiTokens ApiToken[]
|
||||
webhooks Webhook[]
|
||||
}
|
||||
documents Document[]
|
||||
templates Template[]
|
||||
folders Folder[]
|
||||
apiTokens ApiToken[]
|
||||
webhooks Webhook[]
|
||||
teamGroups TeamGroup[]
|
||||
|
||||
model TeamPending {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
url String @unique
|
||||
createdAt DateTime @default(now())
|
||||
customerId String @unique
|
||||
ownerUserId Int
|
||||
organisationId String
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
|
||||
owner User @relation(fields: [ownerUserId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model TeamMember {
|
||||
id Int @id @default(autoincrement())
|
||||
teamId Int
|
||||
createdAt DateTime @default(now())
|
||||
role TeamMemberRole
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, teamId])
|
||||
teamGlobalSettingsId String @unique
|
||||
teamGlobalSettings TeamGlobalSettings @relation(fields: [teamGlobalSettingsId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model TeamEmail {
|
||||
@ -674,33 +807,6 @@ model TeamEmailVerification {
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model TeamTransferVerification {
|
||||
teamId Int @id @unique
|
||||
userId Int
|
||||
name String
|
||||
email String
|
||||
token String @unique
|
||||
completed Boolean @default(false)
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
clearPaymentMethods Boolean @default(false)
|
||||
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model TeamMemberInvite {
|
||||
id Int @id @default(autoincrement())
|
||||
teamId Int
|
||||
createdAt DateTime @default(now())
|
||||
email String
|
||||
status TeamMemberInviteStatus @default(PENDING)
|
||||
role TeamMemberRole
|
||||
token String @unique
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([teamId, email])
|
||||
}
|
||||
|
||||
enum TemplateType {
|
||||
PUBLIC
|
||||
PRIVATE
|
||||
@ -735,8 +841,6 @@ model Template {
|
||||
externalId String?
|
||||
type TemplateType @default(PRIVATE)
|
||||
title String
|
||||
userId Int
|
||||
teamId Int?
|
||||
visibility DocumentVisibility @default(EVERYONE)
|
||||
authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema)
|
||||
templateMeta TemplateMeta?
|
||||
@ -748,17 +852,24 @@ model Template {
|
||||
|
||||
useLegacyFieldInsertion Boolean @default(false)
|
||||
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
templateDocumentData DocumentData @relation(fields: [templateDocumentDataId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
recipients Recipient[]
|
||||
fields Field[]
|
||||
directLink TemplateDirectLink?
|
||||
documents Document[]
|
||||
folder Folder? @relation(fields: [folderId], references: [id], onDelete: Cascade)
|
||||
folderId String?
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
teamId Int
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
|
||||
templateDocumentData DocumentData @relation(fields: [templateDocumentDataId], references: [id], onDelete: Cascade)
|
||||
|
||||
recipients Recipient[]
|
||||
fields Field[]
|
||||
directLink TemplateDirectLink?
|
||||
documents Document[]
|
||||
|
||||
folder Folder? @relation(fields: [folderId], references: [id], onDelete: Cascade)
|
||||
folderId String?
|
||||
|
||||
@@unique([templateDocumentDataId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model TemplateDirectLink {
|
||||
@ -836,6 +947,7 @@ model AvatarImage {
|
||||
id String @id @default(cuid())
|
||||
bytes String
|
||||
|
||||
team Team[]
|
||||
user User[]
|
||||
team Team[]
|
||||
user User[]
|
||||
organisation Organisation[]
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ const examplePdf = fs
|
||||
|
||||
type DocumentToSeed = {
|
||||
sender: User;
|
||||
teamId: number;
|
||||
recipients: (User | string)[];
|
||||
type: DocumentStatus;
|
||||
documentOptions?: Partial<Prisma.DocumentUncheckedCreateInput>;
|
||||
@ -38,19 +39,19 @@ export const seedDocuments = async (documents: DocumentToSeed[]) => {
|
||||
documents.map(async (document, i) =>
|
||||
match(document.type)
|
||||
.with(DocumentStatus.DRAFT, async () =>
|
||||
seedDraftDocument(document.sender, document.recipients, {
|
||||
seedDraftDocument(document.sender, document.teamId, document.recipients, {
|
||||
key: i,
|
||||
createDocumentOptions: document.documentOptions,
|
||||
}),
|
||||
)
|
||||
.with(DocumentStatus.PENDING, async () =>
|
||||
seedPendingDocument(document.sender, document.recipients, {
|
||||
seedPendingDocument(document.sender, document.teamId, document.recipients, {
|
||||
key: i,
|
||||
createDocumentOptions: document.documentOptions,
|
||||
}),
|
||||
)
|
||||
.with(DocumentStatus.COMPLETED, async () =>
|
||||
seedCompletedDocument(document.sender, document.recipients, {
|
||||
seedCompletedDocument(document.sender, document.teamId, document.recipients, {
|
||||
key: i,
|
||||
createDocumentOptions: document.documentOptions,
|
||||
}),
|
||||
@ -59,7 +60,11 @@ export const seedDocuments = async (documents: DocumentToSeed[]) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const seedBlankDocument = async (owner: User, options: CreateDocumentOptions = {}) => {
|
||||
export const seedBlankDocument = async (
|
||||
owner: User,
|
||||
teamId: number,
|
||||
options: CreateDocumentOptions = {},
|
||||
) => {
|
||||
const { key, createDocumentOptions = {} } = options;
|
||||
|
||||
const documentData = await prisma.documentData.create({
|
||||
@ -73,6 +78,7 @@ export const seedBlankDocument = async (owner: User, options: CreateDocumentOpti
|
||||
return await prisma.document.create({
|
||||
data: {
|
||||
source: DocumentSource.DOCUMENT,
|
||||
teamId,
|
||||
title: `[TEST] Document ${key} - Draft`,
|
||||
status: DocumentStatus.DRAFT,
|
||||
documentDataId: documentData.id,
|
||||
@ -99,8 +105,23 @@ export const seedTeamDocumentWithMeta = async (team: Team) => {
|
||||
},
|
||||
});
|
||||
|
||||
const { organisation } = await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: team.id,
|
||||
},
|
||||
include: {
|
||||
organisation: {
|
||||
include: {
|
||||
owner: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const ownerUser = organisation.owner;
|
||||
|
||||
const document = await createDocument({
|
||||
userId: team.ownerUserId,
|
||||
userId: ownerUser.id,
|
||||
teamId: team.id,
|
||||
title: `[TEST] Document ${nanoid(8)} - Draft`,
|
||||
documentDataId: documentData.id,
|
||||
@ -112,12 +133,6 @@ export const seedTeamDocumentWithMeta = async (team: Team) => {
|
||||
},
|
||||
});
|
||||
|
||||
const owner = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: team.ownerUserId,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.document.update({
|
||||
where: {
|
||||
id: document.id,
|
||||
@ -129,8 +144,8 @@ export const seedTeamDocumentWithMeta = async (team: Team) => {
|
||||
|
||||
await prisma.recipient.create({
|
||||
data: {
|
||||
email: owner.email,
|
||||
name: owner.name ?? '',
|
||||
email: ownerUser.email,
|
||||
name: ownerUser.name ?? '',
|
||||
token: nanoid(),
|
||||
readStatus: ReadStatus.OPENED,
|
||||
sendStatus: SendStatus.SENT,
|
||||
@ -176,23 +191,32 @@ export const seedTeamTemplateWithMeta = async (team: Team) => {
|
||||
},
|
||||
});
|
||||
|
||||
const { organisation } = await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: team.id,
|
||||
},
|
||||
include: {
|
||||
organisation: {
|
||||
include: {
|
||||
owner: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const ownerUser = organisation.owner;
|
||||
|
||||
const template = await createTemplate({
|
||||
title: `[TEST] Template ${nanoid(8)} - Draft`,
|
||||
userId: team.ownerUserId,
|
||||
userId: ownerUser.id,
|
||||
teamId: team.id,
|
||||
templateDocumentDataId: documentData.id,
|
||||
});
|
||||
|
||||
const owner = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: team.ownerUserId,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.recipient.create({
|
||||
data: {
|
||||
email: owner.email,
|
||||
name: owner.name ?? '',
|
||||
email: ownerUser.email,
|
||||
name: ownerUser.name ?? '',
|
||||
token: nanoid(),
|
||||
readStatus: ReadStatus.OPENED,
|
||||
sendStatus: SendStatus.SENT,
|
||||
@ -231,6 +255,7 @@ export const seedTeamTemplateWithMeta = async (team: Team) => {
|
||||
|
||||
export const seedDraftDocument = async (
|
||||
sender: User,
|
||||
teamId: number,
|
||||
recipients: (User | string)[],
|
||||
options: CreateDocumentOptions = {},
|
||||
) => {
|
||||
@ -247,6 +272,7 @@ export const seedDraftDocument = async (
|
||||
const document = await prisma.document.create({
|
||||
data: {
|
||||
source: DocumentSource.DOCUMENT,
|
||||
teamId,
|
||||
title: `[TEST] Document ${key} - Draft`,
|
||||
status: DocumentStatus.DRAFT,
|
||||
documentDataId: documentData.id,
|
||||
@ -300,6 +326,7 @@ type CreateDocumentOptions = {
|
||||
|
||||
export const seedPendingDocument = async (
|
||||
sender: User,
|
||||
teamId: number,
|
||||
recipients: (User | string)[],
|
||||
options: CreateDocumentOptions = {},
|
||||
) => {
|
||||
@ -316,6 +343,7 @@ export const seedPendingDocument = async (
|
||||
const document = await prisma.document.create({
|
||||
data: {
|
||||
source: DocumentSource.DOCUMENT,
|
||||
teamId,
|
||||
title: `[TEST] Document ${key} - Pending`,
|
||||
status: DocumentStatus.PENDING,
|
||||
documentDataId: documentData.id,
|
||||
@ -372,13 +400,15 @@ export const seedPendingDocument = async (
|
||||
export const seedPendingDocumentNoFields = async ({
|
||||
owner,
|
||||
recipients,
|
||||
teamId,
|
||||
updateDocumentOptions,
|
||||
}: {
|
||||
owner: User;
|
||||
recipients: (User | string)[];
|
||||
teamId: number;
|
||||
updateDocumentOptions?: Partial<Prisma.DocumentUncheckedUpdateInput>;
|
||||
}) => {
|
||||
const document: Document = await seedBlankDocument(owner);
|
||||
const document: Document = await seedBlankDocument(owner, teamId);
|
||||
|
||||
for (const recipient of recipients) {
|
||||
const email = typeof recipient === 'string' ? recipient : recipient.email;
|
||||
@ -432,14 +462,16 @@ export const seedPendingDocumentWithFullFields = async ({
|
||||
recipientsCreateOptions,
|
||||
updateDocumentOptions,
|
||||
fields = [FieldType.DATE, FieldType.EMAIL, FieldType.NAME, FieldType.SIGNATURE, FieldType.TEXT],
|
||||
teamId,
|
||||
}: {
|
||||
owner: User;
|
||||
recipients: (User | string)[];
|
||||
recipientsCreateOptions?: Partial<Prisma.RecipientCreateInput>[];
|
||||
updateDocumentOptions?: Partial<Prisma.DocumentUncheckedUpdateInput>;
|
||||
fields?: FieldType[];
|
||||
teamId: number;
|
||||
}) => {
|
||||
const document: Document = await seedBlankDocument(owner);
|
||||
const document: Document = await seedBlankDocument(owner, teamId);
|
||||
|
||||
for (const [recipientIndex, recipient] of recipients.entries()) {
|
||||
const email = typeof recipient === 'string' ? recipient : recipient.email;
|
||||
@ -509,6 +541,7 @@ export const seedPendingDocumentWithFullFields = async ({
|
||||
|
||||
export const seedCompletedDocument = async (
|
||||
sender: User,
|
||||
teamId: number,
|
||||
recipients: (User | string)[],
|
||||
options: CreateDocumentOptions = {},
|
||||
) => {
|
||||
@ -525,6 +558,7 @@ export const seedCompletedDocument = async (
|
||||
const document = await prisma.document.create({
|
||||
data: {
|
||||
source: DocumentSource.DOCUMENT,
|
||||
teamId,
|
||||
title: `[TEST] Document ${key} - Completed`,
|
||||
status: DocumentStatus.COMPLETED,
|
||||
documentDataId: documentData.id,
|
||||
@ -597,7 +631,7 @@ export const seedCompletedDocument = async (
|
||||
* - 5 All
|
||||
*/
|
||||
export const seedTeamDocuments = async () => {
|
||||
const team = await seedTeam({
|
||||
const { team, owner, organisation } = await seedTeam({
|
||||
createTeamMembers: 4,
|
||||
});
|
||||
|
||||
@ -605,17 +639,17 @@ export const seedTeamDocuments = async () => {
|
||||
teamId: team.id,
|
||||
};
|
||||
|
||||
const teamMember1 = team.members[1].user;
|
||||
const teamMember2 = team.members[2].user;
|
||||
const teamMember3 = team.members[3].user;
|
||||
const teamMember4 = team.members[4].user;
|
||||
const teamMember1 = organisation.members[1].user;
|
||||
const teamMember2 = organisation.members[2].user;
|
||||
const teamMember3 = organisation.members[3].user;
|
||||
const teamMember4 = organisation.members[4].user;
|
||||
|
||||
const [testUser1, testUser2, testUser3, testUser4] = await Promise.all([
|
||||
seedUser(),
|
||||
seedUser(),
|
||||
seedUser(),
|
||||
seedUser(),
|
||||
]);
|
||||
const [
|
||||
{ user: testUser1, team: testUser1Team },
|
||||
{ user: testUser2, team: testUser2Team },
|
||||
{ user: testUser3, team: testUser3Team },
|
||||
{ user: testUser4, team: testUser4Team },
|
||||
] = await Promise.all([seedUser(), seedUser(), seedUser(), seedUser()]);
|
||||
|
||||
await seedDocuments([
|
||||
/**
|
||||
@ -623,30 +657,35 @@ export const seedTeamDocuments = async () => {
|
||||
*/
|
||||
{
|
||||
sender: teamMember1,
|
||||
teamId: team.id,
|
||||
recipients: [testUser1, testUser2],
|
||||
type: DocumentStatus.COMPLETED,
|
||||
documentOptions,
|
||||
},
|
||||
{
|
||||
sender: teamMember2,
|
||||
teamId: team.id,
|
||||
recipients: [testUser1],
|
||||
type: DocumentStatus.PENDING,
|
||||
documentOptions,
|
||||
},
|
||||
{
|
||||
sender: teamMember2,
|
||||
teamId: team.id,
|
||||
recipients: [testUser1, testUser2, testUser3, testUser4],
|
||||
type: DocumentStatus.PENDING,
|
||||
documentOptions,
|
||||
},
|
||||
{
|
||||
sender: teamMember2,
|
||||
teamId: team.id,
|
||||
recipients: [testUser1, testUser2, teamMember1],
|
||||
type: DocumentStatus.DRAFT,
|
||||
documentOptions,
|
||||
},
|
||||
{
|
||||
sender: team.owner,
|
||||
sender: owner,
|
||||
teamId: team.id,
|
||||
recipients: [testUser1, testUser2],
|
||||
type: DocumentStatus.DRAFT,
|
||||
documentOptions,
|
||||
@ -656,16 +695,19 @@ export const seedTeamDocuments = async () => {
|
||||
*/
|
||||
{
|
||||
sender: teamMember1,
|
||||
teamId: testUser3Team.id, // Not sure.
|
||||
recipients: [testUser1, testUser2],
|
||||
type: DocumentStatus.COMPLETED,
|
||||
},
|
||||
{
|
||||
sender: teamMember2,
|
||||
teamId: testUser3Team.id, // Not sure.
|
||||
recipients: [testUser1],
|
||||
type: DocumentStatus.PENDING,
|
||||
},
|
||||
{
|
||||
sender: teamMember3,
|
||||
teamId: testUser3Team.id, // Not sure.
|
||||
recipients: [testUser1, testUser2],
|
||||
type: DocumentStatus.DRAFT,
|
||||
},
|
||||
@ -674,16 +716,19 @@ export const seedTeamDocuments = async () => {
|
||||
*/
|
||||
{
|
||||
sender: testUser1,
|
||||
teamId: testUser1Team.id,
|
||||
recipients: [teamMember1, teamMember2],
|
||||
type: DocumentStatus.COMPLETED,
|
||||
},
|
||||
{
|
||||
sender: testUser2,
|
||||
teamId: testUser2Team.id,
|
||||
recipients: [teamMember1],
|
||||
type: DocumentStatus.PENDING,
|
||||
},
|
||||
{
|
||||
sender: testUser3,
|
||||
teamId: testUser3Team.id,
|
||||
recipients: [teamMember1, teamMember2],
|
||||
type: DocumentStatus.DRAFT,
|
||||
},
|
||||
@ -691,6 +736,7 @@ export const seedTeamDocuments = async () => {
|
||||
|
||||
return {
|
||||
team,
|
||||
teamOwner: owner,
|
||||
teamMember1,
|
||||
teamMember2,
|
||||
teamMember3,
|
||||
|
||||
@ -1,33 +1,26 @@
|
||||
import type { User } from '@prisma/client';
|
||||
import { DocumentStatus, FolderType } from '@prisma/client';
|
||||
import { FolderType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '..';
|
||||
import type { Prisma } from '../client';
|
||||
import { seedDocuments } from './documents';
|
||||
|
||||
type CreateFolderOptions = {
|
||||
type?: string;
|
||||
createFolderOptions?: Partial<Prisma.FolderUncheckedCreateInput>;
|
||||
};
|
||||
|
||||
export const seedBlankFolder = async (user: User, options: CreateFolderOptions = {}) => {
|
||||
export const seedBlankFolder = async (
|
||||
user: User,
|
||||
teamId: number,
|
||||
options: CreateFolderOptions = {},
|
||||
) => {
|
||||
return await prisma.folder.create({
|
||||
data: {
|
||||
name: 'My folder',
|
||||
userId: user.id,
|
||||
teamId,
|
||||
type: FolderType.DOCUMENT,
|
||||
...options.createFolderOptions,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const seedFolderWithDocuments = async (user: User, options: CreateFolderOptions = {}) => {
|
||||
const folder = await seedBlankFolder(user, options);
|
||||
await seedDocuments([
|
||||
{
|
||||
sender: user,
|
||||
recipients: [user],
|
||||
type: DocumentStatus.DRAFT,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { hashSync } from '@documenso/lib/server-only/auth/hash';
|
||||
|
||||
import { prisma } from '..';
|
||||
import { DocumentDataType, DocumentSource, Role, TeamMemberRole } from '../client';
|
||||
import { DocumentDataType, DocumentSource } from '../client';
|
||||
import { seedPendingDocument } from './documents';
|
||||
import { seedDirectTemplate, seedTemplate } from './templates';
|
||||
import { seedUser } from './users';
|
||||
|
||||
const createDocumentData = async ({ documentData }: { documentData: string }) => {
|
||||
return prisma.documentData.create({
|
||||
@ -23,32 +22,31 @@ export const seedDatabase = async () => {
|
||||
.readFileSync(path.join(__dirname, '../../../assets/example.pdf'))
|
||||
.toString('base64');
|
||||
|
||||
const exampleUser = await prisma.user.upsert({
|
||||
const exampleUserExists = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: 'example@documenso.com',
|
||||
},
|
||||
create: {
|
||||
name: 'Example User',
|
||||
email: 'example@documenso.com',
|
||||
emailVerified: new Date(),
|
||||
password: hashSync('password'),
|
||||
roles: [Role.USER],
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
const adminUser = await prisma.user.upsert({
|
||||
const adminUserExists = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: 'admin@documenso.com',
|
||||
},
|
||||
create: {
|
||||
name: 'Admin User',
|
||||
email: 'admin@documenso.com',
|
||||
emailVerified: new Date(),
|
||||
password: hashSync('password'),
|
||||
roles: [Role.USER, Role.ADMIN],
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
if (exampleUserExists || adminUserExists) {
|
||||
return;
|
||||
}
|
||||
|
||||
const exampleUser = await seedUser({
|
||||
name: 'Example User',
|
||||
email: 'example@documenso.com',
|
||||
});
|
||||
|
||||
const adminUser = await seedUser({
|
||||
name: 'Admin User',
|
||||
email: 'admin@documenso.com',
|
||||
isAdmin: true,
|
||||
});
|
||||
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
@ -59,11 +57,12 @@ export const seedDatabase = async () => {
|
||||
source: DocumentSource.DOCUMENT,
|
||||
title: `Example Document ${i}`,
|
||||
documentDataId: documentData.id,
|
||||
userId: exampleUser.id,
|
||||
userId: exampleUser.user.id,
|
||||
teamId: exampleUser.team.id,
|
||||
recipients: {
|
||||
create: {
|
||||
name: String(adminUser.name),
|
||||
email: adminUser.email,
|
||||
name: String(adminUser.user.name),
|
||||
email: adminUser.user.email,
|
||||
token: Math.random().toString(36).slice(2, 9),
|
||||
},
|
||||
},
|
||||
@ -79,11 +78,12 @@ export const seedDatabase = async () => {
|
||||
source: DocumentSource.DOCUMENT,
|
||||
title: `Document ${i}`,
|
||||
documentDataId: documentData.id,
|
||||
userId: adminUser.id,
|
||||
userId: adminUser.user.id,
|
||||
teamId: adminUser.team.id,
|
||||
recipients: {
|
||||
create: {
|
||||
name: String(exampleUser.name),
|
||||
email: exampleUser.email,
|
||||
name: String(exampleUser.user.name),
|
||||
email: exampleUser.user.email,
|
||||
token: Math.random().toString(36).slice(2, 9),
|
||||
},
|
||||
},
|
||||
@ -91,14 +91,14 @@ export const seedDatabase = async () => {
|
||||
});
|
||||
}
|
||||
|
||||
await seedPendingDocument(exampleUser, [adminUser], {
|
||||
await seedPendingDocument(exampleUser.user, exampleUser.team.id, [adminUser.user], {
|
||||
key: 'example-pending',
|
||||
createDocumentOptions: {
|
||||
title: 'Pending Document',
|
||||
},
|
||||
});
|
||||
|
||||
await seedPendingDocument(adminUser, [exampleUser], {
|
||||
await seedPendingDocument(adminUser.user, adminUser.team.id, [exampleUser.user], {
|
||||
key: 'admin-pending',
|
||||
createDocumentOptions: {
|
||||
title: 'Pending Document',
|
||||
@ -108,80 +108,24 @@ export const seedDatabase = async () => {
|
||||
await Promise.all([
|
||||
seedTemplate({
|
||||
title: 'Template 1',
|
||||
userId: exampleUser.id,
|
||||
userId: exampleUser.user.id,
|
||||
teamId: exampleUser.team.id,
|
||||
}),
|
||||
seedDirectTemplate({
|
||||
title: 'Direct Template 1',
|
||||
userId: exampleUser.id,
|
||||
userId: exampleUser.user.id,
|
||||
teamId: exampleUser.team.id,
|
||||
}),
|
||||
|
||||
seedTemplate({
|
||||
title: 'Template 1',
|
||||
userId: adminUser.id,
|
||||
userId: adminUser.user.id,
|
||||
teamId: adminUser.team.id,
|
||||
}),
|
||||
seedDirectTemplate({
|
||||
title: 'Direct Template 1',
|
||||
userId: adminUser.id,
|
||||
userId: adminUser.user.id,
|
||||
teamId: adminUser.team.id,
|
||||
}),
|
||||
]);
|
||||
|
||||
const testUsers = [
|
||||
'test@documenso.com',
|
||||
'test2@documenso.com',
|
||||
'test3@documenso.com',
|
||||
'test4@documenso.com',
|
||||
];
|
||||
|
||||
const createdUsers = [];
|
||||
|
||||
for (const email of testUsers) {
|
||||
const testUser = await prisma.user.upsert({
|
||||
where: {
|
||||
email: email,
|
||||
},
|
||||
create: {
|
||||
name: 'Test User',
|
||||
email: email,
|
||||
emailVerified: new Date(),
|
||||
password: hashSync('password'),
|
||||
roles: [Role.USER],
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
createdUsers.push(testUser);
|
||||
}
|
||||
|
||||
const team1 = await prisma.team.create({
|
||||
data: {
|
||||
name: 'Team 1',
|
||||
url: 'team1',
|
||||
ownerUserId: createdUsers[0].id,
|
||||
},
|
||||
});
|
||||
|
||||
const team2 = await prisma.team.create({
|
||||
data: {
|
||||
name: 'Team 2',
|
||||
url: 'team2',
|
||||
ownerUserId: createdUsers[1].id,
|
||||
},
|
||||
});
|
||||
|
||||
for (const team of [team1, team2]) {
|
||||
await prisma.teamMember.createMany({
|
||||
data: [
|
||||
{
|
||||
teamId: team.id,
|
||||
userId: createdUsers[1].id,
|
||||
role: TeamMemberRole.ADMIN,
|
||||
},
|
||||
{
|
||||
teamId: team.id,
|
||||
userId: createdUsers[2].id,
|
||||
role: TeamMemberRole.MEMBER,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
101
packages/prisma/seed/organisations.ts
Normal file
101
packages/prisma/seed/organisations.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import type { OrganisationMemberRole, OrganisationType } from '@prisma/client';
|
||||
import { OrganisationMemberInviteStatus, type User } from '@prisma/client';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { hashSync } from '@documenso/lib/server-only/auth/hash';
|
||||
import { acceptOrganisationInvitation } from '@documenso/lib/server-only/organisation/accept-organisation-invitation';
|
||||
import { prefixedId } from '@documenso/lib/universal/id';
|
||||
|
||||
import { prisma } from '..';
|
||||
import { seedTestEmail } from './users';
|
||||
|
||||
export const seedOrganisationMembers = async ({
|
||||
members,
|
||||
organisationId,
|
||||
}: {
|
||||
members: {
|
||||
email?: string;
|
||||
name?: string;
|
||||
organisationRole: OrganisationMemberRole;
|
||||
}[];
|
||||
organisationId: string;
|
||||
}) => {
|
||||
const membersToInvite: {
|
||||
email: string;
|
||||
organisationRole: OrganisationMemberRole;
|
||||
}[] = [];
|
||||
|
||||
const createdMembers: User[] = [];
|
||||
|
||||
for (const member of members) {
|
||||
const email = member.email ?? seedTestEmail();
|
||||
|
||||
let newUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: email.toLowerCase(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!newUser) {
|
||||
newUser = await prisma.user.create({
|
||||
data: {
|
||||
name: member.name ?? 'Test user',
|
||||
email: email.toLowerCase(),
|
||||
password: hashSync('password'),
|
||||
emailVerified: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
createdMembers.push(newUser);
|
||||
|
||||
membersToInvite.push({
|
||||
email: newUser.email,
|
||||
organisationRole: member.organisationRole,
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.organisationMemberInvite.createMany({
|
||||
data: membersToInvite.map((invite) => ({
|
||||
id: prefixedId('member_invite'),
|
||||
email: invite.email,
|
||||
organisationId,
|
||||
organisationRole: invite.organisationRole,
|
||||
token: nanoid(32),
|
||||
})),
|
||||
});
|
||||
|
||||
const invites = await prisma.organisationMemberInvite.findMany({
|
||||
where: {
|
||||
organisationId,
|
||||
status: OrganisationMemberInviteStatus.PENDING,
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
invites.map(async (invite) => {
|
||||
await acceptOrganisationInvitation({
|
||||
token: invite.token,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return createdMembers;
|
||||
};
|
||||
|
||||
export const setOrganisationType = async ({
|
||||
organisationId,
|
||||
type,
|
||||
}: {
|
||||
organisationId: string;
|
||||
type: OrganisationType;
|
||||
}) => {
|
||||
await prisma.organisation.update({
|
||||
where: {
|
||||
id: organisationId,
|
||||
},
|
||||
data: {
|
||||
type,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -1,19 +0,0 @@
|
||||
import { prisma } from '..';
|
||||
|
||||
export const seedTestEmail = () => `user-${Date.now()}@test.documenso.com`;
|
||||
|
||||
type SeedSubscriptionOptions = {
|
||||
userId: number;
|
||||
priceId: string;
|
||||
};
|
||||
|
||||
export const seedUserSubscription = async ({ userId, priceId }: SeedSubscriptionOptions) => {
|
||||
return await prisma.subscription.create({
|
||||
data: {
|
||||
userId,
|
||||
planId: Date.now().toString(),
|
||||
priceId,
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -1,8 +1,11 @@
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
import { createTeamMembers } from '@documenso/trpc/server/team-router/create-team-members';
|
||||
|
||||
import { prisma } from '..';
|
||||
import type { Prisma } from '../client';
|
||||
import { TeamMemberInviteStatus, TeamMemberRole } from '../client';
|
||||
import { OrganisationMemberRole, TeamMemberRole } from '../client';
|
||||
import { seedOrganisationMembers } from './organisations';
|
||||
import { seedUser } from './users';
|
||||
|
||||
const EMAIL_DOMAIN = `test.documenso.com`;
|
||||
@ -19,63 +22,54 @@ export const seedTeam = async ({
|
||||
createTeamEmail,
|
||||
createTeamOptions = {},
|
||||
}: SeedTeamOptions = {}) => {
|
||||
const teamUrl = `team-${nanoid()}`;
|
||||
const teamEmail = createTeamEmail === true ? `${teamUrl}@${EMAIL_DOMAIN}` : createTeamEmail;
|
||||
|
||||
const teamOwner = await seedUser({
|
||||
name: `${teamUrl}-original-owner`,
|
||||
email: `${teamUrl}-original-owner@${EMAIL_DOMAIN}`,
|
||||
const {
|
||||
user: owner,
|
||||
team: seededTeam,
|
||||
organisation: seededOrganisation,
|
||||
} = await seedUser({
|
||||
name: 'Owner',
|
||||
teamEmail: createTeamEmail === true ? `${nanoid()}@${EMAIL_DOMAIN}` : createTeamEmail,
|
||||
});
|
||||
|
||||
const teamMembers = await Promise.all(
|
||||
Array.from({ length: createTeamMembers }).map(async (_, i) => {
|
||||
return seedUser({
|
||||
name: `${teamUrl}-member-${i + 1}`,
|
||||
email: `${teamUrl}-member-${i + 1}@${EMAIL_DOMAIN}`,
|
||||
});
|
||||
}),
|
||||
);
|
||||
const teamUrl = seededTeam.url;
|
||||
|
||||
const team = await prisma.team.create({
|
||||
data: {
|
||||
name: teamUrl,
|
||||
url: teamUrl,
|
||||
ownerUserId: teamOwner.id,
|
||||
members: {
|
||||
createMany: {
|
||||
data: [teamOwner, ...teamMembers].map((user) => ({
|
||||
userId: user.id,
|
||||
role: user === teamOwner ? TeamMemberRole.ADMIN : TeamMemberRole.MEMBER,
|
||||
})),
|
||||
},
|
||||
},
|
||||
teamEmail: teamEmail
|
||||
? {
|
||||
create: {
|
||||
email: teamEmail,
|
||||
name: teamEmail,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
...createTeamOptions,
|
||||
},
|
||||
await seedOrganisationMembers({
|
||||
members: Array.from({ length: createTeamMembers }).map((_, i) => ({
|
||||
name: `${teamUrl}-member-${i + 1}`,
|
||||
email: `${teamUrl}-member-${i + 1}@${EMAIL_DOMAIN}`,
|
||||
organisationRole: OrganisationMemberRole.MEMBER,
|
||||
})),
|
||||
organisationId: seededOrganisation.id,
|
||||
});
|
||||
|
||||
return await prisma.team.findFirstOrThrow({
|
||||
const team = await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: team.id,
|
||||
id: seededTeam.id,
|
||||
},
|
||||
include: {
|
||||
teamEmail: true,
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
const organisation = await prisma.organisation.findFirstOrThrow({
|
||||
where: {
|
||||
id: seededOrganisation.id,
|
||||
},
|
||||
include: {
|
||||
owner: true,
|
||||
members: {
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
teamEmail: true,
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
owner,
|
||||
team,
|
||||
organisation,
|
||||
};
|
||||
};
|
||||
|
||||
export const unseedTeam = async (teamUrl: string) => {
|
||||
@ -83,9 +77,6 @@ export const unseedTeam = async (teamUrl: string) => {
|
||||
where: {
|
||||
url: teamUrl,
|
||||
},
|
||||
include: {
|
||||
members: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
@ -97,14 +88,6 @@ export const unseedTeam = async (teamUrl: string) => {
|
||||
url: teamUrl,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.user.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: team.members.map((member) => member.userId),
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
type SeedTeamMemberOptions = {
|
||||
@ -118,48 +101,49 @@ export const seedTeamMember = async ({
|
||||
name,
|
||||
role = TeamMemberRole.ADMIN,
|
||||
}: SeedTeamMemberOptions) => {
|
||||
const user = await seedUser({ name });
|
||||
const { user } = await seedUser({ name });
|
||||
|
||||
await prisma.teamMember.create({
|
||||
data: {
|
||||
teamId,
|
||||
role,
|
||||
userId: user.id,
|
||||
const team = await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
include: {
|
||||
organisation: true,
|
||||
},
|
||||
});
|
||||
|
||||
await seedOrganisationMembers({
|
||||
members: [
|
||||
{
|
||||
name: user.name ?? '',
|
||||
email: user.email,
|
||||
organisationRole: OrganisationMemberRole.MEMBER,
|
||||
},
|
||||
],
|
||||
organisationId: team.organisationId,
|
||||
});
|
||||
|
||||
const { id: organisationMemberId } = await prisma.organisationMember.findFirstOrThrow({
|
||||
where: {
|
||||
userId: user.id,
|
||||
organisationId: team.organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
await createTeamMembers({
|
||||
userId: team.organisation.ownerUserId,
|
||||
teamId,
|
||||
membersToCreate: [
|
||||
{
|
||||
organisationMemberId,
|
||||
teamRole: role,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
type UnseedTeamMemberOptions = {
|
||||
teamId: number;
|
||||
userId: number;
|
||||
};
|
||||
|
||||
export const unseedTeamMember = async ({ teamId, userId }: UnseedTeamMemberOptions) => {
|
||||
await prisma.teamMember.delete({
|
||||
where: {
|
||||
userId_teamId: {
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const seedTeamTransfer = async (options: { newOwnerUserId: number; teamId: number }) => {
|
||||
return await prisma.teamTransferVerification.create({
|
||||
data: {
|
||||
teamId: options.teamId,
|
||||
token: Date.now().toString(),
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24),
|
||||
userId: options.newOwnerUserId,
|
||||
name: '',
|
||||
email: '',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const seedTeamEmail = async ({ email, teamId }: { email: string; teamId: number }) => {
|
||||
return await prisma.teamEmail.create({
|
||||
data: {
|
||||
@ -178,26 +162,6 @@ export const unseedTeamEmail = async ({ teamId }: { teamId: number }) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const seedTeamInvite = async ({
|
||||
email,
|
||||
teamId,
|
||||
role = TeamMemberRole.ADMIN,
|
||||
}: {
|
||||
email: string;
|
||||
teamId: number;
|
||||
role?: TeamMemberRole;
|
||||
}) => {
|
||||
return await prisma.teamMemberInvite.create({
|
||||
data: {
|
||||
email,
|
||||
teamId,
|
||||
role,
|
||||
status: TeamMemberInviteStatus.PENDING,
|
||||
token: Date.now().toString(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const seedTeamEmailVerification = async ({
|
||||
email,
|
||||
teamId,
|
||||
|
||||
@ -17,7 +17,7 @@ const examplePdf = fs
|
||||
type SeedTemplateOptions = {
|
||||
title?: string;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
createTemplateOptions?: Partial<Prisma.TemplateCreateInput>;
|
||||
};
|
||||
|
||||
@ -26,7 +26,11 @@ type CreateTemplateOptions = {
|
||||
createTemplateOptions?: Partial<Prisma.TemplateUncheckedCreateInput>;
|
||||
};
|
||||
|
||||
export const seedBlankTemplate = async (owner: User, options: CreateTemplateOptions = {}) => {
|
||||
export const seedBlankTemplate = async (
|
||||
owner: User,
|
||||
teamId: number,
|
||||
options: CreateTemplateOptions = {},
|
||||
) => {
|
||||
const { key, createTemplateOptions = {} } = options;
|
||||
|
||||
const documentData = await prisma.documentData.create({
|
||||
@ -40,6 +44,7 @@ export const seedBlankTemplate = async (owner: User, options: CreateTemplateOpti
|
||||
return await prisma.template.create({
|
||||
data: {
|
||||
title: `[TEST] Template ${key}`,
|
||||
teamId,
|
||||
templateDocumentDataId: documentData.id,
|
||||
userId: owner.id,
|
||||
...createTemplateOptions,
|
||||
@ -82,15 +87,11 @@ export const seedTemplate = async (options: SeedTemplateOptions) => {
|
||||
role: RecipientRole.SIGNER,
|
||||
},
|
||||
},
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
connect: {
|
||||
id: teamId,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
team: {
|
||||
connect: {
|
||||
id: teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -126,15 +127,11 @@ export const seedDirectTemplate = async (options: SeedTemplateOptions) => {
|
||||
token: Math.random().toString().slice(2, 7),
|
||||
},
|
||||
},
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
connect: {
|
||||
id: teamId,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
team: {
|
||||
connect: {
|
||||
id: teamId,
|
||||
},
|
||||
},
|
||||
...options.createTemplateOptions,
|
||||
},
|
||||
include: {
|
||||
|
||||
@ -1,14 +1,22 @@
|
||||
import { OrganisationType, Role } from '@prisma/client';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
import { hashSync } from '@documenso/lib/server-only/auth/hash';
|
||||
import { createPersonalOrganisation } from '@documenso/lib/server-only/organisation/create-organisation';
|
||||
|
||||
import { prisma } from '..';
|
||||
import { setOrganisationType } from './organisations';
|
||||
|
||||
type SeedUserOptions = {
|
||||
name?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
verified?: boolean;
|
||||
setTeamEmailAsOwner?: boolean;
|
||||
teamEmail?: string;
|
||||
inheritMembers?: boolean;
|
||||
isAdmin?: boolean;
|
||||
isPersonalOrganisation?: boolean;
|
||||
};
|
||||
|
||||
const nanoid = customAlphabet('1234567890abcdef', 10);
|
||||
@ -16,33 +24,77 @@ const nanoid = customAlphabet('1234567890abcdef', 10);
|
||||
export const seedTestEmail = () => `${nanoid()}@test.documenso.com`;
|
||||
|
||||
export const seedUser = async ({
|
||||
name,
|
||||
name = nanoid(),
|
||||
email,
|
||||
password = 'password',
|
||||
verified = true,
|
||||
setTeamEmailAsOwner = false,
|
||||
teamEmail = '',
|
||||
inheritMembers = true,
|
||||
isAdmin = false,
|
||||
isPersonalOrganisation = false,
|
||||
}: SeedUserOptions = {}) => {
|
||||
let url = name;
|
||||
|
||||
if (name) {
|
||||
url = nanoid();
|
||||
} else {
|
||||
name = nanoid();
|
||||
url = name;
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
email = `${nanoid()}@test.documenso.com`;
|
||||
}
|
||||
|
||||
return await prisma.user.create({
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
name,
|
||||
email,
|
||||
email: email.toLowerCase(),
|
||||
password: hashSync(password),
|
||||
emailVerified: verified ? new Date() : undefined,
|
||||
url,
|
||||
roles: isAdmin ? [Role.USER, Role.ADMIN] : [Role.USER],
|
||||
},
|
||||
});
|
||||
|
||||
await createPersonalOrganisation({
|
||||
userId: user.id,
|
||||
inheritMembers,
|
||||
type: isPersonalOrganisation ? OrganisationType.PERSONAL : OrganisationType.ORGANISATION,
|
||||
});
|
||||
|
||||
const organisation = await prisma.organisation.findFirstOrThrow({
|
||||
where: {
|
||||
ownerUserId: user.id,
|
||||
},
|
||||
include: {
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (setTeamEmailAsOwner) {
|
||||
await prisma.teamEmail.create({
|
||||
data: {
|
||||
name: '',
|
||||
teamId: organisation.teams[0].id,
|
||||
email: user.email,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (teamEmail) {
|
||||
await prisma.teamEmail.create({
|
||||
data: {
|
||||
name: '',
|
||||
teamId: organisation.teams[0].id,
|
||||
email: teamEmail,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!isPersonalOrganisation) {
|
||||
await setOrganisationType({
|
||||
organisationId: organisation.id,
|
||||
type: OrganisationType.ORGANISATION,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
organisation,
|
||||
team: organisation.teams[0],
|
||||
};
|
||||
};
|
||||
|
||||
export const unseedUser = async (userId: number) => {
|
||||
|
||||
3
packages/prisma/types/types.d.ts
vendored
3
packages/prisma/types/types.d.ts
vendored
@ -6,12 +6,15 @@ import type {
|
||||
import type { TDocumentEmailSettings } from '@documenso/lib/types/document-email';
|
||||
import type { TDocumentFormValues } from '@documenso/lib/types/document-form-values';
|
||||
import type { TFieldMetaNotOptionalSchema } from '@documenso/lib/types/field-meta';
|
||||
import type { TClaimFlags } from '@documenso/lib/types/subscription';
|
||||
|
||||
/**
|
||||
* Global types for Prisma.Json instances.
|
||||
*/
|
||||
declare global {
|
||||
namespace PrismaJson {
|
||||
type ClaimFlags = TClaimFlags;
|
||||
|
||||
type DocumentFormValues = TDocumentFormValues;
|
||||
type DocumentAuthOptions = TDocumentAuthOptions;
|
||||
type DocumentEmailSettings = TDocumentEmailSettings;
|
||||
|
||||
Reference in New Issue
Block a user