Skip to content

Instantly share code, notes, and snippets.

@gaurangrshah
Created December 15, 2020 03:32
Show Gist options
  • Save gaurangrshah/1d2931c6ed8836cfe462c2758fc835cd to your computer and use it in GitHub Desktop.
Save gaurangrshah/1d2931c6ed8836cfe462c2758fc835cd to your computer and use it in GitHub Desktop.
git cms example
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