From 48515f9d9412daff1883e62453bf2ebf1e66bf18 Mon Sep 17 00:00:00 2001 From: Jose Javier Date: Fri, 8 Aug 2025 13:54:50 -0500 Subject: [PATCH] feat(admin-api): add types --- packages/admin-api/index.d.ts | 752 ++++++++++++++++++++ packages/admin-api/package.json | 8 +- packages/admin-api/test/types/tsconfig.json | 13 + packages/admin-api/test/types/types.test.ts | 71 ++ yarn.lock | 5 + 5 files changed, 847 insertions(+), 2 deletions(-) create mode 100644 packages/admin-api/index.d.ts create mode 100644 packages/admin-api/test/types/tsconfig.json create mode 100644 packages/admin-api/test/types/types.test.ts diff --git a/packages/admin-api/index.d.ts b/packages/admin-api/index.d.ts new file mode 100644 index 000000000..c7dc48144 --- /dev/null +++ b/packages/admin-api/index.d.ts @@ -0,0 +1,752 @@ +type ArrayOrValue = T | T[]; +type Nullable = T | null; + +// ============================================== +// Interfaces to extend the GhostAdminAPI types +// - Pagination +// - Identification +// - Metadata +// - Excerpt +// - CodeInjection +// - SocialMedia (Facebook, Twitter) +// - Settings +// - Email +// ============================================== + +interface Pagination { + page: number; + limit: number; + pages: number; + total: number; + next: Nullable; + prev: Nullable; +} + +interface Identification { + slug: string; + id: string; +} + +interface Dates { + created_at?: string | undefined; + updated_at?: Nullable | undefined; +} + +interface Metadata { + meta_title?: Nullable | undefined; + meta_description?: Nullable | undefined; +} + +interface Excerpt { + excerpt?: string | undefined; + custom_excerpt?: string | undefined; +} + +interface CodeInjection { + codeinjection_head?: Nullable | undefined; + codeinjection_foot?: Nullable | undefined; +} + +/** Metadata for Facebook */ +interface Facebook { + og_image?: Nullable | undefined; + og_title?: Nullable | undefined; + og_description?: Nullable | undefined; +} + +/** Metadata for Twitter */ +interface Twitter { + twitter_image?: Nullable | undefined; + twitter_title?: Nullable | undefined; + twitter_description?: Nullable | undefined; +} + +interface SocialMedia extends Facebook, Twitter {} + +interface Email extends Dates { + id: string | undefined; + uuid?: string | undefined; + status?: string | undefined; + recipient_filter?: string | undefined; + error?: string | null | undefined; + error_data?: string | undefined; + email_count?: number | undefined; + delivered_count?: number | undefined; + opened_count?: number | undefined; + failed_count?: number | undefined; + subject?: string | undefined; + from?: string | undefined; + reply_to?: string | undefined; + html?: string | undefined; + plaintext?: string | undefined; + track_opens: boolean; + submitted_at?: Nullable | undefined; +} + +// ================= +// Ghost Data Types: +// - Role +// - Author +// - Tag +// - Post +// - Page +// ================= + +// ================= +// Author & Role +// ================= +interface Role extends Dates { + id?: string | undefined; + name?: string | undefined; + description?: string | undefined; +} + +interface AuthorPostRequest { + id?: string | undefined; + name?: string | undefined; +} + +interface AuthorRequest extends Partial, Metadata, Dates { + name?: string | undefined; + profile_image?: Nullable | undefined; + cover_image?: Nullable | undefined; + bio?: Nullable | undefined; + website?: Nullable | undefined; + location?: Nullable | undefined; + facebook?: Nullable | undefined; + twitter?: Nullable | undefined; + url?: Nullable | undefined; + email?: Nullable | undefined; + last_seen?: Nullable | undefined; + tour?: Nullable | undefined; + status?: string | undefined; + accessibility?: Nullable | undefined; + roles?: Nullable | undefined; +} + +interface AuthorResponse extends Identification, Metadata, Dates { + name?: string | undefined; + profile_image?: Nullable | undefined; + cover_image?: Nullable | undefined; + bio?: Nullable | undefined; + website?: Nullable | undefined; + location?: Nullable | undefined; + facebook?: Nullable | undefined; + twitter?: Nullable | undefined; + url?: Nullable | undefined; + email?: Nullable | undefined; + last_seen?: Nullable | undefined; + tour?: Nullable | undefined; + status?: string | undefined; + accessibility?: Nullable | undefined; + roles?: Nullable | undefined; +} + +// ================= +// Tag +// ================= + +type TagVisibility = "public" | "internal"; + +interface TagPostRequest { + id?: string | undefined; + name?: string | undefined; + description?: string | undefined; +} + +interface TagRequest + extends Partial, + Metadata, + SocialMedia, + CodeInjection { + name?: string | undefined; + description?: Nullable | undefined; + feature_image?: Nullable | undefined; + canonical_url?: Nullable | undefined; + accent_color?: Nullable | undefined; + visibility?: TagVisibility | undefined; + url?: string | undefined; + parent?: string | undefined; +} + +interface TagResponse + extends Identification, + Metadata, + SocialMedia, + CodeInjection, + Dates { + name?: string | undefined; + description?: Nullable | undefined; + feature_image?: Nullable | undefined; + canonical_url?: Nullable | undefined; + accent_color?: Nullable | undefined; + visibility?: TagVisibility | undefined; + url?: string | undefined; + parent?: string | undefined; +} + +// ================= +// Webhook +// ================= + +type WebhookEvents = + | "site.changed" + | "post.added" + | "post.deleted" + | "post.edited" + | "post.published" + | "post.published.edited" + | "post.unpublished" + | "post.scheduled" + | "post.unscheduled" + | "post.rescheduled" + | "page.added" + | "page.deleted" + | "page.edited" + | "page.published" + | "page.published.edited" + | "page.unpublished" + | "page.scheduled" + | "page.unscheduled" + | "page.rescheduled" + | "tag.added" + | "tag.edited" + | "tag.deleted" + | "post.tag.attached" + | "post.tag.detached" + | "page.tag.attached" + | "page.tag.detached" + | "member.added" + | "member.edited" + | "member.deleted"; + +interface WebhookRequest extends Dates { + event: WebhookEvents; + target_url: string; + name?: string | undefined; + api_version?: string | undefined; + secret?: string | undefined; + integration_id?: string | undefined; +} + +type WebhookEditRequest = Partial; + +interface WebhookResponse extends WebhookRequest { + id?: string | undefined; + status?: string | undefined; + last_triggered_at?: Nullable | undefined; + last_triggered_status?: Nullable | undefined; + last_triggered_error?: Nullable | undefined; +} + +// ================= +// Newsletter +// ================= + +interface NewsletterMember { + id?: string | undefined; +} + +interface NewsletterRequest extends Partial { + name?: string | undefined; + description?: Nullable | undefined; + sender_name?: string | undefined; + sender_email?: Nullable | undefined; + sender_reply_to?: string | undefined; + visibility?: string | undefined; + subscribe_on_signup?: boolean | undefined; + sort_order?: number | undefined; + header_image?: Nullable | undefined; + show_header_icon?: boolean | undefined; + show_header_title?: boolean | undefined; + title_font_category?: string | undefined; + title_alignment?: string | undefined; + show_feature_image?: boolean | undefined; + body_font_category?: string | undefined; + footer_content?: Nullable | undefined; + show_badge?: boolean | undefined; + show_header_name?: boolean | undefined; +} +interface NewsletterResponse extends Identification, Dates { + uuid?: string | undefined; + name?: string | undefined; + description?: Nullable | undefined; + sender_name?: string | undefined; + sender_email?: Nullable | undefined; + sender_reply_to?: string | undefined; + status?: string | undefined; + visibility?: string | undefined; + subscribe_on_signup?: boolean | undefined; + sort_order?: number | undefined; + header_image?: Nullable | undefined; + show_header_icon?: boolean | undefined; + show_header_title?: boolean | undefined; + title_font_category?: string | undefined; + title_alignment?: string | undefined; + show_feature_image?: boolean | undefined; + body_font_category?: string | undefined; + footer_content?: Nullable | undefined; + show_badge?: boolean | undefined; + show_header_name?: boolean | undefined; +} + +// ====================== +// Member & Subscription +// ====================== + +interface Subscription { + id?: string | undefined; + customer?: { + id?: string | undefined; + name?: string | undefined; + email?: string | undefined; + }; + status?: string | undefined; + start_date?: string | undefined; + default_payment_card_last4?: string | undefined; + cancel_at_period_end?: boolean | undefined; + cancellation_reason?: string | null | undefined; + current_period_end?: string | undefined; + price?: { + id?: string | undefined; + price_id?: string | undefined; + nickname?: string | undefined; + amount?: number | undefined; + interval?: string | undefined; + type?: string | undefined; + currency?: string | undefined; + }; + tier?: any; + offer?: any; +} + +interface MemberLabel extends Dates { + id?: string | undefined; + name?: string | undefined; + slug?: string | undefined; +} + +interface MemberRequest { + email: string | undefined; + name?: string | undefined; + note?: Nullable | undefined; + labels?: MemberLabel[] | undefined; + newsletters?: NewsletterMember[] | undefined; +} + +interface MemberResponse extends Dates { + id?: string | undefined; + uuid?: string | undefined; + email?: string | undefined; + name?: string | undefined; + note?: Nullable | undefined; + geolocation?: Nullable | undefined; + labels?: MemberLabel[] | undefined; + subscriptions?: Subscription[] | undefined; + avatar_image?: Nullable | undefined; + email_count?: number | undefined; + email_opened_count?: number | undefined; + email_open_rate?: Nullable | undefined; + last_seen_at?: Nullable | undefined; + status?: string | undefined; + newsletters?: NewsletterMember[] | undefined; +} + +// ================= +// User +// ================= + +interface UserRequest { + name?: string | undefined; + email?: string | undefined; + profile_image?: Nullable | undefined; + cover_image?: Nullable | undefined; + bio?: Nullable | undefined; + website?: Nullable | undefined; + location?: Nullable | undefined; + facebook?: Nullable | undefined; + twitter?: Nullable | undefined; + accessibility?: Nullable | undefined; + tour?: Nullable | undefined; + comment_notifications?: boolean | undefined; + free_member_signup_notification?: boolean | undefined; + paid_subscription_started_notification?: boolean | undefined; + paid_subscription_canceled_notification?: boolean | undefined; + mention_notifications?: boolean | undefined; + milestone_notifications?: boolean | undefined; + roles?: Nullable | undefined; +} + +interface UserResponse extends Metadata, Dates { + id?: string | undefined; + name?: string | undefined; + email?: string | undefined; + profile_image?: Nullable | undefined; + cover_image?: Nullable | undefined; + bio?: Nullable | undefined; + website?: Nullable | undefined; + location?: Nullable | undefined; + facebook?: Nullable | undefined; + twitter?: Nullable | undefined; + accessibility?: Nullable | undefined; + status?: string | undefined; + tour?: Nullable | undefined; + comment_notifications?: boolean | undefined; + free_member_signup_notification?: boolean | undefined; + paid_subscription_started_notification?: boolean | undefined; + paid_subscription_canceled_notification?: boolean | undefined; + mention_notifications?: boolean | undefined; + milestone_notifications?: boolean | undefined; + permissions?: any[] | undefined; + roles?: Nullable | undefined; + count?: { + posts?: number | undefined; + }; + url?: string | undefined; +} + +// ================= +// Post & Page +// ================= + +type PostStatus = "draft" | "published" | "scheduled" | "archived" | "deleted"; +type PostVisibility = "public" | "members" | "paid"; + +interface PostOrPageRequest + extends Partial, + Excerpt, + CodeInjection, + Metadata, + SocialMedia { + /* Title is required */ + title: string; + + // Identification + featured?: boolean | undefined; + + // Post or Page + html?: Nullable | undefined; + lexical?: object | string | undefined; + + // Image + feature_image?: Nullable | undefined; + feature_image_alt?: Nullable | undefined; + feature_image_caption?: Nullable | undefined; + + // Custom Template for posts and pages + custom_template?: Nullable | undefined; + + // Tags + tags?: (TagPostRequest | string)[] | undefined; + + // Authors + authors?: (AuthorPostRequest | string)[] | undefined; + + // Email + email?: Nullable | undefined; + email_only?: Nullable | undefined; + + // URLs + url?: string | undefined; + canonical_url?: Nullable | undefined; +} + +interface PostOrPageResponse + extends Identification, + Excerpt, + CodeInjection, + Metadata, + SocialMedia, + Dates { + // Identification + uuid?: string | undefined; + comment_id?: string | undefined; + featured?: boolean | undefined; + + // Post or Page + title?: string | undefined; + html?: Nullable | undefined; + lexical?: object | string | undefined; + + // Access + status?: PostStatus | undefined; + visibility?: PostVisibility | undefined; + + // Image + feature_image?: Nullable | undefined; + feature_image_alt?: Nullable | undefined; + feature_image_caption?: Nullable | undefined; + + // Dates + published_at?: Nullable | undefined; + + // Custom Template for posts and pages + custom_template?: Nullable | undefined; + + // Tags - Only shown when using Include param + tags?: TagResponse[] | undefined; + primary_tag?: Nullable | undefined; + + // Authors - Only shown when using Include Param + authors?: AuthorResponse[] | undefined; + primary_author?: Nullable | undefined; + + // Newsletter - Only shown when using Include param + newsletter?: Nullable | undefined; + + // Email + email?: Nullable | undefined; + email_only?: Nullable | undefined; + + // URLs + url?: string | undefined; + canonical_url?: Nullable | undefined; +} + +// ================= +// Config & Site +// ================= + +interface ConfigResponse { + title?: string | undefined; + description?: string | undefined; + logo?: string | undefined; + icon?: string | undefined; +} + +interface SiteResponse { + title?: string | undefined; + description?: string | undefined; + logo?: string | undefined; + url?: string | undefined; + version?: string | undefined; +} + +// ================= +// Query Params +// ================= + +type IncludeParam = string; + +type FormatParam = "html" | "plaintext"; + +type FilterParam = string; + +type LimitParam = number | string; + +type PageParam = number; + +type OrderParam = string; + +interface Params { + include?: ArrayOrValue | undefined; + formats?: ArrayOrValue | undefined; + filter?: ArrayOrValue | undefined; + limit?: ArrayOrValue | undefined; + page?: ArrayOrValue | undefined; + order?: ArrayOrValue | undefined; +} + +// ================= +// Functions +// ================= + +interface BrowseFunction { + (options?: Params): Promise; +} + +interface ReadFunction { + ( + data: + | { id: string; slug?: never; email?: any } + | { id?: any; slug: string; email?: any } + | { id?: any; slug?: any; email: string }, + options?: Params + ): Promise; +} + +interface SiteReadFunction { + (): Promise; +} + +interface AddFunction { + ( + data: { [key: string]: string } & (U extends undefined + ? { [key: string]: string } + : U) + ): Promise; +} +interface EditFunction { + ( + data: { id: string } & (U extends undefined + ? { [key: string]: string } + : U) + ): Promise; +} +interface DeleteFunction { + (data: { id: string } | { email: string }): Promise; +} + +interface UploadFunction { + (data: FormData | ({ file: any; [key: string]: string } & T)): Promise; +} + +interface ActivateFunction { + (name: string): Promise; +} + +interface BrowseResults extends Array { + meta: { pagination: Pagination }; +} + +// ================= +// Interfaces +// ================= + +type PostsOrPages = BrowseResults; + +type Members = BrowseResults; + +type Users = BrowseResults; + +type Tags = BrowseResults; + +type Newsletters = BrowseResults; + +// ================= +// SDK options +// ================= + +interface MakeRequestOptions { + url: string; + method: string; + data?: object; + params?: object; + headers?: object; +} + +interface GenerateTokenOptions { + /** + * API key to sign JWT with + */ + key: string; + /** + * Token audience + */ + audience: string; +} + +interface GhostAdminAPIOptions { + url: string; + /** + * Version of GhostAdminAPI + * - A version string in in 'v{major}.{minor}' format. + * - A boolean value identifying presence of Accept-Version header + * Should be in 'v{major}.{minor}' format. Default is 'v5.0' + * + * * Deprecated options: 'v1', 'v2', 'v3', 'v4', 'v5', 'canary' + */ + version: string; + /** + * Flag controlling if the 'User-Agent' header should be sent with a request + * @default true + */ + userAgent?: string | boolean; + key: string; + /** @deprecated since version v2 */ + host?: string | undefined; + /** @default "ghost" */ + ghostPath?: string | undefined; + makeRequest?: (options: MakeRequestOptions) => Promise; + generateToken?: (options: GenerateTokenOptions) => string | undefined; +} + +// ================= +// SDK +// ================= + +interface GhostAPI { + posts: { + /* Must include either data.id or data.slug or data.email */ + read: ReadFunction; + browse: BrowseFunction; + add: AddFunction; + edit: EditFunction; + delete: DeleteFunction; + }; + pages: { + /* Must include either data.id or data.slug or data.email */ + read: ReadFunction; + browse: BrowseFunction; + add: AddFunction; + edit: EditFunction; + delete: DeleteFunction; + }; + tags: { + /* Must include either data.id or data.slug or data.email */ + read: ReadFunction; + browse: BrowseFunction; + edit: EditFunction; + add: AddFunction; + delete: DeleteFunction; + }; + webhooks: { + edit: EditFunction; + add: AddFunction; + delete: DeleteFunction; + }; + members: { + /* Must include either data.id or data.slug or data.email */ + read: ReadFunction; + browse: BrowseFunction; + add: AddFunction; + edit: EditFunction; + delete: DeleteFunction; + }; + users: { + /* Must include either data.id or data.slug or data.email */ + read: ReadFunction; + browse: BrowseFunction; + edit: EditFunction; + delete: DeleteFunction; + // Adding a user is not available in Ghost Admin API Client + // See more: https://ghost.org/docs/admin-api/#endpoints + // add: AddFunction + }; + newsletters: { + /* Must include either data.id or data.slug or data.email */ + read: ReadFunction; + browse: BrowseFunction; + edit: EditFunction; + add: AddFunction; + delete: DeleteFunction; + }; + images: { + upload: UploadFunction; + }; + subscribers: {}; + media: { + upload: UploadFunction<{ purpose?: string; thumbnail?: string }>; + }; + files: { + upload: UploadFunction<{ ref?: string }>; + }; + config: { + /* Not documented in Ghost Admin API, but present on the admin-api library */ + read: ReadFunction; + }; + site: { + read: SiteReadFunction; + }; + themes: { + upload: UploadFunction; + activate: ActivateFunction; + }; +} + +declare var GhostAdminAPI: { + (options: GhostAdminAPIOptions): GhostAPI; + new (options: GhostAdminAPIOptions): GhostAPI; +}; + +export = GhostAdminAPI; diff --git a/packages/admin-api/package.json b/packages/admin-api/package.json index 2a9d37237..04dc47daf 100644 --- a/packages/admin-api/package.json +++ b/packages/admin-api/package.json @@ -9,16 +9,19 @@ "author": "Ghost Foundation", "license": "MIT", "main": "index.js", + "types": "index.d.ts", "files": [ "LICENSE", "README.md", "cjs/", "lib/", - "index.js" + "index.js", + "index.d.ts" ], "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura mocha './test/**/*.test.js'", + "test:types": "tsc -p test/types/tsconfig.json", "lint": "eslint . --ext .js --cache", "posttest": "yarn lint" }, @@ -29,7 +32,8 @@ "c8": "10.1.3", "mocha": "11.2.2", "should": "13.2.3", - "sinon": "21.0.0" + "sinon": "21.0.0", + "typescript": "5.5.4" }, "dependencies": { "axios": "^1.0.0", diff --git a/packages/admin-api/test/types/tsconfig.json b/packages/admin-api/test/types/tsconfig.json new file mode 100644 index 000000000..63cce714f --- /dev/null +++ b/packages/admin-api/test/types/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "node16", + "strict": true, + "noEmit": true, + "baseUrl": "../..", + "paths": { + "@tryghost/admin-api": ["index.d.ts"] + }, + "types": [] + }, + "files": ["types.test.ts"] +} diff --git a/packages/admin-api/test/types/types.test.ts b/packages/admin-api/test/types/types.test.ts new file mode 100644 index 000000000..8a3384f1e --- /dev/null +++ b/packages/admin-api/test/types/types.test.ts @@ -0,0 +1,71 @@ +import GhostAdminAPI = require("@tryghost/admin-api"); + +const assertType = (_value: T) => {}; + +// Accept arbitrary version strings +const api = new GhostAdminAPI({ url: "test", version: "v5.9999", key: "" }); + +type Ghost = ReturnType; + +// posts +const postsBrowsePromise = api.posts.browse(); +assertType>(postsBrowsePromise); + +postsBrowsePromise.then((posts) => { + const readPromise = api.posts.read(posts[0], { include: "authors" }); + assertType>(readPromise); +}); + +// tags +const tagsBrowsePromise = api.tags.browse(); +assertType>(tagsBrowsePromise); + +tagsBrowsePromise.then((tags) => { + const tagPromise = api.tags.read(tags[0]); + assertType>(tagPromise); + + tagPromise.then((tag) => { + if (tag.name) { + assertType(tag.name); + } + }); +}); + +// webhooks +const webhookAdd = api.webhooks.add({ + event: "post.added", + target_url: "https://example.com/webhook", +}); +assertType>(webhookAdd); + +const webhookEdit = api.webhooks.edit({ + ...{ event: "post.published" }, + id: "webhook-id", +}); +assertType>(webhookEdit); + +const webhookDelete = api.webhooks.delete({ id: "webhook-id" }); +assertType>(webhookDelete); + +// users +assertType>(api.users.browse()); +assertType>( + api.users.read({ id: "user-id" }) +); +assertType>( + api.users.edit({ id: "user-id", name: "Test User" }) +); +assertType>( + api.users.delete({ id: "user-id" }) +); + +// uploads +assertType>( + api.images.upload({ file: "test.jpg" }) +); +assertType>( + api.media.upload({ file: "test-2.png", purpose: "test" }) +); + +// site +assertType>(api.site.read()); diff --git a/yarn.lock b/yarn.lock index 0676f09b7..06f3d96e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12123,6 +12123,11 @@ typescript@5.2.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== +typescript@5.5.4: + version "5.5.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== + typescript@5.8.2: version "5.8.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4"