Add job application quick reference for storing profile links and experience
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m34s

Auth-protected CRUD for personal info (LinkedIn, GitHub, etc.) and
experience entries, stored in the database so nothing sensitive is in
the public repo. Displayed as a categorized panel on the Job Applications page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-13 12:38:53 +01:00
parent 81c5684102
commit 759614e92d
10 changed files with 1207 additions and 18 deletions

View File

@@ -69,3 +69,9 @@ models:
fields:
deletedAt:
resolver: false
JobAppReference:
model:
- adam-french.co.uk/backend/models.JobAppReference
fields:
deletedAt:
resolver: false

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
package graph
// This file will be automatically regenerated based on the schema, any resolver
// implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.88
import (
"context"
"adam-french.co.uk/backend/models"
)
// ID is the resolver for the id field.
func (r *jobAppReferenceResolver) ID(ctx context.Context, obj *models.JobAppReference) (int, error) {
return int(obj.ID), nil
}
// JobAppReference returns JobAppReferenceResolver implementation.
func (r *Resolver) JobAppReference() JobAppReferenceResolver { return &jobAppReferenceResolver{r} }
type jobAppReferenceResolver struct{ *Resolver }

View File

@@ -30,6 +30,13 @@ type CreateFavoriteInput struct {
Link *string `json:"link,omitempty"`
}
type CreateJobAppReferenceInput struct {
Category string `json:"category"`
Label string `json:"label"`
Value string `json:"value"`
SortOrder *int `json:"sortOrder,omitempty"`
}
type CreateJobApplicationInput struct {
JobTitle string `json:"jobTitle"`
Company string `json:"company"`
@@ -112,6 +119,13 @@ type SteamStatus struct {
RecentGames []*SteamGame `json:"recentGames"`
}
type UpdateJobAppReferenceInput struct {
Category *string `json:"category,omitempty"`
Label *string `json:"label,omitempty"`
Value *string `json:"value,omitempty"`
SortOrder *int `json:"sortOrder,omitempty"`
}
type UpdateJobApplicationInput struct {
JobTitle *string `json:"jobTitle,omitempty"`
Company *string `json:"company,omitempty"`

View File

@@ -387,6 +387,63 @@ func (r *mutationResolver) DeleteJobApplication(ctx context.Context, id int) (bo
return true, nil
}
// CreateJobAppReference is the resolver for the createJobAppReference field.
func (r *mutationResolver) CreateJobAppReference(ctx context.Context, input model.CreateJobAppReferenceInput) (*models.JobAppReference, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
ref := models.JobAppReference{
Category: input.Category,
Label: input.Label,
Value: input.Value,
}
if input.SortOrder != nil {
ref.SortOrder = *input.SortOrder
}
if err := r.Store.DB.Create(&ref).Error; err != nil {
return nil, err
}
return &ref, nil
}
// UpdateJobAppReference is the resolver for the updateJobAppReference field.
func (r *mutationResolver) UpdateJobAppReference(ctx context.Context, id int, input model.UpdateJobAppReferenceInput) (*models.JobAppReference, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
var ref models.JobAppReference
if err := r.Store.DB.First(&ref, id).Error; err != nil {
return nil, err
}
if input.Category != nil {
ref.Category = *input.Category
}
if input.Label != nil {
ref.Label = *input.Label
}
if input.Value != nil {
ref.Value = *input.Value
}
if input.SortOrder != nil {
ref.SortOrder = *input.SortOrder
}
if err := r.Store.DB.Save(&ref).Error; err != nil {
return nil, err
}
return &ref, nil
}
// DeleteJobAppReference is the resolver for the deleteJobAppReference field.
func (r *mutationResolver) DeleteJobAppReference(ctx context.Context, id int) (bool, error) {
if !IsAdminFromCtx(ctx) {
return false, fmt.Errorf("admin access required")
}
if err := r.Store.DB.Delete(&models.JobAppReference{}, id).Error; err != nil {
return false, err
}
return true, nil
}
// Users is the resolver for the users field.
func (r *queryResolver) Users(ctx context.Context) ([]*models.User, error) {
var users []models.User
@@ -635,6 +692,18 @@ func (r *queryResolver) JobApplication(ctx context.Context, id int) (*models.Job
return &app, nil
}
// JobAppReferences is the resolver for the jobAppReferences field.
func (r *queryResolver) JobAppReferences(ctx context.Context) ([]*models.JobAppReference, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
var refs []*models.JobAppReference
if err := r.Store.DB.Order("category ASC, sort_order ASC, created_at ASC").Find(&refs).Error; err != nil {
return nil, err
}
return refs, nil
}
// Mutation returns MutationResolver implementation.
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }

View File

@@ -0,0 +1,23 @@
type JobAppReference {
id: ID!
createdAt: Time!
updatedAt: Time!
category: String!
label: String!
value: String!
sortOrder: Int!
}
input CreateJobAppReferenceInput {
category: String!
label: String!
value: String!
sortOrder: Int
}
input UpdateJobAppReferenceInput {
category: String
label: String
value: String
sortOrder: Int
}

View File

@@ -17,6 +17,7 @@ type Query {
bookmarks: [Bookmark!]!
jobApplications: [JobApplication!]!
jobApplication(id: ID!): JobApplication
jobAppReferences: [JobAppReference!]!
}
type Mutation {
@@ -36,4 +37,7 @@ type Mutation {
createBookmark(input: CreateBookmarkInput!): Bookmark!
deleteBookmark(id: ID!): Bookmark!
deleteJobApplication(id: ID!): Boolean!
createJobAppReference(input: CreateJobAppReferenceInput!): JobAppReference!
updateJobAppReference(id: ID!, input: UpdateJobAppReferenceInput!): JobAppReference!
deleteJobAppReference(id: ID!): Boolean!
}

View File

@@ -77,6 +77,17 @@ type Bookmark struct {
Link string `gorm:"not null" json:"link"`
}
type JobAppReference struct {
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt"`
Category string `gorm:"not null" json:"category"`
Label string `gorm:"not null" json:"label"`
Value string `gorm:"not null" json:"value"`
SortOrder int `gorm:"default:0" json:"sortOrder"`
}
type JobApplication struct {
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time `json:"createdAt"`

View File

@@ -39,6 +39,7 @@ func migrateDatabase(db *gorm.DB) error {
&models.Rowing{},
&models.Message{},
&models.JobApplication{},
&models.JobAppReference{},
&models.Bookmark{},
)
if err != nil {