1259 lines
40 KiB
Plaintext
1259 lines
40 KiB
Plaintext
import { z } from 'zod';
|
|
import { SignJWT } from 'jose';
|
|
|
|
// src/schemas/authors.ts
|
|
var ghostIdentitySchema = z.object({
|
|
slug: z.string(),
|
|
id: z.string()
|
|
});
|
|
var ghostIdentityInputSchema = z.object({
|
|
slug: z.string().optional(),
|
|
id: z.string().optional(),
|
|
email: z.string().email().optional()
|
|
});
|
|
var ghostMetaSchema = z.object({
|
|
pagination: z.object({
|
|
pages: z.number(),
|
|
page: z.number(),
|
|
limit: z.union([z.number(), z.literal("all")]),
|
|
total: z.number(),
|
|
prev: z.number().nullable(),
|
|
next: z.number().nullable()
|
|
})
|
|
});
|
|
var ghostExcerptSchema = z.object({
|
|
excerpt: z.string().optional(),
|
|
custom_excerpt: z.string().optional()
|
|
});
|
|
var ghostCodeInjectionSchema = z.object({
|
|
codeinjection_head: z.string().nullable(),
|
|
codeinjection_foot: z.string().nullable()
|
|
});
|
|
var ghostFacebookSchema = z.object({
|
|
og_image: z.string().nullable(),
|
|
og_title: z.string().nullable(),
|
|
og_description: z.string().nullable()
|
|
});
|
|
var ghostTwitterSchema = z.object({
|
|
twitter_image: z.string().nullable(),
|
|
twitter_title: z.string().nullable(),
|
|
twitter_description: z.string().nullable()
|
|
});
|
|
var ghostSocialMediaSchema = z.object({
|
|
...ghostFacebookSchema.shape,
|
|
...ghostTwitterSchema.shape
|
|
});
|
|
var ghostMetadataSchema = z.object({
|
|
meta_title: z.string().nullable(),
|
|
meta_description: z.string().nullable()
|
|
});
|
|
var ghostVisibilitySchema = z.union([
|
|
z.literal("public"),
|
|
z.literal("members"),
|
|
z.literal("none"),
|
|
z.literal("internal"),
|
|
z.literal("paid"),
|
|
z.literal("tiers")
|
|
]);
|
|
var apiVersionsSchema = z.string().regex(/^v5\.\d+/).default("v5.0");
|
|
var contentAPICredentialsSchema = z.object({
|
|
key: z.string().regex(/[0-9a-f]{26}/, { message: "'key' must have 26 hex characters" }),
|
|
version: apiVersionsSchema,
|
|
url: z.string().url()
|
|
});
|
|
var adminAPICredentialsSchema = z.object({
|
|
key: z.string().regex(/[0-9a-f]{24}:[0-9a-f]{64}/, {
|
|
message: "'key' must have the following format {A}:{B}, where A is 24 hex characters and B is 64 hex characters"
|
|
}),
|
|
version: apiVersionsSchema,
|
|
url: z.string().url()
|
|
});
|
|
var slugOrIdSchema = z.union([z.object({ slug: z.string() }), z.object({ id: z.string() })]);
|
|
var emailOrIdSchema = z.union([
|
|
z.object({ email: z.string().email() }),
|
|
z.object({ id: z.string() })
|
|
]);
|
|
var identitySchema = z.union([
|
|
z.object({ email: z.string().email() }),
|
|
z.object({ id: z.string() }),
|
|
z.object({ slug: z.string() })
|
|
]);
|
|
|
|
// src/schemas/authors.ts
|
|
var baseAuthorsSchema = 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()
|
|
});
|
|
var baseTagsSchema = 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(),
|
|
created_at: z.string().nullish(),
|
|
updated_at: z.string().nullish(),
|
|
count: z.object({
|
|
posts: z.number()
|
|
}).optional()
|
|
});
|
|
|
|
// src/schemas/pages.ts
|
|
var postsAuthorSchema = baseAuthorsSchema.extend({
|
|
url: z.string().nullish()
|
|
});
|
|
var basePagesSchema = z.object({
|
|
...ghostIdentitySchema.shape,
|
|
...ghostMetadataSchema.shape,
|
|
title: z.string(),
|
|
html: z.string().nullish(),
|
|
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(baseTagsSchema).optional(),
|
|
primary_author: postsAuthorSchema.nullish(),
|
|
primary_tag: baseTagsSchema.nullish(),
|
|
url: z.string(),
|
|
excerpt: z.string().nullish(),
|
|
reading_time: z.number().optional().default(0),
|
|
created_at: z.string(),
|
|
updated_at: z.string().nullish(),
|
|
published_at: z.string().nullable(),
|
|
email_subject: z.string().nullish(),
|
|
is_page: z.boolean().default(true)
|
|
});
|
|
var postsAuthorSchema2 = baseAuthorsSchema.extend({
|
|
url: z.string().nullish()
|
|
});
|
|
var basePostsSchema = z.object({
|
|
...ghostIdentitySchema.shape,
|
|
...ghostMetadataSchema.shape,
|
|
title: z.string(),
|
|
html: z.string().nullish(),
|
|
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(postsAuthorSchema2).optional(),
|
|
tags: z.array(baseTagsSchema).optional(),
|
|
primary_author: postsAuthorSchema2.nullish(),
|
|
primary_tag: baseTagsSchema.nullish(),
|
|
url: z.string(),
|
|
excerpt: z.string().nullable(),
|
|
reading_time: z.number().optional().default(0),
|
|
created_at: z.string(),
|
|
updated_at: z.string().nullish(),
|
|
published_at: z.string().nullable(),
|
|
email_subject: z.string().nullish(),
|
|
is_page: z.boolean().default(false)
|
|
});
|
|
var baseSettingsSchema = 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()
|
|
});
|
|
var baseTiersSchema = 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()).optional(),
|
|
visibility: ghostVisibilitySchema,
|
|
currency: z.string().nullish(),
|
|
trial_days: z.number().default(0)
|
|
});
|
|
var baseEmailSchema = z.object({
|
|
id: z.string(),
|
|
uuid: z.string(),
|
|
status: z.string(),
|
|
recipient_filter: z.string(),
|
|
error: z.string().nullish(),
|
|
error_data: z.any().nullable(),
|
|
email_count: z.number(),
|
|
delivered_count: z.number(),
|
|
opened_count: z.number(),
|
|
failed_count: z.number(),
|
|
subject: z.string(),
|
|
from: z.string(),
|
|
reply_to: z.string().nullable(),
|
|
source: z.string(),
|
|
// lexical format
|
|
html: z.string().nullable(),
|
|
plaintext: z.string().nullable(),
|
|
track_opens: z.boolean(),
|
|
submitted_at: z.string(),
|
|
created_at: z.string(),
|
|
updated_at: z.string()
|
|
});
|
|
var baseOffersSchema = z.object({
|
|
id: z.string(),
|
|
name: z.string({ description: "Internal name for an offer, must be unique" }).default(""),
|
|
code: z.string({ description: "Shortcode for the offer, for example: https://yoursite.com/black-friday" }),
|
|
display_title: z.string({ description: "Name displayed in the offer window" }).nullish(),
|
|
display_description: z.string({ description: "Text displayed in the offer window" }).nullish(),
|
|
type: z.union([z.literal("percent"), z.literal("fixed"), z.literal("trial")]),
|
|
cadence: z.union([z.literal("month"), z.literal("year")]),
|
|
amount: z.number({
|
|
description: `Offer discount amount, as a percentage or fixed value as set in type.
|
|
Amount is always denoted by the smallest currency unit
|
|
(e.g., 100 cents instead of $1.00 in USD)`
|
|
}),
|
|
duration: z.union([z.literal("once"), z.literal("forever"), z.literal("repeating"), z.literal("trial")], {
|
|
description: "once/forever/repeating. repeating duration is only available when cadence is month"
|
|
}),
|
|
duration_in_months: z.number({ description: "Number of months offer should be repeated when duration is repeating" }).nullish(),
|
|
currency_restriction: z.boolean({
|
|
description: "Denotes whether the offer `currency` is restricted. If so, changing the currency invalidates the offer"
|
|
}).nullish(),
|
|
currency: z.string({
|
|
description: "fixed type offers only - specifies tier's currency as three letter ISO currency code"
|
|
}).nullish(),
|
|
status: z.union([z.literal("active"), z.literal("archived")], {
|
|
description: "active or archived - denotes if the offer is active or archived"
|
|
}),
|
|
redemption_count: z.number({ description: "Number of times the offer has been redeemed" }).nullish(),
|
|
tier: z.object(
|
|
{
|
|
id: z.string(),
|
|
name: z.string().nullish()
|
|
},
|
|
{ description: "Tier on which offer is applied" }
|
|
)
|
|
});
|
|
var baseNewsletterSchema = z.object({
|
|
...ghostIdentitySchema.shape,
|
|
name: z.string({ description: "Public name for the newsletter" }),
|
|
description: z.string({ description: "(nullable) Public description of the newsletter" }).nullish(),
|
|
sender_name: z.string({ description: "(nullable) The sender name of the emails" }).nullish(),
|
|
sender_email: z.string({ description: "(nullable) The email from which to send emails. Requires validation." }).nullish(),
|
|
sender_reply_to: z.string({
|
|
description: "The reply-to email address for sent emails. Can be either newsletter (= use sender_email) or support (use support email from Portal settings)."
|
|
}),
|
|
status: z.union([z.literal("active"), z.literal("archived")], {
|
|
description: "active or archived - denotes if the newsletter is active or archived"
|
|
}),
|
|
visibility: z.union([z.literal("public"), z.literal("members")]),
|
|
subscribe_on_signup: z.boolean({
|
|
description: "true/false. Whether members should automatically subscribe to this newsletter on signup"
|
|
}),
|
|
sort_order: z.number({ description: "The order in which newsletters are displayed in the Portal" }),
|
|
header_image: z.string({
|
|
description: "(nullable) Path to an image to show at the top of emails. Recommended size 1200x600"
|
|
}).nullish(),
|
|
show_header_icon: z.boolean({ description: "true/false. Show the site icon in emails" }),
|
|
show_header_title: z.boolean({ description: "true/false. Show the site name in emails" }),
|
|
title_font_category: z.union([z.literal("serif"), z.literal("sans_serif")], {
|
|
description: "Title font style. Either serif or sans_serif"
|
|
}),
|
|
title_alignment: z.string().nullish(),
|
|
show_feature_image: z.boolean({ description: "true/false. Show the post's feature image in emails" }),
|
|
body_font_category: z.union([z.literal("serif"), z.literal("sans_serif")], {
|
|
description: "Body font style. Either serif or sans_serif"
|
|
}),
|
|
footer_content: z.string({
|
|
description: "(nullable) Extra information or legal text to show in the footer of emails. Should contain valid HTML."
|
|
}).nullish(),
|
|
show_badge: z.boolean({
|
|
description: "true/false. Show you\u2019re a part of the indie publishing movement by adding a small Ghost badge in the footer"
|
|
}),
|
|
created_at: z.string(),
|
|
updated_at: z.string().nullish(),
|
|
show_header_name: z.boolean({ description: "true/false. Show the newsletter name in emails" }),
|
|
uuid: z.string()
|
|
});
|
|
var baseSubscriptionsSchema = z.object({
|
|
id: z.string({ description: "Stripe subscription ID sub_XXXX" }),
|
|
customer: z.object(
|
|
{
|
|
id: z.string(),
|
|
name: z.string().nullable(),
|
|
email: z.string()
|
|
},
|
|
{ description: "Stripe customer attached to the subscription" }
|
|
),
|
|
status: z.string({ description: "Subscription status" }),
|
|
start_date: z.string({ description: "Subscription start date" }),
|
|
default_payment_card_last4: z.string({ description: "Last 4 digits of the card" }).nullable(),
|
|
cancel_at_period_end: z.boolean({
|
|
description: "If the subscription should be canceled or renewed at period end"
|
|
}),
|
|
cancellation_reason: z.string({ description: "Reason for subscription cancellation" }).nullable(),
|
|
current_period_end: z.string({ description: "Subscription end date" }),
|
|
price: z.object({
|
|
id: z.string({ description: "Stripe price ID" }),
|
|
price_id: z.string({ description: "Ghost price ID" }),
|
|
nickname: z.string({ description: "Price nickname" }),
|
|
amount: z.number({ description: "Price amount" }),
|
|
interval: z.string({ description: "Price interval" }),
|
|
type: z.string({ description: "Price type" }),
|
|
currency: z.string({ description: "Price currency" })
|
|
}),
|
|
tier: baseTiersSchema.nullish(),
|
|
offer: baseOffersSchema.nullish()
|
|
});
|
|
|
|
// src/schemas/members.ts
|
|
var baseMembersSchema = z.object({
|
|
id: z.string(),
|
|
email: z.string({ description: "The email address of the member" }),
|
|
name: z.string({ description: "The name of the member" }).nullable(),
|
|
note: z.string({ description: "(nullable) A note about the member" }).nullish(),
|
|
geolocation: z.string({ description: "(nullable) The geolocation of the member" }).nullish(),
|
|
created_at: z.string({ description: "The date and time the member was created" }),
|
|
updated_at: z.string({ description: "(nullable) The date and time the member was last updated" }).nullish(),
|
|
labels: z.array(
|
|
z.object({
|
|
id: z.string({ description: "The ID of the label" }),
|
|
name: z.string({ description: "The name of the label" }),
|
|
slug: z.string({ description: "The slug of the label" }),
|
|
created_at: z.string({ description: "The date and time the label was created" }),
|
|
updated_at: z.string({ description: "(nullable) The date and time the label was last updated" }).nullish()
|
|
}),
|
|
{ description: "The labels associated with the member" }
|
|
),
|
|
subscriptions: z.array(baseSubscriptionsSchema, {
|
|
description: "The subscriptions associated with the member"
|
|
}),
|
|
avatar_image: z.string({ description: "The URL of the member's avatar image" }),
|
|
email_count: z.number({ description: "The number of emails sent to the member" }),
|
|
email_opened_count: z.number({ description: "The number of emails opened by the member" }),
|
|
email_open_rate: z.number({ description: "(nullable) The open rate of the member" }).nullish(),
|
|
status: z.string({ description: "The status of the member" }),
|
|
last_seen_at: z.string({ description: "(nullable) The date and time the member was last seen" }).nullish(),
|
|
newsletters: z.array(baseNewsletterSchema)
|
|
});
|
|
var baseSiteSchema = z.object({
|
|
title: z.string(),
|
|
description: z.string(),
|
|
logo: z.string().nullable(),
|
|
version: z.string(),
|
|
url: z.string()
|
|
});
|
|
|
|
// src/fetchers/formats.ts
|
|
var contentFormats = ["html", "mobiledoc", "plaintext", "lexical"];
|
|
|
|
// src/fetchers/browse-fetcher.ts
|
|
var BrowseFetcher = class _BrowseFetcher {
|
|
constructor(resource, config, _params = { browseParams: {}, include: [], fields: {} }, httpClient) {
|
|
this.resource = resource;
|
|
this.config = config;
|
|
this._params = _params;
|
|
this.httpClient = httpClient;
|
|
this._urlParams = {};
|
|
this._urlSearchParams = void 0;
|
|
this._includeFields = [];
|
|
this._buildUrlParams();
|
|
}
|
|
/**
|
|
* Lets you choose output format for the content of Post and Pages resources
|
|
* The choices are html, mobiledoc or plaintext. It will transform the output of the fetcher to a new shape
|
|
* with the selected formats required.
|
|
*
|
|
* @param formats html, mobiledoc or plaintext
|
|
* @returns A new Fetcher with the fixed output shape and the formats specified
|
|
*/
|
|
formats(formats) {
|
|
const params = {
|
|
...this._params,
|
|
formats: Object.keys(formats).filter((key) => contentFormats.includes(key))
|
|
};
|
|
return new _BrowseFetcher(
|
|
this.resource,
|
|
{
|
|
schema: this.config.schema,
|
|
output: this.config.output.required(formats),
|
|
include: this.config.include
|
|
},
|
|
params,
|
|
this.httpClient
|
|
);
|
|
}
|
|
/**
|
|
* Let's you include special keys into the Ghost API Query to retrieve complimentary info
|
|
* The available keys are defined by the Resource include schema, will not care about unknown keys.
|
|
* Returns a new Fetcher with an Output shape modified with the include keys required.
|
|
*
|
|
* @param include Include specific keys from the include shape
|
|
* @returns A new Fetcher with the fixed output shape and the formats specified
|
|
*/
|
|
include(include) {
|
|
const params = {
|
|
...this._params,
|
|
include: Object.keys(this.config.include.parse(include))
|
|
};
|
|
return new _BrowseFetcher(
|
|
this.resource,
|
|
{
|
|
schema: this.config.schema,
|
|
output: this.config.output.required(include),
|
|
include: this.config.include
|
|
},
|
|
params,
|
|
this.httpClient
|
|
);
|
|
}
|
|
/**
|
|
* Let's you strip the output to only the specified keys of your choice that are in the config Schema
|
|
* Will not care about unknown keys and return a new Fetcher with an Output shape with only the selected keys.
|
|
*
|
|
* @param fields Any keys from the resource Schema
|
|
* @returns A new Fetcher with the fixed output shape having only the selected Fields
|
|
*/
|
|
fields(fields) {
|
|
const newOutput = this.config.output.pick(fields);
|
|
return new _BrowseFetcher(
|
|
this.resource,
|
|
{
|
|
schema: this.config.schema,
|
|
output: newOutput,
|
|
include: this.config.include
|
|
},
|
|
this._params,
|
|
this.httpClient
|
|
);
|
|
}
|
|
getResource() {
|
|
return this.resource;
|
|
}
|
|
getParams() {
|
|
return this._params;
|
|
}
|
|
getOutputFields() {
|
|
return this.config.output.keyof().options;
|
|
}
|
|
getURLSearchParams() {
|
|
return this._urlSearchParams;
|
|
}
|
|
getIncludes() {
|
|
return this._params?.include || [];
|
|
}
|
|
getFormats() {
|
|
return this._params?.formats || [];
|
|
}
|
|
_buildUrlParams() {
|
|
const inputKeys = this.config.schema.keyof().options;
|
|
const outputKeys = this.config.output.keyof().options;
|
|
this._urlParams = {
|
|
...this._urlBrowseParams()
|
|
};
|
|
if (inputKeys.length !== outputKeys.length && outputKeys.length > 0) {
|
|
this._urlParams.fields = outputKeys.join(",");
|
|
}
|
|
if (this._params.include && this._params.include.length > 0) {
|
|
this._urlParams.include = this._params.include.join(",");
|
|
}
|
|
if (this._params.formats && this._params.formats.length > 0) {
|
|
this._urlParams.formats = this._params.formats.join(",");
|
|
}
|
|
this._urlSearchParams = new URLSearchParams();
|
|
for (const [key, value] of Object.entries(this._urlParams)) {
|
|
this._urlSearchParams.append(key, value);
|
|
}
|
|
}
|
|
_urlBrowseParams() {
|
|
let urlBrowseParams = {};
|
|
if (this._params.browseParams === void 0)
|
|
return {};
|
|
const { limit, page, ...params } = this._params.browseParams;
|
|
urlBrowseParams = {
|
|
...params
|
|
};
|
|
if (limit) {
|
|
urlBrowseParams.limit = limit.toString();
|
|
}
|
|
if (page) {
|
|
urlBrowseParams.page = page.toString();
|
|
}
|
|
return urlBrowseParams;
|
|
}
|
|
_getResultSchema() {
|
|
return z.discriminatedUnion("success", [
|
|
z.object({
|
|
success: z.literal(true),
|
|
meta: ghostMetaSchema,
|
|
data: z.array(this.config.output)
|
|
}),
|
|
z.object({
|
|
success: z.literal(false),
|
|
errors: z.array(
|
|
z.object({
|
|
type: z.string(),
|
|
message: z.string()
|
|
})
|
|
)
|
|
})
|
|
]);
|
|
}
|
|
async fetch(options) {
|
|
const resultSchema = this._getResultSchema();
|
|
const result = await this.httpClient.fetch({
|
|
resource: this.resource,
|
|
searchParams: this._urlSearchParams,
|
|
options
|
|
});
|
|
let data = {};
|
|
if (result.errors) {
|
|
data.success = false;
|
|
data.errors = result.errors;
|
|
} else {
|
|
data = {
|
|
success: true,
|
|
meta: result.meta || {
|
|
pagination: {
|
|
pages: 0,
|
|
page: 0,
|
|
limit: 15,
|
|
total: 0,
|
|
prev: null,
|
|
next: null
|
|
}
|
|
},
|
|
data: result[this.resource]
|
|
};
|
|
}
|
|
return resultSchema.parse(data);
|
|
}
|
|
async paginate(options) {
|
|
if (!this._params.browseParams?.page) {
|
|
this._params.browseParams = {
|
|
...this._params.browseParams,
|
|
page: 1
|
|
};
|
|
this._buildUrlParams();
|
|
}
|
|
const resultSchema = this._getResultSchema();
|
|
const result = await this.httpClient.fetch({
|
|
resource: this.resource,
|
|
searchParams: this._urlSearchParams,
|
|
options
|
|
});
|
|
let data = {};
|
|
if (result.errors) {
|
|
data.success = false;
|
|
data.errors = result.errors;
|
|
} else {
|
|
data = {
|
|
success: true,
|
|
meta: result.meta || {
|
|
pagination: {
|
|
pages: 0,
|
|
page: 0,
|
|
limit: 15,
|
|
total: 0,
|
|
prev: null,
|
|
next: null
|
|
}
|
|
},
|
|
data: result[this.resource]
|
|
};
|
|
}
|
|
const response = {
|
|
current: resultSchema.parse(data),
|
|
next: void 0
|
|
};
|
|
if (response.current.success === false)
|
|
return response;
|
|
const { meta } = response.current;
|
|
if (meta.pagination.pages <= 1 || meta.pagination.page === meta.pagination.pages)
|
|
return response;
|
|
const params = {
|
|
...this._params,
|
|
browseParams: {
|
|
...this._params.browseParams,
|
|
page: meta.pagination.page + 1
|
|
}
|
|
};
|
|
const next = new _BrowseFetcher(this.resource, this.config, params, this.httpClient);
|
|
response.next = next;
|
|
return response;
|
|
}
|
|
};
|
|
var ReadFetcher = class _ReadFetcher {
|
|
constructor(resource, config, _params, httpClient) {
|
|
this.resource = resource;
|
|
this.config = config;
|
|
this._params = _params;
|
|
this.httpClient = httpClient;
|
|
this._urlParams = {};
|
|
this._urlSearchParams = void 0;
|
|
this._pathnameIdentity = void 0;
|
|
this._includeFields = [];
|
|
this._buildUrlParams();
|
|
}
|
|
/**
|
|
* Lets you choose output format for the content of Post and Pages resources
|
|
* The choices are html, mobiledoc or plaintext. It will transform the output of the fetcher to a new shape
|
|
* with the selected formats required.
|
|
*
|
|
* @param formats html, mobiledoc or plaintext
|
|
* @returns A new Fetcher with the fixed output shape and the formats specified
|
|
*/
|
|
formats(formats) {
|
|
const params = {
|
|
...this._params,
|
|
formats: Object.keys(formats).filter((key) => contentFormats.includes(key))
|
|
};
|
|
return new _ReadFetcher(
|
|
this.resource,
|
|
{
|
|
schema: this.config.schema,
|
|
output: this.config.output.required(formats),
|
|
include: this.config.include
|
|
},
|
|
params,
|
|
this.httpClient
|
|
);
|
|
}
|
|
/**
|
|
* Let's you include special keys into the Ghost API Query to retrieve complimentary info
|
|
* The available keys are defined by the Resource include schema, will not care about unknown keys.
|
|
* Returns a new Fetcher with an Output shape modified with the include keys required.
|
|
*
|
|
* @param include Include specific keys from the include shape
|
|
* @returns A new Fetcher with the fixed output shape and the formats specified
|
|
*/
|
|
include(include) {
|
|
const params = {
|
|
...this._params,
|
|
include: Object.keys(this.config.include.parse(include))
|
|
};
|
|
return new _ReadFetcher(
|
|
this.resource,
|
|
{
|
|
schema: this.config.schema,
|
|
output: this.config.output.required(include),
|
|
include: this.config.include
|
|
},
|
|
params,
|
|
this.httpClient
|
|
);
|
|
}
|
|
/**
|
|
* Let's you strip the output to only the specified keys of your choice that are in the config Schema
|
|
* Will not care about unknown keys and return a new Fetcher with an Output shape with only the selected keys.
|
|
*
|
|
* @param fields Any keys from the resource Schema
|
|
* @returns A new Fetcher with the fixed output shape having only the selected Fields
|
|
*/
|
|
fields(fields) {
|
|
const newOutput = this.config.output.pick(fields);
|
|
return new _ReadFetcher(
|
|
this.resource,
|
|
{
|
|
schema: this.config.schema,
|
|
output: newOutput,
|
|
include: this.config.include
|
|
},
|
|
this._params,
|
|
this.httpClient
|
|
);
|
|
}
|
|
getResource() {
|
|
return this.resource;
|
|
}
|
|
getParams() {
|
|
return this._params;
|
|
}
|
|
getOutputFields() {
|
|
return this.config.output.keyof().options;
|
|
}
|
|
getIncludes() {
|
|
return this._params?.include || [];
|
|
}
|
|
getFormats() {
|
|
return this._params?.formats || [];
|
|
}
|
|
_buildUrlParams() {
|
|
const inputKeys = this.config.schema.keyof().options;
|
|
const outputKeys = this.config.output.keyof().options;
|
|
if (inputKeys.length !== outputKeys.length && outputKeys.length > 0) {
|
|
this._urlParams.fields = outputKeys.join(",");
|
|
}
|
|
if (this._params.include && this._params.include.length > 0) {
|
|
this._urlParams.include = this._params.include.join(",");
|
|
}
|
|
if (this._params.formats && this._params.formats.length > 0) {
|
|
this._urlParams.formats = this._params.formats.join(",");
|
|
}
|
|
if (this._params.identity.id) {
|
|
this._pathnameIdentity = `${this._params.identity.id}`;
|
|
} else if (this._params.identity.slug) {
|
|
this._pathnameIdentity = `slug/${this._params.identity.slug}`;
|
|
} else if (this._params.identity.email) {
|
|
this._pathnameIdentity = `email/${this._params.identity.email}`;
|
|
} else {
|
|
throw new Error("Identity is not defined");
|
|
}
|
|
this._urlSearchParams = new URLSearchParams();
|
|
for (const [key, value] of Object.entries(this._urlParams)) {
|
|
this._urlSearchParams.append(key, value);
|
|
}
|
|
}
|
|
async fetch(options) {
|
|
const res = z.discriminatedUnion("success", [
|
|
z.object({
|
|
success: z.literal(true),
|
|
data: this.config.output
|
|
}),
|
|
z.object({
|
|
success: z.literal(false),
|
|
errors: z.array(
|
|
z.object({
|
|
type: z.string(),
|
|
message: z.string()
|
|
})
|
|
)
|
|
})
|
|
]);
|
|
const result = await this.httpClient.fetch({
|
|
resource: this.resource,
|
|
pathnameIdentity: this._pathnameIdentity,
|
|
searchParams: this._urlSearchParams,
|
|
options
|
|
});
|
|
let data = {};
|
|
if (result.errors) {
|
|
data.success = false;
|
|
data.errors = result.errors;
|
|
} else {
|
|
data = {
|
|
success: true,
|
|
data: result[this.resource][0]
|
|
};
|
|
}
|
|
return res.parse(data);
|
|
}
|
|
};
|
|
var BasicFetcher = class {
|
|
constructor(resource, config, httpClient) {
|
|
this.resource = resource;
|
|
this.config = config;
|
|
this.httpClient = httpClient;
|
|
}
|
|
getResource() {
|
|
return this.resource;
|
|
}
|
|
async fetch(options) {
|
|
const res = z.discriminatedUnion("success", [
|
|
z.object({
|
|
success: z.literal(true),
|
|
data: this.config.output
|
|
}),
|
|
z.object({
|
|
success: z.literal(false),
|
|
errors: z.array(
|
|
z.object({
|
|
type: z.string(),
|
|
message: z.string()
|
|
})
|
|
)
|
|
})
|
|
]);
|
|
const result = await this.httpClient.fetch({ options, resource: this.resource });
|
|
let data = {};
|
|
if (result.errors) {
|
|
data.success = false;
|
|
data.errors = result.errors;
|
|
} else {
|
|
data = {
|
|
success: true,
|
|
data: result[this.resource]
|
|
};
|
|
}
|
|
return res.parse(data);
|
|
}
|
|
};
|
|
var MutationFetcher = class {
|
|
constructor(resource, config, _params, _options, httpClient) {
|
|
this.resource = resource;
|
|
this.config = config;
|
|
this._params = _params;
|
|
this._options = _options;
|
|
this.httpClient = httpClient;
|
|
this._urlParams = {};
|
|
this._urlSearchParams = void 0;
|
|
this._pathnameIdentity = void 0;
|
|
this._buildUrlParams();
|
|
}
|
|
getResource() {
|
|
return this.resource;
|
|
}
|
|
getParams() {
|
|
return this._params;
|
|
}
|
|
_buildUrlParams() {
|
|
if (this._params) {
|
|
for (const [key, value] of Object.entries(this._params)) {
|
|
if (key !== "id") {
|
|
this._urlParams[key] = value;
|
|
}
|
|
}
|
|
}
|
|
this._urlSearchParams = new URLSearchParams();
|
|
if (this._params?.id) {
|
|
this._pathnameIdentity = `${this._params.id}`;
|
|
}
|
|
for (const [key, value] of Object.entries(this._urlParams)) {
|
|
this._urlSearchParams.append(key, value);
|
|
}
|
|
}
|
|
async submit() {
|
|
const schema = z.discriminatedUnion("success", [
|
|
z.object({
|
|
success: z.literal(true),
|
|
data: this.config.output
|
|
}),
|
|
z.object({
|
|
success: z.literal(false),
|
|
errors: z.array(
|
|
z.object({
|
|
type: z.string(),
|
|
message: z.string(),
|
|
context: z.string().nullish()
|
|
})
|
|
)
|
|
})
|
|
]);
|
|
const createData = {
|
|
[this.resource]: [this._options.body]
|
|
};
|
|
const response = await this.httpClient.fetch({
|
|
resource: this.resource,
|
|
searchParams: this._urlSearchParams,
|
|
pathnameIdentity: this._pathnameIdentity,
|
|
options: {
|
|
method: this._options.method,
|
|
body: JSON.stringify(createData)
|
|
}
|
|
});
|
|
let result = {};
|
|
if (response.errors) {
|
|
result.success = false;
|
|
result.errors = response.errors;
|
|
} else {
|
|
result = {
|
|
success: true,
|
|
data: response[this.resource][0]
|
|
};
|
|
}
|
|
return schema.parse(result);
|
|
}
|
|
};
|
|
var DeleteFetcher = class {
|
|
constructor(resource, _params, httpClient) {
|
|
this.resource = resource;
|
|
this._params = _params;
|
|
this.httpClient = httpClient;
|
|
this._pathnameIdentity = void 0;
|
|
this._buildPathnameIdentity();
|
|
}
|
|
getResource() {
|
|
return this.resource;
|
|
}
|
|
getParams() {
|
|
return this._params;
|
|
}
|
|
_buildPathnameIdentity() {
|
|
if (!this._params.id) {
|
|
throw new Error("Missing id in params");
|
|
}
|
|
this._pathnameIdentity = this._params.id;
|
|
}
|
|
async submit() {
|
|
const schema = z.discriminatedUnion("success", [
|
|
z.object({
|
|
success: z.literal(true)
|
|
}),
|
|
z.object({
|
|
success: z.literal(false),
|
|
errors: z.array(
|
|
z.object({
|
|
type: z.string(),
|
|
message: z.string(),
|
|
context: z.string().nullish()
|
|
})
|
|
)
|
|
})
|
|
]);
|
|
let result = {};
|
|
try {
|
|
const response = await this.httpClient.fetchRawResponse({
|
|
resource: this.resource,
|
|
pathnameIdentity: this._pathnameIdentity,
|
|
options: {
|
|
method: "DELETE"
|
|
}
|
|
});
|
|
if (response.status === 204) {
|
|
result = {
|
|
success: true
|
|
};
|
|
} else {
|
|
const res = await response.json();
|
|
if (res.errors) {
|
|
result.success = false;
|
|
result.errors = res.errors;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
result = {
|
|
success: false,
|
|
errors: [
|
|
{
|
|
type: "FetchError",
|
|
message: e.toString()
|
|
}
|
|
]
|
|
};
|
|
}
|
|
return schema.parse(result);
|
|
}
|
|
};
|
|
var browseParamsSchema = z.object({
|
|
order: z.string().optional(),
|
|
limit: z.union([
|
|
z.literal("all"),
|
|
z.number().refine((n) => n && n > 0 && n <= 15, {
|
|
message: "Limit must be between 1 and 15"
|
|
})
|
|
]).optional(),
|
|
page: z.number().refine((n) => n && n >= 1, {
|
|
message: "Page must be greater than 1"
|
|
}).optional(),
|
|
filter: z.string().optional()
|
|
});
|
|
var parseBrowseParams = (args, schema, includeSchema) => {
|
|
const keys = [
|
|
...schema.keyof().options,
|
|
...includeSchema && includeSchema.keyof().options || []
|
|
];
|
|
const augmentedSchema = browseParamsSchema.merge(
|
|
z.object({
|
|
order: z.string().superRefine((val, ctx) => {
|
|
const orderPredicates = val.split(",");
|
|
for (const orderPredicate of orderPredicates) {
|
|
const [field, direction] = orderPredicate.split(" ");
|
|
if (!keys.includes(field)) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: `Field "${field}" is not a valid field`,
|
|
fatal: true
|
|
});
|
|
}
|
|
if (direction && !(direction.toUpperCase() === "ASC" || direction.toUpperCase() === "DESC")) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "Order direction must be ASC or DESC",
|
|
fatal: true
|
|
});
|
|
}
|
|
}
|
|
}).optional(),
|
|
filter: z.string().superRefine((val, ctx) => {
|
|
const filterPredicates = val.replace(/ *\[[^)]*\] */g, "").split(/[+(,]+/);
|
|
for (const filterPredicate of filterPredicates) {
|
|
const field = filterPredicate.split(":")[0].split(".")[0];
|
|
if (!keys.includes(field)) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: `Field "${field}" is not a valid field`,
|
|
fatal: true
|
|
});
|
|
}
|
|
}
|
|
}).optional()
|
|
})
|
|
);
|
|
return augmentedSchema.parse(args);
|
|
};
|
|
|
|
// src/api-composer.ts
|
|
function isZodObject(schema) {
|
|
return schema.partial !== void 0;
|
|
}
|
|
var APIComposer = class {
|
|
constructor(resource, config, httpClient) {
|
|
this.resource = resource;
|
|
this.config = config;
|
|
this.httpClient = httpClient;
|
|
}
|
|
/**
|
|
* Browse function that accepts browse params order, filter, page and limit. Will return an instance
|
|
* of BrowseFetcher class.
|
|
*/
|
|
browse(options) {
|
|
return new BrowseFetcher(
|
|
this.resource,
|
|
{
|
|
schema: this.config.schema,
|
|
output: this.config.schema,
|
|
include: this.config.include
|
|
},
|
|
{
|
|
browseParams: options && parseBrowseParams(options, this.config.schema, this.config.include) || void 0
|
|
},
|
|
this.httpClient
|
|
);
|
|
}
|
|
/**
|
|
* Read function that accepts Identify fields like id, slug or email. Will return an instance
|
|
* of ReadFetcher class.
|
|
*/
|
|
read(options) {
|
|
return new ReadFetcher(
|
|
this.resource,
|
|
{
|
|
schema: this.config.schema,
|
|
output: this.config.schema,
|
|
include: this.config.include
|
|
},
|
|
{
|
|
identity: this.config.identitySchema.parse(options)
|
|
},
|
|
this.httpClient
|
|
);
|
|
}
|
|
async add(data, options) {
|
|
if (!this.config.createSchema) {
|
|
throw new Error("No createSchema defined");
|
|
}
|
|
const parsedData = this.config.createSchema.parse(data);
|
|
const parsedOptions = this.config.createOptionsSchema && options ? this.config.createOptionsSchema.parse(options) : void 0;
|
|
const fetcher = new MutationFetcher(
|
|
this.resource,
|
|
{
|
|
output: this.config.schema,
|
|
paramsShape: this.config.createOptionsSchema
|
|
},
|
|
parsedOptions,
|
|
{ method: "POST", body: parsedData },
|
|
this.httpClient
|
|
);
|
|
return fetcher.submit();
|
|
}
|
|
async edit(id, data, options) {
|
|
let updateSchema = this.config.updateSchema;
|
|
if (!this.config.updateSchema && this.config.createSchema && isZodObject(this.config.createSchema)) {
|
|
updateSchema = this.config.createSchema.partial();
|
|
}
|
|
if (!updateSchema) {
|
|
throw new Error("No updateSchema defined");
|
|
}
|
|
const cleanId = z.string().nonempty().parse(id);
|
|
const parsedData = updateSchema.parse(data);
|
|
const parsedOptions = this.config.updateOptionsSchema && options ? this.config.updateOptionsSchema.parse(options) : {};
|
|
if (Object.keys(parsedData).length === 0) {
|
|
throw new Error("No data to edit");
|
|
}
|
|
const fetcher = new MutationFetcher(
|
|
this.resource,
|
|
{
|
|
output: this.config.schema,
|
|
paramsShape: this.config.updateOptionsSchema
|
|
},
|
|
{ id: cleanId, ...parsedOptions },
|
|
{ method: "PUT", body: parsedData },
|
|
this.httpClient
|
|
);
|
|
return fetcher.submit();
|
|
}
|
|
async delete(id) {
|
|
const cleanId = z.string().nonempty().parse(id);
|
|
const fetcher = new DeleteFetcher(this.resource, { id: cleanId }, this.httpClient);
|
|
return fetcher.submit();
|
|
}
|
|
access(keys) {
|
|
const d = {};
|
|
keys.forEach((key) => {
|
|
d[key] = this[key].bind(this);
|
|
});
|
|
return d;
|
|
}
|
|
};
|
|
|
|
// src/helpers/fields.ts
|
|
var schemaWithPickedFields = (schema, fields) => {
|
|
return schema.pick(fields || {});
|
|
};
|
|
var HTTPClient = class {
|
|
constructor(config) {
|
|
this.config = config;
|
|
this._baseURL = void 0;
|
|
let prefixPath = new URL(config.url).pathname;
|
|
if (prefixPath.slice(-1) === "/") {
|
|
prefixPath = prefixPath.slice(0, -1);
|
|
}
|
|
this._baseURL = new URL(`${prefixPath}/ghost/api/${config.endpoint}/`, config.url);
|
|
}
|
|
get baseURL() {
|
|
return this._baseURL;
|
|
}
|
|
get jwt() {
|
|
return this._jwt;
|
|
}
|
|
async generateJWT(key) {
|
|
const [id, _secret] = key.split(":");
|
|
this._jwtExpiresAt = Date.now() + 5 * 60 * 1e3;
|
|
return new SignJWT({}).setProtectedHeader({ kid: id, alg: "HS256" }).setExpirationTime("5m").setIssuedAt().setAudience("/admin/").sign(
|
|
Uint8Array.from(_secret.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)))
|
|
);
|
|
}
|
|
async genHeaders() {
|
|
const headers = {
|
|
"Content-Type": "application/json",
|
|
"Accept-Version": this.config.version
|
|
};
|
|
if (this.config.endpoint === "admin") {
|
|
if (this._jwt === void 0 || this._jwtExpiresAt === void 0 || this._jwtExpiresAt < Date.now()) {
|
|
this._jwt = await this.generateJWT(this.config.key);
|
|
}
|
|
headers["Authorization"] = `Ghost ${this.jwt}`;
|
|
}
|
|
return headers;
|
|
}
|
|
async fetch({
|
|
resource,
|
|
searchParams,
|
|
options,
|
|
pathnameIdentity
|
|
}) {
|
|
if (this._baseURL === void 0)
|
|
throw new Error("URL is undefined");
|
|
let path = `${resource}/`;
|
|
if (pathnameIdentity !== void 0) {
|
|
path += `${pathnameIdentity}/`;
|
|
}
|
|
const url = new URL(path, this._baseURL);
|
|
if (searchParams !== void 0) {
|
|
for (const [key, value] of searchParams.entries()) {
|
|
url.searchParams.append(key, value);
|
|
}
|
|
}
|
|
if (this.config.endpoint === "content") {
|
|
url.searchParams.append("key", this.config.key);
|
|
}
|
|
let result = void 0;
|
|
const headers = await this.genHeaders();
|
|
try {
|
|
result = await (await fetch(url.toString(), {
|
|
...options,
|
|
headers
|
|
})).json();
|
|
} catch (e) {
|
|
return {
|
|
status: "error",
|
|
errors: [
|
|
{
|
|
type: "FetchError",
|
|
message: e.toString()
|
|
}
|
|
]
|
|
};
|
|
}
|
|
return result;
|
|
}
|
|
async fetchRawResponse({
|
|
resource,
|
|
searchParams,
|
|
options,
|
|
pathnameIdentity
|
|
}) {
|
|
if (this._baseURL === void 0)
|
|
throw new Error("URL is undefined");
|
|
this._baseURL.pathname += `${resource}/`;
|
|
if (pathnameIdentity !== void 0) {
|
|
this._baseURL.pathname += `${pathnameIdentity}/`;
|
|
}
|
|
if (searchParams !== void 0) {
|
|
for (const [key, value] of searchParams.entries()) {
|
|
this._baseURL.searchParams.append(key, value);
|
|
}
|
|
}
|
|
if (this.config.endpoint === "content") {
|
|
this._baseURL.searchParams.append("key", this.config.key);
|
|
}
|
|
const headers = await this.genHeaders();
|
|
return await fetch(this._baseURL.toString(), {
|
|
...options,
|
|
headers
|
|
});
|
|
}
|
|
};
|
|
|
|
export { APIComposer, BasicFetcher, BrowseFetcher, DeleteFetcher, HTTPClient, MutationFetcher, ReadFetcher, adminAPICredentialsSchema, apiVersionsSchema, baseAuthorsSchema, baseEmailSchema, baseMembersSchema, baseNewsletterSchema, baseOffersSchema, basePagesSchema, basePostsSchema, baseSettingsSchema, baseSiteSchema, baseTagsSchema, baseTiersSchema, browseParamsSchema, contentAPICredentialsSchema, emailOrIdSchema, ghostCodeInjectionSchema, ghostExcerptSchema, ghostFacebookSchema, ghostIdentityInputSchema, ghostIdentitySchema, ghostMetaSchema, ghostMetadataSchema, ghostSocialMediaSchema, ghostTwitterSchema, ghostVisibilitySchema, identitySchema, parseBrowseParams, schemaWithPickedFields, slugOrIdSchema };
|
|
//# sourceMappingURL=out.js.map
|
|
//# sourceMappingURL=index.mjs.map |