Created
December 15, 2020 03:32
-
-
Save gaurangrshah/1d2931c6ed8836cfe462c2758fc835cd to your computer and use it in GitHub Desktop.
git cms example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Octokit } from '@octokit/rest' | |
import { nanoid } from 'nanoid' | |
export interface GitHubCMSOptions { | |
owner: string; | |
repo: string; | |
token: string; | |
} | |
export interface Comment { | |
id?: string, | |
userId: string; | |
name: string; | |
avatar: string; | |
content: string; | |
createdAt: number; | |
} | |
export interface Post { | |
slug: string; | |
title: string; | |
content: string; | |
createdAt: number; | |
updatedAt?: number; | |
ownerId?: string; | |
} | |
export interface User { | |
id: string; | |
email: string; | |
profile: { | |
name: string; | |
avatar: string; | |
} | |
github?: any | |
local?: any | |
} | |
export class GitHubCMS { | |
rootOptions: GitHubCMSOptions; | |
octokit: Octokit; | |
constructor(o: GitHubCMSOptions) { | |
this.rootOptions = o; | |
this.octokit = new Octokit({ | |
auth: this.rootOptions.token | |
}) | |
} | |
async getPostList(options: { | |
ownerId?: string | |
sha?: string | |
} = {}) { | |
if (options.ownerId) { | |
const jsonPostList = await this.getJsonPostList(options) | |
const ownerPosts = jsonPostList.filter(p => p.ownerId === options.ownerId) | |
return ownerPosts.sort(((a, b) => (b.createdAt - a.createdAt))) | |
} | |
const [jsonPostList, markdownPostList] = await Promise.all([ | |
this.getJsonPostList(options), | |
this.getMarkdownPostList(options) | |
]) | |
const allPosts = [...jsonPostList, ...markdownPostList] | |
return allPosts.sort(((a, b) => (b.createdAt - a.createdAt))) | |
} | |
async getJsonPostList(options: { | |
sha?: string | |
} = {}) { | |
const payload = { | |
owner: this.rootOptions.owner, | |
repo: this.rootOptions.repo, | |
path: 'data/posts', | |
ref: options.sha | |
} | |
if (!payload.ref ) { | |
delete payload.ref | |
} | |
try { | |
const response = await this.octokit.repos.getContent(payload) | |
if (!Array.isArray(response.data)) { | |
throw new Error(`data/posts directory does not exists`) | |
} | |
const posts = await Promise.all( | |
response.data.map(async file => { | |
const slug = file.name.replace(/.json$/, '') | |
const jsonPost = await this.getFile(`data/posts/${file.name}`) | |
const post = JSON.parse(jsonPost) | |
delete post.content | |
post.slug = slug | |
return post | |
}) | |
) | |
return posts | |
} catch(err) { | |
if (err.status === 404) { | |
return [] | |
} | |
} | |
} | |
async getMarkdownPostList(options: { | |
sha?: string | |
} = {}) { | |
const payload = { | |
owner: this.rootOptions.owner, | |
repo: this.rootOptions.repo, | |
path: 'data', | |
ref: options.sha | |
} | |
if (!payload.ref ) { | |
delete payload.ref | |
} | |
const response = await this.octokit.repos.getContent(payload) | |
if (!Array.isArray(response.data)) { | |
throw new Error(`data directory does not exists`) | |
} | |
return response.data | |
.filter(file => /.md$/.test(file.name)) | |
.map(file => { | |
const slug = file.name.replace(/.md$/, '') | |
return { | |
slug, | |
...parseSlug(slug) | |
} | |
}) | |
} | |
async getPost(slug: string, options: {sha?: string} = {}): Promise<Post> { | |
const path = `data/${slug}.md` | |
const content = await this.getFile(path) | |
if (!content) { | |
// try to get from the post directory | |
const jsonPost = await this.getFile(`data/posts/${slug}.json`, options.sha) | |
if (!jsonPost) { | |
return null | |
} | |
return JSON.parse(jsonPost) | |
} | |
return { | |
slug, | |
...parseSlug(slug), | |
content | |
} | |
} | |
async getAllComments(slug: string, options: {sha?: string} = {}) { | |
const path = `data/comments/${slug}.json`; | |
const jsonString = await this.getFile(path, options.sha); | |
if (!jsonString) { | |
return [] as Comment[]; | |
} | |
return JSON.parse(jsonString) as Comment[] | |
} | |
async getComments(slug: string, options: {sha?: string} = {}) { | |
return this.getAllComments(slug, options); | |
} | |
async getCommentsWithPagination(slug: string, options: { | |
sha?: string, | |
sort?: number, | |
limit?: number, | |
offset?: number | |
} = {}) { | |
const { sha, sort = 1, limit = 5, offset=null } = options; | |
const comments = await this.getAllComments(slug, { sha }); | |
// sort it | |
comments.sort((a, b) => { | |
return sort === 1? a.createdAt - b.createdAt : b.createdAt - a.createdAt; | |
}) | |
// remove everything upto the offset | |
let foundOffset = false; | |
const commentsWithOffset = offset? comments.filter(c => { | |
if (foundOffset) { | |
return true | |
} | |
foundOffset = c.createdAt == offset | |
return false | |
}): comments | |
// apply the limit | |
const commentsWithLimit = commentsWithOffset.slice(0, limit) | |
return commentsWithLimit | |
} | |
async addComment(slug: string, comment: Comment, options: {sha?: string} = {}): Promise<Comment> { | |
const path = `data/comments/${slug}.json`; | |
if (!comment.id) { | |
comment.id = nanoid(30) | |
} | |
const comments = await this.getAllComments(slug, options) | |
comments.push(comment) | |
await this.saveFile(path, JSON.stringify(comments, null, 2), options.sha); | |
return comment | |
} | |
async saveUser(type: string, profile: { | |
id: string; | |
name: string; | |
avatar: string; | |
}) { | |
const user = { | |
id: `${type}-${profile.id}`, | |
[type]: profile, | |
profile: { | |
name: profile.name, | |
avatar: profile.avatar | |
} | |
} | |
const path = `data/users/${user.id}.json` | |
await this.saveFile(path, JSON.stringify(user, null, 2)) | |
return user.id | |
} | |
async getUser(id: string) { | |
const path = `data/users/${id}.json` | |
const jsonUser = await this.getFile(path); | |
if (!jsonUser) { | |
return null | |
} | |
return JSON.parse(jsonUser) as User; | |
} | |
async createPost(slug: string, {ownerId, title, content}: { | |
ownerId: string; | |
title: string; | |
content: string; | |
}) { | |
const createdAt = Date.now() | |
const post: Post = { | |
ownerId, | |
slug, | |
title, | |
content, | |
createdAt, | |
updatedAt: createdAt | |
} | |
const path = `data/posts/${slug}.json` | |
await this.saveFile(path, JSON.stringify(post, null, 2)) | |
return post; | |
} | |
async updatePost(slug: string, {ownerId, title, content}: { | |
ownerId: string; | |
title: string; | |
content: string; | |
}) { | |
const post = await this.getPost(slug); | |
if (!post) { | |
throw new Error(`Post not found`) | |
} | |
if (post.ownerId !== ownerId) { | |
throw new Error(`Invalid ownerId`) | |
} | |
post.title = title | |
post.content = content | |
post.updatedAt = Date.now() | |
const path = `data/posts/${slug}.json` | |
await this.saveFile(path, JSON.stringify(post, null, 2)) | |
return post | |
} | |
async deletePost(slug: string, {ownerId}: { | |
ownerId: string; | |
}) { | |
const post = await this.getPost(slug); | |
if (!post) { | |
throw new Error(`Post not found`) | |
} | |
if (post.ownerId !== ownerId) { | |
throw new Error(`Invalid ownerId`) | |
} | |
const path = `data/posts/${slug}.json` | |
await this.deleteFile(path) | |
} | |
async getFile(path: string, sha?: string) { | |
const baseInfo = { | |
owner: this.rootOptions.owner, | |
repo: this.rootOptions.repo, | |
path, | |
ref: sha | |
} | |
if (!sha) { | |
delete baseInfo.ref | |
} | |
try { | |
const response = await this.octokit.repos.getContent(baseInfo); | |
if (Array.isArray(response.data)) { | |
throw new Error(`Provided path("${path}") is a directory`) | |
} | |
return Buffer.from(response.data.content, 'base64').toString('utf8') | |
} catch(err) { | |
if (err.status === 404) { | |
return null | |
} | |
throw err | |
} | |
} | |
async deleteFile(path: string, sha?: string) { | |
const baseInfo = { | |
owner: this.rootOptions.owner, | |
repo: this.rootOptions.repo, | |
path, | |
ref: sha | |
} | |
if (!sha) { | |
delete baseInfo.ref | |
} | |
try { | |
const response = await this.octokit.repos.getContent(baseInfo); | |
await this.octokit.repos.deleteFile({ | |
...baseInfo, | |
sha: response.data.sha, | |
message: `Updated content at ${path}` | |
}); | |
} catch(err) { | |
if (err.status === 404) { | |
return null | |
} | |
throw err | |
} | |
} | |
async saveFile(path: string, content: string, sha?: string) { | |
const baseInfo = { | |
owner: this.rootOptions.owner, | |
repo: this.rootOptions.repo, | |
path, | |
ref: sha | |
} | |
if (!sha) { | |
delete baseInfo.ref | |
} | |
try { | |
const response = await this.octokit.repos.getContent(baseInfo); | |
await this.octokit.repos | |
.createOrUpdateFileContents({ | |
...baseInfo, | |
sha: response.data.sha, | |
content: Buffer.from(content).toString('base64'), | |
message: `Updated content at ${path}` | |
}) | |
} catch(err) { | |
if (err.status === 404) { | |
await this.octokit.repos | |
.createOrUpdateFileContents({ | |
...baseInfo, | |
content: Buffer.from(content).toString('base64'), | |
message: `Added content to ${path}` | |
}) | |
return | |
} | |
throw err; | |
} | |
} | |
} | |
export function parseSlug(slug: string) { | |
const [year, month, day, ...titleParts] = slug.split('-') | |
const title = titleParts | |
.map(w => { | |
if (w.length < 4) { | |
return w; | |
} | |
return `${w[0].toUpperCase()}${w.substr(1)}` | |
}) | |
.join(' ') | |
const createdAt = (new Date(`${year}-${month}-${day}`)).getTime() | |
return { | |
title, | |
createdAt | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment