From 4c2dfb0f923695c3b49c9960405b66519c55ce59 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Sun, 9 Jun 2024 15:03:56 +0100 Subject: [PATCH] refactor: Add CodeRepository model, service, and permissions This code change adds the CodeRepository model, service, and permissions to the project. It includes the following modifications: - Added CodeRepository model to the Models/Index.ts file. - Added CodeRepositoryService to the Services/Index.ts file. - Created CodeRepositoryService.ts file with the necessary implementation. - Updated Permission enum in the Permission.ts file to include CreateCodeRepository, DeleteCodeRepository, EditCodeRepository, and ReadCodeRepository permissions. These changes enable the project to manage code repositories and define the necessary permissions for them. --- .github/workflows/test.e2e.yaml | 55 ++- Common/Types/Permission.ts | 38 ++ .../Services/CodeRepositoryService.ts | 25 + CommonServer/Services/Index.ts | 3 + Model/Models/CodeRepository.ts | 463 ++++++++++++++++++ Model/Models/Index.ts | 3 + 6 files changed, 586 insertions(+), 1 deletion(-) create mode 100644 CommonServer/Services/CodeRepositoryService.ts create mode 100644 Model/Models/CodeRepository.ts diff --git a/.github/workflows/test.e2e.yaml b/.github/workflows/test.e2e.yaml index 3f7e3783441..a28d8fb31f8 100644 --- a/.github/workflows/test.e2e.yaml +++ b/.github/workflows/test.e2e.yaml @@ -58,7 +58,7 @@ jobs: # Optional. Defaults to repository settings. retention-days: 7 - test-e2e-release: + test-e2e-release-saas: runs-on: ubuntu-latest #needs: [test-helm-chart] env: @@ -109,6 +109,59 @@ jobs: # Maximum 90 days unless changed from the repository settings page. # Optional. Defaults to repository settings. retention-days: 7 + + + test-e2e-release-self-hosted: + runs-on: ubuntu-latest + #needs: [test-helm-chart] + env: + CI_PIPELINE_ID: ${{github.run_number}} + steps: + # Docker compose needs a lot of space to build images, so we need to free up some space first in the GitHub Actions runner + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + # this might remove tools that are actually needed, + # if set to "true" but frees about 6 GB + tool-cache: false + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: true + swap-storage: true + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 18.3.0 + - run: npm run prerun + - name: Start Server with release tag + run: npm run start + - name: Wait for server to start + run: bash ./Tests/Scripts/status-check.sh http://localhost + - name: Run E2E Tests. Run docker container e2e in docker compose file + run: export $(grep -v '^#' config.env | xargs) && docker-compose -f docker-compose.dev.yml up --exit-code-from e2e --abort-on-container-exit e2e || (docker-compose -f docker-compose.dev.yml logs e2e && exit 1) + - name: Upload test results + uses: actions/upload-artifact@v4 + # Run this on failure + if: failure() + with: + # Name of the artifact to upload. + # Optional. Default is 'artifact' + name: test-results + + # A file, directory or wildcard pattern that describes what to upload + # Required. + path: | + ./E2E/playwright-report + ./E2E/test-results + + + # Duration after which artifact will expire in days. 0 means using default retention. + # Minimum 1 day. + # Maximum 90 days unless changed from the repository settings page. + # Optional. Defaults to repository settings. + retention-days: 7 diff --git a/Common/Types/Permission.ts b/Common/Types/Permission.ts index 97f5397694a..e0edba775c1 100644 --- a/Common/Types/Permission.ts +++ b/Common/Types/Permission.ts @@ -459,6 +459,11 @@ enum Permission { DeleteServiceCatalog = 'DeleteServiceCatalog', EditServiceCatalog = 'EditServiceCatalog', ReadServiceCatalog = 'ReadServiceCatalog', + + CreateCodeRepository = 'CreateCodeRepository', + DeleteCodeRepository = 'DeleteCodeRepository', + EditCodeRepository = 'EditCodeRepository', + ReadCodeRepository = 'ReadCodeRepository', } export class PermissionHelper { @@ -2442,6 +2447,39 @@ export class PermissionHelper { isAccessControlPermission: false, }, + { + permission: Permission.CreateCodeRepository, + title: 'Create Code Repository', + description: + 'This permission can create Code Repository this project.', + isAssignableToTenant: true, + isAccessControlPermission: true, + }, + { + permission: Permission.DeleteCodeRepository, + title: 'Delete Code Repository', + description: + 'This permission can delete Code Repository of this project.', + isAssignableToTenant: true, + isAccessControlPermission: true, + }, + { + permission: Permission.EditCodeRepository, + title: 'Edit Code Repository', + description: + 'This permission can edit Code Repository of this project.', + isAssignableToTenant: true, + isAccessControlPermission: true, + }, + { + permission: Permission.ReadCodeRepository, + title: 'Read Code Repository', + description: + 'This permission can read Code Repository of this project.', + isAssignableToTenant: true, + isAccessControlPermission: true, + }, + { permission: Permission.CreateServiceCatalog, title: 'Create Service Catalog', diff --git a/CommonServer/Services/CodeRepositoryService.ts b/CommonServer/Services/CodeRepositoryService.ts new file mode 100644 index 00000000000..de9a05ab341 --- /dev/null +++ b/CommonServer/Services/CodeRepositoryService.ts @@ -0,0 +1,25 @@ +import PostgresDatabase from '../Infrastructure/PostgresDatabase'; +import CreateBy from '../Types/Database/CreateBy'; +import { OnCreate } from '../Types/Database/Hooks'; +import DatabaseService from './DatabaseService'; +import ObjectID from 'Common/Types/ObjectID'; +import Model from 'Model/Models/CodeRepository'; + +export class Service extends DatabaseService { + public constructor(postgresDatabase?: PostgresDatabase) { + super(Model, postgresDatabase); + } + + protected override async onBeforeCreate( + createBy: CreateBy + ): Promise> { + createBy.data.secretToken = ObjectID.generate(); + + return { + carryForward: null, + createBy: createBy, + }; + } +} + +export default new Service(); diff --git a/CommonServer/Services/Index.ts b/CommonServer/Services/Index.ts index a8bc25fb45b..33eb7151ead 100644 --- a/CommonServer/Services/Index.ts +++ b/CommonServer/Services/Index.ts @@ -11,6 +11,7 @@ import BillingPaymentMethodsService from './BillingPaymentMethodService'; import BillingService from './BillingService'; import CallLogService from './CallLogService'; import CallService from './CallService'; +import CodeRepositoryService from './CodeRepositoryService'; import DataMigrationService from './DataMigrationService'; import DomainService from './DomainService'; import EmailLogService from './EmailLogService'; @@ -253,6 +254,8 @@ const services: Array = [ ServiceCatalogService, ServiceCatalogOwnerTeamService, ServiceCatalogOwnerUserService, + + CodeRepositoryService, ]; export const AnalyticsServices: Array< diff --git a/Model/Models/CodeRepository.ts b/Model/Models/CodeRepository.ts new file mode 100644 index 00000000000..78d1954fcaf --- /dev/null +++ b/Model/Models/CodeRepository.ts @@ -0,0 +1,463 @@ +import Label from './Label'; +import Project from './Project'; +import User from './User'; +import BaseModel from 'Common/Models/BaseModel'; +import Route from 'Common/Types/API/Route'; +import { PlanSelect } from 'Common/Types/Billing/SubscriptionPlan'; +import ColumnAccessControl from 'Common/Types/Database/AccessControl/ColumnAccessControl'; +import TableAccessControl from 'Common/Types/Database/AccessControl/TableAccessControl'; +import TableBillingAccessControl from 'Common/Types/Database/AccessControl/TableBillingAccessControl'; +import AccessControlColumn from 'Common/Types/Database/AccessControlColumn'; +import ColumnLength from 'Common/Types/Database/ColumnLength'; +import ColumnType from 'Common/Types/Database/ColumnType'; +import CrudApiEndpoint from 'Common/Types/Database/CrudApiEndpoint'; +import EnableDocumentation from 'Common/Types/Database/EnableDocumentation'; +import EnableWorkflow from 'Common/Types/Database/EnableWorkflow'; +import SlugifyColumn from 'Common/Types/Database/SlugifyColumn'; +import TableColumn from 'Common/Types/Database/TableColumn'; +import TableColumnType from 'Common/Types/Database/TableColumnType'; +import TableMetadata from 'Common/Types/Database/TableMetadata'; +import TenantColumn from 'Common/Types/Database/TenantColumn'; +import UniqueColumnBy from 'Common/Types/Database/UniqueColumnBy'; +import IconProp from 'Common/Types/Icon/IconProp'; +import ObjectID from 'Common/Types/ObjectID'; +import Permission from 'Common/Types/Permission'; +import { + Column, + Entity, + Index, + JoinColumn, + JoinTable, + ManyToMany, + ManyToOne, +} from 'typeorm'; + +@AccessControlColumn('labels') +@EnableDocumentation() +@TenantColumn('projectId') +@TableBillingAccessControl({ + create: PlanSelect.Growth, + read: PlanSelect.Growth, + update: PlanSelect.Growth, + delete: PlanSelect.Growth, +}) +@TableAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateCodeRepository, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ProjectMember, + Permission.ReadCodeRepository, + ], + delete: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.DeleteCodeRepository, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditCodeRepository, + ], +}) +@EnableWorkflow({ + create: true, + delete: true, + update: true, + read: true, +}) +@CrudApiEndpoint(new Route('/code-repository')) +@SlugifyColumn('name', 'slug') +@TableMetadata({ + tableName: 'CodeRepository', + singularName: 'Git Repository', + pluralName: 'Git Repositories', + icon: IconProp.SquareStack, + tableDescription: + 'A Git Repository is a place where you can store your code and collaborate with others. You can connect your Git Repository to OneUptime and we will improve your code automatically for you.', +}) +@Entity({ + name: 'CodeRepository', +}) +export default class CodeRepository extends BaseModel { + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateCodeRepository, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ProjectMember, + Permission.ReadCodeRepository, + ], + update: [], + }) + @TableColumn({ + manyToOneRelationColumn: 'projectId', + type: TableColumnType.Entity, + modelType: Project, + title: 'Project', + description: + 'Relation to Project Resource in which this object belongs', + }) + @ManyToOne( + (_type: string) => { + return Project; + }, + { + eager: false, + nullable: true, + onDelete: 'CASCADE', + orphanedRowAction: 'nullify', + } + ) + @JoinColumn({ name: 'projectId' }) + public project?: Project = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateCodeRepository, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ProjectMember, + Permission.ReadCodeRepository, + ], + update: [], + }) + @Index() + @TableColumn({ + type: TableColumnType.ObjectID, + required: true, + canReadOnRelationQuery: true, + title: 'Project ID', + description: + 'ID of your OneUptime Project in which this object belongs', + }) + @Column({ + type: ColumnType.ObjectID, + nullable: false, + transformer: ObjectID.getDatabaseTransformer(), + }) + public projectId?: ObjectID = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateCodeRepository, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ProjectMember, + Permission.ReadCodeRepository, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditCodeRepository, + ], + }) + @TableColumn({ + required: true, + type: TableColumnType.ShortText, + canReadOnRelationQuery: true, + title: 'Name', + description: 'Any friendly name of this object', + }) + @Column({ + nullable: false, + type: ColumnType.ShortText, + length: ColumnLength.ShortText, + }) + @UniqueColumnBy('projectId') + public name?: string = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ProjectMember, + Permission.ReadCodeRepository, + ], + update: [], + }) + @TableColumn({ + required: true, + unique: true, + type: TableColumnType.Slug, + title: 'Slug', + description: 'Friendly globally unique name for your object', + }) + @Column({ + nullable: false, + type: ColumnType.Slug, + length: ColumnLength.Slug, + }) + public slug?: string = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateCodeRepository, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ProjectMember, + Permission.ReadCodeRepository, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditCodeRepository, + ], + }) + @TableColumn({ + required: false, + type: TableColumnType.LongText, + title: 'Description', + description: 'Friendly description that will help you remember', + }) + @Column({ + nullable: true, + type: ColumnType.LongText, + length: ColumnLength.LongText, + }) + public description?: string = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateCodeRepository, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ProjectMember, + Permission.ReadCodeRepository, + ], + update: [], + }) + @TableColumn({ + manyToOneRelationColumn: 'createdByUserId', + type: TableColumnType.Entity, + modelType: User, + title: 'Created by User', + description: + 'Relation to User who created this object (if this object was created by a User)', + }) + @ManyToOne( + (_type: string) => { + return User; + }, + { + eager: false, + nullable: true, + onDelete: 'CASCADE', + orphanedRowAction: 'nullify', + } + ) + @JoinColumn({ name: 'createdByUserId' }) + public createdByUser?: User = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateCodeRepository, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ProjectMember, + Permission.ReadCodeRepository, + ], + update: [], + }) + @TableColumn({ + type: TableColumnType.ObjectID, + title: 'Created by User ID', + description: + 'User ID who created this object (if this object was created by a User)', + }) + @Column({ + type: ColumnType.ObjectID, + nullable: true, + transformer: ObjectID.getDatabaseTransformer(), + }) + public createdByUserId?: ObjectID = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ProjectMember, + Permission.ReadCodeRepository, + ], + update: [], + }) + @TableColumn({ + manyToOneRelationColumn: 'deletedByUserId', + type: TableColumnType.Entity, + title: 'Deleted by User', + description: + 'Relation to User who deleted this object (if this object was deleted by a User)', + }) + @ManyToOne( + (_type: string) => { + return User; + }, + { + cascade: false, + eager: false, + nullable: true, + onDelete: 'CASCADE', + orphanedRowAction: 'nullify', + } + ) + @JoinColumn({ name: 'deletedByUserId' }) + public deletedByUser?: User = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ProjectMember, + Permission.ReadCodeRepository, + ], + update: [], + }) + @TableColumn({ + type: TableColumnType.ObjectID, + title: 'Deleted by User ID', + description: + 'User ID who deleted this object (if this object was deleted by a User)', + }) + @Column({ + type: ColumnType.ObjectID, + nullable: true, + transformer: ObjectID.getDatabaseTransformer(), + }) + public deletedByUserId?: ObjectID = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateCodeRepository, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ProjectMember, + Permission.ReadCodeRepository, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditCodeRepository, + ], + }) + @TableColumn({ + required: false, + type: TableColumnType.EntityArray, + modelType: Label, + title: 'Labels', + description: + 'Relation to Labels Array where this object is categorized in.', + }) + @ManyToMany( + () => { + return Label; + }, + { eager: false } + ) + @JoinTable({ + name: 'CodeRepositoryLabel', + inverseJoinColumn: { + name: 'labelId', + referencedColumnName: '_id', + }, + joinColumn: { + name: 'CodeRepositoryId', + referencedColumnName: '_id', + }, + }) + public labels?: Array