4 changed files with 276 additions and 16 deletions
@ -0,0 +1,67 @@ |
|||
// app/models/post.ts
|
|||
// Copyright (C) 2025 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
import { Schema, Types, model } from "mongoose"; |
|||
|
|||
import { |
|||
BroadcastShowWriterSchema, |
|||
IBroadcastShow, |
|||
IBroadcastShowWriter, |
|||
} from "./broadcast-show.js"; |
|||
import { IImage } from "./image.js"; |
|||
|
|||
export enum PostStatus { |
|||
Draft = "draft", |
|||
Live = "live", |
|||
Removed = "removed", |
|||
} |
|||
|
|||
/** |
|||
* Virtual authors will compose written articles for the site in blog format. |
|||
*/ |
|||
export interface IPost { |
|||
_id: Types.ObjectId; // MongoDB concern
|
|||
__v: number; // MongoDB concern
|
|||
|
|||
created: Date; |
|||
status: PostStatus; |
|||
show: IBroadcastShow | Types.ObjectId; |
|||
slug: string; |
|||
author: IBroadcastShowWriter; |
|||
title: string; |
|||
summary: string; |
|||
body: string; |
|||
tags: Array<string>; |
|||
|
|||
headerImage: IImage | Types.ObjectId; |
|||
bodyImages?: Array<IImage | Types.ObjectId>; |
|||
} |
|||
|
|||
export const PostSchema = new Schema<IPost>({ |
|||
created: { type: Date, default: Date.now, required: true, index: -1 }, |
|||
status: { |
|||
type: String, |
|||
enum: PostStatus, |
|||
default: PostStatus.Draft, |
|||
required: true, |
|||
index: 1, |
|||
}, |
|||
show: { |
|||
type: Schema.ObjectId, |
|||
required: true, |
|||
index: 1, |
|||
ref: "BroadcastShow", |
|||
}, |
|||
slug: { type: String, lowercase: true, required: true, index: 1 }, |
|||
author: { type: BroadcastShowWriterSchema, required: true }, |
|||
title: { type: String, required: true }, |
|||
summary: { type: String, required: true }, |
|||
body: { type: String, required: true }, |
|||
tags: { type: [String], default: [], required: true }, |
|||
headerImage: { Type: Schema.ObjectId, required: true, ref: "Image" }, |
|||
bodyImages: { Type: [Schema.ObjectId], ref: "Image" }, |
|||
}); |
|||
|
|||
export const Post = model<IPost>("Post", PostSchema); |
|||
export default Post; |
@ -0,0 +1,168 @@ |
|||
// app/services/broadcast-show.ts
|
|||
// Copyright (C) 2025 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
import mongoose from "mongoose"; |
|||
import slug from "slug"; |
|||
|
|||
import { IImage } from "../models/image.js"; |
|||
import { |
|||
IBroadcastShow, |
|||
IBroadcastShowWriter, |
|||
} from "../models/broadcast-show.js"; |
|||
import Post, { IPost, PostStatus } from "../models/post.js"; |
|||
|
|||
import { |
|||
DtpPlatform, |
|||
DtpService, |
|||
DtpServiceUpdate, |
|||
WebError, |
|||
WebPaginationParameters, |
|||
} from "../../lib/dtplib.js"; |
|||
|
|||
export interface PostDefinition { |
|||
show: IBroadcastShow | mongoose.Types.ObjectId; |
|||
author: IBroadcastShowWriter; |
|||
title: string; |
|||
summary: string; |
|||
body: string; |
|||
tags: Array<string>; |
|||
headerImage: IImage | mongoose.Types.ObjectId; |
|||
} |
|||
|
|||
export interface PostLibrary { |
|||
posts: Array<IPost | mongoose.Types.ObjectId>; |
|||
totalPostCount: number; |
|||
} |
|||
|
|||
/** |
|||
* A service for managing blog posts on the Newsroom platform. |
|||
*/ |
|||
export class PostService extends DtpService { |
|||
populatePost: Array<mongoose.PopulateOptions>; |
|||
|
|||
static get name() { |
|||
return "PostService"; |
|||
} |
|||
static get slug() { |
|||
return "post"; |
|||
} |
|||
|
|||
constructor(platform: DtpPlatform) { |
|||
super(platform, PostService); |
|||
this.populatePost = [ |
|||
{ |
|||
path: "headerImage", |
|||
}, |
|||
{ |
|||
path: "bodyImages", |
|||
}, |
|||
]; |
|||
} |
|||
|
|||
async create( |
|||
show: IBroadcastShow | mongoose.Types.ObjectId, |
|||
definition: PostDefinition |
|||
): Promise<IPost> { |
|||
const NOW = new Date(); |
|||
const post = new Post(); |
|||
|
|||
let titleSlug = definition.title.split(" ").slice(0, 5).join(" "); |
|||
titleSlug = slug(titleSlug); |
|||
titleSlug += `-${post._id.toString()}`; |
|||
|
|||
post.created = NOW; |
|||
post.show = show._id; |
|||
post.slug = titleSlug; |
|||
post.author = definition.author; |
|||
post.title = definition.title; |
|||
post.summary = definition.summary; |
|||
post.body = definition.body; |
|||
post.tags = definition.tags; |
|||
post.headerImage = definition.headerImage._id; |
|||
|
|||
this.log.info("creating blog post", { |
|||
_id: post._id, |
|||
slug: titleSlug, |
|||
title: post.title, |
|||
}); |
|||
|
|||
await post.save(); |
|||
return post.toObject(); |
|||
} |
|||
|
|||
async update(post: IPost, definition: PostDefinition): Promise<IPost> { |
|||
const update: DtpServiceUpdate = {}; |
|||
update.$set = {}; |
|||
|
|||
update.$set.author = definition.author; |
|||
update.$set.title = definition.title; |
|||
update.$set.summary = definition.summary; |
|||
update.$set.body = definition.body; |
|||
update.$set.tags = definition.tags; |
|||
update.$set.headerImage = definition.headerImage._id; |
|||
|
|||
const newPost = await Post.findByIdAndUpdate(post._id, update, { |
|||
new: true, |
|||
populate: this.populatePost, |
|||
}).lean(); |
|||
if (!newPost) { |
|||
throw new WebError(404, "Post not found"); |
|||
} |
|||
|
|||
return newPost; |
|||
} |
|||
|
|||
async getBySlug(urlSlug: string): Promise<IPost | null> { |
|||
return Post.findOne({ slug: urlSlug.trim().toLowerCase() }) |
|||
.populate(this.populatePost) |
|||
.lean(); |
|||
} |
|||
|
|||
async getById(postId: mongoose.Types.ObjectId): Promise<IPost | null> { |
|||
return Post.findById(postId).populate(this.populatePost).lean(); |
|||
} |
|||
|
|||
async getLive(pagination: WebPaginationParameters): Promise<PostLibrary> { |
|||
const search = { status: PostStatus.Live }; |
|||
const posts = await Post.find(search) |
|||
.sort({ created: -1 }) |
|||
.skip(pagination.skip) |
|||
.limit(pagination.cpp) |
|||
.populate(this.populatePost) |
|||
.lean(); |
|||
|
|||
const totalPostCount = await Post.countDocuments(search); |
|||
return { posts, totalPostCount }; |
|||
} |
|||
|
|||
async getForShow( |
|||
show: IBroadcastShow | mongoose.Types.ObjectId, |
|||
pagination: WebPaginationParameters |
|||
): Promise<PostLibrary> { |
|||
const search = { show: show._id, status: PostStatus.Live }; |
|||
const posts = await Post.find(search) |
|||
.sort({ created: -1 }) |
|||
.skip(pagination.skip) |
|||
.limit(pagination.cpp) |
|||
.populate(this.populatePost) |
|||
.lean(); |
|||
|
|||
const totalPostCount = await Post.countDocuments(search); |
|||
return { posts, totalPostCount }; |
|||
} |
|||
|
|||
async getAll(pagination: WebPaginationParameters): Promise<PostLibrary> { |
|||
const posts = await Post.find() |
|||
.sort({ created: -1 }) |
|||
.skip(pagination.skip) |
|||
.limit(pagination.cpp) |
|||
.populate(this.populatePost) |
|||
.lean(); |
|||
|
|||
const totalPostCount = await Post.estimatedDocumentCount(); |
|||
return { posts, totalPostCount }; |
|||
} |
|||
} |
|||
|
|||
export default PostService; |
Loading…
Reference in new issue