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
```bash
yarn build
yarn build --ignore frontend
```
- Change directory to `packages/backend`
@ -24,6 +24,12 @@
cd packages/backend
```
- Load fixtures in database
```bash
yarn db:load:fixtures
```
- Start the server
```bash

View File

@ -29,7 +29,7 @@
"lint": "eslint .",
"format": "prettier --write .",
"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": {
"@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 DB_PATH = '../db/snowball';
const USER_DATA_PATH = './fixtures/users.json';
const PROJECT_DATA_PATH = './fixtures/projects.json';
const ORGANIZATION_DATA_PATH = './fixtures/organizations.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 entities = JSON.parse(entitiesData) as DeepPartial<Entity>[];
const entities = JSON.parse(entitiesData);
const entityRepository = dataSource.getRepository(entityType);
const savedEntity:Entity[] = [];
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);
savedEntity.push(dbEntity);
}
@ -38,52 +55,69 @@ const loadAndSaveData = async <Entity extends ObjectLiteral>(entityType: EntityT
const generateTestData = async (dataSource: DataSource) => {
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 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 projects = JSON.parse(projectsData);
const projectRepository = dataSource.getRepository(Project);
const projectRelations = {
owner: savedUsers,
organization: savedOrgs
};
for (const projectData of projects) {
const project = projectRepository.create(projectData as DeepPartial<Project>);
project.owner = savedUsers[projectData.ownerIndex];
project.organization = savedOrgs[projectData.organizationIndex];
await projectRepository.save(project);
}
const savedProjects = await loadAndSaveData(Project, dataSource, path.resolve(__dirname, PROJECT_DATA_PATH), projectRelations);
const userOrgData = await fs.readFile(path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH), 'utf-8');
const userOrgs = JSON.parse(userOrgData);
const userOrgRepository = dataSource.getRepository(UserOrganization);
const userOrganizationRelations = {
member: savedUsers,
organization: savedOrgs
};
for (const userOrgData of userOrgs) {
const userOrg = userOrgRepository.create(userOrgData as DeepPartial<UserOrganization>);
userOrg.member = savedUsers[userOrgData.memberIndex];
userOrg.organization = savedOrgs[userOrgData.organizationIndex];
await loadAndSaveData(UserOrganization, dataSource, path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH), userOrganizationRelations);
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 dataSource = new DataSource({
type: 'better-sqlite3',
database: 'db/snowball',
synchronize: true,
logging: true,
entities: [
User,
Organization,
Project,
UserOrganization,
EnvironmentVariable,
Domain,
ProjectMember,
Deployment
]
});
const isDbPresent = await checkFileExists(path.resolve(__dirname, DB_PATH));
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(() => {

View File

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

View File

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

View File

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