Add fixtures for remaining entities in database initialization script (#36)

* Create fixture data for remaining entities and load it in db

* Rename currProject to currentProject in frontend package

* Handle review changes

* Update readme for loading fixtures

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
Nabarun Gogoi 2024-01-24 16:58:07 +05:30 committed by Ashwin Phatak
parent 890603061f
commit 2d7e56c0e1
11 changed files with 436 additions and 171 deletions

View File

@ -15,7 +15,7 @@
- Build packages - Build packages
```bash ```bash
yarn build yarn build --ignore frontend
``` ```
- Change directory to `packages/backend` - Change directory to `packages/backend`
@ -24,6 +24,12 @@
cd packages/backend cd packages/backend
``` ```
- Load fixtures in database
```bash
yarn db:load:fixtures
```
- Start the server - Start the server
```bash ```bash

View File

@ -29,7 +29,7 @@
"lint": "eslint .", "lint": "eslint .",
"format": "prettier --write .", "format": "prettier --write .",
"format:check": "prettier --check .", "format:check": "prettier --check .",
"db:load:fixtures": "ts-node ./test/initialize-db.ts" "db:load:fixtures": "DEBUG=snowball:* ts-node ./test/initialize-db.ts"
}, },
"devDependencies": { "devDependencies": {
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",

View File

@ -0,0 +1,92 @@
[
{
"projectIndex": 0,
"domainIndex":0,
"title": "nextjs-boilerplate-1",
"status": "Building",
"environment": "Production",
"isCurrent": false,
"branch": "prod",
"commitHash": "testXyz"
},
{
"projectIndex": 0,
"domainIndex":1,
"title": "nextjs-boilerplate-2",
"status": "Ready",
"environment": "Preview",
"isCurrent": true,
"branch": "prod",
"commitHash": "testXyz"
},
{
"projectIndex": 0,
"domainIndex":2,
"title": "nextjs-boilerplate-3",
"status": "Error",
"environment": "Development",
"isCurrent": false,
"branch": "prod",
"commitHash": "testXyz"
},
{
"projectIndex": 1,
"domainIndex":3,
"title": "nextjs-boilerplate-1",
"status": "Building",
"environment": "Production",
"isCurrent": false,
"branch": "prod",
"commitHash": "testXyz"
},
{
"projectIndex": 1,
"domainIndex":4,
"title": "nextjs-boilerplate-2",
"status": "Ready",
"environment": "Preview",
"isCurrent": true,
"branch": "prod",
"commitHash": "testXyz"
},
{
"projectIndex": 1,
"domainIndex":5,
"title": "nextjs-boilerplate-3",
"status": "Error",
"environment": "Development",
"isCurrent": false,
"branch": "prod",
"commitHash": "testXyz"
},
{
"projectIndex": 2,
"domainIndex":6,
"title": "nextjs-boilerplate-1",
"status": "Building",
"environment": "Production",
"isCurrent": false,
"branch": "prod",
"commitHash": "testXyz"
},
{
"projectIndex": 2,
"domainIndex":7,
"title": "nextjs-boilerplate-2",
"status": "Ready",
"environment": "Preview",
"isCurrent": true,
"branch": "prod",
"commitHash": "testXyz"
},
{
"projectIndex": 2,
"domainIndex":8,
"title": "nextjs-boilerplate-3",
"status": "Error",
"environment": "Development",
"isCurrent": false,
"branch": "prod",
"commitHash": "testXyz"
}
]

View File

@ -0,0 +1,56 @@
[
{
"name": "randomurl.snowballtools.xyz",
"status": "Live",
"isRedirected": false,
"branch": "test"
},
{
"name": "saugatt.com",
"status": "Pending",
"isRedirected": false,
"branch": "test"
},
{
"name": "www.saugatt.com",
"status": "Pending",
"isRedirected": true,
"branch": "test"
},
{
"name": "randomurl.snowballtools.xyz",
"status": "Live",
"isRedirected": false,
"branch": "test"
},
{
"name": "saugatt.com",
"status": "Pending",
"isRedirected": false,
"branch": "test"
},
{
"name": "www.saugatt.com",
"status": "Pending",
"isRedirected": true,
"branch": "test"
},
{
"name": "randomurl.snowballtools.xyz",
"status": "Live",
"isRedirected": false,
"branch": "test"
},
{
"name": "saugatt.com",
"status": "Pending",
"isRedirected": false,
"branch": "test"
},
{
"name": "www.saugatt.com",
"status": "Pending",
"isRedirected": true,
"branch": "test"
}
]

View File

@ -0,0 +1,62 @@
[
{
"projectIndex": 0,
"key": "ABC",
"value": "ABC",
"environments": ["Production", "Preview"]
},
{
"projectIndex": 0,
"key": "XYZ",
"value": "abc3",
"environments": ["Preview"]
},
{
"projectIndex": 1,
"key": "ABC",
"value": "ABC",
"environments": ["Production", "Preview"]
},
{
"projectIndex": 1,
"key": "XYZ",
"value": "abc3",
"environments": ["Preview"]
},
{
"projectIndex": 2,
"key": "ABC",
"value": "ABC",
"environments": ["Production", "Preview"]
},
{
"projectIndex": 2,
"key": "XYZ",
"value": "abc3",
"environments": ["Preview"]
},
{
"projectIndex": 3,
"key": "ABC",
"value": "ABC",
"environments": ["Production", "Preview"]
},
{
"projectIndex": 3,
"key": "XYZ",
"value": "abc3",
"environments": ["Preview"]
},
{
"projectIndex": 4,
"key": "ABC",
"value": "ABC",
"environments": ["Production", "Preview"]
},
{
"projectIndex": 4,
"key": "XYZ",
"value": "abc3",
"environments": ["Preview"]
}
]

View File

@ -0,0 +1,12 @@
[
{
"memberIndex": 1,
"projectIndex": 0,
"permissions": ["View", "Edit"]
},
{
"memberIndex": 2,
"projectIndex": 0,
"permissions": ["View", "Edit"]
}
]

View File

@ -14,20 +14,37 @@ import { Deployment } from '../src/entity/Deployment';
const log = debug('snowball:initialize-database'); const log = debug('snowball:initialize-database');
const DB_PATH = '../db/snowball';
const USER_DATA_PATH = './fixtures/users.json'; const USER_DATA_PATH = './fixtures/users.json';
const PROJECT_DATA_PATH = './fixtures/projects.json'; const PROJECT_DATA_PATH = './fixtures/projects.json';
const ORGANIZATION_DATA_PATH = './fixtures/organizations.json'; const ORGANIZATION_DATA_PATH = './fixtures/organizations.json';
const USER_ORGANIZATION_DATA_PATH = './fixtures/user-orgnizations.json'; const USER_ORGANIZATION_DATA_PATH = './fixtures/user-orgnizations.json';
const PROJECT_MEMBER_DATA_PATH = './fixtures/project-members.json';
const DOMAIN_DATA_PATH = './fixtures/domains.json';
const DEPLOYMENT_DATA_PATH = './fixtures/deployments.json';
const ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json';
const loadAndSaveData = async <Entity extends ObjectLiteral>(entityType: EntityTarget<Entity>, dataSource: DataSource, filePath: string) => { const loadAndSaveData = async <Entity extends ObjectLiteral>(entityType: EntityTarget<Entity>, dataSource: DataSource, filePath: string, relations?: any | undefined) => {
const entitiesData = await fs.readFile(filePath, 'utf-8'); const entitiesData = await fs.readFile(filePath, 'utf-8');
const entities = JSON.parse(entitiesData) as DeepPartial<Entity>[]; const entities = JSON.parse(entitiesData);
const entityRepository = dataSource.getRepository(entityType); const entityRepository = dataSource.getRepository(entityType);
const savedEntity:Entity[] = []; const savedEntity:Entity[] = [];
for (const entityData of entities) { for (const entityData of entities) {
const entity = entityRepository.create(entityData); let entity = entityRepository.create(entityData as DeepPartial<Entity>);
if (relations) {
for (const field in relations) {
const valueIndex = String(field + 'Index');
entity = {
...entity,
[field]: relations[field][entityData[valueIndex]]
};
}
}
const dbEntity = await entityRepository.save(entity); const dbEntity = await entityRepository.save(entity);
savedEntity.push(dbEntity); savedEntity.push(dbEntity);
} }
@ -38,52 +55,69 @@ const loadAndSaveData = async <Entity extends ObjectLiteral>(entityType: EntityT
const generateTestData = async (dataSource: DataSource) => { const generateTestData = async (dataSource: DataSource) => {
const savedUsers = await loadAndSaveData(User, dataSource, path.resolve(__dirname, USER_DATA_PATH)); const savedUsers = await loadAndSaveData(User, dataSource, path.resolve(__dirname, USER_DATA_PATH));
const savedOrgs = await loadAndSaveData(Organization, dataSource, path.resolve(__dirname, ORGANIZATION_DATA_PATH)); const savedOrgs = await loadAndSaveData(Organization, dataSource, path.resolve(__dirname, ORGANIZATION_DATA_PATH));
const savedDomains = await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, DOMAIN_DATA_PATH));
const projectsData = await fs.readFile(path.resolve(__dirname, PROJECT_DATA_PATH), 'utf-8'); const projectRelations = {
const projects = JSON.parse(projectsData); owner: savedUsers,
const projectRepository = dataSource.getRepository(Project); organization: savedOrgs
};
for (const projectData of projects) { const savedProjects = await loadAndSaveData(Project, dataSource, path.resolve(__dirname, PROJECT_DATA_PATH), projectRelations);
const project = projectRepository.create(projectData as DeepPartial<Project>);
project.owner = savedUsers[projectData.ownerIndex];
project.organization = savedOrgs[projectData.organizationIndex];
await projectRepository.save(project);
}
const userOrgData = await fs.readFile(path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH), 'utf-8'); const userOrganizationRelations = {
const userOrgs = JSON.parse(userOrgData); member: savedUsers,
const userOrgRepository = dataSource.getRepository(UserOrganization); organization: savedOrgs
};
for (const userOrgData of userOrgs) { await loadAndSaveData(UserOrganization, dataSource, path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH), userOrganizationRelations);
const userOrg = userOrgRepository.create(userOrgData as DeepPartial<UserOrganization>);
userOrg.member = savedUsers[userOrgData.memberIndex];
userOrg.organization = savedOrgs[userOrgData.organizationIndex];
await userOrgRepository.save(userOrg); const projectMemberRelations = {
member: savedUsers,
project: savedProjects
};
await loadAndSaveData(ProjectMember, dataSource, path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH), projectMemberRelations);
const deploymentRelations = {
project: savedProjects,
domain: savedDomains
};
await loadAndSaveData(Deployment, dataSource, path.resolve(__dirname, DEPLOYMENT_DATA_PATH), deploymentRelations);
const environmentVariableRelations = {
project: savedProjects
};
await loadAndSaveData(EnvironmentVariable, dataSource, path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH), environmentVariableRelations);
};
const checkFileExists = async (filePath: string) => {
try {
await fs.access(filePath, fs.constants.F_OK);
return true;
} catch (err) {
log(err);
return false;
} }
}; };
const main = async () => { const main = async () => {
const dataSource = new DataSource({ const isDbPresent = await checkFileExists(path.resolve(__dirname, DB_PATH));
type: 'better-sqlite3',
database: 'db/snowball',
synchronize: true,
logging: true,
entities: [
User,
Organization,
Project,
UserOrganization,
EnvironmentVariable,
Domain,
ProjectMember,
Deployment
]
});
await dataSource.initialize(); if (!isDbPresent) {
const dataSource = new DataSource({
type: 'better-sqlite3',
database: 'db/snowball',
synchronize: true,
logging: true,
entities: [path.join(__dirname, '../src/entity/*')]
});
await generateTestData(dataSource); await dataSource.initialize();
await generateTestData(dataSource);
}
}; };
main().then(() => { main().then(() => {

View File

@ -14,19 +14,19 @@ const Domains = () => {
const { projects } = useOutletContext<ProjectsOutletContext>(); const { projects } = useOutletContext<ProjectsOutletContext>();
const currProject = useMemo(() => { const currentProject = useMemo(() => {
return projects.find((project) => { return projects.find((project) => {
return project.id === id; return project.id === id;
}); });
}, [id, projects]); }, [id, projects]);
const linkedRepo = useMemo(() => { const linkedRepo = useMemo(() => {
return currProject?.repositories.find( return currentProject?.repositories.find(
(repo: any) => repo.id === Number(currProject?.repositoryId), (repo: any) => repo.id === Number(currentProject?.repositoryId),
); );
}, [currProject]); }, [currentProject]);
const domains = currProject?.deployments const domains = currentProject?.deployments
.filter((deployment) => { .filter((deployment) => {
return deployment.domain != null; return deployment.domain != null;
}) })
@ -49,7 +49,7 @@ const Domains = () => {
domain={domain} domain={domain}
key={domain.id} key={domain.id}
repo={linkedRepo!} repo={linkedRepo!}
project={currProject!} project={currentProject!}
/> />
); );
})} })}

