another progress marker

This commit is contained in:
Adam Matthiesen 2024-01-23 14:48:03 -08:00
parent 694432ad97
commit f854abe9dd
23 changed files with 1429 additions and 257 deletions

View File

@ -64,28 +64,38 @@
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint ."
},
"peerDependencies": {
"astro": "^4.2.1",
"zod": "^3.22.4"
"astro": "^4.2.1"
},
"dependencies": {
"@matthiesenxyz/astro-ghostcms-theme-default": "workspace:*",
"devDependencies": {
"@astrojs/check": "^0.3.4",
"@astrojs/rss": "^4.0.2",
"@astrojs/sitemap": "^3.0.5",
"@ts-ghost/core-api": "*",
"@ts-ghost/tsconfig": "*",
"@types/node": "^20.11.5",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser": "^6.19.0",
"@types/node": "^20.11.5",
"astro-robots-txt": "^1.0.0",
"axios": "^1.6.5",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-astro": "^0.31.3",
"jiti": "^1.21.0",
"prettier": "^3.2.4",
"prettier-plugin-astro": "^0.13.0",
"sass": "^1.70.0",
"tiny-invariant": "^1.3.1",
"tsup": "^8.0.0",
"typescript": "^5.3.3",
"vite": "^4.5.2",
"vite-tsconfig-paths": "^4.2.2",
"vitest": "^1.1.0",
"vitest-fetch-mock": "^0.2.2",
"zod": "^3.22.4",
"zod-validation-error": "^3.0.0"
},
"dependencies": {
"@matthiesenxyz/astro-ghostcms-theme-default": "workspace:*",
"@astrojs/rss": "^4.0.2",
"@astrojs/sitemap": "^3.0.5",
"@ts-ghost/core-api": "^5.1.2",
"astro-robots-txt": "^1.0.0",
"axios": "^1.6.5",
"sass": "^1.70.0",
"tiny-invariant": "^1.3.1"
}
}

View File

@ -0,0 +1,117 @@
import type { Page, Post } from "./index";
import { TSGhostContentAPI } from "./content-api";
// LOAD ENVIRONMENT VARIABLES
import { loadEnv } from 'vite';
const {CONTENT_API_KEY, CONTENT_API_URL} = loadEnv(
'all',
process.cwd(),
'CONTENT_'
);
let ghostApiKey = CONTENT_API_KEY;
let ghostUrl = CONTENT_API_URL;
// SETUP API
const version = "v5.0";
export const getGhostAuthors = async () => {
const api = new TSGhostContentAPI(ghostUrl, ghostApiKey, version);
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 api = new TSGhostContentAPI(ghostUrl, ghostApiKey, version);
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 api = new TSGhostContentAPI(ghostUrl, ghostApiKey, version);
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 api = new TSGhostContentAPI(ghostUrl, ghostApiKey, version);
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 api = new TSGhostContentAPI(ghostUrl, ghostApiKey, version);
const res = await api.settings.fetch();
if (res.success) {
return res.data;
}
return null;
};
export type NonNullable<T> = T extends null | undefined ? never : T;
export type Settings = NonNullable<Awaited<ReturnType<typeof getSettings>>>;
export const getAllTags = async () => {
const api = new TSGhostContentAPI(ghostUrl, ghostApiKey, version);
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,
};
};

View File

@ -0,0 +1,94 @@
import {
APIComposer, BasicFetcher, contentAPICredentialsSchema,
HTTPClient, 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 class TSGhostContentAPI<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);
}
}

View File

@ -1,22 +1,4 @@
// FUNCTION EXPORTS
export {
getGhostPosts, getGhostRecentPosts, getGhostFeaturedPosts,
getGhostPostbySlug, getGhostPostsbyTag, getGhostTags,
getGhostTagbySlug, getGhostAuthors, getGhostPages,
getGhostPage, getGhostSettings
} from './functions';
export * from './content-api';
export * from './schemas';
// TYPE EXPORTS
export type {
PostOrPage, ArrayOrValue, Author,
Authors, BrowseFunction, CodeInjection,
Excerpt, Facebook, FieldParam,
FilterParam, FormatParam, GhostAPI,
GhostContentAPIOptions, GhostData, GhostError,
Identification, IncludeParam, LimitParam,
Metadata, Nullable, OrderParam,
PageParam, Pagination, Params,
PostsOrPages, ReadFunction, Settings,
SettingsResponse, SocialMedia, Tag,
TagVisibility, Tags, Twitter
} from './tryghost-content-api.d';
export type { InferFetcherDataShape, InferResponseDataShape, BrowseParams } from "@ts-ghost/core-api";

