mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
1674 lines
54 KiB
TypeScript
1674 lines
54 KiB
TypeScript
/* eslint-disable no-restricted-syntax */
|
|
import { teamMembershipsCrudHandlers } from '@/app/api/latest/team-memberships/crud';
|
|
import { teamsCrudHandlers } from '@/app/api/latest/teams/crud';
|
|
import { usersCrudHandlers } from '@/app/api/latest/users/crud';
|
|
import { CustomerType, EmailOutboxCreatedWith, Prisma, PurchaseCreationSource, SubscriptionStatus } from '@/generated/prisma/client';
|
|
import { overrideEnvironmentConfigOverride } from '@/lib/config';
|
|
import { ensurePermissionDefinition, grantTeamPermission } from '@/lib/permissions';
|
|
import { createOrUpdateProjectWithLegacyConfig, getProject } from '@/lib/projects';
|
|
import { DEFAULT_BRANCH_ID, getSoleTenancyFromProjectBranch, type Tenancy } from '@/lib/tenancies';
|
|
import { getPrismaClientForTenancy, globalPrismaClient, PrismaClientTransaction } from '@/prisma-client';
|
|
import { ALL_APPS } from '@stackframe/stack-shared/dist/apps/apps-config';
|
|
import { DEFAULT_EMAIL_THEME_ID } from '@stackframe/stack-shared/dist/helpers/emails';
|
|
import { AdminUserProjectsCrud, ProjectsCrud } from '@stackframe/stack-shared/dist/interface/crud/projects';
|
|
import { DayInterval } from '@stackframe/stack-shared/dist/utils/dates';
|
|
import { throwErr } from '@stackframe/stack-shared/dist/utils/errors';
|
|
import { typedEntries, typedFromEntries } from '@stackframe/stack-shared/dist/utils/objects';
|
|
import { generateUuid } from '@stackframe/stack-shared/dist/utils/uuids';
|
|
|
|
const DUMMY_PROJECT_ID = '6fbbf22e-f4b2-4c6e-95a1-beab6fa41063';
|
|
const EXPLORATORY_TEAM_DISPLAY_NAME = 'Exploratory Research and Insight Partnership With Very Long Collaborative Name For Testing';
|
|
|
|
export async function seed() {
|
|
process.env.STACK_SEED_MODE = 'true';
|
|
console.log('Seeding database...');
|
|
|
|
// Optional default admin user
|
|
const adminEmail = process.env.STACK_SEED_INTERNAL_PROJECT_USER_EMAIL;
|
|
const adminPassword = process.env.STACK_SEED_INTERNAL_PROJECT_USER_PASSWORD;
|
|
const adminInternalAccess = process.env.STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS === 'true';
|
|
const adminGithubId = process.env.STACK_SEED_INTERNAL_PROJECT_USER_GITHUB_ID;
|
|
|
|
// dashboard settings
|
|
const dashboardDomain = process.env.NEXT_PUBLIC_STACK_DASHBOARD_URL;
|
|
const oauthProviderIds = process.env.STACK_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS?.split(',') ?? [];
|
|
const otpEnabled = process.env.STACK_SEED_INTERNAL_PROJECT_OTP_ENABLED === 'true';
|
|
const signUpEnabled = process.env.STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED === 'true';
|
|
const allowLocalhost = process.env.STACK_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST === 'true';
|
|
|
|
const emulatorEnabled = process.env.STACK_EMULATOR_ENABLED === 'true';
|
|
const emulatorProjectId = process.env.STACK_EMULATOR_PROJECT_ID;
|
|
|
|
const apiKeyId = '3142e763-b230-44b5-8636-aa62f7489c26';
|
|
const defaultUserId = '33e7c043-d2d1-4187-acd3-f91b5ed64b46';
|
|
const internalTeamId = 'a23e1b7f-ab18-41fc-9ee6-7a9ca9fa543c';
|
|
const emulatorAdminUserId = '63abbc96-5329-454a-ba56-e0460173c6c1';
|
|
const emulatorAdminTeamId = '5a0c858b-d9e9-49d4-9943-8ce385d86428';
|
|
|
|
let internalProject = await getProject('internal');
|
|
|
|
if (!internalProject) {
|
|
internalProject = await createOrUpdateProjectWithLegacyConfig({
|
|
type: 'create',
|
|
projectId: 'internal',
|
|
data: {
|
|
display_name: 'Stack Dashboard',
|
|
owner_team_id: internalTeamId,
|
|
description: 'Stack\'s admin dashboard',
|
|
is_production_mode: false,
|
|
config: {
|
|
allow_localhost: true,
|
|
oauth_providers: oauthProviderIds.map((id) => ({
|
|
id: id as any,
|
|
type: 'shared',
|
|
})),
|
|
sign_up_enabled: signUpEnabled,
|
|
credential_enabled: true,
|
|
magic_link_enabled: otpEnabled,
|
|
},
|
|
},
|
|
});
|
|
|
|
console.log('Internal project created');
|
|
}
|
|
|
|
const internalTenancy = await getSoleTenancyFromProjectBranch("internal", DEFAULT_BRANCH_ID);
|
|
const internalPrisma = await getPrismaClientForTenancy(internalTenancy);
|
|
|
|
internalProject = await createOrUpdateProjectWithLegacyConfig({
|
|
projectId: 'internal',
|
|
branchId: DEFAULT_BRANCH_ID,
|
|
type: 'update',
|
|
data: {
|
|
config: {
|
|
create_team_on_sign_up: true,
|
|
sign_up_enabled: signUpEnabled,
|
|
magic_link_enabled: otpEnabled,
|
|
allow_localhost: allowLocalhost,
|
|
client_team_creation_enabled: true,
|
|
domains: [
|
|
...(dashboardDomain && new URL(dashboardDomain).hostname !== 'localhost' ? [{ domain: dashboardDomain, handler_path: '/handler' }] : []),
|
|
...Object.values(internalTenancy.config.domains.trustedDomains)
|
|
.filter((d) => d.baseUrl !== dashboardDomain && d.baseUrl)
|
|
.map((d) => ({ domain: d.baseUrl || throwErr('Domain base URL is required'), handler_path: d.handlerPath })),
|
|
],
|
|
},
|
|
},
|
|
});
|
|
|
|
await overrideEnvironmentConfigOverride({
|
|
projectId: 'internal',
|
|
branchId: DEFAULT_BRANCH_ID,
|
|
environmentConfigOverrideOverride: {
|
|
dataVault: {
|
|
stores: {
|
|
'neon-connection-strings': {
|
|
displayName: 'Neon Connection Strings',
|
|
}
|
|
}
|
|
},
|
|
payments: {
|
|
catalogs: {
|
|
plans: {
|
|
displayName: "Plans",
|
|
}
|
|
},
|
|
products: {
|
|
team: {
|
|
catalogId: "plans",
|
|
displayName: "Team",
|
|
customerType: "team",
|
|
serverOnly: false,
|
|
stackable: false,
|
|
prices: {
|
|
monthly: {
|
|
USD: "49",
|
|
interval: [1, "month"] as any,
|
|
serverOnly: false
|
|
}
|
|
},
|
|
includedItems: {
|
|
dashboard_admins: {
|
|
quantity: 3,
|
|
repeat: "never",
|
|
expires: "when-purchase-expires"
|
|
}
|
|
}
|
|
},
|
|
growth: {
|
|
catalogId: "plans",
|
|
displayName: "Growth",
|
|
customerType: "team",
|
|
serverOnly: false,
|
|
stackable: false,
|
|
prices: {
|
|
monthly: {
|
|
USD: "299",
|
|
interval: [1, "month"] as any,
|
|
serverOnly: false
|
|
}
|
|
},
|
|
includedItems: {
|
|
dashboard_admins: {
|
|
quantity: 5,
|
|
repeat: "never",
|
|
expires: "when-purchase-expires"
|
|
}
|
|
}
|
|
},
|
|
free: {
|
|
catalogId: "plans",
|
|
displayName: "Free",
|
|
customerType: "team",
|
|
serverOnly: false,
|
|
stackable: false,
|
|
prices: "include-by-default",
|
|
includedItems: {
|
|
dashboard_admins: {
|
|
quantity: 1,
|
|
repeat: "never",
|
|
expires: "when-purchase-expires"
|
|
}
|
|
}
|
|
},
|
|
"extra-admins": {
|
|
catalogId: "plans",
|
|
displayName: "Extra Admins",
|
|
customerType: "team",
|
|
serverOnly: false,
|
|
stackable: true,
|
|
prices: {
|
|
monthly: {
|
|
USD: "49",
|
|
interval: [1, "month"] as any,
|
|
serverOnly: false
|
|
}
|
|
},
|
|
includedItems: {
|
|
dashboard_admins: {
|
|
quantity: 1,
|
|
repeat: "never",
|
|
expires: "when-purchase-expires"
|
|
}
|
|
},
|
|
isAddOnTo: {
|
|
team: true,
|
|
growth: true,
|
|
}
|
|
}
|
|
},
|
|
items: {
|
|
dashboard_admins: {
|
|
displayName: "Dashboard Admins",
|
|
customerType: "team"
|
|
}
|
|
},
|
|
},
|
|
apps: {
|
|
installed: typedFromEntries(typedEntries(ALL_APPS).map(([key, value]) => [key, { enabled: true }])),
|
|
},
|
|
}
|
|
});
|
|
|
|
await ensurePermissionDefinition(
|
|
globalPrismaClient,
|
|
internalPrisma,
|
|
{
|
|
id: "team_member",
|
|
scope: "team",
|
|
tenancy: internalTenancy,
|
|
data: {
|
|
description: "1",
|
|
contained_permission_ids: ["$read_members"],
|
|
}
|
|
}
|
|
);
|
|
const updatedInternalTenancy = await getSoleTenancyFromProjectBranch("internal", DEFAULT_BRANCH_ID);
|
|
await ensurePermissionDefinition(
|
|
globalPrismaClient,
|
|
internalPrisma,
|
|
{
|
|
id: "team_admin",
|
|
scope: "team",
|
|
tenancy: updatedInternalTenancy,
|
|
data: {
|
|
description: "2",
|
|
contained_permission_ids: ["$read_members", "$remove_members", "$update_team"],
|
|
}
|
|
}
|
|
);
|
|
|
|
|
|
const internalTeam = await internalPrisma.team.findUnique({
|
|
where: {
|
|
tenancyId_teamId: {
|
|
tenancyId: internalTenancy.id,
|
|
teamId: internalTeamId,
|
|
},
|
|
},
|
|
});
|
|
if (!internalTeam) {
|
|
await internalPrisma.team.create({
|
|
data: {
|
|
tenancyId: internalTenancy.id,
|
|
teamId: internalTeamId,
|
|
displayName: 'Internal Team',
|
|
mirroredProjectId: 'internal',
|
|
mirroredBranchId: DEFAULT_BRANCH_ID,
|
|
},
|
|
});
|
|
console.log('Internal team created');
|
|
}
|
|
|
|
const shouldSeedDummyProject = process.env.STACK_SEED_ENABLE_DUMMY_PROJECT === 'true';
|
|
if (shouldSeedDummyProject) {
|
|
await seedDummyProject({
|
|
ownerTeamId: internalTeamId,
|
|
oauthProviderIds,
|
|
});
|
|
}
|
|
|
|
const keySet = {
|
|
publishableClientKey: process.env.STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY is not set'),
|
|
secretServerKey: process.env.STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY is not set'),
|
|
superSecretAdminKey: process.env.STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY is not set'),
|
|
};
|
|
|
|
await globalPrismaClient.apiKeySet.upsert({
|
|
where: { projectId_id: { projectId: 'internal', id: apiKeyId } },
|
|
update: {
|
|
...keySet,
|
|
},
|
|
create: {
|
|
id: apiKeyId,
|
|
projectId: 'internal',
|
|
description: "Internal API key set",
|
|
expiresAt: new Date('2099-12-31T23:59:59Z'),
|
|
...keySet,
|
|
}
|
|
});
|
|
|
|
console.log('Updated internal API key set');
|
|
|
|
// Create optional default admin user if credentials are provided.
|
|
// This user will be able to login to the dashboard with both email/password and magic link.
|
|
|
|
if ((adminEmail && adminPassword) || adminGithubId) {
|
|
const oldAdminUser = await internalPrisma.projectUser.findFirst({
|
|
where: {
|
|
mirroredProjectId: 'internal',
|
|
mirroredBranchId: DEFAULT_BRANCH_ID,
|
|
projectUserId: defaultUserId
|
|
}
|
|
});
|
|
|
|
if (oldAdminUser) {
|
|
console.log(`Admin user already exists, skipping creation`);
|
|
} else {
|
|
const newUser = await internalPrisma.projectUser.create({
|
|
data: {
|
|
displayName: 'Administrator (created by seed script)',
|
|
projectUserId: defaultUserId,
|
|
tenancyId: internalTenancy.id,
|
|
mirroredProjectId: 'internal',
|
|
mirroredBranchId: DEFAULT_BRANCH_ID,
|
|
}
|
|
});
|
|
|
|
if (adminInternalAccess) {
|
|
await internalPrisma.teamMember.create({
|
|
data: {
|
|
tenancyId: internalTenancy.id,
|
|
teamId: internalTeamId,
|
|
projectUserId: defaultUserId,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (adminEmail && adminPassword) {
|
|
await usersCrudHandlers.adminUpdate({
|
|
tenancy: internalTenancy,
|
|
user_id: defaultUserId,
|
|
data: {
|
|
password: adminPassword,
|
|
primary_email: adminEmail,
|
|
primary_email_auth_enabled: true,
|
|
},
|
|
});
|
|
|
|
console.log(`Added admin user with email ${adminEmail}`);
|
|
}
|
|
|
|
if (adminGithubId) {
|
|
const githubAccount = await internalPrisma.projectUserOAuthAccount.findFirst({
|
|
where: {
|
|
tenancyId: internalTenancy.id,
|
|
configOAuthProviderId: 'github',
|
|
providerAccountId: adminGithubId,
|
|
}
|
|
});
|
|
|
|
if (githubAccount) {
|
|
console.log(`GitHub account already exists, skipping creation`);
|
|
} else {
|
|
await internalPrisma.projectUserOAuthAccount.create({
|
|
data: {
|
|
tenancyId: internalTenancy.id,
|
|
projectUserId: newUser.projectUserId,
|
|
configOAuthProviderId: 'github',
|
|
providerAccountId: adminGithubId
|
|
}
|
|
});
|
|
|
|
await internalPrisma.authMethod.create({
|
|
data: {
|
|
tenancyId: internalTenancy.id,
|
|
projectUserId: newUser.projectUserId,
|
|
oauthAuthMethod: {
|
|
create: {
|
|
projectUserId: newUser.projectUserId,
|
|
configOAuthProviderId: 'github',
|
|
providerAccountId: adminGithubId,
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log(`Added admin user with GitHub ID ${adminGithubId}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
await grantTeamPermission(internalPrisma, {
|
|
tenancy: internalTenancy,
|
|
teamId: internalTeamId,
|
|
userId: defaultUserId,
|
|
permissionId: "team_admin",
|
|
});
|
|
}
|
|
|
|
if (emulatorEnabled) {
|
|
if (!emulatorProjectId) {
|
|
throw new Error('STACK_EMULATOR_PROJECT_ID is not set');
|
|
}
|
|
|
|
const emulatorTeam = await internalPrisma.team.findUnique({
|
|
where: {
|
|
tenancyId_teamId: {
|
|
tenancyId: internalTenancy.id,
|
|
teamId: emulatorAdminTeamId,
|
|
},
|
|
},
|
|
});
|
|
if (!emulatorTeam) {
|
|
await internalPrisma.team.create({
|
|
data: {
|
|
tenancyId: internalTenancy.id,
|
|
teamId: emulatorAdminTeamId,
|
|
displayName: 'Emulator Team',
|
|
mirroredProjectId: "internal",
|
|
mirroredBranchId: DEFAULT_BRANCH_ID,
|
|
},
|
|
});
|
|
console.log('Created emulator team');
|
|
}
|
|
|
|
const existingUser = await internalPrisma.projectUser.findFirst({
|
|
where: {
|
|
mirroredProjectId: 'internal',
|
|
mirroredBranchId: DEFAULT_BRANCH_ID,
|
|
projectUserId: emulatorAdminUserId,
|
|
}
|
|
});
|
|
|
|
if (existingUser) {
|
|
console.log('Emulator user already exists, skipping creation');
|
|
} else {
|
|
const newEmulatorUser = await internalPrisma.projectUser.create({
|
|
data: {
|
|
displayName: 'Local Emulator User',
|
|
projectUserId: emulatorAdminUserId,
|
|
tenancyId: internalTenancy.id,
|
|
mirroredProjectId: 'internal',
|
|
mirroredBranchId: DEFAULT_BRANCH_ID,
|
|
}
|
|
});
|
|
|
|
await internalPrisma.teamMember.create({
|
|
data: {
|
|
tenancyId: internalTenancy.id,
|
|
teamId: emulatorAdminTeamId,
|
|
projectUserId: newEmulatorUser.projectUserId,
|
|
},
|
|
});
|
|
|
|
await usersCrudHandlers.adminUpdate({
|
|
tenancy: internalTenancy,
|
|
user_id: newEmulatorUser.projectUserId,
|
|
data: {
|
|
password: 'LocalEmulatorPassword',
|
|
primary_email: 'local-emulator@stack-auth.com',
|
|
primary_email_auth_enabled: true,
|
|
},
|
|
});
|
|
|
|
console.log('Created emulator user');
|
|
}
|
|
|
|
const existingProject = await internalPrisma.project.findUnique({
|
|
where: {
|
|
id: emulatorProjectId,
|
|
},
|
|
});
|
|
|
|
if (existingProject) {
|
|
console.log('Emulator project already exists, skipping creation');
|
|
} else {
|
|
await createOrUpdateProjectWithLegacyConfig({
|
|
projectId: emulatorProjectId,
|
|
type: 'create',
|
|
data: {
|
|
display_name: 'Emulator Project',
|
|
owner_team_id: emulatorAdminTeamId,
|
|
config: {
|
|
allow_localhost: true,
|
|
create_team_on_sign_up: false,
|
|
client_team_creation_enabled: false,
|
|
passkey_enabled: true,
|
|
oauth_providers: oauthProviderIds.map((id) => ({
|
|
id: id as any,
|
|
type: 'shared',
|
|
})),
|
|
}
|
|
},
|
|
});
|
|
|
|
console.log('Created emulator project');
|
|
}
|
|
}
|
|
|
|
console.log('Seeding complete!');
|
|
}
|
|
|
|
type DummyProjectSeedOptions = {
|
|
ownerTeamId: string,
|
|
oauthProviderIds: string[],
|
|
};
|
|
|
|
type TeamSeed = {
|
|
displayName: string,
|
|
profileImageUrl?: string,
|
|
};
|
|
|
|
type UserSeedOauthProvider = {
|
|
providerId: string,
|
|
accountId: string,
|
|
};
|
|
|
|
type UserSeed = {
|
|
displayName?: string,
|
|
email: string,
|
|
profileImageUrl?: string,
|
|
teamDisplayNames: string[],
|
|
primaryEmailVerified: boolean,
|
|
isAnonymous: boolean,
|
|
oauthProviders: UserSeedOauthProvider[],
|
|
};
|
|
|
|
type SeedDummyTeamsOptions = {
|
|
prisma: PrismaClientTransaction,
|
|
tenancy: Tenancy,
|
|
};
|
|
|
|
type SeedDummyUsersOptions = {
|
|
prisma: PrismaClientTransaction,
|
|
tenancy: Tenancy,
|
|
teamNameToId: Map<string, string>,
|
|
};
|
|
|
|
type PaymentsSetup = {
|
|
paymentsProducts: Record<string, unknown>,
|
|
paymentsOverride: Record<string, unknown>,
|
|
};
|
|
|
|
async function seedDummyTeams(options: SeedDummyTeamsOptions): Promise<Map<string, string>> {
|
|
const { prisma, tenancy } = options;
|
|
|
|
const teamSeeds = [
|
|
{ displayName: 'Design Systems Lab', profileImageUrl: 'https://avatar.vercel.sh/design-systems?size=96&background=312e81&color=fff' },
|
|
{ displayName: 'Prototype Garage' },
|
|
{ displayName: EXPLORATORY_TEAM_DISPLAY_NAME, profileImageUrl: 'https://avatar.vercel.sh/exploratory-research?size=96&background=2563eb&color=fff' },
|
|
{ displayName: 'Launch Council', profileImageUrl: 'https://avatar.vercel.sh/launch-council?size=96&background=172554&color=fff' },
|
|
{ displayName: 'Growth Loop', profileImageUrl: 'https://avatar.vercel.sh/growth-loop?size=96&background=16a34a&color=fff' },
|
|
{ displayName: 'QA Collective' },
|
|
{ displayName: 'Customer Advisory Board', profileImageUrl: 'https://avatar.vercel.sh/customer-advisory?size=96&background=854d0e&color=fff' },
|
|
{ displayName: 'Ops', profileImageUrl: 'https://avatar.vercel.sh/ops-team?size=96&background=ea580c&color=fff' },
|
|
] satisfies TeamSeed[];
|
|
|
|
const teamNameToId = new Map<string, string>();
|
|
for (const team of teamSeeds) {
|
|
const existingTeam = await prisma.team.findFirst({
|
|
where: {
|
|
tenancyId: tenancy.id,
|
|
displayName: team.displayName,
|
|
},
|
|
});
|
|
if (existingTeam) {
|
|
teamNameToId.set(team.displayName, existingTeam.teamId);
|
|
continue;
|
|
}
|
|
|
|
const createdTeam = await teamsCrudHandlers.adminCreate({
|
|
tenancy,
|
|
data: {
|
|
display_name: team.displayName,
|
|
profile_image_url: team.profileImageUrl ?? null,
|
|
},
|
|
});
|
|
teamNameToId.set(team.displayName, createdTeam.id);
|
|
}
|
|
|
|
return teamNameToId;
|
|
}
|
|
|
|
async function seedDummyUsers(options: SeedDummyUsersOptions): Promise<Map<string, string>> {
|
|
const { prisma, tenancy, teamNameToId } = options;
|
|
|
|
const userSeeds = [
|
|
{
|
|
displayName: 'Amelia Chen',
|
|
email: 'amelia.chen@dummy.dev',
|
|
teamDisplayNames: ['Design Systems Lab', 'Prototype Garage'],
|
|
primaryEmailVerified: true,
|
|
isAnonymous: false,
|
|
oauthProviders: [
|
|
{ providerId: 'github', accountId: 'amelia-chen-gh' },
|
|
],
|
|
},
|
|
{
|
|
email: 'leo.park@dummy.dev',
|
|
teamDisplayNames: ['Design Systems Lab', 'QA Collective'],
|
|
primaryEmailVerified: false,
|
|
isAnonymous: false,
|
|
oauthProviders: [],
|
|
},
|
|
{
|
|
displayName: 'Some-long-display-name with-middle-name with-last-name',
|
|
email: 'isla.rodriguez@dummy.dev',
|
|
teamDisplayNames: [EXPLORATORY_TEAM_DISPLAY_NAME, 'Customer Advisory Board'],
|
|
primaryEmailVerified: true,
|
|
isAnonymous: false,
|
|
oauthProviders: [
|
|
{ providerId: 'google', accountId: 'isla-rodriguez-google' },
|
|
{ providerId: 'microsoft', accountId: 'isla-rodriguez-msft' },
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Al',
|
|
email: 'milo.adeyemi@dummy.dev',
|
|
teamDisplayNames: [EXPLORATORY_TEAM_DISPLAY_NAME, 'Launch Council'],
|
|
primaryEmailVerified: true,
|
|
isAnonymous: true,
|
|
oauthProviders: [],
|
|
},
|
|
{
|
|
displayName: 'Priya Narang',
|
|
email: 'priya.narang@dummy.dev',
|
|
teamDisplayNames: ['Launch Council', 'Ops'],
|
|
primaryEmailVerified: false,
|
|
isAnonymous: false,
|
|
oauthProviders: [
|
|
{ providerId: 'spotify', accountId: 'priya-narang-spotify' },
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Jonas Richter',
|
|
email: 'jonas.richter@dummy.dev',
|
|
profileImageUrl: 'https://avatar.vercel.sh/jonas-richter?size=96',
|
|
teamDisplayNames: ['Launch Council', 'QA Collective'],
|
|
primaryEmailVerified: true,
|
|
isAnonymous: false,
|
|
oauthProviders: [],
|
|
},
|
|
{
|
|
displayName: 'Chioma Mensah',
|
|
email: 'chioma.mensah@dummy.dev',
|
|
profileImageUrl: 'https://avatar.vercel.sh/chioma-mensah?size=96',
|
|
teamDisplayNames: ['Design Systems Lab', 'Ops'],
|
|
primaryEmailVerified: false,
|
|
isAnonymous: true,
|
|
oauthProviders: [
|
|
{ providerId: 'google', accountId: 'chioma-mensah-google' },
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Nia Holloway',
|
|
email: 'nia.holloway@dummy.dev',
|
|
teamDisplayNames: ['QA Collective'],
|
|
primaryEmailVerified: true,
|
|
isAnonymous: false,
|
|
oauthProviders: [],
|
|
},
|
|
{
|
|
displayName: 'Mateo Silva',
|
|
email: 'mateo.silva@dummy.dev',
|
|
teamDisplayNames: ['Growth Loop', 'Launch Council'],
|
|
primaryEmailVerified: false,
|
|
isAnonymous: false,
|
|
oauthProviders: [
|
|
{ providerId: 'github', accountId: 'mateo-silva-gh' },
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Harper Lin',
|
|
email: 'harper.lin@dummy.dev',
|
|
teamDisplayNames: ['Growth Loop', 'Customer Advisory Board'],
|
|
primaryEmailVerified: true,
|
|
isAnonymous: false,
|
|
oauthProviders: [],
|
|
},
|
|
{
|
|
displayName: 'Zara Malik',
|
|
email: 'zara.malik@dummy.dev',
|
|
profileImageUrl: 'https://avatar.vercel.sh/zara-malik?size=96',
|
|
teamDisplayNames: ['Prototype Garage', EXPLORATORY_TEAM_DISPLAY_NAME],
|
|
primaryEmailVerified: true,
|
|
isAnonymous: false,
|
|
oauthProviders: [],
|
|
},
|
|
{
|
|
displayName: 'Luca Bennett',
|
|
email: 'luca.bennett@dummy.dev',
|
|
teamDisplayNames: ['Growth Loop', 'Ops'],
|
|
primaryEmailVerified: false,
|
|
isAnonymous: false,
|
|
oauthProviders: [],
|
|
},
|
|
{
|
|
displayName: 'Evelyn Brooks',
|
|
email: 'evelyn.brooks@dummy.dev',
|
|
profileImageUrl: 'https://avatar.vercel.sh/evelyn-brooks?size=96&background=15803d&color=fff',
|
|
teamDisplayNames: ['Customer Advisory Board'],
|
|
primaryEmailVerified: true,
|
|
isAnonymous: false,
|
|
oauthProviders: [],
|
|
},
|
|
{
|
|
displayName: 'Theo Fischer',
|
|
email: 'theo.fischer@dummy.dev',
|
|
profileImageUrl: 'https://avatar.vercel.sh/theo-fischer?size=96&background=5b21b6&color=fff',
|
|
teamDisplayNames: ['QA Collective', 'Prototype Garage'],
|
|
primaryEmailVerified: true,
|
|
isAnonymous: false,
|
|
oauthProviders: [
|
|
{ providerId: 'microsoft', accountId: 'theo-fischer-msft' },
|
|
],
|
|
},
|
|
{
|
|
email: 'naomi.patel@dummy.dev',
|
|
teamDisplayNames: ['Prototype Garage', 'Design Systems Lab'],
|
|
primaryEmailVerified: false,
|
|
isAnonymous: false,
|
|
oauthProviders: [],
|
|
},
|
|
{
|
|
displayName: 'Kai Romero',
|
|
email: 'kai.romero@dummy.dev',
|
|
teamDisplayNames: ['Growth Loop'],
|
|
primaryEmailVerified: true,
|
|
isAnonymous: false,
|
|
oauthProviders: [],
|
|
},
|
|
] satisfies UserSeed[];
|
|
|
|
const userEmailToId = new Map<string, string>();
|
|
|
|
for (const user of userSeeds) {
|
|
const existingUser = await prisma.projectUser.findFirst({
|
|
where: {
|
|
tenancyId: tenancy.id,
|
|
contactChannels: {
|
|
some: {
|
|
type: 'EMAIL',
|
|
value: user.email,
|
|
},
|
|
},
|
|
},
|
|
select: {
|
|
projectUserId: true,
|
|
},
|
|
});
|
|
|
|
let userId = existingUser?.projectUserId;
|
|
if (!userId) {
|
|
const createdUser = await usersCrudHandlers.adminCreate({
|
|
tenancy,
|
|
data: {
|
|
display_name: user.displayName ?? null,
|
|
primary_email: user.email,
|
|
primary_email_auth_enabled: true,
|
|
primary_email_verified: user.primaryEmailVerified,
|
|
otp_auth_enabled: false,
|
|
is_anonymous: user.isAnonymous,
|
|
oauth_providers: user.oauthProviders.map((provider) => ({
|
|
id: provider.providerId,
|
|
account_id: provider.accountId,
|
|
email: user.email,
|
|
})),
|
|
profile_image_url: user.profileImageUrl ?? null,
|
|
},
|
|
});
|
|
userId = createdUser.id;
|
|
}
|
|
|
|
userEmailToId.set(user.email, userId);
|
|
|
|
for (const teamName of user.teamDisplayNames) {
|
|
const teamId = teamNameToId.get(teamName) ?? throwErr(`Unknown dummy project team ${teamName}`);
|
|
const existingMembership = await prisma.teamMember.findUnique({
|
|
where: {
|
|
tenancyId_projectUserId_teamId: {
|
|
tenancyId: tenancy.id,
|
|
projectUserId: userId,
|
|
teamId,
|
|
},
|
|
},
|
|
});
|
|
if (existingMembership) continue;
|
|
|
|
await teamMembershipsCrudHandlers.adminCreate({
|
|
tenancy,
|
|
team_id: teamId,
|
|
user_id: userId,
|
|
data: {},
|
|
});
|
|
}
|
|
}
|
|
|
|
return userEmailToId;
|
|
}
|
|
|
|
function buildDummyPaymentsSetup(): PaymentsSetup {
|
|
const monthlyInterval: DayInterval = [1, 'month'];
|
|
const yearlyInterval: DayInterval = [1, 'year'];
|
|
const twoWeekInterval: DayInterval = [2, 'week'];
|
|
|
|
const paymentsProducts = {
|
|
'starter': {
|
|
displayName: 'Starter',
|
|
catalogId: 'workspace',
|
|
customerType: 'user',
|
|
serverOnly: false,
|
|
stackable: false,
|
|
freeTrial: twoWeekInterval as any,
|
|
prices: {
|
|
monthly: {
|
|
USD: '39',
|
|
interval: monthlyInterval as any,
|
|
serverOnly: false,
|
|
freeTrial: twoWeekInterval as any,
|
|
},
|
|
},
|
|
includedItems: {
|
|
studio_seats: {
|
|
quantity: 5,
|
|
repeat: monthlyInterval as any,
|
|
expires: 'when-repeated',
|
|
},
|
|
review_passes: {
|
|
quantity: 50,
|
|
repeat: monthlyInterval as any,
|
|
expires: 'when-repeated',
|
|
},
|
|
},
|
|
},
|
|
'growth': {
|
|
displayName: 'Growth',
|
|
catalogId: 'workspace',
|
|
customerType: 'user',
|
|
serverOnly: false,
|
|
stackable: false,
|
|
prices: {
|
|
monthly: {
|
|
USD: '129',
|
|
interval: monthlyInterval as any,
|
|
serverOnly: false,
|
|
},
|
|
annual: {
|
|
USD: '1290',
|
|
interval: yearlyInterval as any,
|
|
serverOnly: false,
|
|
},
|
|
},
|
|
includedItems: {
|
|
studio_seats: {
|
|
quantity: 25,
|
|
repeat: monthlyInterval as any,
|
|
expires: 'when-repeated',
|
|
},
|
|
review_passes: {
|
|
quantity: 250,
|
|
repeat: monthlyInterval as any,
|
|
expires: 'when-repeated',
|
|
},
|
|
automation_minutes: {
|
|
quantity: 1000,
|
|
repeat: monthlyInterval as any,
|
|
expires: 'when-repeated',
|
|
},
|
|
},
|
|
},
|
|
'regression-addon': {
|
|
displayName: 'Regression Add-on',
|
|
catalogId: 'add_ons',
|
|
customerType: 'user',
|
|
serverOnly: false,
|
|
stackable: true,
|
|
prices: {
|
|
monthly: {
|
|
USD: '199',
|
|
interval: monthlyInterval as any,
|
|
serverOnly: false,
|
|
},
|
|
},
|
|
includedItems: {
|
|
snapshot_credits: {
|
|
quantity: 500,
|
|
repeat: monthlyInterval as any,
|
|
expires: 'when-repeated',
|
|
},
|
|
},
|
|
isAddOnTo: {
|
|
'starter': true,
|
|
'growth': true,
|
|
},
|
|
},
|
|
};
|
|
|
|
const paymentsOverride = {
|
|
testMode: true,
|
|
catalogs: {
|
|
workspace: {
|
|
displayName: 'Workspace Plans',
|
|
},
|
|
add_ons: {
|
|
displayName: 'Add-ons',
|
|
},
|
|
},
|
|
items: {
|
|
studio_seats: {
|
|
displayName: 'Studio Seats',
|
|
customerType: 'user',
|
|
},
|
|
review_passes: {
|
|
displayName: 'Reviewer Passes',
|
|
customerType: 'user',
|
|
},
|
|
automation_minutes: {
|
|
displayName: 'Automation Minutes',
|
|
customerType: 'user',
|
|
},
|
|
snapshot_credits: {
|
|
displayName: 'Snapshot Credits',
|
|
customerType: 'user',
|
|
},
|
|
},
|
|
products: paymentsProducts,
|
|
};
|
|
|
|
return {
|
|
paymentsProducts,
|
|
paymentsOverride,
|
|
};
|
|
}
|
|
|
|
async function seedDummyProject(options: DummyProjectSeedOptions) {
|
|
const baseProjectData = {
|
|
display_name: 'Dummy Project',
|
|
is_production_mode: false,
|
|
config: {
|
|
allow_localhost: true,
|
|
sign_up_enabled: true,
|
|
credential_enabled: true,
|
|
magic_link_enabled: true,
|
|
passkey_enabled: true,
|
|
client_team_creation_enabled: true,
|
|
client_user_deletion_enabled: true,
|
|
allow_user_api_keys: true,
|
|
allow_team_api_keys: true,
|
|
create_team_on_sign_up: false,
|
|
email_theme: DEFAULT_EMAIL_THEME_ID,
|
|
email_config: {
|
|
type: 'shared',
|
|
},
|
|
oauth_providers: options.oauthProviderIds.map((id) => ({
|
|
id: id as any,
|
|
type: 'shared',
|
|
})),
|
|
domains: [],
|
|
},
|
|
} satisfies ProjectsCrud["Admin"]["Update"];
|
|
const projectCreateData: AdminUserProjectsCrud["Admin"]["Create"] = {
|
|
...baseProjectData,
|
|
owner_team_id: options.ownerTeamId,
|
|
};
|
|
|
|
const existingProject = await getProject(DUMMY_PROJECT_ID);
|
|
if (!existingProject) {
|
|
await createOrUpdateProjectWithLegacyConfig({
|
|
type: 'create',
|
|
projectId: DUMMY_PROJECT_ID,
|
|
data: projectCreateData,
|
|
});
|
|
console.log('Created dummy project');
|
|
} else {
|
|
await createOrUpdateProjectWithLegacyConfig({
|
|
type: 'update',
|
|
projectId: DUMMY_PROJECT_ID,
|
|
branchId: DEFAULT_BRANCH_ID,
|
|
data: baseProjectData,
|
|
});
|
|
}
|
|
|
|
const dummyTenancy = await getSoleTenancyFromProjectBranch(DUMMY_PROJECT_ID, DEFAULT_BRANCH_ID);
|
|
const dummyPrisma = await getPrismaClientForTenancy(dummyTenancy);
|
|
|
|
const teamNameToId = await seedDummyTeams({
|
|
prisma: dummyPrisma,
|
|
tenancy: dummyTenancy,
|
|
});
|
|
const userEmailToId = await seedDummyUsers({
|
|
prisma: dummyPrisma,
|
|
tenancy: dummyTenancy,
|
|
teamNameToId,
|
|
});
|
|
const { paymentsProducts, paymentsOverride } = buildDummyPaymentsSetup();
|
|
|
|
await overrideEnvironmentConfigOverride({
|
|
projectId: DUMMY_PROJECT_ID,
|
|
branchId: DEFAULT_BRANCH_ID,
|
|
environmentConfigOverrideOverride: {
|
|
payments: paymentsOverride as any,
|
|
apps: {
|
|
installed: typedFromEntries(typedEntries(ALL_APPS).map(([key]) => [key, { enabled: true }])),
|
|
},
|
|
},
|
|
});
|
|
|
|
await globalPrismaClient.project.update({
|
|
where: {
|
|
id: DUMMY_PROJECT_ID,
|
|
},
|
|
data: {
|
|
stripeAccountId: "sample-stripe-account-id"
|
|
},
|
|
});
|
|
|
|
await seedDummyTransactions({
|
|
prisma: dummyPrisma,
|
|
tenancyId: dummyTenancy.id,
|
|
teamNameToId,
|
|
userEmailToId,
|
|
paymentsProducts,
|
|
});
|
|
|
|
await seedDummyEmails({
|
|
prisma: dummyPrisma,
|
|
tenancyId: dummyTenancy.id,
|
|
userEmailToId,
|
|
});
|
|
|
|
await seedDummySessionActivityEvents({
|
|
tenancyId: dummyTenancy.id,
|
|
projectId: DUMMY_PROJECT_ID,
|
|
userEmailToId,
|
|
});
|
|
|
|
console.log('Seeded dummy project data');
|
|
}
|
|
|
|
type TransactionsSeedOptions = {
|
|
prisma: PrismaClientTransaction,
|
|
tenancyId: string,
|
|
teamNameToId: Map<string, string>,
|
|
userEmailToId: Map<string, string>,
|
|
paymentsProducts: Record<string, unknown>,
|
|
};
|
|
|
|
type SubscriptionSeed = {
|
|
id: string,
|
|
customerType: CustomerType,
|
|
customerId: string,
|
|
productId?: string,
|
|
priceId?: string,
|
|
product: Prisma.InputJsonValue,
|
|
quantity: number,
|
|
status: SubscriptionStatus,
|
|
creationSource: PurchaseCreationSource,
|
|
currentPeriodStart: Date,
|
|
currentPeriodEnd: Date,
|
|
cancelAtPeriodEnd: boolean,
|
|
stripeSubscriptionId?: string | null,
|
|
createdAt: Date,
|
|
};
|
|
|
|
type ItemQuantityChangeSeed = {
|
|
id: string,
|
|
customerType: CustomerType,
|
|
customerId: string,
|
|
itemId: string,
|
|
quantity: number,
|
|
description?: string,
|
|
expiresAt?: Date | null,
|
|
createdAt: Date,
|
|
};
|
|
|
|
type OneTimePurchaseSeed = {
|
|
id: string,
|
|
customerType: CustomerType,
|
|
customerId: string,
|
|
productId?: string,
|
|
priceId?: string,
|
|
product: Prisma.InputJsonValue,
|
|
quantity: number,
|
|
creationSource: PurchaseCreationSource,
|
|
stripePaymentIntentId?: string | null,
|
|
createdAt: Date,
|
|
};
|
|
|
|
type EmailSeedOptions = {
|
|
prisma: PrismaClientTransaction,
|
|
tenancyId: string,
|
|
userEmailToId: Map<string, string>,
|
|
};
|
|
|
|
type EmailOutboxSeed = {
|
|
id: string,
|
|
subject: string,
|
|
html?: string,
|
|
text?: string,
|
|
createdAt: Date,
|
|
userEmail?: string,
|
|
hasError?: boolean,
|
|
};
|
|
|
|
const cloneJson = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;
|
|
|
|
const DUMMY_SEED_IDS = {
|
|
subscriptions: {
|
|
designSystemsGrowth: 'a296195f-c460-4cd6-b4c4-6cd359b4c643',
|
|
prototypeStarterTrial: '5a255248-4d42-4d61-95f9-f53e97c3f2dd',
|
|
growthLoopRegression: '813c4380-475b-4cb8-ac1a-5204d9839b36',
|
|
mateoGrowthAnnual: 'c4acea49-302a-43b9-82a7-446b19e0e662',
|
|
legacyEnterprise: '11664974-38ff-4356-8e39-2fa9105ed84f',
|
|
},
|
|
itemQuantityChanges: {
|
|
designSeatsGrant: '44ca1801-0732-4273-ae14-4fd1c3999e24',
|
|
growthSnapshotAdjustment: 'aad066cd-d853-4bec-97ae-91154bcd4897',
|
|
opsAutomationCredit: 'a3e515dd-9332-4b15-b41a-90b9d6a37276',
|
|
legacyReviewPass: 'b3c20e4f-608d-4c34-9c18-4a5c63780666',
|
|
},
|
|
oneTimePurchases: {
|
|
qaAddon: '9718d5b1-d993-4979-ad68-532f225605ab',
|
|
ameliaSeatPack: '0b696a83-c54e-4a74-ae47-3ac5a4db49e6',
|
|
launchCouncilUpfront: '10766081-37fd-410c-8b2e-1c3351e2d364',
|
|
designAuditPass: '5939f45e-1cf0-4f76-98f9-d999ed45405b',
|
|
},
|
|
emails: {
|
|
welcomeAmelia: 'af8cfd90-8912-4bf7-93a7-20ff2be54767',
|
|
passkeyMilo: 'd534d777-5aa2-4014-a198-6484bbadcbf2',
|
|
invitePriya: 'b7e31f58-cfd7-46cd-920f-d7616ad66bed',
|
|
statusDigest: '2423e8d8-72cf-4355-a475-c2028e3ea958',
|
|
templateFailure: 'faa33233-ba8d-4819-a89a-d442003cd589',
|
|
},
|
|
} as const;
|
|
|
|
async function seedDummyTransactions(options: TransactionsSeedOptions) {
|
|
const {
|
|
prisma,
|
|
tenancyId,
|
|
teamNameToId,
|
|
userEmailToId,
|
|
paymentsProducts,
|
|
} = options;
|
|
|
|
const resolveTeamId = (teamName: string) => teamNameToId.get(teamName) ?? throwErr(`Unknown dummy project team ${teamName}`);
|
|
const resolveUserId = (email: string) => userEmailToId.get(email) ?? throwErr(`Unknown dummy project user ${email}`);
|
|
const resolveProduct = (productId: string): Prisma.InputJsonValue => {
|
|
const product = paymentsProducts[productId];
|
|
if (!product) {
|
|
throwErr(`Unknown payments product ${productId}`);
|
|
}
|
|
return cloneJson(product) as Prisma.InputJsonValue;
|
|
};
|
|
|
|
const subscriptionSeeds: SubscriptionSeed[] = [
|
|
{
|
|
id: DUMMY_SEED_IDS.subscriptions.designSystemsGrowth,
|
|
customerType: CustomerType.TEAM,
|
|
customerId: resolveTeamId('Design Systems Lab'),
|
|
productId: 'growth',
|
|
priceId: 'monthly',
|
|
product: resolveProduct('growth'),
|
|
quantity: 25,
|
|
status: SubscriptionStatus.active,
|
|
creationSource: PurchaseCreationSource.PURCHASE_PAGE,
|
|
currentPeriodStart: new Date('2024-05-01T00:00:00.000Z'),
|
|
currentPeriodEnd: new Date('2024-06-01T00:00:00.000Z'),
|
|
cancelAtPeriodEnd: false,
|
|
stripeSubscriptionId: 'sub_growth_designsystems',
|
|
createdAt: new Date('2024-04-15T10:00:00.000Z'),
|
|
},
|
|
{
|
|
id: DUMMY_SEED_IDS.subscriptions.prototypeStarterTrial,
|
|
customerType: CustomerType.TEAM,
|
|
customerId: resolveTeamId('Prototype Garage'),
|
|
productId: 'starter',
|
|
priceId: 'monthly',
|
|
product: resolveProduct('starter'),
|
|
quantity: 5,
|
|
status: SubscriptionStatus.trialing,
|
|
creationSource: PurchaseCreationSource.TEST_MODE,
|
|
currentPeriodStart: new Date('2024-05-20T00:00:00.000Z'),
|
|
currentPeriodEnd: new Date('2024-06-03T00:00:00.000Z'),
|
|
cancelAtPeriodEnd: false,
|
|
stripeSubscriptionId: 'sub_starter_prototype',
|
|
createdAt: new Date('2024-05-19T08:00:00.000Z'),
|
|
},
|
|
{
|
|
id: DUMMY_SEED_IDS.subscriptions.growthLoopRegression,
|
|
customerType: CustomerType.TEAM,
|
|
customerId: resolveTeamId('Growth Loop'),
|
|
productId: 'regression-addon',
|
|
priceId: 'monthly',
|
|
product: resolveProduct('regression-addon'),
|
|
quantity: 3,
|
|
status: SubscriptionStatus.past_due,
|
|
creationSource: PurchaseCreationSource.PURCHASE_PAGE,
|
|
currentPeriodStart: new Date('2024-04-15T00:00:00.000Z'),
|
|
currentPeriodEnd: new Date('2024-05-15T00:00:00.000Z'),
|
|
cancelAtPeriodEnd: true,
|
|
stripeSubscriptionId: 'sub_regression_growthloop',
|
|
createdAt: new Date('2024-04-10T12:00:00.000Z'),
|
|
},
|
|
{
|
|
id: DUMMY_SEED_IDS.subscriptions.mateoGrowthAnnual,
|
|
customerType: CustomerType.USER,
|
|
customerId: resolveUserId('mateo.silva@dummy.dev'),
|
|
productId: 'growth',
|
|
priceId: 'annual',
|
|
product: resolveProduct('growth'),
|
|
quantity: 1,
|
|
status: SubscriptionStatus.paused,
|
|
creationSource: PurchaseCreationSource.API_GRANT,
|
|
currentPeriodStart: new Date('2024-02-01T00:00:00.000Z'),
|
|
currentPeriodEnd: new Date('2025-02-01T00:00:00.000Z'),
|
|
cancelAtPeriodEnd: true,
|
|
stripeSubscriptionId: null,
|
|
createdAt: new Date('2024-02-01T00:00:00.000Z'),
|
|
},
|
|
{
|
|
id: DUMMY_SEED_IDS.subscriptions.legacyEnterprise,
|
|
customerType: CustomerType.CUSTOM,
|
|
customerId: 'enterprise-alpha',
|
|
productId: 'legacy-enterprise',
|
|
priceId: undefined,
|
|
product: cloneJson({
|
|
displayName: 'Legacy Enterprise Pilot',
|
|
catalogId: 'workspace',
|
|
customerType: 'user',
|
|
prices: 'include-by-default',
|
|
}),
|
|
quantity: 1,
|
|
status: SubscriptionStatus.canceled,
|
|
creationSource: PurchaseCreationSource.PURCHASE_PAGE,
|
|
currentPeriodStart: new Date('2023-11-01T00:00:00.000Z'),
|
|
currentPeriodEnd: new Date('2024-05-01T00:00:00.000Z'),
|
|
cancelAtPeriodEnd: true,
|
|
stripeSubscriptionId: 'sub_legacy_enterprise_alpha',
|
|
createdAt: new Date('2023-11-01T00:00:00.000Z'),
|
|
},
|
|
];
|
|
|
|
for (const subscription of subscriptionSeeds) {
|
|
await prisma.subscription.upsert({
|
|
where: {
|
|
tenancyId_id: {
|
|
tenancyId,
|
|
id: subscription.id,
|
|
},
|
|
},
|
|
update: {
|
|
customerId: subscription.customerId,
|
|
customerType: subscription.customerType,
|
|
productId: subscription.productId ?? null,
|
|
priceId: subscription.priceId ?? null,
|
|
product: subscription.product,
|
|
quantity: subscription.quantity,
|
|
status: subscription.status,
|
|
currentPeriodEnd: subscription.currentPeriodEnd,
|
|
currentPeriodStart: subscription.currentPeriodStart,
|
|
cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
|
|
creationSource: subscription.creationSource,
|
|
stripeSubscriptionId: subscription.stripeSubscriptionId ?? null,
|
|
},
|
|
create: {
|
|
tenancyId,
|
|
id: subscription.id,
|
|
customerId: subscription.customerId,
|
|
customerType: subscription.customerType,
|
|
productId: subscription.productId ?? null,
|
|
priceId: subscription.priceId ?? null,
|
|
product: subscription.product,
|
|
quantity: subscription.quantity,
|
|
status: subscription.status,
|
|
currentPeriodEnd: subscription.currentPeriodEnd,
|
|
currentPeriodStart: subscription.currentPeriodStart,
|
|
cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
|
|
creationSource: subscription.creationSource,
|
|
stripeSubscriptionId: subscription.stripeSubscriptionId ?? null,
|
|
createdAt: subscription.createdAt,
|
|
},
|
|
});
|
|
}
|
|
|
|
const itemQuantityChangeSeeds: ItemQuantityChangeSeed[] = [
|
|
{
|
|
id: DUMMY_SEED_IDS.itemQuantityChanges.designSeatsGrant,
|
|
customerType: CustomerType.TEAM,
|
|
customerId: resolveTeamId('Design Systems Lab'),
|
|
itemId: 'studio_seats',
|
|
quantity: 15,
|
|
description: 'Bonus seats for cross-team design sprint',
|
|
expiresAt: new Date('2024-07-15T00:00:00.000Z'),
|
|
createdAt: new Date('2024-05-01T00:00:00.000Z'),
|
|
},
|
|
{
|
|
id: DUMMY_SEED_IDS.itemQuantityChanges.growthSnapshotAdjustment,
|
|
customerType: CustomerType.TEAM,
|
|
customerId: resolveTeamId('Growth Loop'),
|
|
itemId: 'snapshot_credits',
|
|
quantity: -80,
|
|
description: 'Snapshot overage adjustment',
|
|
expiresAt: null,
|
|
createdAt: new Date('2024-05-28T12:00:00.000Z'),
|
|
},
|
|
{
|
|
id: DUMMY_SEED_IDS.itemQuantityChanges.opsAutomationCredit,
|
|
customerType: CustomerType.TEAM,
|
|
customerId: resolveTeamId('Ops'),
|
|
itemId: 'automation_minutes',
|
|
quantity: 1200,
|
|
description: 'Reliability incident credit',
|
|
expiresAt: new Date('2024-08-01T00:00:00.000Z'),
|
|
createdAt: new Date('2024-05-10T09:30:00.000Z'),
|
|
},
|
|
{
|
|
id: DUMMY_SEED_IDS.itemQuantityChanges.legacyReviewPass,
|
|
customerType: CustomerType.CUSTOM,
|
|
customerId: 'visual-review-partner',
|
|
itemId: 'legacy_review_pass',
|
|
quantity: 25,
|
|
description: 'Legacy migration allowance',
|
|
expiresAt: new Date('2024-09-15T00:00:00.000Z'),
|
|
createdAt: new Date('2024-04-18T00:00:00.000Z'),
|
|
},
|
|
];
|
|
|
|
for (const quantityChange of itemQuantityChangeSeeds) {
|
|
await prisma.itemQuantityChange.upsert({
|
|
where: {
|
|
tenancyId_id: {
|
|
tenancyId,
|
|
id: quantityChange.id,
|
|
},
|
|
},
|
|
update: {
|
|
customerId: quantityChange.customerId,
|
|
customerType: quantityChange.customerType,
|
|
itemId: quantityChange.itemId,
|
|
quantity: quantityChange.quantity,
|
|
description: quantityChange.description ?? null,
|
|
expiresAt: quantityChange.expiresAt ?? null,
|
|
},
|
|
create: {
|
|
tenancyId,
|
|
id: quantityChange.id,
|
|
customerId: quantityChange.customerId,
|
|
customerType: quantityChange.customerType,
|
|
itemId: quantityChange.itemId,
|
|
quantity: quantityChange.quantity,
|
|
description: quantityChange.description ?? null,
|
|
expiresAt: quantityChange.expiresAt ?? null,
|
|
createdAt: quantityChange.createdAt,
|
|
},
|
|
});
|
|
}
|
|
|
|
const oneTimePurchaseSeeds: OneTimePurchaseSeed[] = [
|
|
{
|
|
id: DUMMY_SEED_IDS.oneTimePurchases.qaAddon,
|
|
customerType: CustomerType.TEAM,
|
|
customerId: resolveTeamId('QA Collective'),
|
|
productId: 'regression-addon',
|
|
priceId: 'monthly',
|
|
product: resolveProduct('regression-addon'),
|
|
quantity: 1,
|
|
creationSource: PurchaseCreationSource.PURCHASE_PAGE,
|
|
stripePaymentIntentId: 'pi_regression_addon',
|
|
createdAt: new Date('2024-05-05T00:00:00.000Z'),
|
|
},
|
|
{
|
|
id: DUMMY_SEED_IDS.oneTimePurchases.ameliaSeatPack,
|
|
customerType: CustomerType.USER,
|
|
customerId: resolveUserId('amelia.chen@dummy.dev'),
|
|
productId: 'starter',
|
|
priceId: 'monthly',
|
|
product: resolveProduct('starter'),
|
|
quantity: 2,
|
|
creationSource: PurchaseCreationSource.TEST_MODE,
|
|
stripePaymentIntentId: null,
|
|
createdAt: new Date('2024-05-25T15:00:00.000Z'),
|
|
},
|
|
{
|
|
id: DUMMY_SEED_IDS.oneTimePurchases.launchCouncilUpfront,
|
|
customerType: CustomerType.TEAM,
|
|
customerId: resolveTeamId('Launch Council'),
|
|
productId: 'growth',
|
|
priceId: 'annual',
|
|
product: resolveProduct('growth'),
|
|
quantity: 1,
|
|
creationSource: PurchaseCreationSource.PURCHASE_PAGE,
|
|
stripePaymentIntentId: 'pi_launch_council_growth',
|
|
createdAt: new Date('2024-03-12T00:00:00.000Z'),
|
|
},
|
|
{
|
|
id: DUMMY_SEED_IDS.oneTimePurchases.designAuditPass,
|
|
customerType: CustomerType.CUSTOM,
|
|
customerId: 'design-audit-partner',
|
|
productId: 'design-audit-pass',
|
|
priceId: 'one_time',
|
|
product: cloneJson({
|
|
displayName: 'Design Audit Pass',
|
|
catalogId: 'add_ons',
|
|
customerType: 'custom',
|
|
prices: {
|
|
one_time: {
|
|
USD: '2500',
|
|
},
|
|
},
|
|
}),
|
|
quantity: 2,
|
|
creationSource: PurchaseCreationSource.API_GRANT,
|
|
stripePaymentIntentId: 'pi_design_audit_pass',
|
|
createdAt: new Date('2024-04-22T00:00:00.000Z'),
|
|
},
|
|
];
|
|
|
|
for (const purchase of oneTimePurchaseSeeds) {
|
|
await prisma.oneTimePurchase.upsert({
|
|
where: {
|
|
tenancyId_id: {
|
|
tenancyId,
|
|
id: purchase.id,
|
|
},
|
|
},
|
|
update: {
|
|
customerId: purchase.customerId,
|
|
customerType: purchase.customerType,
|
|
productId: purchase.productId ?? null,
|
|
priceId: purchase.priceId ?? null,
|
|
product: purchase.product,
|
|
quantity: purchase.quantity,
|
|
creationSource: purchase.creationSource,
|
|
stripePaymentIntentId: purchase.stripePaymentIntentId ?? null,
|
|
},
|
|
create: {
|
|
tenancyId,
|
|
id: purchase.id,
|
|
customerId: purchase.customerId,
|
|
customerType: purchase.customerType,
|
|
productId: purchase.productId ?? null,
|
|
priceId: purchase.priceId ?? null,
|
|
product: purchase.product,
|
|
quantity: purchase.quantity,
|
|
creationSource: purchase.creationSource,
|
|
stripePaymentIntentId: purchase.stripePaymentIntentId ?? null,
|
|
createdAt: purchase.createdAt,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
async function seedDummyEmails(options: EmailSeedOptions) {
|
|
const { prisma, tenancyId, userEmailToId } = options;
|
|
const resolveOptionalUserId = (email?: string) => {
|
|
if (!email) return null;
|
|
const userId = userEmailToId.get(email);
|
|
if (!userId) {
|
|
throwErr(`Unknown dummy project user ${email}`);
|
|
}
|
|
return userId;
|
|
};
|
|
|
|
const emailSeeds: EmailOutboxSeed[] = [
|
|
{
|
|
id: DUMMY_SEED_IDS.emails.welcomeAmelia,
|
|
subject: 'Welcome to Dummy Project',
|
|
html: '<p>Hi Amelia,<br/>Welcome to Dummy Project.</p>',
|
|
text: 'Hi Amelia,\nWelcome to Dummy Project.',
|
|
createdAt: new Date('2024-05-01T13:00:00.000Z'),
|
|
userEmail: 'amelia.chen@dummy.dev',
|
|
},
|
|
{
|
|
id: DUMMY_SEED_IDS.emails.passkeyMilo,
|
|
subject: 'Your passkey sign-in link',
|
|
html: '<p>Complete your sign-in within <strong>10 minutes</strong>.</p>',
|
|
text: 'Complete your sign-in within 10 minutes.',
|
|
createdAt: new Date('2024-05-02T10:00:00.000Z'),
|
|
userEmail: 'milo.adeyemi@dummy.dev',
|
|
},
|
|
{
|
|
id: DUMMY_SEED_IDS.emails.invitePriya,
|
|
subject: 'Dashboard invite for Ops',
|
|
html: '<p>Welcome to the dashboard!</p>',
|
|
hasError: true,
|
|
createdAt: new Date('2024-05-04T18:30:00.000Z'),
|
|
userEmail: 'priya.narang@dummy.dev',
|
|
},
|
|
{
|
|
id: DUMMY_SEED_IDS.emails.statusDigest,
|
|
subject: 'Nightly status digest',
|
|
text: 'All services operational. 3 warnings acknowledged.',
|
|
createdAt: new Date('2024-05-06T07:45:00.000Z'),
|
|
},
|
|
{
|
|
id: DUMMY_SEED_IDS.emails.templateFailure,
|
|
subject: 'Template rendering failed - Review',
|
|
html: '<p>Rendering failed due to <code>undefined</code> data from billing.</p>',
|
|
hasError: true,
|
|
createdAt: new Date('2024-05-08T12:05:00.000Z'),
|
|
},
|
|
];
|
|
|
|
for (const email of emailSeeds) {
|
|
const userId = resolveOptionalUserId(email.userEmail);
|
|
const recipient = userId
|
|
? { type: 'user-primary-email', userId }
|
|
: { type: 'custom-emails', emails: ['unknown@dummy.dev'] };
|
|
|
|
await globalPrismaClient.emailOutbox.upsert({
|
|
where: {
|
|
tenancyId_id: {
|
|
tenancyId,
|
|
id: email.id,
|
|
},
|
|
},
|
|
update: {},
|
|
create: {
|
|
tenancyId,
|
|
id: email.id,
|
|
tsxSource: '',
|
|
isHighPriority: false,
|
|
to: recipient,
|
|
extraRenderVariables: {},
|
|
shouldSkipDeliverabilityCheck: false,
|
|
createdWith: EmailOutboxCreatedWith.PROGRAMMATIC_CALL,
|
|
scheduledAt: email.createdAt,
|
|
// Rendering fields - renderedByWorkerId and startedRenderingAt must both be set or both be null
|
|
renderedByWorkerId: email.id, // use the email id as a dummy worker id
|
|
startedRenderingAt: email.createdAt,
|
|
finishedRenderingAt: email.createdAt,
|
|
renderedSubject: email.subject,
|
|
renderedHtml: email.html ?? null,
|
|
renderedText: email.text ?? null,
|
|
// Sending fields
|
|
startedSendingAt: email.createdAt,
|
|
finishedSendingAt: email.createdAt,
|
|
canHaveDeliveryInfo: false,
|
|
sendServerErrorExternalMessage: email.hasError ? 'Delivery failed' : null,
|
|
sendServerErrorExternalDetails: email.hasError ? {} : Prisma.DbNull,
|
|
sendServerErrorInternalMessage: email.hasError ? "Delivery failed. This is the internal error message." : null,
|
|
sendServerErrorInternalDetails: email.hasError ? { internalError: "No internal error details." } : Prisma.DbNull,
|
|
createdAt: email.createdAt,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
type SessionActivityEventSeedOptions = {
|
|
tenancyId: string,
|
|
projectId: string,
|
|
userEmailToId: Map<string, string>,
|
|
};
|
|
|
|
async function seedDummySessionActivityEvents(options: SessionActivityEventSeedOptions) {
|
|
const { tenancyId, projectId, userEmailToId } = options;
|
|
|
|
// List of diverse locations around the world with realistic coordinates
|
|
const locations = [
|
|
{ countryCode: 'US', regionCode: 'CA', cityName: 'San Francisco', latitude: 37.7749, longitude: -122.4194, tzIdentifier: 'America/Los_Angeles' },
|
|
{ countryCode: 'US', regionCode: 'NY', cityName: 'New York', latitude: 40.7128, longitude: -74.0060, tzIdentifier: 'America/New_York' },
|
|
{ countryCode: 'GB', regionCode: 'ENG', cityName: 'London', latitude: 51.5074, longitude: -0.1278, tzIdentifier: 'Europe/London' },
|
|
{ countryCode: 'DE', regionCode: 'BE', cityName: 'Berlin', latitude: 52.5200, longitude: 13.4050, tzIdentifier: 'Europe/Berlin' },
|
|
{ countryCode: 'JP', regionCode: '13', cityName: 'Tokyo', latitude: 35.6762, longitude: 139.6503, tzIdentifier: 'Asia/Tokyo' },
|
|
{ countryCode: 'AU', regionCode: 'NSW', cityName: 'Sydney', latitude: -33.8688, longitude: 151.2093, tzIdentifier: 'Australia/Sydney' },
|
|
{ countryCode: 'IN', regionCode: 'KA', cityName: 'Bangalore', latitude: 12.9716, longitude: 77.5946, tzIdentifier: 'Asia/Kolkata' },
|
|
{ countryCode: 'BR', regionCode: 'SP', cityName: 'São Paulo', latitude: -23.5505, longitude: -46.6333, tzIdentifier: 'America/Sao_Paulo' },
|
|
{ countryCode: 'CA', regionCode: 'ON', cityName: 'Toronto', latitude: 43.6532, longitude: -79.3832, tzIdentifier: 'America/Toronto' },
|
|
{ countryCode: 'FR', regionCode: 'IDF', cityName: 'Paris', latitude: 48.8566, longitude: 2.3522, tzIdentifier: 'Europe/Paris' },
|
|
{ countryCode: 'SG', regionCode: 'SG', cityName: 'Singapore', latitude: 1.3521, longitude: 103.8198, tzIdentifier: 'Asia/Singapore' },
|
|
{ countryCode: 'NL', regionCode: 'NH', cityName: 'Amsterdam', latitude: 52.3676, longitude: 4.9041, tzIdentifier: 'Europe/Amsterdam' },
|
|
{ countryCode: 'SE', regionCode: 'AB', cityName: 'Stockholm', latitude: 59.3293, longitude: 18.0686, tzIdentifier: 'Europe/Stockholm' },
|
|
{ countryCode: 'ES', regionCode: 'MD', cityName: 'Madrid', latitude: 40.4168, longitude: -3.7038, tzIdentifier: 'Europe/Madrid' },
|
|
{ countryCode: 'IT', regionCode: 'RM', cityName: 'Rome', latitude: 41.9028, longitude: 12.4964, tzIdentifier: 'Europe/Rome' },
|
|
{ countryCode: 'MX', regionCode: 'CMX', cityName: 'Mexico City', latitude: 19.4326, longitude: -99.1332, tzIdentifier: 'America/Mexico_City' },
|
|
{ countryCode: 'KR', regionCode: '11', cityName: 'Seoul', latitude: 37.5665, longitude: 126.9780, tzIdentifier: 'Asia/Seoul' },
|
|
{ countryCode: 'ZA', regionCode: 'GT', cityName: 'Johannesburg', latitude: -26.2041, longitude: 28.0473, tzIdentifier: 'Africa/Johannesburg' },
|
|
{ countryCode: 'AE', regionCode: 'DU', cityName: 'Dubai', latitude: 25.2048, longitude: 55.2708, tzIdentifier: 'Asia/Dubai' },
|
|
{ countryCode: 'CH', regionCode: 'ZH', cityName: 'Zurich', latitude: 47.3769, longitude: 8.5417, tzIdentifier: 'Europe/Zurich' },
|
|
];
|
|
|
|
const now = new Date();
|
|
const twoMonthsAgo = new Date(now);
|
|
twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() - 2);
|
|
|
|
const userEmails = Array.from(userEmailToId.keys());
|
|
|
|
console.log(`Seeding session activity events for ${userEmails.length} users...`);
|
|
|
|
for (const email of userEmails) {
|
|
const userId = userEmailToId.get(email);
|
|
if (!userId) continue;
|
|
|
|
// Create 15-25 session events per user over the past 2 months
|
|
const eventCount = 15 + Math.floor(Math.random() * 11);
|
|
|
|
for (let i = 0; i < eventCount; i++) {
|
|
// Random timestamp within the past 2 months
|
|
const randomTime = new Date(
|
|
twoMonthsAgo.getTime() + Math.random() * (now.getTime() - twoMonthsAgo.getTime())
|
|
);
|
|
|
|
// Pick a random location
|
|
const location = locations[Math.floor(Math.random() * locations.length)];
|
|
|
|
// Generate a session ID (simulating a refresh token ID)
|
|
const sessionId = `session-${userId.substring(0, 8)}-${i.toString().padStart(3, '0')}-${randomTime.getTime().toString(36)}`;
|
|
|
|
// Generate a unique IP address for this session
|
|
const ipAddress = `${10 + Math.floor(Math.random() * 200)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}`;
|
|
|
|
// Create EventIpInfo entry with a proper UUID
|
|
const ipInfoId = generateUuid(); // TODO: This should be a deterministic UUID so we don't keep recreating the session info
|
|
await globalPrismaClient.eventIpInfo.upsert({
|
|
where: { id: ipInfoId },
|
|
update: {
|
|
ip: ipAddress,
|
|
countryCode: location.countryCode,
|
|
regionCode: location.regionCode,
|
|
cityName: location.cityName,
|
|
latitude: location.latitude,
|
|
longitude: location.longitude,
|
|
tzIdentifier: location.tzIdentifier,
|
|
updatedAt: randomTime,
|
|
},
|
|
create: {
|
|
id: ipInfoId,
|
|
ip: ipAddress,
|
|
countryCode: location.countryCode,
|
|
regionCode: location.regionCode,
|
|
cityName: location.cityName,
|
|
latitude: location.latitude,
|
|
longitude: location.longitude,
|
|
tzIdentifier: location.tzIdentifier,
|
|
createdAt: randomTime,
|
|
updatedAt: randomTime,
|
|
},
|
|
});
|
|
|
|
// Create the Event entry with a proper UUID
|
|
const eventId = generateUuid();
|
|
await globalPrismaClient.event.upsert({
|
|
where: { id: eventId },
|
|
update: {
|
|
systemEventTypeIds: ['$session-activity', '$user-activity', '$project-activity', '$project'],
|
|
data: {
|
|
projectId,
|
|
branchId: DEFAULT_BRANCH_ID,
|
|
userId,
|
|
sessionId,
|
|
isAnonymous: false,
|
|
},
|
|
isEndUserIpInfoGuessTrusted: true,
|
|
endUserIpInfoGuessId: ipInfoId,
|
|
isWide: false,
|
|
eventStartedAt: randomTime,
|
|
eventEndedAt: randomTime,
|
|
updatedAt: randomTime,
|
|
},
|
|
create: {
|
|
id: eventId,
|
|
systemEventTypeIds: ['$session-activity', '$user-activity', '$project-activity', '$project'],
|
|
data: {
|
|
projectId,
|
|
branchId: DEFAULT_BRANCH_ID,
|
|
userId,
|
|
sessionId,
|
|
isAnonymous: false,
|
|
},
|
|
isEndUserIpInfoGuessTrusted: true,
|
|
endUserIpInfoGuessId: ipInfoId,
|
|
isWide: false,
|
|
eventStartedAt: randomTime,
|
|
eventEndedAt: randomTime,
|
|
createdAt: randomTime,
|
|
updatedAt: randomTime,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
console.log('Finished seeding session activity events');
|
|
}
|