Refactor: improve code quality and worker flow
This commit is contained in:
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"moku-backend/internal/database"
|
||||
"moku-backend/internal/posixproj"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -48,6 +47,7 @@ const (
|
||||
var (
|
||||
ErrInstallationNotConfigured = errors.New("bootstrap installation step has not been completed")
|
||||
ErrAdminNotConfigured = errors.New("bootstrap admin step has not been completed")
|
||||
ErrBootstrapStructureMissing = errors.New("bootstrap structure is incomplete")
|
||||
ErrProjectNotFound = errors.New("project not found")
|
||||
ErrProjectFolderNotFound = errors.New("project folder not found")
|
||||
ErrInvalidProjectFolderMove = errors.New("invalid project folder move")
|
||||
@@ -83,13 +83,15 @@ type SaveStructureInput struct {
|
||||
}
|
||||
|
||||
type InstallationRecord struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Mode string `json:"mode"`
|
||||
Access string `json:"access"`
|
||||
Protocol string `json:"protocol"`
|
||||
Host string `json:"host"`
|
||||
IsBootstrapped bool `json:"isBootstrapped"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Mode string `json:"mode"`
|
||||
Access string `json:"access"`
|
||||
Protocol string `json:"protocol"`
|
||||
Host string `json:"host"`
|
||||
IsBootstrapped bool `json:"isBootstrapped"`
|
||||
MaterializationStatus string `json:"materializationStatus"`
|
||||
MaterializationError *string `json:"materializationError,omitempty"`
|
||||
}
|
||||
|
||||
type AdminRecord struct {
|
||||
@@ -194,29 +196,29 @@ type ProjectHierarchyFolderRecord struct {
|
||||
}
|
||||
|
||||
type CreateProjectFolderInput struct {
|
||||
ProjectID string
|
||||
ParentFolderID string
|
||||
Name string
|
||||
ProjectID string
|
||||
ParentFolderPath string
|
||||
Name string
|
||||
}
|
||||
|
||||
type DeleteProjectFolderInput struct {
|
||||
ProjectID string
|
||||
FolderID string
|
||||
ProjectID string
|
||||
FolderPath string
|
||||
}
|
||||
|
||||
type RenameProjectFolderInput struct {
|
||||
ProjectID string
|
||||
FolderID string
|
||||
Name string
|
||||
ProjectID string
|
||||
FolderPath string
|
||||
Name string
|
||||
}
|
||||
|
||||
type MoveProjectFolderInput struct {
|
||||
ProjectID string
|
||||
FolderID string
|
||||
FolderNodeID string
|
||||
ParentFolderID string
|
||||
ParentNodeID string
|
||||
TargetIndex int
|
||||
ProjectID string
|
||||
FolderPath string
|
||||
FolderStableID string
|
||||
ParentFolderPath string
|
||||
ParentStableID string
|
||||
TargetIndex int
|
||||
}
|
||||
|
||||
type CreateProjectFolderResult struct {
|
||||
@@ -226,26 +228,26 @@ type CreateProjectFolderResult struct {
|
||||
}
|
||||
|
||||
type DeleteProjectFolderResult struct {
|
||||
ProjectID string `json:"projectId"`
|
||||
DeletedFolderID string `json:"deletedFolderId"`
|
||||
DeletedFolderPath string `json:"deletedFolderPath"`
|
||||
Folders []ProjectHierarchyFolderRecord `json:"folders"`
|
||||
ProjectID string `json:"projectId"`
|
||||
DeletedFolderStableID string `json:"deletedFolderId"`
|
||||
DeletedFolderPath string `json:"deletedFolderPath"`
|
||||
Folders []ProjectHierarchyFolderRecord `json:"folders"`
|
||||
}
|
||||
|
||||
type RenameProjectFolderResult struct {
|
||||
ProjectID string `json:"projectId"`
|
||||
PreviousFolderID string `json:"previousFolderId"`
|
||||
PreviousFolderPath string `json:"previousFolderPath"`
|
||||
RenamedFolder ProjectHierarchyFolderRecord `json:"renamedFolder"`
|
||||
Folders []ProjectHierarchyFolderRecord `json:"folders"`
|
||||
ProjectID string `json:"projectId"`
|
||||
PreviousFolderStableID string `json:"previousFolderId"`
|
||||
PreviousFolderPath string `json:"previousFolderPath"`
|
||||
RenamedFolder ProjectHierarchyFolderRecord `json:"renamedFolder"`
|
||||
Folders []ProjectHierarchyFolderRecord `json:"folders"`
|
||||
}
|
||||
|
||||
type MoveProjectFolderResult struct {
|
||||
ProjectID string `json:"projectId"`
|
||||
PreviousFolderID string `json:"previousFolderId"`
|
||||
PreviousFolderPath string `json:"previousFolderPath"`
|
||||
MovedFolder ProjectHierarchyFolderRecord `json:"movedFolder"`
|
||||
Folders []ProjectHierarchyFolderRecord `json:"folders"`
|
||||
ProjectID string `json:"projectId"`
|
||||
PreviousFolderStableID string `json:"previousFolderId"`
|
||||
PreviousFolderPath string `json:"previousFolderPath"`
|
||||
MovedFolder ProjectHierarchyFolderRecord `json:"movedFolder"`
|
||||
Folders []ProjectHierarchyFolderRecord `json:"folders"`
|
||||
}
|
||||
|
||||
type projectHierarchyFolderRow struct {
|
||||
@@ -276,7 +278,16 @@ func (service *Service) SaveInstance(ctx context.Context, input SaveInstanceInpu
|
||||
protocol = EXCLUDED.protocol,
|
||||
host = EXCLUDED.host,
|
||||
updated_at = NOW()
|
||||
RETURNING id::text, name, mode::text, access::text, protocol::text, host, is_bootstrapped;
|
||||
RETURNING
|
||||
id::text,
|
||||
name,
|
||||
mode::text,
|
||||
access::text,
|
||||
protocol::text,
|
||||
host,
|
||||
is_bootstrapped,
|
||||
materialization_status::text,
|
||||
materialization_error;
|
||||
`, input.Access, input.Protocol, input.Host)
|
||||
|
||||
return scanInstallationRecord(row)
|
||||
@@ -298,7 +309,16 @@ func (service *Service) SaveMode(ctx context.Context, input SaveModeInput) (Inst
|
||||
name = EXCLUDED.name,
|
||||
mode = EXCLUDED.mode,
|
||||
updated_at = NOW()
|
||||
RETURNING id::text, name, mode::text, access::text, protocol::text, host, is_bootstrapped;
|
||||
RETURNING
|
||||
id::text,
|
||||
name,
|
||||
mode::text,
|
||||
access::text,
|
||||
protocol::text,
|
||||
host,
|
||||
is_bootstrapped,
|
||||
materialization_status::text,
|
||||
materialization_error;
|
||||
`, input.Mode, input.Name, defaultInstallationHost)
|
||||
|
||||
return scanInstallationRecord(row)
|
||||
@@ -360,6 +380,11 @@ func (service *Service) SaveAdmin(ctx context.Context, input SaveAdminInput) (Ad
|
||||
return record, nil
|
||||
}
|
||||
|
||||
// SaveStructure persists the bootstrap domain records synchronously, then hands the
|
||||
// slow POSIX/projector materialization work to the background worker.
|
||||
//
|
||||
// This keeps the API request responsible for validation and durable relational writes,
|
||||
// while the worker owns retryable filesystem/projection side effects.
|
||||
func (service *Service) SaveStructure(ctx context.Context, input SaveStructureInput) (StructureRecord, error) {
|
||||
tx, err := service.db.Pool.BeginTx(ctx, pgx.TxOptions{})
|
||||
if err != nil {
|
||||
@@ -369,27 +394,19 @@ func (service *Service) SaveStructure(ctx context.Context, input SaveStructureIn
|
||||
_ = tx.Rollback(ctx)
|
||||
}()
|
||||
|
||||
installation, err := loadInstallation(ctx, tx)
|
||||
prerequisites, err := service.loadBootstrapStructurePrerequisites(ctx, tx)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return StructureRecord{}, ErrInstallationNotConfigured
|
||||
}
|
||||
|
||||
return StructureRecord{}, err
|
||||
}
|
||||
|
||||
admin, err := loadPrimaryAdmin(ctx, tx)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return StructureRecord{}, ErrAdminNotConfigured
|
||||
}
|
||||
|
||||
return StructureRecord{}, err
|
||||
}
|
||||
|
||||
organizationName := strings.TrimSpace(input.OrganizationName)
|
||||
if organizationName == "" {
|
||||
organizationName = defaultRootOrganizationName(installation.Name, installation.Mode, installation.Host, admin.DisplayName)
|
||||
organizationName = defaultRootOrganizationName(
|
||||
prerequisites.installation.Name,
|
||||
prerequisites.installation.Mode,
|
||||
prerequisites.installation.Host,
|
||||
prerequisites.admin.DisplayName,
|
||||
)
|
||||
}
|
||||
|
||||
organization, err := upsertNamedRecord(ctx, tx, `
|
||||
@@ -398,7 +415,7 @@ func (service *Service) SaveStructure(ctx context.Context, input SaveStructureIn
|
||||
ON CONFLICT (slug) DO UPDATE
|
||||
SET name = EXCLUDED.name, created_by_user_id = EXCLUDED.created_by_user_id, updated_at = NOW()
|
||||
RETURNING id::text, name, slug;
|
||||
`, organizationName, primaryOrganizationSlug, admin.ID)
|
||||
`, organizationName, primaryOrganizationSlug, prerequisites.admin.ID)
|
||||
if err != nil {
|
||||
return StructureRecord{}, err
|
||||
}
|
||||
@@ -408,7 +425,7 @@ func (service *Service) SaveStructure(ctx context.Context, input SaveStructureIn
|
||||
VALUES ($1::uuid, $2::uuid, 'owner'::membership_role)
|
||||
ON CONFLICT (organization_id, user_id) DO UPDATE
|
||||
SET role = EXCLUDED.role;
|
||||
`, organization.ID, admin.ID); err != nil {
|
||||
`, organization.ID, prerequisites.admin.ID); err != nil {
|
||||
return StructureRecord{}, err
|
||||
}
|
||||
|
||||
@@ -418,7 +435,7 @@ func (service *Service) SaveStructure(ctx context.Context, input SaveStructureIn
|
||||
ON CONFLICT (organization_id, slug) DO UPDATE
|
||||
SET name = EXCLUDED.name, created_by_user_id = EXCLUDED.created_by_user_id, updated_at = NOW()
|
||||
RETURNING id::text, name, slug;
|
||||
`, organization.ID, input.DepartmentName, primaryDepartmentSlug, admin.ID)
|
||||
`, organization.ID, input.DepartmentName, primaryDepartmentSlug, prerequisites.admin.ID)
|
||||
if err != nil {
|
||||
return StructureRecord{}, err
|
||||
}
|
||||
@@ -429,7 +446,7 @@ func (service *Service) SaveStructure(ctx context.Context, input SaveStructureIn
|
||||
ON CONFLICT (organization_id, slug) DO UPDATE
|
||||
SET department_id = EXCLUDED.department_id, name = EXCLUDED.name, created_by_user_id = EXCLUDED.created_by_user_id, updated_at = NOW()
|
||||
RETURNING id::text, name, slug;
|
||||
`, organization.ID, department.ID, input.TeamName, primaryTeamSlug, admin.ID)
|
||||
`, organization.ID, department.ID, input.TeamName, primaryTeamSlug, prerequisites.admin.ID)
|
||||
if err != nil {
|
||||
return StructureRecord{}, err
|
||||
}
|
||||
@@ -439,7 +456,7 @@ func (service *Service) SaveStructure(ctx context.Context, input SaveStructureIn
|
||||
VALUES ($1::uuid, $2::uuid, 'owner'::membership_role)
|
||||
ON CONFLICT (team_id, user_id) DO UPDATE
|
||||
SET role = EXCLUDED.role;
|
||||
`, team.ID, admin.ID); err != nil {
|
||||
`, team.ID, prerequisites.admin.ID); err != nil {
|
||||
return StructureRecord{}, err
|
||||
}
|
||||
|
||||
@@ -449,7 +466,7 @@ func (service *Service) SaveStructure(ctx context.Context, input SaveStructureIn
|
||||
ON CONFLICT (organization_id, slug) DO UPDATE
|
||||
SET department_id = EXCLUDED.department_id, team_id = EXCLUDED.team_id, name = EXCLUDED.name, created_by_user_id = EXCLUDED.created_by_user_id, updated_at = NOW()
|
||||
RETURNING id::text, name, slug;
|
||||
`, organization.ID, department.ID, team.ID, input.ProjectName, primaryProjectSlug, admin.ID)
|
||||
`, organization.ID, department.ID, team.ID, input.ProjectName, primaryProjectSlug, prerequisites.admin.ID)
|
||||
if err != nil {
|
||||
return StructureRecord{}, err
|
||||
}
|
||||
@@ -459,27 +476,27 @@ func (service *Service) SaveStructure(ctx context.Context, input SaveStructureIn
|
||||
VALUES ($1::uuid, $2::uuid, 'owner'::membership_role)
|
||||
ON CONFLICT (project_id, user_id) DO UPDATE
|
||||
SET role = EXCLUDED.role;
|
||||
`, project.ID, admin.ID); err != nil {
|
||||
`, project.ID, prerequisites.admin.ID); err != nil {
|
||||
return StructureRecord{}, err
|
||||
}
|
||||
|
||||
if err := upsertWorkspace(ctx, tx, organization.ID, organization.Name, organizationWorkspaceSlug, bootstrapWorkspaceKindOrg, admin.ID, nil, nil, nil); err != nil {
|
||||
if err := upsertWorkspace(ctx, tx, organization.ID, organization.Name, organizationWorkspaceSlug, bootstrapWorkspaceKindOrg, prerequisites.admin.ID, nil, nil, nil); err != nil {
|
||||
return StructureRecord{}, err
|
||||
}
|
||||
|
||||
if err := upsertWorkspace(ctx, tx, organization.ID, department.Name, departmentWorkspaceSlug, bootstrapWorkspaceKindDept, admin.ID, &department.ID, nil, nil); err != nil {
|
||||
if err := upsertWorkspace(ctx, tx, organization.ID, department.Name, departmentWorkspaceSlug, bootstrapWorkspaceKindDept, prerequisites.admin.ID, &department.ID, nil, nil); err != nil {
|
||||
return StructureRecord{}, err
|
||||
}
|
||||
|
||||
if err := upsertWorkspace(ctx, tx, organization.ID, team.Name, teamWorkspaceSlug, bootstrapWorkspaceKindTeam, admin.ID, &department.ID, &team.ID, nil); err != nil {
|
||||
if err := upsertWorkspace(ctx, tx, organization.ID, team.Name, teamWorkspaceSlug, bootstrapWorkspaceKindTeam, prerequisites.admin.ID, &department.ID, &team.ID, nil); err != nil {
|
||||
return StructureRecord{}, err
|
||||
}
|
||||
|
||||
if err := upsertWorkspace(ctx, tx, organization.ID, project.Name, projectWorkspaceSlug, bootstrapWorkspaceKindProject, admin.ID, &department.ID, &team.ID, &project.ID); err != nil {
|
||||
if err := upsertWorkspace(ctx, tx, organization.ID, project.Name, projectWorkspaceSlug, bootstrapWorkspaceKindProject, prerequisites.admin.ID, &department.ID, &team.ID, &project.ID); err != nil {
|
||||
return StructureRecord{}, err
|
||||
}
|
||||
|
||||
installation, err = updateBootstrappedInstallation(ctx, tx)
|
||||
installation, err := updateBootstrappedInstallation(ctx, tx)
|
||||
if err != nil {
|
||||
return StructureRecord{}, err
|
||||
}
|
||||
@@ -488,21 +505,17 @@ func (service *Service) SaveStructure(ctx context.Context, input SaveStructureIn
|
||||
return StructureRecord{}, err
|
||||
}
|
||||
|
||||
if err := service.ensureBootstrapPOSIXSkeleton(installation, admin, organization, department, team, project); err != nil {
|
||||
if err := service.enqueueBootstrapStructureMaterialization(ctx, &installation); err != nil {
|
||||
return StructureRecord{}, err
|
||||
}
|
||||
|
||||
if err := posixproj.NewProjector(service.db, service.posixRoot).Rebuild(ctx); err != nil {
|
||||
return StructureRecord{}, fmt.Errorf("rebuild POSIX projection: %w", err)
|
||||
}
|
||||
|
||||
return StructureRecord{
|
||||
Installation: installation,
|
||||
Organization: organization,
|
||||
Department: department,
|
||||
Team: team,
|
||||
Project: project,
|
||||
Admin: admin,
|
||||
Admin: prerequisites.admin,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -527,6 +540,7 @@ func (service *Service) ResetDevelopmentState(ctx context.Context) error {
|
||||
user_homes,
|
||||
users,
|
||||
organizations,
|
||||
background_jobs,
|
||||
installations
|
||||
RESTART IDENTITY;
|
||||
`); err != nil {
|
||||
@@ -536,24 +550,6 @@ func (service *Service) ResetDevelopmentState(ctx context.Context) error {
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (service *Service) GetInstallation(ctx context.Context) (*InstallationRecord, error) {
|
||||
record, err := scanInstallationRecord(service.db.Pool.QueryRow(ctx, `
|
||||
SELECT id::text, name, mode::text, access::text, protocol::text, host, is_bootstrapped
|
||||
FROM installations
|
||||
WHERE singleton = TRUE
|
||||
LIMIT 1;
|
||||
`))
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
func (service *Service) GetAdmin(ctx context.Context) (*AdminRecord, error) {
|
||||
var record AdminRecord
|
||||
err := service.db.Pool.QueryRow(ctx, `
|
||||
@@ -620,86 +616,6 @@ func (service *Service) GetStructure(ctx context.Context) (BootstrapStructureSta
|
||||
Workspaces: workspaces,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) GetState(ctx context.Context) (BootstrapState, error) {
|
||||
installation, err := service.GetInstallation(ctx)
|
||||
if err != nil {
|
||||
return BootstrapState{}, err
|
||||
}
|
||||
|
||||
admin, err := service.GetAdmin(ctx)
|
||||
if err != nil {
|
||||
return BootstrapState{}, err
|
||||
}
|
||||
|
||||
structure, err := service.GetStructure(ctx)
|
||||
if err != nil {
|
||||
return BootstrapState{}, err
|
||||
}
|
||||
|
||||
return BootstrapState{
|
||||
Installation: installation,
|
||||
Admin: admin,
|
||||
Structure: structure,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) GetAppShellState(ctx context.Context) (AppShellState, error) {
|
||||
installation, err := service.GetInstallation(ctx)
|
||||
if err != nil {
|
||||
return AppShellState{}, err
|
||||
}
|
||||
|
||||
admin, err := service.GetAdmin(ctx)
|
||||
if err != nil {
|
||||
return AppShellState{}, err
|
||||
}
|
||||
|
||||
organizations, err := service.listOrganizations(ctx)
|
||||
if err != nil {
|
||||
return AppShellState{}, err
|
||||
}
|
||||
|
||||
departments, err := service.listDepartments(ctx)
|
||||
if err != nil {
|
||||
return AppShellState{}, err
|
||||
}
|
||||
|
||||
teams, err := service.listTeams(ctx)
|
||||
if err != nil {
|
||||
return AppShellState{}, err
|
||||
}
|
||||
|
||||
projects, err := service.listProjects(ctx)
|
||||
if err != nil {
|
||||
return AppShellState{}, err
|
||||
}
|
||||
|
||||
workspaces, err := service.listWorkspaces(ctx)
|
||||
if err != nil {
|
||||
return AppShellState{}, err
|
||||
}
|
||||
|
||||
return AppShellState{
|
||||
Installation: installation,
|
||||
Admin: admin,
|
||||
Organizations: organizations,
|
||||
Departments: departments,
|
||||
Teams: teams,
|
||||
Projects: projects,
|
||||
Workspaces: workspaces,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func scanInstallationRecord(row pgx.Row) (InstallationRecord, error) {
|
||||
var record InstallationRecord
|
||||
if err := row.Scan(&record.ID, &record.Name, &record.Mode, &record.Access, &record.Protocol, &record.Host, &record.IsBootstrapped); err != nil {
|
||||
return InstallationRecord{}, err
|
||||
}
|
||||
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func (service *Service) loadPrimaryOrganization(ctx context.Context) (*OrganizationRecord, error) {
|
||||
var record OrganizationRecord
|
||||
err := service.db.Pool.QueryRow(ctx, `
|
||||
@@ -945,6 +861,9 @@ func (service *Service) getProjectHierarchyFoldersByRootPath(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The projection gives us an unordered tree snapshot. Sibling order is stored in
|
||||
// the project settings file, so the read path has to rebuild the tree first and
|
||||
// then apply persisted ordering on top.
|
||||
folders := buildProjectHierarchyFolderTree(folderRows, rootParentPath)
|
||||
folderOrder := service.readProjectFolderOrder(project.Slug, rootParentPath)
|
||||
|
||||
@@ -987,8 +906,14 @@ func (service *Service) createProjectHierarchyFolder(
|
||||
ctx context.Context,
|
||||
input CreateProjectFolderInput,
|
||||
rootPath func(projectSlug string) string,
|
||||
createOnDisk func(projectSlug, parentFolderID, name string) (string, string, error),
|
||||
createOnDisk func(projectSlug, parentFolderPath, name string) (string, string, error),
|
||||
) (CreateProjectFolderResult, error) {
|
||||
// Folder mutations follow the same pattern:
|
||||
// 1. validate/resolve against the current ordered tree
|
||||
// 2. mutate POSIX on disk
|
||||
// 3. rebuild the projection snapshot
|
||||
// 4. rewrite sibling ordering metadata
|
||||
// 5. re-read the ordered tree that the frontend should trust
|
||||
project, err := service.loadProjectByID(ctx, strings.TrimSpace(input.ProjectID))
|
||||
if err != nil {
|
||||
return CreateProjectFolderResult{}, err
|
||||
@@ -1000,22 +925,22 @@ func (service *Service) createProjectHierarchyFolder(
|
||||
}
|
||||
|
||||
parentOrderID := ""
|
||||
trimmedParentFolderID := strings.TrimSpace(input.ParentFolderID)
|
||||
if trimmedParentFolderID != "" {
|
||||
parentFolder, found := findProjectHierarchyFolderByPath(currentFolders, trimmedParentFolderID)
|
||||
trimmedParentFolderPath := strings.TrimSpace(input.ParentFolderPath)
|
||||
if trimmedParentFolderPath != "" {
|
||||
parentFolder, found := findProjectHierarchyFolderByPath(currentFolders, trimmedParentFolderPath)
|
||||
if !found {
|
||||
return CreateProjectFolderResult{}, ErrProjectFolderNotFound
|
||||
}
|
||||
parentOrderID = parentFolder.ID
|
||||
}
|
||||
|
||||
createdPath, _, err := createOnDisk(project.Slug, strings.TrimSpace(input.ParentFolderID), input.Name)
|
||||
createdPath, _, err := createOnDisk(project.Slug, strings.TrimSpace(input.ParentFolderPath), input.Name)
|
||||
if err != nil {
|
||||
return CreateProjectFolderResult{}, err
|
||||
}
|
||||
|
||||
if err := posixproj.NewProjector(service.db, service.posixRoot).Rebuild(ctx); err != nil {
|
||||
return CreateProjectFolderResult{}, fmt.Errorf("rebuild POSIX projection: %w", err)
|
||||
if err := service.rebuildProjection(ctx); err != nil {
|
||||
return CreateProjectFolderResult{}, err
|
||||
}
|
||||
|
||||
folders, err := service.getProjectHierarchyFoldersByRootPath(ctx, project.ID, rootPath)
|
||||
@@ -1056,7 +981,7 @@ func (service *Service) deleteProjectHierarchyFolder(
|
||||
ctx context.Context,
|
||||
input DeleteProjectFolderInput,
|
||||
rootPath func(projectSlug string) string,
|
||||
deleteOnDisk func(projectSlug, folderID string) (string, error),
|
||||
deleteOnDisk func(projectSlug, folderPath string) (string, error),
|
||||
) (DeleteProjectFolderResult, error) {
|
||||
project, err := service.loadProjectByID(ctx, strings.TrimSpace(input.ProjectID))
|
||||
if err != nil {
|
||||
@@ -1068,18 +993,18 @@ func (service *Service) deleteProjectHierarchyFolder(
|
||||
return DeleteProjectFolderResult{}, err
|
||||
}
|
||||
|
||||
deletedFolder, found := findProjectHierarchyFolderByPath(currentFolders, strings.TrimSpace(input.FolderID))
|
||||
deletedFolder, found := findProjectHierarchyFolderByPath(currentFolders, strings.TrimSpace(input.FolderPath))
|
||||
if !found {
|
||||
return DeleteProjectFolderResult{}, ErrProjectFolderNotFound
|
||||
}
|
||||
|
||||
deletedFolderID, err := deleteOnDisk(project.Slug, input.FolderID)
|
||||
deletedFolderPath, err := deleteOnDisk(project.Slug, input.FolderPath)
|
||||
if err != nil {
|
||||
return DeleteProjectFolderResult{}, err
|
||||
}
|
||||
|
||||
if err := posixproj.NewProjector(service.db, service.posixRoot).Rebuild(ctx); err != nil {
|
||||
return DeleteProjectFolderResult{}, fmt.Errorf("rebuild POSIX projection: %w", err)
|
||||
if err := service.rebuildProjection(ctx); err != nil {
|
||||
return DeleteProjectFolderResult{}, err
|
||||
}
|
||||
|
||||
folders, err := service.getProjectHierarchyFoldersByRootPath(ctx, project.ID, rootPath)
|
||||
@@ -1087,7 +1012,7 @@ func (service *Service) deleteProjectHierarchyFolder(
|
||||
return DeleteProjectFolderResult{}, err
|
||||
}
|
||||
|
||||
if _, found := findProjectHierarchyFolderByPath(folders, deletedFolderID); found {
|
||||
if _, found := findProjectHierarchyFolderByPath(folders, deletedFolderPath); found {
|
||||
return DeleteProjectFolderResult{}, fmt.Errorf("deleted project folder still present in projection")
|
||||
}
|
||||
|
||||
@@ -1103,10 +1028,10 @@ func (service *Service) deleteProjectHierarchyFolder(
|
||||
}
|
||||
|
||||
return DeleteProjectFolderResult{
|
||||
ProjectID: project.ID,
|
||||
DeletedFolderID: deletedFolder.ID,
|
||||
DeletedFolderPath: deletedFolderID,
|
||||
Folders: folders,
|
||||
ProjectID: project.ID,
|
||||
DeletedFolderStableID: deletedFolder.ID,
|
||||
DeletedFolderPath: deletedFolderPath,
|
||||
Folders: folders,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1114,20 +1039,20 @@ func (service *Service) renameProjectHierarchyFolder(
|
||||
ctx context.Context,
|
||||
input RenameProjectFolderInput,
|
||||
rootPath func(projectSlug string) string,
|
||||
renameOnDisk func(projectSlug, folderID, name string) (string, string, error),
|
||||
renameOnDisk func(projectSlug, folderPath, name string) (string, string, error),
|
||||
) (RenameProjectFolderResult, error) {
|
||||
project, err := service.loadProjectByID(ctx, strings.TrimSpace(input.ProjectID))
|
||||
if err != nil {
|
||||
return RenameProjectFolderResult{}, err
|
||||
}
|
||||
|
||||
previousFolderID, renamedFolderID, err := renameOnDisk(project.Slug, input.FolderID, input.Name)
|
||||
previousFolderPath, renamedFolderPath, err := renameOnDisk(project.Slug, input.FolderPath, input.Name)
|
||||
if err != nil {
|
||||
return RenameProjectFolderResult{}, err
|
||||
}
|
||||
|
||||
if err := posixproj.NewProjector(service.db, service.posixRoot).Rebuild(ctx); err != nil {
|
||||
return RenameProjectFolderResult{}, fmt.Errorf("rebuild POSIX projection: %w", err)
|
||||
if err := service.rebuildProjection(ctx); err != nil {
|
||||
return RenameProjectFolderResult{}, err
|
||||
}
|
||||
|
||||
folders, err := service.getProjectHierarchyFoldersByRootPath(ctx, project.ID, rootPath)
|
||||
@@ -1135,21 +1060,21 @@ func (service *Service) renameProjectHierarchyFolder(
|
||||
return RenameProjectFolderResult{}, err
|
||||
}
|
||||
|
||||
renamedFolder, found := findProjectHierarchyFolderByPath(folders, renamedFolderID)
|
||||
renamedFolder, found := findProjectHierarchyFolderByPath(folders, renamedFolderPath)
|
||||
if !found {
|
||||
return RenameProjectFolderResult{}, fmt.Errorf("renamed project folder missing from projection")
|
||||
}
|
||||
|
||||
if _, found := findProjectHierarchyFolderByPath(folders, previousFolderID); found {
|
||||
if _, found := findProjectHierarchyFolderByPath(folders, previousFolderPath); found {
|
||||
return RenameProjectFolderResult{}, fmt.Errorf("previous project folder path still present in projection")
|
||||
}
|
||||
|
||||
return RenameProjectFolderResult{
|
||||
ProjectID: project.ID,
|
||||
PreviousFolderID: renamedFolder.ID,
|
||||
PreviousFolderPath: previousFolderID,
|
||||
RenamedFolder: renamedFolder,
|
||||
Folders: folders,
|
||||
ProjectID: project.ID,
|
||||
PreviousFolderStableID: renamedFolder.ID,
|
||||
PreviousFolderPath: previousFolderPath,
|
||||
RenamedFolder: renamedFolder,
|
||||
Folders: folders,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1157,7 +1082,7 @@ func (service *Service) moveProjectHierarchyFolder(
|
||||
ctx context.Context,
|
||||
input MoveProjectFolderInput,
|
||||
rootPath func(projectSlug string) string,
|
||||
moveOnDisk func(projectSlug, folderID, parentFolderID string) (string, string, error),
|
||||
moveOnDisk func(projectSlug, folderPath, parentFolderPath string) (string, string, error),
|
||||
) (MoveProjectFolderResult, error) {
|
||||
project, err := service.loadProjectByID(ctx, strings.TrimSpace(input.ProjectID))
|
||||
if err != nil {
|
||||
@@ -1169,40 +1094,40 @@ func (service *Service) moveProjectHierarchyFolder(
|
||||
return MoveProjectFolderResult{}, err
|
||||
}
|
||||
|
||||
currentFolder, found := findProjectHierarchyFolderByPath(currentFolders, strings.TrimSpace(input.FolderID))
|
||||
currentFolder, found := findProjectHierarchyFolderByPath(currentFolders, strings.TrimSpace(input.FolderPath))
|
||||
if !found {
|
||||
return MoveProjectFolderResult{}, ErrProjectFolderNotFound
|
||||
}
|
||||
|
||||
movedFolderOrderID := currentFolder.ID
|
||||
providedFolderNodeID := strings.TrimSpace(input.FolderNodeID)
|
||||
if providedFolderNodeID != "" && providedFolderNodeID != movedFolderOrderID {
|
||||
movedFolderStableID := currentFolder.ID
|
||||
providedFolderStableID := strings.TrimSpace(input.FolderStableID)
|
||||
if providedFolderStableID != "" && providedFolderStableID != movedFolderStableID {
|
||||
return MoveProjectFolderResult{}, ErrInvalidProjectFolderMove
|
||||
}
|
||||
|
||||
parentOrderID := ""
|
||||
trimmedParentFolderID := strings.TrimSpace(input.ParentFolderID)
|
||||
providedParentNodeID := strings.TrimSpace(input.ParentNodeID)
|
||||
if trimmedParentFolderID != "" {
|
||||
parentFolder, found := findProjectHierarchyFolderByPath(currentFolders, trimmedParentFolderID)
|
||||
trimmedParentFolderPath := strings.TrimSpace(input.ParentFolderPath)
|
||||
providedParentStableID := strings.TrimSpace(input.ParentStableID)
|
||||
if trimmedParentFolderPath != "" {
|
||||
parentFolder, found := findProjectHierarchyFolderByPath(currentFolders, trimmedParentFolderPath)
|
||||
if !found {
|
||||
return MoveProjectFolderResult{}, ErrProjectFolderNotFound
|
||||
}
|
||||
parentOrderID = parentFolder.ID
|
||||
if providedParentNodeID != "" && providedParentNodeID != parentOrderID {
|
||||
if providedParentStableID != "" && providedParentStableID != parentOrderID {
|
||||
return MoveProjectFolderResult{}, ErrInvalidProjectFolderMove
|
||||
}
|
||||
} else if providedParentNodeID != "" {
|
||||
} else if providedParentStableID != "" {
|
||||
return MoveProjectFolderResult{}, ErrInvalidProjectFolderMove
|
||||
}
|
||||
|
||||
previousFolderID, movedFolderID, err := moveOnDisk(project.Slug, input.FolderID, input.ParentFolderID)
|
||||
previousFolderPath, movedFolderPath, err := moveOnDisk(project.Slug, input.FolderPath, input.ParentFolderPath)
|
||||
if err != nil {
|
||||
return MoveProjectFolderResult{}, err
|
||||
}
|
||||
|
||||
if err := posixproj.NewProjector(service.db, service.posixRoot).Rebuild(ctx); err != nil {
|
||||
return MoveProjectFolderResult{}, fmt.Errorf("rebuild POSIX projection: %w", err)
|
||||
if err := service.rebuildProjection(ctx); err != nil {
|
||||
return MoveProjectFolderResult{}, err
|
||||
}
|
||||
|
||||
folders, err := service.getProjectHierarchyFoldersByRootPath(ctx, project.ID, rootPath)
|
||||
@@ -1210,20 +1135,20 @@ func (service *Service) moveProjectHierarchyFolder(
|
||||
return MoveProjectFolderResult{}, err
|
||||
}
|
||||
|
||||
movedFolder, found := findProjectHierarchyFolderByPath(folders, movedFolderID)
|
||||
movedFolder, found := findProjectHierarchyFolderByPath(folders, movedFolderPath)
|
||||
if !found {
|
||||
return MoveProjectFolderResult{}, fmt.Errorf("moved project folder missing from projection")
|
||||
}
|
||||
|
||||
if previousFolderID != movedFolderID {
|
||||
if _, found := findProjectHierarchyFolderByPath(folders, previousFolderID); found {
|
||||
if previousFolderPath != movedFolderPath {
|
||||
if _, found := findProjectHierarchyFolderByPath(folders, previousFolderPath); found {
|
||||
return MoveProjectFolderResult{}, fmt.Errorf("previous project folder path still present in projection")
|
||||
}
|
||||
}
|
||||
|
||||
folderOrder := service.readProjectFolderOrder(project.Slug, rootPath(project.Slug))
|
||||
seedFolderOrderParent(folderOrder, currentFolders, parentOrderID)
|
||||
removeFolderOrderReference(folderOrder, movedFolderOrderID)
|
||||
removeFolderOrderReference(folderOrder, movedFolderStableID)
|
||||
removeFolderOrderReference(folderOrder, movedFolder.ID)
|
||||
insertFolderOrder(folderOrder, parentOrderID, movedFolder.ID, input.TargetIndex)
|
||||
if err := service.writeProjectFolderOrder(project.Slug, rootPath(project.Slug), folderOrder); err != nil {
|
||||
@@ -1235,17 +1160,17 @@ func (service *Service) moveProjectHierarchyFolder(
|
||||
return MoveProjectFolderResult{}, err
|
||||
}
|
||||
|
||||
movedFolder, found = findProjectHierarchyFolderByPath(folders, movedFolderID)
|
||||
movedFolder, found = findProjectHierarchyFolderByPath(folders, movedFolderPath)
|
||||
if !found {
|
||||
return MoveProjectFolderResult{}, fmt.Errorf("moved project folder missing from ordered projection")
|
||||
}
|
||||
|
||||
return MoveProjectFolderResult{
|
||||
ProjectID: project.ID,
|
||||
PreviousFolderID: movedFolder.ID,
|
||||
PreviousFolderPath: previousFolderID,
|
||||
MovedFolder: movedFolder,
|
||||
Folders: folders,
|
||||
ProjectID: project.ID,
|
||||
PreviousFolderStableID: movedFolder.ID,
|
||||
PreviousFolderPath: previousFolderPath,
|
||||
MovedFolder: movedFolder,
|
||||
Folders: folders,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1273,39 +1198,6 @@ func (service *Service) listWorkspaces(ctx context.Context) ([]WorkspaceRecord,
|
||||
return records, rows.Err()
|
||||
}
|
||||
|
||||
func loadInstallation(ctx context.Context, tx pgx.Tx) (InstallationRecord, error) {
|
||||
return scanInstallationRecord(tx.QueryRow(ctx, `
|
||||
SELECT id::text, name, mode::text, access::text, protocol::text, host, is_bootstrapped
|
||||
FROM installations
|
||||
WHERE singleton = TRUE
|
||||
LIMIT 1;
|
||||
`))
|
||||
}
|
||||
|
||||
func loadPrimaryAdmin(ctx context.Context, tx pgx.Tx) (AdminSummary, error) {
|
||||
var admin AdminSummary
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT id::text, email, display_name
|
||||
FROM users
|
||||
WHERE is_instance_admin = TRUE
|
||||
ORDER BY created_at ASC
|
||||
LIMIT 1;
|
||||
`).Scan(&admin.ID, &admin.Email, &admin.DisplayName); err != nil {
|
||||
return AdminSummary{}, err
|
||||
}
|
||||
|
||||
return admin, nil
|
||||
}
|
||||
|
||||
func updateBootstrappedInstallation(ctx context.Context, tx pgx.Tx) (InstallationRecord, error) {
|
||||
return scanInstallationRecord(tx.QueryRow(ctx, `
|
||||
UPDATE installations
|
||||
SET is_bootstrapped = TRUE, bootstrapped_at = COALESCE(bootstrapped_at, NOW()), updated_at = NOW()
|
||||
WHERE singleton = TRUE
|
||||
RETURNING id::text, name, mode::text, access::text, protocol::text, host, is_bootstrapped;
|
||||
`))
|
||||
}
|
||||
|
||||
func upsertNamedRecord(ctx context.Context, tx pgx.Tx, query string, args ...any) (namedRecord, error) {
|
||||
var record namedRecord
|
||||
if err := tx.QueryRow(ctx, query, args...).Scan(&record.ID, &record.Name, &record.Slug); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user