View File

@ -0,0 +1,33 @@
import { z } from "zod";
import { ghostIdentitySchema, ghostMetadataSchema, ghostMetaSchema } from "@ts-ghost/core-api";
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>;

View File

@ -0,0 +1 @@
export * from "./socials";

View File

@ -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(/^\//, "");
};

View File

@ -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';

View File

@ -0,0 +1,55 @@
import { z } from "zod";
import {
ghostCodeInjectionSchema,
ghostIdentitySchema,
ghostMetadataSchema,
ghostSocialMediaSchema,
ghostVisibilitySchema,
} from "@ts-ghost/core-api";
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>;

View File

@ -0,0 +1,54 @@
import { z } from "zod";
import {
ghostCodeInjectionSchema,
ghostIdentitySchema,
ghostMetadataSchema,
ghostSocialMediaSchema,
ghostVisibilitySchema,
} from "@ts-ghost/core-api";
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>;

View File

@ -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>;

View File

@ -0,0 +1,34 @@
import { z } from "zod";
import {
ghostCodeInjectionSchema,
ghostIdentitySchema,
ghostMetadataSchema,
ghostSocialMediaSchema,
ghostVisibilitySchema,
} from "@ts-ghost/core-api";
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>;

View File

@ -0,0 +1,40 @@
import { z } from "zod";
import { ghostIdentitySchema, ghostVisibilitySchema } from "@ts-ghost/core-api";
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>;

View File

@ -0,0 +1,22 @@
// FUNCTION EXPORTS
export {
getGhostPosts, getGhostRecentPosts, getGhostFeaturedPosts,
getGhostPostbySlug, getGhostPostsbyTag, getGhostTags,
getGhostTagbySlug, getGhostAuthors, getGhostPages,
getGhostPage, getGhostSettings
} from './functions';
// TYPE EXPORTS
export type {
PostOrPage, ArrayOrValue, Author,
Authors, BrowseFunction, CodeInjection,
Excerpt, Facebook, FieldParam,
FilterParam, FormatParam, GhostAPI,
GhostContentAPIOptions, GhostData, GhostError,
Identification, IncludeParam, LimitParam,
Metadata, Nullable, OrderParam,
PageParam, Pagination, Params,
PostsOrPages, ReadFunction, Settings,
SettingsResponse, SocialMedia, Tag,
TagVisibility, Tags, Twitter
} from './tryghost-content-api';

View File

@ -1,5 +1,5 @@
import rss from "@astrojs/rss";
import { getGhostPosts, getGhostSettings } from '../api';
import { getGhostPosts, getGhostSettings } from '../api_old';
import invariant from "tiny-invariant";
export async function GET(context) {

View File

@ -0,0 +1,22 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Default",
"compilerOptions": {
"composite": false,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"isolatedModules": true,
"moduleResolution": "node",
"noUnusedLocals": false,
"noUnusedParameters": false,
"preserveWatchOutput": true,
"skipLibCheck": true,
"strict": true,
"sourceMap": true,
"resolveJsonModule": true
},
"exclude": ["node_modules", "**/*/lib", "**/*/dist"]
}

View File

@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Node 16",
"extends": "./base.json",
"compilerOptions": {
"lib": ["ES2021", "ESNext", "DOM"],
"module": "commonjs",
"target": "ES2021",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}

View File

@ -0,0 +1,9 @@
{
"name": "@ts-ghost/tsconfig",
"version": "0.0.0",
"private": true,
"license": "MIT",
"publishConfig": {
"access": "public"
}
}

View File

@ -0,0 +1,11 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "React Library",
"extends": "./base.json",
"compilerOptions": {
"lib": ["ES2015"],
"module": "ESNext",
"target": "ES6",
"jsx": "react-jsx"
}
}

File diff suppressed because it is too large Load Diff