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