View File

@ -38,7 +38,7 @@ export const EnvironmentVariablesTabPanel = () => {
const { projects } = useOutletContext<ProjectsOutletContext>(); const { projects } = useOutletContext<ProjectsOutletContext>();
const currProject = useMemo(() => { const currentProject = useMemo(() => {
return projects.find((project) => { return projects.find((project) => {
return project.id === id; return project.id === id;
}); });
@ -77,9 +77,9 @@ export const EnvironmentVariablesTabPanel = () => {
}, [isSubmitSuccessful, reset]); }, [isSubmitSuccessful, reset]);
const getEnvironmentVariable = useCallback((environment: Environments) => { const getEnvironmentVariable = useCallback((environment: Environments) => {
return (currProject?.environmentVariables as EnvironmentVariable[]).filter( return (
(item) => item.environments.includes(environment), currentProject?.environmentVariables as EnvironmentVariable[]
); ).filter((item) => item.environments.includes(environment));
}, []); }, []);
const isFieldEmpty = useMemo(() => { const isFieldEmpty = useMemo(() => {

View File

@ -15,7 +15,6 @@ import DeleteProjectDialog from './DeleteProjectDialog';
import ConfirmDialog from '../../../shared/ConfirmDialog'; import ConfirmDialog from '../../../shared/ConfirmDialog';
import { ProjectsOutletContext } from '../../../../types/project'; import { ProjectsOutletContext } from '../../../../types/project';
const PROJECT_ID = '62f87575-7a2b-4951-8156-9f9821j380d';
const TEAMS = ['Airfoil']; const TEAMS = ['Airfoil'];
const DEFAULT_SELECT_TEAM = undefined; const DEFAULT_SELECT_TEAM = undefined;
@ -37,7 +36,7 @@ const GeneralTabPanel = () => {
const { id } = useParams(); const { id } = useParams();
const { projects } = useOutletContext<ProjectsOutletContext>(); const { projects } = useOutletContext<ProjectsOutletContext>();
const currProject = useMemo(() => { const currentProject = useMemo(() => {
return projects.find((project: any) => project.id === id); return projects.find((project: any) => project.id === id);
}, [id]); }, [id]);
@ -61,128 +60,132 @@ const GeneralTabPanel = () => {
const { handleSubmit, register } = useForm({ const { handleSubmit, register } = useForm({
defaultValues: { defaultValues: {
appName: currProject?.name, appName: currentProject?.name,
description: currProject?.description, description: currentProject?.description,
}, },
}); });
return ( return (
<> <>
<form onSubmit={handleSubmit(() => {})}> {currentProject && (
<Typography variant="h6">Project info</Typography> <>
<Typography variant="small" className="font-medium text-gray-800"> <form onSubmit={handleSubmit(() => {})}>
App name <Typography variant="h6">Project info</Typography>
</Typography> <Typography variant="small" className="font-medium text-gray-800">
<Input App name
variant="outlined" </Typography>
// TODO: Debug issue: https://github.com/creativetimofficial/material-tailwind/issues/427 <Input
crossOrigin={undefined} variant="outlined"
size="md" // TODO: Debug issue: https://github.com/creativetimofficial/material-tailwind/issues/427
{...register('appName')} crossOrigin={undefined}
/> size="md"
<Typography variant="small" className="font-medium text-gray-800"> {...register('appName')}
Description (Optional) />
</Typography> <Typography variant="small" className="font-medium text-gray-800">
<Input Description (Optional)
variant="outlined" </Typography>
crossOrigin={undefined} <Input
size="md" variant="outlined"
{...register('description')} crossOrigin={undefined}
/> size="md"
<Typography variant="small" className="font-medium text-gray-800"> {...register('description')}
Project ID />
</Typography> <Typography variant="small" className="font-medium text-gray-800">
<Input Project ID
crossOrigin={undefined} </Typography>
variant="outlined" <Input
value={PROJECT_ID} crossOrigin={undefined}
size="md" variant="outlined"
disabled value={currentProject.id}
icon={<CopyIcon value={PROJECT_ID} />} size="md"
/> disabled
<Button type="submit" variant="gradient" size="sm" className="mt-1"> icon={<CopyIcon value={currentProject.id} />}
Save />
</Button> <Button type="submit" variant="gradient" size="sm" className="mt-1">
</form> Save
<div className="mb-1"> </Button>
<Typography variant="h6">Transfer project</Typography> </form>
<Typography variant="small"> <div className="mb-1">
Transfer this app to your personal account or a team you are a member <Typography variant="h6">Transfer project</Typography>
of. <Typography variant="small">
<Link to="" className="text-blue-500"> Transfer this app to your personal account or a team you are a
Learn more member of.
</Link> <Link to="" className="text-blue-500">
</Typography> Learn more
<form </Link>
onSubmit={handleTransfer(() => { </Typography>
handleTransferProjectDialog(); <form
})} onSubmit={handleTransfer(() => {
> handleTransferProjectDialog();
<Typography variant="small" className="font-medium text-gray-800"> })}
Choose team >
</Typography> <Typography variant="small" className="font-medium text-gray-800">
<Controller Choose team
name="team" </Typography>
rules={{ required: 'This field is required' }} <Controller
control={control} name="team"
render={({ field }) => ( rules={{ required: 'This field is required' }}
<Select control={control}
{...field} render={({ field }) => (
// TODO: Implement placeholder for select <Select
label={!field.value ? 'Select an account / team' : ''} {...field}
// TODO: Implement placeholder for select
label={!field.value ? 'Select an account / team' : ''}
>
{TEAMS.map((team, key) => (
<Option key={key} value={team}>
^ {team}
</Option>
))}
</Select>
)}
/>
<Button
variant="gradient"
size="sm"
className="mt-1"
disabled={!formState.isValid}
type="submit"
> >
{TEAMS.map((team, key) => ( Transfer
<Option key={key} value={team}> </Button>
^ {team} </form>
</Option> <ConfirmDialog
))} dialogTitle="Transfer project"
</Select> handleOpen={handleTransferProjectDialog}
)} open={openTransferDialog}
/> confirmButtonTitle="Yes, Confirm transfer"
<Button handleConfirm={handleTransferProjectDialog}
variant="gradient" color="blue"
size="sm" >
className="mt-1" <Typography variant="small">
disabled={!formState.isValid} Upon confirmation, your project nextjs-boilerplate will be
type="submit" transferred from saugat to Airfoil.
> </Typography>
Transfer </ConfirmDialog>
</Button> </div>
</form> <div className="mb-1">
<ConfirmDialog <Typography variant="h6">Delete project</Typography>
dialogTitle="Transfer project" <Typography variant="small">
handleOpen={handleTransferProjectDialog} The project will be permanently deleted, including its deployments
open={openTransferDialog} and domains. This action is irreversible and can not be undone.
confirmButtonTitle="Yes, Confirm transfer" </Typography>
handleConfirm={handleTransferProjectDialog} <Button
color="blue" variant="gradient"
> size="sm"
<Typography variant="small"> color="red"
Upon confirmation, your project nextjs-boilerplate will be onClick={handleDeleteProjectDialog}
transferred from saugat to Airfoil. >
</Typography> ^ Delete project
</ConfirmDialog> </Button>
</div> <DeleteProjectDialog
<div className="mb-1"> handleOpen={handleDeleteProjectDialog}
<Typography variant="h6">Delete project</Typography> open={openDeleteDialog}
<Typography variant="small"> project={{ name: 'Iglootools' }}
The project will be permanently deleted, including its deployments and />
domains. This action is irreversible and can not be undone. </div>
</Typography> </>
<Button )}
variant="gradient"
size="sm"
color="red"
onClick={handleDeleteProjectDialog}
>
^ Delete project
</Button>
<DeleteProjectDialog
handleOpen={handleDeleteProjectDialog}
open={openDeleteDialog}
project={{ name: 'Iglootools' }}
/>
</div>
</> </>
); );
}; };

View File

@ -18,12 +18,12 @@ const MembersTabPanel = () => {
// @ts-expect-error create context type for projects // @ts-expect-error create context type for projects
const { projects } = useOutletContext(); const { projects } = useOutletContext();
const currProject = useMemo(() => { const currentProject = useMemo(() => {
return projects.find((project: any) => project.id === id); return projects.find((project: any) => project.id === id);
}, [id]); }, [id]);
const [updatedMembers, setUpdatedMembers] = useState([ const [updatedMembers, setUpdatedMembers] = useState([
...currProject?.members, ...currentProject?.members,
]); ]);
const addMemberHandler = useCallback((member: Member) => { const addMemberHandler = useCallback((member: Member) => {
@ -59,7 +59,7 @@ const MembersTabPanel = () => {
member={member.member} member={member.member}
key={member.id} key={member.id}
isFirstCard={index === FIRST_MEMBER_CARD} isFirstCard={index === FIRST_MEMBER_CARD}
isOwner={member.member.id === currProject?.owner.id} isOwner={member.member.id === currentProject?.owner.id}
isPending={member.name === ''} isPending={member.name === ''}
permissions={member.permissions} permissions={member.permissions}
handleDeletePendingMember={(id: number) => { handleDeletePendingMember={(id: number) => {