From cbd92e6491fa95624fc03c85edb217d03188de35 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 19 Jun 2021 22:19:12 -0400 Subject: [PATCH] Add job listing table (#15) also: - Add source to success story - Move all IDs to one source file - Update DbContext with all of the above --- src/JobsJobsJobs.sln | 1 + .../Api/Controllers/SuccessController.cs | 3 +- src/JobsJobsJobs/Server/Data/Converters.cs | 6 + src/JobsJobsJobs/Server/Data/JobsDbContext.cs | 33 ++++ src/JobsJobsJobs/Shared/Domain/CitizenId.cs | 26 --- src/JobsJobsJobs/Shared/Domain/ContinentId.cs | 26 --- src/JobsJobsJobs/Shared/Domain/Ids.cs | 148 ++++++++++++++++++ src/JobsJobsJobs/Shared/Domain/Listing.cs | 32 ++++ src/JobsJobsJobs/Shared/Domain/ShortId.cs | 38 ----- src/JobsJobsJobs/Shared/Domain/SkillId.cs | 26 --- src/JobsJobsJobs/Shared/Domain/Success.cs | 1 + src/JobsJobsJobs/Shared/Domain/SuccessId.cs | 26 --- src/database/16-job-listing.sql | 56 +++++++ 13 files changed, 279 insertions(+), 143 deletions(-) delete mode 100644 src/JobsJobsJobs/Shared/Domain/CitizenId.cs delete mode 100644 src/JobsJobsJobs/Shared/Domain/ContinentId.cs create mode 100644 src/JobsJobsJobs/Shared/Domain/Ids.cs create mode 100644 src/JobsJobsJobs/Shared/Domain/Listing.cs delete mode 100644 src/JobsJobsJobs/Shared/Domain/ShortId.cs delete mode 100644 src/JobsJobsJobs/Shared/Domain/SkillId.cs delete mode 100644 src/JobsJobsJobs/Shared/Domain/SuccessId.cs create mode 100644 src/database/16-job-listing.sql diff --git a/src/JobsJobsJobs.sln b/src/JobsJobsJobs.sln index 23ff365..f33e0c9 100644 --- a/src/JobsJobsJobs.sln +++ b/src/JobsJobsJobs.sln @@ -13,6 +13,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject .dockerignore = .dockerignore database\12-add-real-name.sql = database\12-add-real-name.sql + database\16-job-listing.sql = database\16-job-listing.sql JobsJobsJobs\Directory.Build.props = JobsJobsJobs\Directory.Build.props Dockerfile = Dockerfile database\tables.sql = database\tables.sql diff --git a/src/JobsJobsJobs/Server/Areas/Api/Controllers/SuccessController.cs b/src/JobsJobsJobs/Server/Areas/Api/Controllers/SuccessController.cs index 55eeff2..e63ef93 100644 --- a/src/JobsJobsJobs/Server/Areas/Api/Controllers/SuccessController.cs +++ b/src/JobsJobsJobs/Server/Areas/Api/Controllers/SuccessController.cs @@ -53,7 +53,8 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers if (form.Id == "new") { var story = new Success(await SuccessId.Create(), CurrentCitizenId, _clock.GetCurrentInstant(), - form.FromHere, string.IsNullOrWhiteSpace(form.Story) ? null : new MarkdownString(form.Story)); + form.FromHere, "profile", + string.IsNullOrWhiteSpace(form.Story) ? null : new MarkdownString(form.Story)); await _db.AddAsync(story); } else diff --git a/src/JobsJobsJobs/Server/Data/Converters.cs b/src/JobsJobsJobs/Server/Data/Converters.cs index 66230a2..9acccfd 100644 --- a/src/JobsJobsJobs/Server/Data/Converters.cs +++ b/src/JobsJobsJobs/Server/Data/Converters.cs @@ -20,6 +20,12 @@ namespace JobsJobsJobs.Server.Data public static readonly ValueConverter ContinentIdConverter = new(v => v.ToString(), v => ContinentId.Parse(v)); + /// + /// Job Listing ID converter + /// + public static readonly ValueConverter ListingIdConverter = + new(v => v.ToString(), v => ListingId.Parse(v)); + /// /// Markdown converter /// diff --git a/src/JobsJobsJobs/Server/Data/JobsDbContext.cs b/src/JobsJobsJobs/Server/Data/JobsDbContext.cs index 84b701c..7a4e490 100644 --- a/src/JobsJobsJobs/Server/Data/JobsDbContext.cs +++ b/src/JobsJobsJobs/Server/Data/JobsDbContext.cs @@ -18,6 +18,11 @@ namespace JobsJobsJobs.Server.Data /// public DbSet Continents { get; set; } = default!; + /// + /// Job listings + /// + public DbSet Listings { get; set; } = default!; + /// /// Employment profiles /// @@ -66,6 +71,33 @@ namespace JobsJobsJobs.Server.Data m.Property(e => e.Name).HasColumnName("name").IsRequired().HasMaxLength(255); }); + modelBuilder.Entity(m => + { + m.ToTable("listing", "jjj").HasKey(e => e.Id); + m.Property(e => e.Id).HasColumnName("id").IsRequired().HasMaxLength(12) + .HasConversion(Converters.ListingIdConverter); + m.Property(e => e.CitizenId).HasColumnName("citizen_id").IsRequired().HasMaxLength(12) + .HasConversion(Converters.CitizenIdConverter); + m.Property(e => e.CreatedOn).HasColumnName("created_on").IsRequired(); + m.Property(e => e.Title).HasColumnName("title").IsRequired().HasMaxLength(100); + m.Property(e => e.ContinentId).HasColumnName("continent_id").IsRequired().HasMaxLength(12) + .HasConversion(Converters.ContinentIdConverter); + m.Property(e => e.Region).HasColumnName("region").IsRequired().HasMaxLength(255); + m.Property(e => e.RemoteWork).HasColumnName("remote_work").IsRequired(); + m.Property(e => e.IsExpired).HasColumnName("expired").IsRequired(); + m.Property(e => e.UpdatedOn).HasColumnName("updated_on").IsRequired(); + m.Property(e => e.Text).HasColumnName("listing").IsRequired() + .HasConversion(Converters.MarkdownStringConverter); + m.Property(e => e.NeededBy).HasColumnName("needed_by"); + m.Property(e => e.WasFilledHere).HasColumnName("filled_here"); + m.HasOne(e => e.Citizen) + .WithMany() + .HasForeignKey(e => e.CitizenId); + m.HasOne(e => e.Continent) + .WithMany() + .HasForeignKey(e => e.ContinentId); + }); + modelBuilder.Entity(m => { m.ToTable("profile", "jjj").HasKey(e => e.Id); @@ -111,6 +143,7 @@ namespace JobsJobsJobs.Server.Data .HasConversion(Converters.CitizenIdConverter); m.Property(e => e.RecordedOn).HasColumnName("recorded_on").IsRequired(); m.Property(e => e.FromHere).HasColumnName("from_here").IsRequired(); + m.Property(e => e.Source).HasColumnName("source").IsRequired().HasMaxLength(7); m.Property(e => e.Story).HasColumnName("story") .HasConversion(Converters.OptionalMarkdownStringConverter); }); diff --git a/src/JobsJobsJobs/Shared/Domain/CitizenId.cs b/src/JobsJobsJobs/Shared/Domain/CitizenId.cs deleted file mode 100644 index fac183c..0000000 --- a/src/JobsJobsJobs/Shared/Domain/CitizenId.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Threading.Tasks; - -namespace JobsJobsJobs.Shared -{ - /// - /// The ID of a user (a citizen of Gitmo Nation) - /// - public record CitizenId(ShortId Id) - { - /// - /// Create a new citizen ID - /// - /// A new citizen ID - public static async Task Create() => new CitizenId(await ShortId.Create()); - - /// - /// Attempt to create a citizen ID from a string - /// - /// The prospective ID - /// The citizen ID - /// If the string is not a valid citizen ID - public static CitizenId Parse(string id) => new CitizenId(ShortId.Parse(id)); - - public override string ToString() => Id.ToString(); - } -} diff --git a/src/JobsJobsJobs/Shared/Domain/ContinentId.cs b/src/JobsJobsJobs/Shared/Domain/ContinentId.cs deleted file mode 100644 index 59ea252..0000000 --- a/src/JobsJobsJobs/Shared/Domain/ContinentId.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Threading.Tasks; - -namespace JobsJobsJobs.Shared -{ - /// - /// The ID of a continent - /// - public record ContinentId(ShortId Id) - { - /// - /// Create a new continent ID - /// - /// A new continent ID - public static async Task Create() => new ContinentId(await ShortId.Create()); - - /// - /// Attempt to create a continent ID from a string - /// - /// The prospective ID - /// The continent ID - /// If the string is not a valid continent ID - public static ContinentId Parse(string id) => new ContinentId(ShortId.Parse(id)); - - public override string ToString() => Id.ToString(); - } -} diff --git a/src/JobsJobsJobs/Shared/Domain/Ids.cs b/src/JobsJobsJobs/Shared/Domain/Ids.cs new file mode 100644 index 0000000..4c573b2 --- /dev/null +++ b/src/JobsJobsJobs/Shared/Domain/Ids.cs @@ -0,0 +1,148 @@ +using System; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace JobsJobsJobs.Shared +{ + /// + /// A short ID + /// + public record ShortId(string Id) + { + /// + /// Validate the format of the short ID + /// + private static readonly Regex ValidShortId = + new Regex("^[a-z0-9_-]{12}", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + /// + /// Create a new short ID + /// + /// A new short ID + public static async Task Create() => new ShortId(await Nanoid.Nanoid.GenerateAsync(size: 12)); + + /// + /// Try to parse a string of text into a short ID + /// + /// The text of the prospective short ID + /// The short ID + /// If the format is not valid + public static ShortId Parse(string text) + { + if (text.Length == 12 && ValidShortId.IsMatch(text)) return new ShortId(text); + throw new FormatException($"The string {text} is not a valid short ID"); + } + + public override string ToString() => Id; + } + + /// + /// The ID of a user (a citizen of Gitmo Nation) + /// + public record CitizenId(ShortId Id) + { + /// + /// Create a new citizen ID + /// + /// A new citizen ID + public static async Task Create() => new CitizenId(await ShortId.Create()); + + /// + /// Attempt to create a citizen ID from a string + /// + /// The prospective ID + /// The citizen ID + /// If the string is not a valid citizen ID + public static CitizenId Parse(string id) => new(ShortId.Parse(id)); + + public override string ToString() => Id.ToString(); + } + + /// + /// The ID of a continent + /// + public record ContinentId(ShortId Id) + { + /// + /// Create a new continent ID + /// + /// A new continent ID + public static async Task Create() => new ContinentId(await ShortId.Create()); + + /// + /// Attempt to create a continent ID from a string + /// + /// The prospective ID + /// The continent ID + /// If the string is not a valid continent ID + public static ContinentId Parse(string id) => new(ShortId.Parse(id)); + + public override string ToString() => Id.ToString(); + } + + /// + /// The ID of a job listing + /// + public record ListingId(ShortId Id) + { + /// + /// Create a new job listing ID + /// + /// A new job listing ID + public static async Task Create() => new ListingId(await ShortId.Create()); + + /// + /// Attempt to create a job listing ID from a string + /// + /// The prospective ID + /// The job listing ID + /// If the string is not a valid job listing ID + public static ListingId Parse(string id) => new(ShortId.Parse(id)); + + public override string ToString() => Id.ToString(); + } + + /// + /// The ID of a skill + /// + public record SkillId(ShortId Id) + { + /// + /// Create a new skill ID + /// + /// A new skill ID + public static async Task Create() => new SkillId(await ShortId.Create()); + + /// + /// Attempt to create a skill ID from a string + /// + /// The prospective ID + /// The skill ID + /// If the string is not a valid skill ID + public static SkillId Parse(string id) => new(ShortId.Parse(id)); + + public override string ToString() => Id.ToString(); + } + + /// + /// The ID of a success report + /// + public record SuccessId(ShortId Id) + { + /// + /// Create a new success report ID + /// + /// A new success report ID + public static async Task Create() => new SuccessId(await ShortId.Create()); + + /// + /// Attempt to create a success report ID from a string + /// + /// The prospective ID + /// The success report ID + /// If the string is not a valid success report ID + public static SuccessId Parse(string id) => new(ShortId.Parse(id)); + + public override string ToString() => Id.ToString(); + } +} diff --git a/src/JobsJobsJobs/Shared/Domain/Listing.cs b/src/JobsJobsJobs/Shared/Domain/Listing.cs new file mode 100644 index 0000000..9717dd6 --- /dev/null +++ b/src/JobsJobsJobs/Shared/Domain/Listing.cs @@ -0,0 +1,32 @@ +using System; + +namespace JobsJobsJobs.Shared +{ + /// + /// A job listing + /// + public record Listing( + ListingId Id, + CitizenId CitizenId, + DateTime CreatedOn, + string Title, + ContinentId ContinentId, + string Region, + bool RemoteWork, + bool IsExpired, + DateTime UpdatedOn, + MarkdownString Text, + DateTime? NeededBy, + bool? WasFilledHere) + { + /// + /// Navigation property for the citizen who created the job listing + /// + public Citizen? Citizen { get; set; } + + /// + /// Navigation property for the continent + /// + public Continent? Continent { get; set; } + } +} diff --git a/src/JobsJobsJobs/Shared/Domain/ShortId.cs b/src/JobsJobsJobs/Shared/Domain/ShortId.cs deleted file mode 100644 index a834c19..0000000 --- a/src/JobsJobsJobs/Shared/Domain/ShortId.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Text.RegularExpressions; -using System.Threading.Tasks; - -namespace JobsJobsJobs.Shared -{ - /// - /// A short ID - /// - public record ShortId(string Id) - { - /// - /// Validate the format of the short ID - /// - private static readonly Regex ValidShortId = - new Regex("^[a-z0-9_-]{12}", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - /// - /// Create a new short ID - /// - /// A new short ID - public static async Task Create() => new ShortId(await Nanoid.Nanoid.GenerateAsync(size: 12)); - - /// - /// Try to parse a string of text into a short ID - /// - /// The text of the prospective short ID - /// The short ID - /// If the format is not valid - public static ShortId Parse(string text) - { - if (text.Length == 12 && ValidShortId.IsMatch(text)) return new ShortId(text); - throw new FormatException($"The string {text} is not a valid short ID"); - } - - public override string ToString() => Id; - } -} diff --git a/src/JobsJobsJobs/Shared/Domain/SkillId.cs b/src/JobsJobsJobs/Shared/Domain/SkillId.cs deleted file mode 100644 index d571e7e..0000000 --- a/src/JobsJobsJobs/Shared/Domain/SkillId.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Threading.Tasks; - -namespace JobsJobsJobs.Shared -{ - /// - /// The ID of a skill - /// - public record SkillId(ShortId Id) - { - /// - /// Create a new skill ID - /// - /// A new skill ID - public static async Task Create() => new SkillId(await ShortId.Create()); - - /// - /// Attempt to create a skill ID from a string - /// - /// The prospective ID - /// The skill ID - /// If the string is not a valid skill ID - public static SkillId Parse(string id) => new SkillId(ShortId.Parse(id)); - - public override string ToString() => Id.ToString(); - } -} diff --git a/src/JobsJobsJobs/Shared/Domain/Success.cs b/src/JobsJobsJobs/Shared/Domain/Success.cs index 502d379..e049b10 100644 --- a/src/JobsJobsJobs/Shared/Domain/Success.cs +++ b/src/JobsJobsJobs/Shared/Domain/Success.cs @@ -10,5 +10,6 @@ namespace JobsJobsJobs.Shared CitizenId CitizenId, Instant RecordedOn, bool FromHere, + string Source, MarkdownString? Story); } diff --git a/src/JobsJobsJobs/Shared/Domain/SuccessId.cs b/src/JobsJobsJobs/Shared/Domain/SuccessId.cs deleted file mode 100644 index aace867..0000000 --- a/src/JobsJobsJobs/Shared/Domain/SuccessId.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Threading.Tasks; - -namespace JobsJobsJobs.Shared -{ - /// - /// The ID of a success report - /// - public record SuccessId(ShortId Id) - { - /// - /// Create a new success report ID - /// - /// A new success report ID - public static async Task Create() => new SuccessId(await ShortId.Create()); - - /// - /// Attempt to create a success report ID from a string - /// - /// The prospective ID - /// The success report ID - /// If the string is not a valid success report ID - public static SuccessId Parse(string id) => new SuccessId(ShortId.Parse(id)); - - public override string ToString() => Id.ToString(); - } -} diff --git a/src/database/16-job-listing.sql b/src/database/16-job-listing.sql new file mode 100644 index 0000000..9d76eca --- /dev/null +++ b/src/database/16-job-listing.sql @@ -0,0 +1,56 @@ + +-- Add job listing table + +CREATE TABLE jjj.job_listing ( + id VARCHAR(12) NOT NULL, + citizen_id VARCHAR(12) NOT NULL, + created_on TIMESTAMP NOT NULL, + title VARCHAR(100) NOT NULL, + continent_id VARCHAR(12) NOT NULL, + region VARCHAR(255) NOT NULL, + remote_work BOOLEAN NOT NULL, + expired BOOLEAN NOT NULL, + updated_on TIMESTAMP NOT NULL, + listing TEXT NOT NULL, + needed_by DATE, + filled_here BOOLEAN, + CONSTRAINT pk_job_listing PRIMARY KEY (id), + CONSTRAINT fk_listing_citizen FOREIGN KEY (citizen_id) REFERENCES jjj.citizen (id), + CONSTRAINT fk_listing_continent FOREIGN KEY (continent_id) REFERENCES jjj.continent (id) +); + +COMMENT ON TABLE jjj.job_listing IS 'Job Listings'; +COMMENT ON COLUMN jjj.job_listing.id + IS 'A unique identifier for a job listing'; +COMMENT ON COLUMN jjj.job_listing.created_on + IS 'The date/time a job listing was created'; +COMMENT ON COLUMN jjj.job_listing.title + IS 'The title of the job listing'; +COMMENT ON COLUMN jjj.job_listing.continent_id + IS 'The ID of the continent on which this job is based'; +COMMENT ON COLUMN jjj.job_listing.region + IS 'The region in which this job is based'; +COMMENT ON COLUMN jjj.job_listing.remote_work + IS 'Whether this job is a remote job'; +COMMENT ON COLUMN jjj.job_listing.expired + IS 'Whether this listing is expired'; +COMMENT ON COLUMN jjj.job_listing.updated_on + IS 'The date/time this job listing was last updated'; +COMMENT ON COLUMN jjj.job_listing.listing + IS 'The text of the job listing'; +COMMENT ON COLUMN jjj.job_listing.needed_by + IS 'The date by which this job needs to be filled'; +COMMENT ON COLUMN jjj.job_listing.filled_here + IS 'Whether this job listing was filled because it appeared here'; + +CREATE INDEX idx_listing_citizen ON jjj.job_listing (citizen_id); +CREATE INDEX idx_listing_continent ON jjj.job_listing (continent_id); +COMMENT ON INDEX jjj.idx_listing_citizen IS 'FK index'; +COMMENT ON INDEX jjj.idx_listing_continent IS 'FK index'; + +-- Add source column to success story + +ALTER TABLE jjj.success ADD COLUMN source VARCHAR(7) NOT NULL DEFAULT 'profile'; +ALTER TABLE jjj.success ADD CONSTRAINT ck_source CHECK (source IN ('profile', 'listing')); +COMMENT ON COLUMN jjj.success.source + IS 'The source of the success story (profile or listing)';