Add new Starlight-GhostCMS plugin #66
|
@ -0,0 +1,17 @@
|
||||||
|
declare module 'astro:content' {
|
||||||
|
export interface AstroCollectionEntry<TData> {
|
||||||
|
body: string
|
||||||
|
collection: string
|
||||||
|
data: TData
|
||||||
|
id: string
|
||||||
|
render: () => Promise<{
|
||||||
|
Content: import('astro').MarkdownInstance<object>['Content']
|
||||||
|
}>
|
||||||
|
slug: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCollection<TData>(
|
||||||
|
collection: string,
|
||||||
|
filter?: (entry: AstroCollectionEntry<TData>) => boolean,
|
||||||
|
): Promise<AstroCollectionEntry<TData>[]>
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ export default function starlightBlogPlugin(userConfig?: StarlightGhostConfig):
|
||||||
...starlightConfig.components,
|
...starlightConfig.components,
|
||||||
...overrideStarlightComponent(starlightConfig.components, logger, 'MarkdownContent'),
|
...overrideStarlightComponent(starlightConfig.components, logger, 'MarkdownContent'),
|
||||||
...overrideStarlightComponent(starlightConfig.components, logger, 'Sidebar'),
|
...overrideStarlightComponent(starlightConfig.components, logger, 'Sidebar'),
|
||||||
|
...overrideStarlightComponent(starlightConfig.components, logger, "SiteTitle"),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -32,22 +32,29 @@
|
||||||
"email": "issues@astro-ghostcms.xyz"
|
"email": "issues@astro-ghostcms.xyz"
|
||||||
},
|
},
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"types": "types.d.ts",
|
"types": "index.ts",
|
||||||
"files": [
|
"files": [
|
||||||
"src",
|
"src",
|
||||||
".env.demo",
|
|
||||||
"index.ts",
|
"index.ts",
|
||||||
"tsconfig.json",
|
"tsconfig.json",
|
||||||
"types.d.ts"
|
"types.d.ts"
|
||||||
],
|
],
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./index.ts"
|
".": "./index.ts",
|
||||||
|
"./overrides/MarkdownContent.astro": "./src/overrides/MarkdownContent.astro",
|
||||||
|
"./overrides/Sidebar.astro": "./src/overrides/SideBar.astro",
|
||||||
|
"./overrides/SiteTitle.astro": "./src/overrides/SiteTitle.astro",
|
||||||
|
"./routes/index.astro": "./src/routes/index.astro",
|
||||||
|
"./routes/[slug].astro": "./src/routes/[slug].astro",
|
||||||
|
"./schema": "./src/schemas/config.ts",
|
||||||
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/starlight": "0.19.0",
|
"@astrojs/starlight": "0.19.0",
|
||||||
"@ts-ghost/core-api": "5.1.2",
|
"@ts-ghost/core-api": "5.1.2",
|
||||||
|
"vite": "^5.1.2",
|
||||||
"astro": "4.3.7"
|
"astro": "4.3.7"
|
||||||
},
|
},
|
||||||
"peerdependencies": {
|
"peerdependencies": {
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
---
|
---
|
||||||
import { getBlogEntryMetadata, type StarlightBlogEntry } from '../utils/content'
|
|
||||||
|
|
||||||
|
import type { Post } from '../schemas/posts'
|
||||||
import Author from './Author.astro'
|
import Author from './Author.astro'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
entry: StarlightBlogEntry
|
entry: Post
|
||||||
}
|
}
|
||||||
|
|
||||||
const { entry } = Astro.props
|
const { entry } = Astro.props
|
||||||
const { authors, date } = getBlogEntryMetadata(entry)
|
const { authors, published_at, created_at } = entry
|
||||||
|
|
||||||
const hasAuthors = authors.length > 0
|
const hasAuthors = authors !== undefined
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="metadata not-content">
|
<div class="metadata not-content">
|
||||||
<time datetime={entry.data.date.toISOString()}>
|
<time datetime={published_at?published_at:created_at.toString()}>
|
||||||
{date}
|
{published_at}
|
||||||
</time>
|
</time>
|
||||||
{
|
{
|
||||||
hasAuthors ? (
|
hasAuthors ? (
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
---
|
---
|
||||||
import type { StarlightBlogEntry } from '../utils/content'
|
|
||||||
|
|
||||||
import Preview from './Preview.astro'
|
import Preview from './Preview.astro'
|
||||||
|
import type { Post } from '../schemas/posts'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
entries: StarlightBlogEntry[]
|
entries: Post[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const { entries } = Astro.props
|
const { entries } = Astro.props
|
||||||
|
|
|
@ -1,26 +1,25 @@
|
||||||
---
|
---
|
||||||
import { getBlogEntryExcerpt, type StarlightBlogEntry } from '../utils/content'
|
import type { Post } from '../schemas/posts'
|
||||||
|
|
||||||
import Metadata from './Metadata.astro'
|
import Metadata from './Metadata.astro'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
entry: StarlightBlogEntry
|
entry: Post
|
||||||
}
|
}
|
||||||
|
|
||||||
const { entry } = Astro.props
|
const { entry } = Astro.props
|
||||||
|
|
||||||
const Excerpt = await getBlogEntryExcerpt(entry)
|
const Excerpt = entry.excerpt
|
||||||
---
|
---
|
||||||
|
|
||||||
<article class="preview">
|
<article class="preview">
|
||||||
<header>
|
<header>
|
||||||
<h2>
|
<h2>
|
||||||
<a href={`/${entry.slug}`}>{entry.data.title}</a>
|
<a href={`/${entry.slug}`}>{entry.title}</a>
|
||||||
</h2>
|
</h2>
|
||||||
<Metadata entry={entry} />
|
<Metadata entry={entry} />
|
||||||
</header>
|
</header>
|
||||||
<div class="sl-markdown-content">
|
<div class="sl-markdown-content">
|
||||||
{typeof Excerpt === 'string' ? Excerpt : <Excerpt />}
|
{typeof Excerpt === 'string' ? Excerpt : entry.excerpt}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
import type { Props } from "@astrojs/starlight/props";
|
||||||
|
import AstrolightSiteTitle from "@astrojs/starlight/components/SiteTitle.astro";
|
||||||
|
//import config from 'virtual:starlight-ghost-config'
|
||||||
|
---
|
||||||
|
|
||||||
|
<AstrolightSiteTitle {...Astro.props} />
|
||||||
|
<div>
|
||||||
|
<a href="/blog">Blog</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
border-inline-start: 1px solid var(--sl-color-gray-5);
|
||||||
|
display: none;
|
||||||
|
padding-inline-start: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 50rem) {
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--sl-color-text-accent);
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,37 @@
|
||||||
|
---
|
||||||
|
import type { InferGetStaticPropsType } from 'astro'
|
||||||
|
import config from 'virtual:starlight-ghost-config'
|
||||||
|
|
||||||
|
import Page from '../components/Page.astro'
|
||||||
|
import Posts from '../components/Posts.astro'
|
||||||
|
import PrevNextLinks from '../components/PrevNextLinks.astro'
|
||||||
|
import { getPageProps } from '../utils/page'
|
||||||
|
|
||||||
|
export const prerender = true
|
||||||
|
|
||||||
|
export function getStaticPaths() {
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = InferGetStaticPropsType<typeof getStaticPaths>
|
||||||
|
|
||||||
|
const { entries, nextLink, prevLink } = Astro.props
|
||||||
|
|
||||||
|
const pageProps = getPageProps(config.title)
|
||||||
|
---
|
||||||
|
|
||||||
|
<Page {...pageProps}>
|
||||||
|
<Posts {entries} />
|
||||||
|
<footer class="not-content">
|
||||||
|
<PrevNextLinks next={nextLink} prev={prevLink} />
|
||||||
|
</footer>
|
||||||
|
</Page>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(.content-panel:first-of-type) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.content-panel:nth-of-type(2)) {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,9 +1,16 @@
|
||||||
/** @ts-expect-error */
|
|
||||||
import { AstroError } from 'astro/errors'
|
import { AstroError } from 'astro/errors'
|
||||||
import { z } from 'astro/zod'
|
import { z } from 'astro/zod'
|
||||||
|
|
||||||
const configSchema = z
|
const configSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
/**
|
||||||
|
* The number of blog posts to display per page in the blog post list.
|
||||||
|
*/
|
||||||
|
postCount: z.number().min(1).default(5),
|
||||||
|
/**
|
||||||
|
* The number of recent blog posts to display in the sidebar.
|
||||||
|
*/
|
||||||
|
recentPostCount: z.number().min(1).default(10),
|
||||||
/**
|
/**
|
||||||
* The title of the blog.
|
* The title of the blog.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import {
|
||||||
|
ghostCodeInjectionSchema,
|
||||||
|
ghostIdentitySchema,
|
||||||
|
ghostMetadataSchema,
|
||||||
|
ghostSocialMediaSchema,
|
||||||
|
ghostVisibilitySchema,
|
||||||
|
} from "@ts-ghost/core-api";
|
||||||
|
import { z } from "astro/zod";
|
||||||
|
|
||||||
|
import { authorsSchema } from "./authors";
|
||||||
|
import { tagsSchema } from "./tags";
|
||||||
|
|
||||||
|
const postsAuthorSchema = authorsSchema.extend({
|
||||||
|
url: z.string().nullish(),
|
||||||
|
});
|
||||||
|
export const postsSchema = z.object({
|
||||||
|
...ghostIdentitySchema.shape,
|
||||||
|
...ghostMetadataSchema.shape,
|
||||||
|
title: z.string(),
|
||||||
|
html: z.string().catch(""),
|
||||||
|
plaintext: z.string().nullish(),
|
||||||
|
comment_id: z.string().nullable(),
|
||||||
|
feature_image: z.string().nullable(),
|
||||||
|
feature_image_alt: z.string().nullable(),
|
||||||
|
feature_image_caption: z.string().nullable(),
|
||||||
|
featured: z.boolean(),
|
||||||
|
custom_excerpt: z.string().nullable(),
|
||||||
|
...ghostCodeInjectionSchema.shape,
|
||||||
|
...ghostSocialMediaSchema.shape,
|
||||||
|
visibility: ghostVisibilitySchema,
|
||||||
|
custom_template: z.string().nullable(),
|
||||||
|
canonical_url: z.string().nullable(),
|
||||||
|
authors: z.array(postsAuthorSchema).optional(),
|
||||||
|
tags: z.array(tagsSchema).optional(),
|
||||||
|
primary_author: postsAuthorSchema.nullish(),
|
||||||
|
primary_tag: tagsSchema.nullish(),
|
||||||
|
url: z.string(),
|
||||||
|
excerpt: z.string().catch(""),
|
||||||
|
reading_time: z.number().optional().default(0),
|
||||||
|
created_at: z.string(),
|
||||||
|
updated_at: z.string().nullish(),
|
||||||
|
published_at: z.string().nullish(),
|
||||||
|
access: z.boolean(),
|
||||||
|
comments: z.boolean(),
|
||||||
|
email_subject: z.string().nullish(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Post = z.infer<typeof postsSchema>;
|
||||||
|
|
||||||
|
export const postsIncludeSchema = z.object({
|
||||||
|
authors: z.literal(true).optional(),
|
||||||
|
tags: z.literal(true).optional(),
|
||||||
|
});
|
||||||
|
export type PostsIncludeSchema = z.infer<typeof postsIncludeSchema>;
|
|
@ -0,0 +1,34 @@
|
||||||
|
import {
|
||||||
|
ghostCodeInjectionSchema,
|
||||||
|
ghostIdentitySchema,
|
||||||
|
ghostMetadataSchema,
|
||||||
|
ghostSocialMediaSchema,
|
||||||
|
ghostVisibilitySchema,
|
||||||
|
} from "@ts-ghost/core-api";
|
||||||
|
import { z } from "astro/zod";
|
||||||
|
|
||||||
|
export const tagsSchema = z.object({
|
||||||
|
...ghostIdentitySchema.shape,
|
||||||
|
...ghostMetadataSchema.shape,
|
||||||
|
...ghostCodeInjectionSchema.shape,
|
||||||
|
...ghostSocialMediaSchema.shape,
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
feature_image: z.string().nullable(),
|
||||||
|
visibility: ghostVisibilitySchema,
|
||||||
|
canonical_url: z.string().nullable(),
|
||||||
|
accent_color: z.string().nullable(),
|
||||||
|
url: z.string(),
|
||||||
|
count: z
|
||||||
|
.object({
|
||||||
|
posts: z.number(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Tag = z.infer<typeof tagsSchema>;
|
||||||
|
|
||||||
|
export const tagsIncludeSchema = z.object({
|
||||||
|
"count.posts": z.literal(true).optional(),
|
||||||
|
});
|
||||||
|
export type TagsIncludeSchema = z.infer<typeof tagsIncludeSchema>;
|
|
@ -0,0 +1,124 @@
|
||||||
|
import { TS_API } from "./content-api";
|
||||||
|
import type { Page, Post } from "./content-api/schemas";
|
||||||
|
import type { ContentAPICredentials } from './content-api/content-api'
|
||||||
|
// LOAD ENVIRONMENT VARIABLES
|
||||||
|
import { loadEnv } from "vite";
|
||||||
|
import { invariant } from "./invariant";
|
||||||
|
|
||||||
|
const { CONTENT_API_KEY, CONTENT_API_URL } = loadEnv(
|
||||||
|
"all",
|
||||||
|
process.cwd(),
|
||||||
|
"CONTENT_",
|
||||||
|
);
|
||||||
|
|
||||||
|
invariant(CONTENT_API_KEY)
|
||||||
|
invariant(CONTENT_API_URL)
|
||||||
|
|
||||||
|
const key:ContentAPICredentials["key"] = CONTENT_API_KEY;
|
||||||
|
const url:ContentAPICredentials["url"] = CONTENT_API_URL;
|
||||||
|
const version = "v5.0";
|
||||||
|
const api = new TS_API(url, key, version);
|
||||||
|
|
||||||
|
export const getAllAuthors = async () => {
|
||||||
|
const results = await api.authors
|
||||||
|
.browse()
|
||||||
|
.include({ "count.posts": true })
|
||||||
|
.fetch();
|
||||||
|
if (!results.success) {
|
||||||
|
throw new Error(results.errors.map((e) => e.message).join(", "));
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
authors: results.data,
|
||||||
|
meta: results.meta,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPosts = async () => {
|
||||||
|
const results = await api.posts
|
||||||
|
.browse()
|
||||||
|
.include({
|
||||||
|
authors: true,
|
||||||
|
tags: true,
|
||||||
|
})
|
||||||
|
.fetch();
|
||||||
|
if (!results.success) {
|
||||||
|
throw new Error(results.errors.map((e) => e.message).join(", "));
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
posts: results.data,
|
||||||
|
meta: results.meta,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllPosts = async () => {
|
||||||
|
const posts: Post[] = [];
|
||||||
|
let cursor = await api.posts
|
||||||
|
.browse()
|
||||||
|
.include({
|
||||||
|
authors: true,
|
||||||
|
tags: true,
|
||||||
|
})
|
||||||
|
.paginate();
|
||||||
|
if (cursor.current.success) posts.push(...cursor.current.data);
|
||||||
|
while (cursor.next) {
|
||||||
|
cursor = await cursor.next.paginate();
|
||||||
|
if (cursor.current.success) posts.push(...cursor.current.data);
|
||||||
|
}
|
||||||
|
return posts;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllPages = async () => {
|
||||||
|
const pages: Page[] = [];
|
||||||
|
let cursor = await api.pages
|
||||||
|
.browse()
|
||||||
|
.include({
|
||||||
|
authors: true,
|
||||||
|
tags: true,
|
||||||
|
})
|
||||||
|
.paginate();
|
||||||
|
if (cursor.current.success) pages.push(...cursor.current.data);
|
||||||
|
while (cursor.next) {
|
||||||
|
cursor = await cursor.next.paginate();
|
||||||
|
if (cursor.current.success) pages.push(...cursor.current.data);
|
||||||
|
}
|
||||||
|
return pages;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSettings = async () => {
|
||||||
|
const res = await api.settings.fetch();
|
||||||
|
if (res.success) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllTags = async () => {
|
||||||
|
const results = await api.tags
|
||||||
|
.browse()
|
||||||
|
.include({ "count.posts": true })
|
||||||
|
.fetch();
|
||||||
|
if (!results.success) {
|
||||||
|
throw new Error(results.errors.map((e) => e.message).join(", "));
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
tags: results.data,
|
||||||
|
meta: results.meta,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFeaturedPosts = async () => {
|
||||||
|
const results = await api.posts
|
||||||
|
.browse({ filter: "featured:true" })
|
||||||
|
.include({
|
||||||
|
authors: true,
|
||||||
|
tags: true,
|
||||||
|
})
|
||||||
|
.fetch();
|
||||||
|
if (!results.success) {
|
||||||
|
throw new Error(results.errors.map((e) => e.message).join(", "));
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
posts: results.data,
|
||||||
|
meta: results.meta,
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { assert, beforeEach, describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import TS_API from "./content-api";
|
||||||
|
|
||||||
|
describe("content-api", () => {
|
||||||
|
let api: TS_API;
|
||||||
|
beforeEach(() => {
|
||||||
|
api = new TS_API("https://ghost.org", "59d4bf56c73c04a18c867dc3ba", "v5.0");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("content-api", () => {
|
||||||
|
expect(api).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("content-api shouldn't instantiate with an incorrect url", () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
const api = new TS_API("ghost.org", "59d4bf56c73c04a18c867dc3ba", "v5.0");
|
||||||
|
api.settings;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("content-api shouldn't instantiate with an incorrect key", () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
const api = new TS_API("https://ghost.org", "a", "v5.0");
|
||||||
|
api.settings;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("content-api shouldn't instantiate with an incorrect version", () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
const api = new TS_API(
|
||||||
|
"https://ghost.org",
|
||||||
|
"1efedd9db174adee2d23d982:4b74dca0219bad629852191af326a45037346c2231240e0f7aec1f9371cc14e8",
|
||||||
|
// @ts-expect-error
|
||||||
|
"v4.0",
|
||||||
|
);
|
||||||
|
api.settings;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("content-api.posts", () => {
|
||||||
|
expect(api.posts).toBeDefined();
|
||||||
|
expect(api.posts.browse).toBeDefined();
|
||||||
|
expect(api.posts.read).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("content-api.pages", () => {
|
||||||
|
expect(api.pages).toBeDefined();
|
||||||
|
expect(api.pages.browse).toBeDefined();
|
||||||
|
expect(api.pages.read).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("content-api.tags", () => {
|
||||||
|
expect(api.tags).toBeDefined();
|
||||||
|
expect(api.tags.browse).toBeDefined();
|
||||||
|
expect(api.tags.read).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("content-api.tiers", () => {
|
||||||
|
expect(api.tiers).toBeDefined();
|
||||||
|
expect(api.tiers.browse).toBeDefined();
|
||||||
|
expect(api.tiers.read).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("content-api.authors", () => {
|
||||||
|
expect(api.authors).toBeDefined();
|
||||||
|
expect(api.authors.browse).toBeDefined();
|
||||||
|
expect(api.authors.read).toBeDefined();
|
||||||
|
// @ts-expect-error
|
||||||
|
expect(api.authors.add).toBeUndefined();
|
||||||
|
// @ts-expect-error
|
||||||
|
expect(api.authors.edit).toBeUndefined();
|
||||||
|
expect(api.authors).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("content-api.settings", () => {
|
||||||
|
expect(api.settings).toBeDefined();
|
||||||
|
expect(api.settings.fetch).toBeDefined();
|
||||||
|
// @ts-expect-error
|
||||||
|
expect(api.settings.read).toBeUndefined();
|
||||||
|
// @ts-expect-error
|
||||||
|
expect(api.settings.browse).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,116 @@
|
||||||
|
import {
|
||||||
|
APIComposer,
|
||||||
|
BasicFetcher,
|
||||||
|
HTTPClient,
|
||||||
|
contentAPICredentialsSchema,
|
||||||
|
slugOrIdSchema,
|
||||||
|
} from "@ts-ghost/core-api";
|
||||||
|
|
||||||
|
import {
|
||||||
|
authorsIncludeSchema,
|
||||||
|
authorsSchema,
|
||||||
|
pagesIncludeSchema,
|
||||||
|
pagesSchema,
|
||||||
|
postsIncludeSchema,
|
||||||
|
postsSchema,
|
||||||
|
settingsSchema,
|
||||||
|
tagsIncludeSchema,
|
||||||
|
tagsSchema,
|
||||||
|
tiersIncludeSchema,
|
||||||
|
tiersSchema,
|
||||||
|
} from "./schemas";
|
||||||
|
|
||||||
|
export type { ContentAPICredentials, APIVersions } from "@ts-ghost/core-api";
|
||||||
|
|
||||||
|
export enum BrowseEndpointType {
|
||||||
|
authors = "authors",
|
||||||
|
tiers = "tiers",
|
||||||
|
posts = "posts",
|
||||||
|
pages = "pages",
|
||||||
|
tags = "tags",
|
||||||
|
settings = "settings",
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TS_API<Version extends `v5.${string}` = any> {
|
||||||
|
private httpClient: HTTPClient;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected readonly url: string,
|
||||||
|
protected readonly key: string,
|
||||||
|
protected readonly version: Version,
|
||||||
|
) {
|
||||||
|
const apiCredentials = contentAPICredentialsSchema.parse({
|
||||||
|
key,
|
||||||
|
version,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
this.httpClient = new HTTPClient({
|
||||||
|
...apiCredentials,
|
||||||
|
endpoint: "content",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get authors() {
|
||||||
|
return new APIComposer(
|
||||||
|
"authors",
|
||||||
|
{
|
||||||
|
schema: authorsSchema,
|
||||||
|
identitySchema: slugOrIdSchema,
|
||||||
|
include: authorsIncludeSchema,
|
||||||
|
},
|
||||||
|
this.httpClient,
|
||||||
|
).access(["read", "browse"]);
|
||||||
|
}
|
||||||
|
get tiers() {
|
||||||
|
return new APIComposer(
|
||||||
|
"tiers",
|
||||||
|
{
|
||||||
|
schema: tiersSchema,
|
||||||
|
identitySchema: slugOrIdSchema,
|
||||||
|
include: tiersIncludeSchema,
|
||||||
|
},
|
||||||
|
this.httpClient,
|
||||||
|
).access(["browse", "read"]);
|
||||||
|
}
|
||||||
|
get posts() {
|
||||||
|
return new APIComposer(
|
||||||
|
"posts",
|
||||||
|
{
|
||||||
|
schema: postsSchema,
|
||||||
|
identitySchema: slugOrIdSchema,
|
||||||
|
include: postsIncludeSchema,
|
||||||
|
},
|
||||||
|
this.httpClient,
|
||||||
|
).access(["browse", "read"]);
|
||||||
|
}
|
||||||
|
get pages() {
|
||||||
|
return new APIComposer(
|
||||||
|
"pages",
|
||||||
|
{
|
||||||
|
schema: pagesSchema,
|
||||||
|
identitySchema: slugOrIdSchema,
|
||||||
|
include: pagesIncludeSchema,
|
||||||
|
},
|
||||||
|
this.httpClient,
|
||||||
|
).access(["browse", "read"]);
|
||||||
|
}
|
||||||
|
get tags() {
|
||||||
|
return new APIComposer(
|
||||||
|
"tags",
|
||||||
|
{
|
||||||
|
schema: tagsSchema,
|
||||||
|
identitySchema: slugOrIdSchema,
|
||||||
|
include: tagsIncludeSchema,
|
||||||
|
},
|
||||||
|
this.httpClient,
|
||||||
|
).access(["browse", "read"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
get settings() {
|
||||||
|
return new BasicFetcher(
|
||||||
|
"settings",
|
||||||
|
{ output: settingsSchema },
|
||||||
|
this.httpClient,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
export { default as TS_API } from "./content-api";
|
||||||
|
export * from "./schemas";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
InferFetcherDataShape,
|
||||||
|
InferResponseDataShape,
|
||||||
|
BrowseParams,
|
||||||
|
} from "@ts-ghost/core-api";
|
|
@ -0,0 +1,164 @@
|
||||||
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import createFetchMock from "vitest-fetch-mock";
|
||||||
|
|
||||||
|
import TS_API from "../../content-api";
|
||||||
|
|
||||||
|
const fetchMocker = createFetchMock(vi);
|
||||||
|
|
||||||
|
describe("authors api .browse() Args Type-safety", () => {
|
||||||
|
const url = process.env.VITE_GHOST_URL || "https://my-ghost-blog.com";
|
||||||
|
const key =
|
||||||
|
process.env.VITE_GHOST_CONTENT_API_KEY || "59d4bf56c73c04a18c867dc3ba";
|
||||||
|
const api = new TS_API(url, key, "v5.0");
|
||||||
|
test(".browse() params shouldnt accept invalid params", () => {
|
||||||
|
// @ts-expect-error - shouldnt accept invalid params
|
||||||
|
const browse = api.authors.browse({ pp: 2 });
|
||||||
|
expect(browse.getParams().browseParams).toStrictEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test(".browse() 'order' params should ony accept fields values", () => {
|
||||||
|
// @ts-expect-error - order should ony contain field
|
||||||
|
expect(() => api.authors.browse({ order: "foo ASC" })).toThrow();
|
||||||
|
// valid
|
||||||
|
expect(
|
||||||
|
api.authors.browse({ order: "name ASC" }).getParams().browseParams,
|
||||||
|
).toStrictEqual({
|
||||||
|
order: "name ASC",
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
api.authors.browse({ order: "name ASC,slug DESC" }).getParams()
|
||||||
|
.browseParams,
|
||||||
|
).toStrictEqual({
|
||||||
|
order: "name ASC,slug DESC",
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
api.authors
|
||||||
|
.browse({ order: "name ASC,slug DESC,location ASC" })
|
||||||
|
.getParams().browseParams,
|
||||||
|
).toStrictEqual({
|
||||||
|
order: "name ASC,slug DESC,location ASC",
|
||||||
|
});
|
||||||
|
// @ts-expect-error - order should ony contain field (There is a typo in location)
|
||||||
|
expect(() =>
|
||||||
|
api.authors.browse({ order: "name ASC,slug DESC,locaton ASC" }),
|
||||||
|
).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(".browse() 'filter' params should ony accept valid field", () => {
|
||||||
|
expect(() =>
|
||||||
|
api.authors.browse({
|
||||||
|
// @ts-expect-error - order should ony contain field
|
||||||
|
filter: "foo:bar",
|
||||||
|
}),
|
||||||
|
).toThrow();
|
||||||
|
expect(
|
||||||
|
api.authors
|
||||||
|
.browse({
|
||||||
|
filter: "name:bar",
|
||||||
|
})
|
||||||
|
.getParams().browseParams,
|
||||||
|
).toStrictEqual({
|
||||||
|
filter: "name:bar",
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
api.authors
|
||||||
|
.browse({
|
||||||
|
filter: "name:bar+slug:-test",
|
||||||
|
})
|
||||||
|
.getParams().browseParams,
|
||||||
|
).toStrictEqual({
|
||||||
|
filter: "name:bar+slug:-test",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test(".browse 'fields' argument should ony accept valid fields", () => {
|
||||||
|
expect(
|
||||||
|
api.authors
|
||||||
|
.browse()
|
||||||
|
.fields({
|
||||||
|
// @ts-expect-error - order should ony contain field
|
||||||
|
foo: true,
|
||||||
|
})
|
||||||
|
.getOutputFields(),
|
||||||
|
).toEqual([]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
api.authors.browse().fields({ location: true }).getOutputFields(),
|
||||||
|
).toEqual(["location"]);
|
||||||
|
expect(
|
||||||
|
api.authors
|
||||||
|
.browse()
|
||||||
|
.fields({ name: true, website: true })
|
||||||
|
.getOutputFields(),
|
||||||
|
).toEqual(["name", "website"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("authors resource mocked", () => {
|
||||||
|
let api: TS_API;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = new TS_API(
|
||||||
|
"https://my-ghost-blog.com",
|
||||||
|
"59d4bf56c73c04a18c867dc3ba",
|
||||||
|
"v5.0",
|
||||||
|
);
|
||||||
|
fetchMocker.enableMocks();
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("aouthors should be fetched correctly", async () => {
|
||||||
|
const authors = api.authors;
|
||||||
|
expect(authors).not.toBeUndefined();
|
||||||
|
const browseQuery = authors
|
||||||
|
.browse({
|
||||||
|
page: 2,
|
||||||
|
})
|
||||||
|
.fields({
|
||||||
|
name: true,
|
||||||
|
id: true,
|
||||||
|
});
|
||||||
|
expect(browseQuery).not.toBeUndefined();
|
||||||
|
expect(browseQuery.getOutputFields()).toStrictEqual(["name", "id"]);
|
||||||
|
|
||||||
|
fetchMocker.doMockOnce(
|
||||||
|
JSON.stringify({
|
||||||
|
authors: [
|
||||||
|
{
|
||||||
|
name: "foo",
|
||||||
|
id: "eaoizdjoa1321123",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
pagination: {
|
||||||
|
page: 1,
|
||||||
|
limit: 15,
|
||||||
|
pages: 1,
|
||||||
|
total: 1,
|
||||||
|
next: null,
|
||||||
|
prev: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const result = await browseQuery.fetch();
|
||||||
|
expect(fetchMocker).toHaveBeenCalledTimes(1);
|
||||||
|
expect(fetchMocker).toHaveBeenCalledWith(
|
||||||
|
"https://my-ghost-blog.com/ghost/api/content/authors/?page=2&fields=name%2Cid&key=59d4bf56c73c04a18c867dc3ba",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept-Version": "v5.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(result).not.toBeUndefined();
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.data.length).toBe(1);
|
||||||
|
expect(result.data[0].name).toBe("foo");
|
||||||
|
expect(result.data[0].id).toBe("eaoizdjoa1321123");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,37 @@
|
||||||
|
import {
|
||||||
|
ghostIdentitySchema,
|
||||||
|
ghostMetaSchema,
|
||||||
|
ghostMetadataSchema,
|
||||||
|
} from "@ts-ghost/core-api";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const authorsSchema = z.object({
|
||||||
|
...ghostIdentitySchema.shape,
|
||||||
|
...ghostMetadataSchema.shape,
|
||||||
|
name: z.string(),
|
||||||
|
profile_image: z.string().nullable(),
|
||||||
|
cover_image: z.string().nullable(),
|
||||||
|
bio: z.string().nullable(),
|
||||||
|
website: z.string().nullable(),
|
||||||
|
location: z.string().nullable(),
|
||||||
|
facebook: z.string().nullable(),
|
||||||
|
twitter: z.string().nullable(),
|
||||||
|
count: z
|
||||||
|
.object({
|
||||||
|
posts: z.number(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
url: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Author = z.infer<typeof authorsSchema>;
|
||||||
|
|
||||||
|
export const ghostFetchAuthorsSchema = z.object({
|
||||||
|
meta: ghostMetaSchema,
|
||||||
|
authors: z.array(authorsSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const authorsIncludeSchema = z.object({
|
||||||
|
"count.posts": z.literal(true).optional(),
|
||||||
|
});
|
||||||
|
export type AuthorsIncludeSchema = z.infer<typeof authorsIncludeSchema>;
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./authors";
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./socials";
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Transformed to TypeScript, original Code by Ghost Foundation, License:
|
||||||
|
// MIT License
|
||||||
|
|
||||||
|
// Copyright (c) 2013-2022 Ghost Foundation
|
||||||
|
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
// The above copyright notice and this permission notice shall be included in all
|
||||||
|
// copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
// SOFTWARE.
|
||||||
|
|
||||||
|
export const twitter = (username: string) => {
|
||||||
|
// Creates the canonical twitter URL without the '@'
|
||||||
|
return `https://twitter.com/${username.replace(/^@/, "")}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const facebook = (username: string) => {
|
||||||
|
// Handles a starting slash, this shouldn't happen, but just in case
|
||||||
|
return `https://www.facebook.com/${username.replace(/^\//, "")}`;
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
export * from "./authors";
|
||||||
|
export * from "./helpers";
|
||||||
|
export * from "./pages";
|
||||||
|
export * from "./posts";
|
||||||
|
export * from "./settings";
|
||||||
|
export * from "./tags";
|
||||||
|
export * from "./tiers";
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./pages";
|
|
@ -0,0 +1,55 @@
|
||||||
|
import {
|
||||||
|
ghostCodeInjectionSchema,
|
||||||
|
ghostIdentitySchema,
|
||||||
|
ghostMetadataSchema,
|
||||||
|
ghostSocialMediaSchema,
|
||||||
|
ghostVisibilitySchema,
|
||||||
|
} from "@ts-ghost/core-api";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { authorsSchema } from "../authors";
|
||||||
|
import { tagsSchema } from "../tags";
|
||||||
|
|
||||||
|
const postsAuthorSchema = authorsSchema.extend({
|
||||||
|
url: z.string().nullish(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const pagesSchema = z.object({
|
||||||
|
...ghostIdentitySchema.shape,
|
||||||
|
...ghostMetadataSchema.shape,
|
||||||
|
title: z.string(),
|
||||||
|
html: z.string().catch(""),
|
||||||
|
plaintext: z.string().nullish(),
|
||||||
|
comment_id: z.string().nullable(),
|
||||||
|
feature_image: z.string().nullable(),
|
||||||
|
feature_image_alt: z.string().nullable(),
|
||||||
|
feature_image_caption: z.string().nullable(),
|
||||||
|
featured: z.boolean(),
|
||||||
|
custom_excerpt: z.string().nullable(),
|
||||||
|
...ghostCodeInjectionSchema.shape,
|
||||||
|
...ghostSocialMediaSchema.shape,
|
||||||
|
visibility: ghostVisibilitySchema,
|
||||||
|
custom_template: z.string().nullable(),
|
||||||
|
canonical_url: z.string().nullable(),
|
||||||
|
authors: z.array(postsAuthorSchema).optional(),
|
||||||
|
tags: z.array(tagsSchema).optional(),
|
||||||
|
primary_author: postsAuthorSchema.nullish(),
|
||||||
|
primary_tag: tagsSchema.nullish(),
|
||||||
|
url: z.string(),
|
||||||
|
excerpt: z.string().catch(""),
|
||||||
|
reading_time: z.number().optional().default(0),
|
||||||
|
created_at: z.string(),
|
||||||
|
updated_at: z.string(),
|
||||||
|
published_at: z.string(),
|
||||||
|
access: z.boolean(),
|
||||||
|
comments: z.boolean(),
|
||||||
|
email_subject: z.string().nullish(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Page = z.infer<typeof pagesSchema>;
|
||||||
|
|
||||||
|
export const pagesIncludeSchema = z.object({
|
||||||
|
authors: z.literal(true).optional(),
|
||||||
|
tags: z.literal(true).optional(),
|
||||||
|
});
|
||||||
|
export type PagesIncludeSchema = z.infer<typeof pagesIncludeSchema>;
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./posts";
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import TS_API from "../../content-api";
|
||||||
|
import type { Post } from "./posts";
|
||||||
|
|
||||||
|
const url = process.env.VITE_GHOST_URL || "https://my-ghost-blog.com";
|
||||||
|
const key =
|
||||||
|
process.env.VITE_GHOST_CONTENT_API_KEY || "59d4bf56c73c04a18c867dc3ba";
|
||||||
|
|
||||||
|
describe("posts api .browse() Args Type-safety", () => {
|
||||||
|
const api = new TS_API(url, key, "v5.0");
|
||||||
|
test(".browse() params shouldnt accept invalid params", () => {
|
||||||
|
// @ts-expect-error - shouldnt accept invalid params
|
||||||
|
const browse = api.posts.browse({ pp: 2 });
|
||||||
|
expect(browse.getParams().browseParams).toStrictEqual({});
|
||||||
|
|
||||||
|
const outputFields = {
|
||||||
|
slug: true,
|
||||||
|
title: true,
|
||||||
|
// @ts-expect-error - shouldnt accept invalid params
|
||||||
|
foo: true,
|
||||||
|
} satisfies { [k in keyof Post]?: true | undefined };
|
||||||
|
|
||||||
|
// biome-ignore lint/style/useConst: <explanation>
|
||||||
|
let test = api.posts
|
||||||
|
.browse()
|
||||||
|
// @ts-expect-error - shouldnt accept invalid params
|
||||||
|
.fields(outputFields);
|
||||||
|
expect(test.getOutputFields()).toEqual(["slug", "title"]);
|
||||||
|
|
||||||
|
const fields = ["slug", "title", "foo"] as const;
|
||||||
|
const unknownOriginFields = fields.reduce(
|
||||||
|
(acc, k) => {
|
||||||
|
acc[k as keyof Post] = true;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as { [k in keyof Post]?: true | undefined },
|
||||||
|
);
|
||||||
|
const result = api.posts.browse().fields(unknownOriginFields);
|
||||||
|
expect(result.getOutputFields()).toEqual(["slug", "title"]);
|
||||||
|
});
|
||||||
|
test(".browse() params, output fields declare const", () => {
|
||||||
|
const outputFields = {
|
||||||
|
slug: true,
|
||||||
|
title: true,
|
||||||
|
} satisfies { [k in keyof Post]?: true | undefined };
|
||||||
|
|
||||||
|
// biome-ignore lint/style/useConst: <explanation>
|
||||||
|
let test = api.posts.browse().fields(outputFields);
|
||||||
|
expect(test.getOutputFields()).toEqual(["slug", "title"]);
|
||||||
|
|
||||||
|
// @ts-expect-error - shouldnt accept invalid params
|
||||||
|
expect(() => api.posts.browse({ filter: "slugg:test" })).toThrow();
|
||||||
|
// @ts-expect-error - shouldnt accept invalid params
|
||||||
|
expect(() =>
|
||||||
|
api.posts.browse({ filter: "slug:test,foo:-[bar,baz]" }),
|
||||||
|
).toThrow();
|
||||||
|
expect(
|
||||||
|
api.posts.browse({ filter: "slug:test,tags:-[bar,baz]" }),
|
||||||
|
).toBeDefined();
|
||||||
|
expect(
|
||||||
|
api.posts.browse({ filter: "slug:test,tags:[bar,baz]" }),
|
||||||
|
).toBeDefined();
|
||||||
|
// @ts-expect-error - shouldnt accept invalid params
|
||||||
|
expect(() =>
|
||||||
|
api.posts.browse({ filter: "slug:test,food:-[bar,baz]" }),
|
||||||
|
).toThrow();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,54 @@
|
||||||
|
import {
|
||||||
|
ghostCodeInjectionSchema,
|
||||||
|
ghostIdentitySchema,
|
||||||
|
ghostMetadataSchema,
|
||||||
|
ghostSocialMediaSchema,
|
||||||
|
ghostVisibilitySchema,
|
||||||
|
} from "@ts-ghost/core-api";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { authorsSchema } from "../authors";
|
||||||
|
import { tagsSchema } from "../tags";
|
||||||
|
|
||||||
|
const postsAuthorSchema = authorsSchema.extend({
|
||||||
|
url: z.string().nullish(),
|
||||||
|
});
|
||||||
|
export const postsSchema = z.object({
|
||||||
|
...ghostIdentitySchema.shape,
|
||||||
|
...ghostMetadataSchema.shape,
|
||||||
|
title: z.string(),
|
||||||
|
html: z.string().catch(""),
|
||||||
|
plaintext: z.string().nullish(),
|
||||||
|
comment_id: z.string().nullable(),
|
||||||
|
feature_image: z.string().nullable(),
|
||||||
|
feature_image_alt: z.string().nullable(),
|
||||||
|
feature_image_caption: z.string().nullable(),
|
||||||
|
featured: z.boolean(),
|
||||||
|
custom_excerpt: z.string().nullable(),
|
||||||
|
...ghostCodeInjectionSchema.shape,
|
||||||
|
...ghostSocialMediaSchema.shape,
|
||||||
|
visibility: ghostVisibilitySchema,
|
||||||
|
custom_template: z.string().nullable(),
|
||||||
|
canonical_url: z.string().nullable(),
|
||||||
|
authors: z.array(postsAuthorSchema).optional(),
|
||||||
|
tags: z.array(tagsSchema).optional(),
|
||||||
|
primary_author: postsAuthorSchema.nullish(),
|
||||||
|
primary_tag: tagsSchema.nullish(),
|
||||||
|
url: z.string(),
|
||||||
|
excerpt: z.string().catch(""),
|
||||||
|
reading_time: z.number().optional().default(0),
|
||||||
|
created_at: z.string(),
|
||||||
|
updated_at: z.string().nullish(),
|
||||||
|
published_at: z.string().nullish(),
|
||||||
|
access: z.boolean(),
|
||||||
|
comments: z.boolean(),
|
||||||
|
email_subject: z.string().nullish(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Post = z.infer<typeof postsSchema>;
|
||||||
|
|
||||||
|
export const postsIncludeSchema = z.object({
|
||||||
|
authors: z.literal(true).optional(),
|
||||||
|
tags: z.literal(true).optional(),
|
||||||
|
});
|
||||||
|
export type PostsIncludeSchema = z.infer<typeof postsIncludeSchema>;
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./settings";
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const settingsSchema = z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
logo: z.string().nullable(),
|
||||||
|
icon: z.string().nullable(),
|
||||||
|
accent_color: z.string().nullable(),
|
||||||
|
cover_image: z.string().nullable(),
|
||||||
|
facebook: z.string().nullable(),
|
||||||
|
twitter: z.string().nullable(),
|
||||||
|
lang: z.string(),
|
||||||
|
timezone: z.string(),
|
||||||
|
codeinjection_head: z.string().nullable(),
|
||||||
|
codeinjection_foot: z.string().nullable(),
|
||||||
|
navigation: z.array(
|
||||||
|
z.object({
|
||||||
|
label: z.string(),
|
||||||
|
url: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
secondary_navigation: z.array(
|
||||||
|
z.object({
|
||||||
|
label: z.string(),
|
||||||
|
url: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
meta_title: z.string().nullable(),
|
||||||
|
meta_description: z.string().nullable(),
|
||||||
|
og_image: z.string().nullable(),
|
||||||
|
og_title: z.string().nullable(),
|
||||||
|
og_description: z.string().nullable(),
|
||||||
|
twitter_image: z.string().nullable(),
|
||||||
|
twitter_title: z.string().nullable(),
|
||||||
|
twitter_description: z.string().nullable(),
|
||||||
|
members_support_address: z.string(),
|
||||||
|
url: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Settings = z.infer<typeof settingsSchema>;
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./tags";
|
|
@ -0,0 +1,34 @@
|
||||||
|
import {
|
||||||
|
ghostCodeInjectionSchema,
|
||||||
|
ghostIdentitySchema,
|
||||||
|
ghostMetadataSchema,
|
||||||
|
ghostSocialMediaSchema,
|
||||||
|
ghostVisibilitySchema,
|
||||||
|
} from "@ts-ghost/core-api";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const tagsSchema = z.object({
|
||||||
|
...ghostIdentitySchema.shape,
|
||||||
|
...ghostMetadataSchema.shape,
|
||||||
|
...ghostCodeInjectionSchema.shape,
|
||||||
|
...ghostSocialMediaSchema.shape,
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
feature_image: z.string().nullable(),
|
||||||
|
visibility: ghostVisibilitySchema,
|
||||||
|
canonical_url: z.string().nullable(),
|
||||||
|
accent_color: z.string().nullable(),
|
||||||
|
url: z.string(),
|
||||||
|
count: z
|
||||||
|
.object({
|
||||||
|
posts: z.number(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Tag = z.infer<typeof tagsSchema>;
|
||||||
|
|
||||||
|
export const tagsIncludeSchema = z.object({
|
||||||
|
"count.posts": z.literal(true).optional(),
|
||||||
|
});
|
||||||
|
export type TagsIncludeSchema = z.infer<typeof tagsIncludeSchema>;
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./tiers";
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { ghostIdentitySchema, ghostVisibilitySchema } from "@ts-ghost/core-api";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const tiersSchema = z.object({
|
||||||
|
...ghostIdentitySchema.shape,
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
active: z.boolean(),
|
||||||
|
type: z.union([z.literal("free"), z.literal("paid")]),
|
||||||
|
welcome_page_url: z.string().nullable(),
|
||||||
|
created_at: z.string(),
|
||||||
|
updated_at: z.string().nullable(),
|
||||||
|
stripe_prices: z
|
||||||
|
.array(z.number())
|
||||||
|
.optional()
|
||||||
|
.transform((v) => (v?.length ? v : [])),
|
||||||
|
monthly_price: z
|
||||||
|
.number()
|
||||||
|
.nullable()
|
||||||
|
.optional()
|
||||||
|
.transform((v) => (v ? v : null)),
|
||||||
|
yearly_price: z
|
||||||
|
.number()
|
||||||
|
.nullable()
|
||||||
|
.optional()
|
||||||
|
.transform((v) => (v ? v : null)),
|
||||||
|
benefits: z.array(z.string()),
|
||||||
|
visibility: ghostVisibilitySchema,
|
||||||
|
currency: z.string().nullish(),
|
||||||
|
trial_days: z.number().default(0),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Tier = z.infer<typeof tiersSchema>;
|
||||||
|
|
||||||
|
export const tiersIncludeSchema = z.object({
|
||||||
|
monthly_price: z.literal(true).optional(),
|
||||||
|
yearly_price: z.literal(true).optional(),
|
||||||
|
benefits: z.literal(true).optional(),
|
||||||
|
});
|
||||||
|
export type TiersIncludeSchema = z.infer<typeof tiersIncludeSchema>;
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from "./api-functions";
|
||||||
|
export * from "./content-api/schemas";
|
||||||
|
export * from "./invariant";
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
// Modified version of invariant script to allow tests
|
||||||
|
const isProduction = false;
|
||||||
|
const prefix: string = "Invariant failed";
|
||||||
|
function invariant(condition: any, message?: string | (() => string)) {
|
||||||
|
if (condition) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isProduction) {
|
||||||
|
throw new Error(prefix);
|
||||||
|
}
|
||||||
|
const provided: string | undefined =
|
||||||
|
typeof message === "function" ? message() : message;
|
||||||
|
const value: string = provided ? `${prefix}: ${provided}` : prefix;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TEST SECTION
|
||||||
|
const testTrue = true;
|
||||||
|
const testFalse = false;
|
||||||
|
describe("test invariant", () => {
|
||||||
|
it("Test `true` value", () => {
|
||||||
|
invariant(testTrue, "This should not error");
|
||||||
|
expect(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Test `false` value", () => {
|
||||||
|
invariant(testFalse, "This should Error");
|
||||||
|
expect(String("Invariant failed"));
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,48 @@
|
||||||
|
/** MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 Alexander Reardon
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
*/
|
||||||
|
const tinyinvariant = "merged";
|
||||||
|
const isProduction: boolean = process.env.NODE_ENV === "production";
|
||||||
|
const prefix: string = "Invariant failed";
|
||||||
|
|
||||||
|
/** Throw an error if the condition is false
|
||||||
|
* @example
|
||||||
|
* import { invariant } from '@matthiesenxyz/astro-ghostcms/api';
|
||||||
|
* invariant(var, "var is false but its not supposed to be!")
|
||||||
|
*/
|
||||||
|
export function invariant(
|
||||||
|
condition: any,
|
||||||
|
message?: string | (() => string),
|
||||||
|
): asserts condition {
|
||||||
|
if (condition) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isProduction) {
|
||||||
|
throw new Error(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
const provided: string | undefined =
|
||||||
|
typeof message === "function" ? message() : message;
|
||||||
|
|
||||||
|
const value: string = provided ? `${prefix}: ${provided}` : prefix;
|
||||||
|
throw new Error(value);
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"allowUnreachableCode": false,
|
||||||
|
"allowUnusedLabels": false,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"incremental": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"verbatimModuleSyntax": true
|
||||||
|
}
|
||||||
|
}
|
634
pnpm-lock.yaml
634
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue