feat: add organisations (#1820)

This commit is contained in:
David Nguyen
2025-06-10 11:49:52 +10:00
committed by GitHub
parent 0b37f19641
commit e6dc237ad2
631 changed files with 37616 additions and 25695 deletions

View File

@ -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;

View File

@ -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";

View File

@ -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"

View File

@ -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[]
}

View File

@ -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,

View File

@ -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,
},
]);
};

View File

@ -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,
},
],
});
}
};

View 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,
},
});
};

View File

@ -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',
},
});
};

View File

@ -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,

View File

@ -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: {

View File

@ -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) => {

View File

@ -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;