first commit
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled

This commit is contained in:
2026-04-22 19:51:20 +07:00
commit 93d1b7c3d3
579 changed files with 99797 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
# Ghost Data Factory
A minimal test data factory for Ghost e2e tests, written in TypeScript.
## Project Structure
```
e2e/data-factory/ # Source files (TypeScript) - committed to git
├── factory.ts # Base factory class
├── factories/ # Factory implementations
│ ├── post-factory.ts
│ ├── tag-factory.ts
│ └── user-factory.ts
├── persistence/
│ ├── adapter.ts # Persistence interface
│ └── adapters/ # Adapter implementations (API, Knex, etc)
├── setup.ts # Setup helper functions
├── index.ts # Main exports
└── utils.ts # Utility functions
```
## Setup
This is part of the Ghost e2e test suite. All dependencies are managed by the main Ghost monorepo.
1. **Start Ghost development server** (provides database):
```bash
pnpm dev
```
2. **Configure database connection** (optional - uses Ghost's database by default):
```bash
cp e2e/data-factory/.env.example e2e/data-factory/.env
# Edit .env if using different database credentials
```
3. **Build the e2e package** (includes data-factory):
```bash
cd e2e && pnpm build
```
## Usage
### In Tests
**Option 1: Use setup helpers (recommended)**
```typescript
import {createPostFactory, PostFactory} from '../data-factory';
// Create factory with API persistence
const postFactory: PostFactory = createPostFactory(page.request);
// Build in-memory only (not persisted)
const draftPost = postFactory.build({
title: 'My Draft',
status: 'draft'
});
// Create and persist to database
const publishedPost = await postFactory.create({
title: 'My Published Post',
status: 'published'
});
```
**Option 2: Manual setup**
```typescript
import {PostFactory} from '../data-factory/factories/post-factory';
import {GhostAdminApiAdapter} from '../data-factory/persistence/adapters/ghost-api';
const adapter = new GhostAdminApiAdapter(page.request, 'posts');
const postFactory = new PostFactory(adapter);
// Now you can build or create
const post = await postFactory.create({
title: 'My Published Post',
status: 'published'
});
```
## Development
### Adding New Factories
1. Create a new factory class extending `Factory<TOptions, TResult>`
2. Implement the `build()` method (returns in-memory object)
3. Set `entityType` property (used for persistence)
4. Create a setup helper in `setup.ts` for convenient usage in tests
Example:
```typescript
import {Factory} from '../factory';
export class MemberFactory extends Factory<Partial<Member>, Member> {
entityType = 'members';
build(options: Partial<Member> = {}): Member {
return {
id: generateId(),
email: options.email || faker.internet.email(),
name: options.name || faker.person.fullName(),
// ... more fields
};
}
}
```
Then create a setup helper:
```typescript
// In setup.ts
export function createMemberFactory(httpClient: HttpClient): MemberFactory {
const adapter = new GhostAdminApiAdapter(httpClient, 'members');
return new MemberFactory(adapter);
}
```
@@ -0,0 +1,60 @@
import {Factory} from '@/data-factory';
import {generateId} from '@/data-factory';
export interface AutomatedEmail {
id: string;
status: 'active' | 'inactive';
name: string;
slug: string;
subject: string;
lexical: string;
sender_name: string | null;
sender_email: string | null;
sender_reply_to: string | null;
created_at: Date;
updated_at: Date | null;
}
export class AutomatedEmailFactory extends Factory<Partial<AutomatedEmail>, AutomatedEmail> {
entityType = 'automated_emails';
build(options: Partial<AutomatedEmail> = {}): AutomatedEmail {
const now = new Date();
const defaults: AutomatedEmail = {
id: generateId(),
status: 'active',
name: 'Welcome Email (Free)',
slug: 'member-welcome-email-free',
subject: 'Welcome to {site_title}!',
lexical: JSON.stringify(this.defaultLexicalContent()),
sender_name: null,
sender_email: null,
sender_reply_to: null,
created_at: now,
updated_at: null
};
return {...defaults, ...options} as AutomatedEmail;
}
private defaultLexicalContent() {
return {
root: {
children: [{
type: 'paragraph',
children: [{
type: 'text',
text: 'Welcome to {site_title}!'
}]
}],
direction: null,
format: '',
indent: 0,
type: 'root',
version: 1
}
};
}
}
@@ -0,0 +1,32 @@
import {Factory} from '@/data-factory';
import {faker} from '@faker-js/faker';
import {generateId} from '@/data-factory';
export interface Comment {
id: string;
post_id: string;
member_id: string;
parent_id?: string;
in_reply_to_id?: string;
status: 'published' | 'hidden' | 'deleted';
html: string;
created_at?: string;
edited_at?: string;
}
export class CommentFactory extends Factory<Partial<Comment>, Comment> {
entityType = 'comments';
build(options: Partial<Comment> = {}): Comment {
const content = options.html || `<p>${faker.lorem.sentence()}</p>`;
return {
id: generateId(),
post_id: options.post_id || '',
member_id: options.member_id || '',
status: 'published',
html: content,
...options
};
}
}
+108
View File
@@ -0,0 +1,108 @@
import {faker} from '@faker-js/faker';
interface LexicalTextNode {
detail: number;
format: number;
mode: string;
style: string;
text: string;
type: 'text';
version: number;
}
interface LexicalParagraphNode {
children: LexicalTextNode[];
direction: string;
format: string;
indent: number;
type: 'paragraph';
version: number;
}
interface CardNode {
type: string;
[key: string]: unknown;
}
const CARD_DEFAULTS: Record<string, CardNode> = {
transistor: {
type: 'transistor',
version: 1,
accentColor: '#15171A',
backgroundColor: '#FFFFFF',
visibility: {
web: {
nonMember: false,
memberSegment: 'status:free,status:-free'
},
email: {
memberSegment: 'status:free,status:-free'
}
}
}
};
export type CardSpec = string | {[cardType: string]: Record<string, unknown>};
function resolveCard(spec: CardSpec): CardNode {
if (typeof spec === 'string') {
const defaults = CARD_DEFAULTS[spec];
if (!defaults) {
throw new Error(`Unknown card type: "${spec}". Register it in CARD_DEFAULTS in lexical.ts.`);
}
return {...defaults};
}
const [cardType, overrides] = Object.entries(spec)[0];
const defaults = CARD_DEFAULTS[cardType];
if (!defaults) {
throw new Error(`Unknown card type: "${cardType}". Register it in CARD_DEFAULTS in lexical.ts.`);
}
return {...defaults, ...overrides};
}
function buildParagraphNode(text: string): LexicalParagraphNode {
return {
children: [{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text,
type: 'text',
version: 1
}],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1
};
}
export function buildLexical(...cards: CardSpec[]): string {
let children: (LexicalParagraphNode | CardNode)[];
if (cards.length === 0) {
children = [buildParagraphNode(faker.lorem.paragraphs(3))];
} else {
children = [];
for (const spec of cards) {
const card = resolveCard(spec);
children.push(buildParagraphNode(`Before ${card.type}`));
children.push(card);
children.push(buildParagraphNode(`After ${card.type}`));
}
}
return JSON.stringify({
root: {
children,
direction: 'ltr',
format: '',
indent: 0,
type: 'root',
version: 1
}
});
}
@@ -0,0 +1,68 @@
import {Factory} from '@/data-factory';
import {faker} from '@faker-js/faker';
import {generateId, generateUuid} from '@/data-factory';
export interface Tier {
id: string;
name: string;
slug: string;
type: 'free' | 'paid';
active: boolean;
}
export interface Member {
id: string;
uuid: string;
name: string | null;
email: string;
note?: string | null;
geolocation: string | null;
labels?: string[];
email_count: number;
email_opened_count: number;
email_open_rate: number | null;
status: 'free' | 'paid' | 'comped';
last_seen_at: Date | null;
last_commented_at: Date | null;
newsletters: string[];
tiers?: Partial<Tier>[];
created_at?: string; // ISO 8601 format for backdating
complimentary_plan?: boolean;
stripe_customer_id?: string;
subscribed_to_emails?: string;
}
export class MemberFactory extends Factory<Partial<Member>, Member> {
entityType = 'members';
build(options: Partial<Member> = {}): Member {
return {
...this.buildDefaultMember(),
...options
};
}
private buildDefaultMember(): Member {
const firstName = faker.person.firstName();
const lastName = faker.person.lastName();
const name = `${firstName} ${lastName}`;
return {
id: generateId(),
uuid: generateUuid(),
name: name,
email: faker.internet.email({firstName, lastName}).toLowerCase(),
note: faker.lorem.sentence(),
geolocation: null,
labels: [],
email_count: 0,
email_opened_count: 0,
email_open_rate: null,
status: 'free',
last_seen_at: null,
last_commented_at: null,
newsletters: [],
subscribed_to_emails: 'false'
};
}
}
+169
View File
@@ -0,0 +1,169 @@
import {Factory, generateId, generateSlug} from '@/data-factory';
import {faker} from '@faker-js/faker';
import type {HttpClient, PersistenceAdapter} from '@/data-factory';
export interface AdminOffer {
id: string;
name: string;
code: string;
cadence: 'month' | 'year';
redemption_type?: 'signup' | 'retention';
status: 'active' | 'archived';
display_title: string | null;
display_description: string | null;
type: 'fixed' | 'percent' | 'trial';
amount: number;
duration: 'once' | 'repeating' | 'forever' | 'trial';
duration_in_months: number | null;
currency: string | null;
stripe_coupon_id?: string | null;
tier: {
id: string;
} | null;
}
export interface OfferCreateInput {
[key: string]: unknown;
name: string;
code: string;
cadence: 'month' | 'year';
amount: number;
duration: 'once' | 'repeating' | 'forever' | 'trial';
type: 'fixed' | 'percent' | 'trial';
tierId?: string | null;
currency?: string | null;
display_title?: string | null;
display_description?: string | null;
duration_in_months?: number | null;
redemption_type?: 'signup' | 'retention';
status?: 'active' | 'archived';
}
export interface OfferUpdateInput {
status?: 'active' | 'archived';
}
export class OfferFactory extends Factory<OfferCreateInput, AdminOffer> {
entityType = 'offers';
private readonly request?: HttpClient;
constructor(adapter?: PersistenceAdapter, request?: HttpClient) {
super(adapter);
this.request = request;
}
build(options: Partial<OfferCreateInput> = {}): AdminOffer {
const name = options.name ?? `Offer ${faker.commerce.productName()}`;
const code = options.code ?? `${generateSlug(name)}-${faker.string.alphanumeric(6).toLowerCase()}`;
const redemptionType = options.redemption_type ?? (options.tierId ? 'signup' : 'retention');
return {
id: generateId(),
name,
code,
cadence: options.cadence ?? 'month',
redemption_type: redemptionType,
status: options.status ?? 'active',
display_title: options.display_title ?? name,
display_description: options.display_description ?? null,
type: options.type ?? 'percent',
amount: options.amount ?? 10,
duration: options.duration ?? 'once',
duration_in_months: options.duration_in_months ?? null,
currency: options.currency ?? null,
stripe_coupon_id: null,
tier: options.tierId ? {id: options.tierId} : null
};
}
async create(options: Partial<OfferCreateInput> = {}): Promise<AdminOffer> {
if (!this.request) {
throw new Error('Cannot create without an HTTP client. Use createOfferFactory() for persisted test data access.');
}
const offer = this.build(options);
const response = await this.request.post('/ghost/api/admin/offers', {
data: {
offers: [{
name: offer.name,
code: offer.code,
cadence: offer.cadence,
status: offer.status,
redemption_type: offer.redemption_type ?? 'signup',
currency: offer.currency,
type: offer.type,
amount: offer.amount,
duration: offer.duration,
duration_in_months: offer.duration_in_months,
display_title: offer.display_title,
display_description: offer.display_description,
tier: offer.tier
}]
}
});
return await this.extractFirstOfferOrThrow('create offer', response.status(), response);
}
async update(id: string, input: OfferUpdateInput): Promise<AdminOffer> {
if (!this.request) {
throw new Error('Cannot update without an HTTP client. Use createOfferFactory() for persisted test data access.');
}
const response = await this.request.put(`/ghost/api/admin/offers/${id}`, {
data: {
offers: [input]
}
});
return await this.extractFirstOfferOrThrow('update offer', response.status(), response);
}
async getOffers(): Promise<AdminOffer[]> {
if (!this.request) {
throw new Error('Cannot fetch offers without an HTTP client. Use createOfferFactory() for persisted test data access.');
}
const response = await this.request.get('/ghost/api/admin/offers');
if (!response.ok()) {
throw new Error(`Failed to fetch offers: ${response.status()}`);
}
const data = await response.json() as {offers: AdminOffer[]};
return data.offers;
}
async getById(id: string): Promise<AdminOffer> {
if (!this.request) {
throw new Error('Cannot fetch an offer without an HTTP client. Use createOfferFactory() for persisted test data access.');
}
const response = await this.request.get(`/ghost/api/admin/offers/${id}`);
return await this.extractFirstOfferOrThrow('fetch offer', response.status(), response);
}
private async extractFirstOfferOrThrow(action: string, status: number, response: {ok(): boolean; json(): Promise<unknown>}): Promise<AdminOffer> {
if (!response.ok()) {
throw new Error(`Failed to ${action}: ${status}`);
}
const data = await response.json() as {offers?: AdminOffer[]};
const offers = data.offers;
if (!Array.isArray(offers) || offers.length === 0) {
let responseBody = '[unserializable]';
try {
responseBody = JSON.stringify(data);
} catch {
// Ignore serialization errors and keep fallback marker.
}
throw new Error(
`Failed to ${action}: expected response.offers to be a non-empty array (status ${status}). Response: ${responseBody}`
);
}
return offers[0];
}
}
@@ -0,0 +1,89 @@
import {Factory} from '@/data-factory';
import {buildLexical} from './lexical';
import {faker} from '@faker-js/faker';
import {generateId, generateSlug, generateUuid} from '@/data-factory';
import type {CardSpec} from './lexical';
export interface Post {
id: string;
uuid: string;
title: string;
slug: string;
mobiledoc: string | null;
lexical: string | null;
html: string;
comment_id: string;
plaintext: string;
feature_image: string | null;
featured: boolean;
type: string;
status: 'draft' | 'published' | 'scheduled';
locale: string | null;
visibility: string;
email_recipient_filter: string;
created_at: Date;
updated_at: Date;
published_at: Date | null;
custom_excerpt: string;
codeinjection_head: string | null;
codeinjection_foot: string | null;
custom_template: string | null;
canonical_url: string | null;
newsletter_id: string | null;
show_title_and_feature_image: boolean;
tags?: Array<{id: string}>;
tiers?: Array<{id: string}>;
}
export class PostFactory extends Factory<Partial<Post>, Post> {
entityType = 'posts'; // Entity name (for adapter; currently API endpoint)
build(options: Partial<Post> = {}): Post {
const now = new Date();
const title = options.title || faker.lorem.sentence();
const content = faker.lorem.paragraphs(3);
const defaults: Post = {
id: generateId(),
uuid: generateUuid(),
title: title,
slug: options.slug || generateSlug(title) + '-' + Date.now().toString(16),
mobiledoc: null,
lexical: buildLexical(),
html: `<p>${content}</p>`,
comment_id: generateId(),
plaintext: content,
feature_image: null,
featured: faker.datatype.boolean(),
type: 'post',
status: 'draft',
locale: null,
visibility: 'public',
email_recipient_filter: 'none',
created_at: now,
updated_at: now,
published_at: null,
custom_excerpt: faker.lorem.paragraph(),
codeinjection_head: null,
codeinjection_foot: null,
custom_template: null,
canonical_url: null,
newsletter_id: null,
show_title_and_feature_image: true,
tags: undefined
};
// Determine published_at based on status and user options
let publishedAt = options.published_at ?? defaults.published_at;
if (options.status === 'published' && !options.published_at) {
publishedAt = now;
}
return {...defaults, ...options, published_at: publishedAt} as Post;
}
async createWithCards(cards: CardSpec | CardSpec[], options: Partial<Post> = {}): Promise<Post> {
const cardArray = Array.isArray(cards) ? cards : [cards];
return this.create({...options, lexical: buildLexical(...cardArray)});
}
}
+72
View File
@@ -0,0 +1,72 @@
import {Factory} from '@/data-factory';
import {faker} from '@faker-js/faker';
import {generateId, generateSlug} from '@/data-factory';
export interface Tag {
id: string;
name: string;
slug: string;
description: string | null;
feature_image: string | null;
parent_id: string | null;
visibility: 'public' | 'internal';
url?: string;
og_image: string | null;
og_title: string | null;
og_description: string | null;
twitter_image: string | null;
twitter_title: string | null;
twitter_description: string | null;
meta_title: string | null;
meta_description: string | null;
codeinjection_head: string | null;
codeinjection_foot: string | null;
canonical_url: string | null;
accent_color: string | null;
count?: {
posts: number;
};
created_at: Date;
updated_at: Date | null;
}
export class TagFactory extends Factory<Partial<Tag>, Tag> {
entityType = 'tags';
build(options: Partial<Tag> = {}): Tag {
return {
...this.buildDefaultTag(),
...options
};
}
private buildDefaultTag(): Tag {
const now = new Date();
const tagName = faker.commerce.department();
return {
id: generateId(),
name: tagName,
slug: `${generateSlug(tagName)}-${faker.string.alphanumeric(6).toLowerCase()}`,
description: faker.lorem.sentence(),
feature_image: `https://picsum.photos/seed/tag-${faker.string.alphanumeric(8)}/1200/630`,
parent_id: null,
visibility: 'public',
url: undefined,
og_image: null,
og_title: null,
og_description: faker.lorem.sentence(),
twitter_image: null,
twitter_title: null,
twitter_description: faker.lorem.sentence(),
meta_title: null,
meta_description: faker.lorem.sentence(),
codeinjection_head: null,
codeinjection_foot: null,
canonical_url: null,
accent_color: null,
count: {posts: 0},
created_at: now,
updated_at: now
};
}
}
@@ -0,0 +1,83 @@
import {Factory} from '@/data-factory';
import {faker} from '@faker-js/faker';
import {generateId, generateSlug} from '@/data-factory';
import type {HttpClient, PersistenceAdapter} from '@/data-factory';
import type {Tier} from './member-factory';
export interface AdminTier extends Tier {
description?: string | null;
visibility?: 'public' | 'none';
welcome_page_url?: string | null;
benefits?: string[] | null;
currency?: string;
monthly_price?: number;
yearly_price?: number;
trial_days?: number;
created_at?: Date;
updated_at?: Date | null;
}
export interface TierCreateInput {
name: string;
description?: string;
visibility?: 'public' | 'none';
welcome_page_url?: string;
benefits?: string[];
currency: string;
monthly_price: number;
yearly_price: number;
trial_days?: number;
}
export class TierFactory extends Factory<Partial<AdminTier>, AdminTier> {
entityType = 'tiers';
private readonly request?: HttpClient;
constructor(adapter?: PersistenceAdapter, request?: HttpClient) {
super(adapter);
this.request = request;
}
build(options: Partial<AdminTier> = {}): AdminTier {
const tierName = options.name ?? `Tier ${faker.commerce.productName()}`;
const now = new Date();
const defaults: AdminTier = {
id: generateId(),
name: tierName,
slug: `${generateSlug(tierName)}-${faker.string.alphanumeric(6).toLowerCase()}`,
type: 'paid',
active: true,
description: faker.lorem.sentence(),
visibility: 'public',
welcome_page_url: null,
benefits: [],
currency: 'usd',
monthly_price: 500,
yearly_price: 5000,
trial_days: 0,
created_at: now,
updated_at: now
};
return {...defaults, ...options};
}
async getFirstPaidTier(): Promise<AdminTier> {
if (!this.request) {
throw new Error('Cannot fetch tiers without an HTTP client. Use createTierFactory() for persisted test data access.');
}
const response = await this.request.get('/ghost/api/admin/tiers');
if (!response.ok()) {
throw new Error(`Failed to fetch tiers: ${response.status()}`);
}
const {tiers} = await response.json() as {tiers: AdminTier[]};
const paidTier = tiers.find(tier => tier.type === 'paid' && tier.active);
if (!paidTier) {
throw new Error('No paid tiers found');
}
return paidTier;
}
}
@@ -0,0 +1,26 @@
import {Factory} from '@/data-factory';
export interface User {
name: string;
email: string;
password: string;
blogTitle: string;
}
export class UserFactory extends Factory<Partial<User>, User> {
entityType = 'users';
public build(overrides: Partial<User> = {}): User {
return {
...this.defaults,
...overrides
};
}
private defaults: User = {
name: 'Test Admin',
email: 'test@example.com',
password: 'test123',
blogTitle: 'Test Blog'
};
}
+38
View File
@@ -0,0 +1,38 @@
import type {PersistenceAdapter} from './persistence/adapter';
export abstract class Factory<TOptions extends Record<string, unknown> = Record<string, unknown>, TResult = TOptions> {
abstract entityType: string;
protected adapter?: PersistenceAdapter;
constructor(adapter?: PersistenceAdapter) {
this.adapter = adapter;
}
abstract build(options?: Partial<TOptions>): TResult;
buildMany(optionsList: Partial<TOptions>[]): TResult[] {
return optionsList.map(options => this.build(options));
}
async create(options?: Partial<TOptions>): Promise<TResult> {
if (!this.adapter) {
throw new Error('Cannot create without a persistence adapter. Use build() for in-memory objects.');
}
const data = this.build(options);
return await this.adapter.insert(this.entityType, data) as Promise<TResult>;
}
async createMany(optionsList: Partial<TOptions>[]): Promise<TResult[]> {
if (!this.adapter) {
throw new Error('Cannot create without a persistence adapter. Use buildMany() for in-memory objects.');
}
const results: TResult[] = [];
for (const options of optionsList) {
const result = await this.create(options);
results.push(result);
}
return results;
}
}
+37
View File
@@ -0,0 +1,37 @@
// Core Factory exports
export {Factory} from './factory';
export {PostFactory} from './factories/post-factory';
export type {Post} from './factories/post-factory';
export {TagFactory} from './factories/tag-factory';
export type {Tag} from './factories/tag-factory';
export {MemberFactory} from './factories/member-factory';
export type {Member, Tier} from './factories/member-factory';
export {TierFactory} from './factories/tier-factory';
export type {AdminTier, TierCreateInput} from './factories/tier-factory';
export {OfferFactory} from './factories/offer-factory';
export type {AdminOffer, OfferCreateInput, OfferUpdateInput} from './factories/offer-factory';
export {AutomatedEmailFactory} from './factories/automated-email-factory';
export type {AutomatedEmail} from './factories/automated-email-factory';
export {CommentFactory} from './factories/comment-factory';
export type {Comment} from './factories/comment-factory';
export * from './factories/user-factory';
// Persistence Adapters
export {KnexPersistenceAdapter} from './persistence/adapters/knex';
export {ApiPersistenceAdapter} from './persistence/adapters/api';
export type {HttpClient, HttpResponse} from './persistence/adapters/http-client';
export {GhostAdminApiAdapter} from './persistence/adapters/ghost-api';
export type {PersistenceAdapter} from './persistence/adapter';
// Utilities
export {generateId, generateUuid, generateSlug} from './utils';
// Factory Setup Helpers
export {createPostFactory} from './setup';
export {createTagFactory} from './setup';
export {createMemberFactory} from './setup';
export {createTierFactory} from './setup';
export {createOfferFactory} from './setup';
export {createAutomatedEmailFactory} from './setup';
export {createCommentFactory} from './setup';
export {createFactories} from './setup';
+12
View File
@@ -0,0 +1,12 @@
/**
* Core persistence adapter interface
*/
export interface PersistenceAdapter {
insert<T>(entityType: string, data: T): Promise<T>;
update<T>(entityType: string, id: string, data: Partial<T>): Promise<T>;
delete(entityType: string, id: string): Promise<void>;
findById<T>(entityType: string, id: string): Promise<T>;
// Optional methods - implement as needed
deleteMany?(entityType: string, ids: string[]): Promise<void>;
findMany?<T>(entityType: string, query?: Record<string, unknown>): Promise<T[]>;
}
+102
View File
@@ -0,0 +1,102 @@
import {AutomatedEmailFactory} from './factories/automated-email-factory';
import {CommentFactory} from './factories/comment-factory';
import {GhostAdminApiAdapter} from './persistence/adapters/ghost-api';
import {HttpClient} from './persistence/adapters/http-client';
import {MemberFactory} from './factories/member-factory';
import {OfferFactory} from './factories/offer-factory';
import {PostFactory} from './factories/post-factory';
import {TagFactory} from './factories/tag-factory';
import {TierFactory} from './factories/tier-factory';
/**
* Create a new PostFactory with API persistence
* Uses the http client which already has the proper authentication headers and baseURL
* configured (this would be Playwright's page.request)
*
* @param httpClient - client for requests with pre-defined authorization and base url
* @returns PostFactory ready to use with the specified Ghost backend
*/
export function createPostFactory(httpClient: HttpClient): PostFactory {
const adapter = new GhostAdminApiAdapter(
httpClient,
'posts',
{formats: 'mobiledoc,lexical,html'}
);
return new PostFactory(adapter);
}
export function createTagFactory(httpClient: HttpClient): TagFactory {
const adapter = new GhostAdminApiAdapter(
httpClient,
'tags'
);
return new TagFactory(adapter);
}
export function createMemberFactory(httpClient: HttpClient): MemberFactory {
const adapter = new GhostAdminApiAdapter(
httpClient,
'members'
);
return new MemberFactory(adapter);
}
export function createTierFactory(httpClient: HttpClient): TierFactory {
const adapter = new GhostAdminApiAdapter(
httpClient,
'tiers'
);
return new TierFactory(adapter, httpClient);
}
export function createOfferFactory(httpClient: HttpClient): OfferFactory {
const adapter = new GhostAdminApiAdapter(
httpClient,
'offers'
);
return new OfferFactory(adapter, httpClient);
}
export function createAutomatedEmailFactory(httpClient: HttpClient): AutomatedEmailFactory {
const adapter = new GhostAdminApiAdapter(
httpClient,
'automated_emails'
);
return new AutomatedEmailFactory(adapter);
}
export function createCommentFactory(httpClient: HttpClient): CommentFactory {
const adapter = new GhostAdminApiAdapter(
httpClient,
'comments'
);
return new CommentFactory(adapter);
}
export interface Factories {
postFactory: PostFactory;
tagFactory: TagFactory;
memberFactory: MemberFactory;
tierFactory: TierFactory;
offerFactory: OfferFactory;
automatedEmailFactory: AutomatedEmailFactory;
commentFactory: CommentFactory;
}
/**
* Helper for creating all factories with the same http client
* @param httpClient - client for requests with pre-defined authorization and base url
*
* @returns All factories ready to use with the specified Ghost backend
*/
export function createFactories(httpClient: HttpClient): Factories {
return {
postFactory: createPostFactory(httpClient),
tagFactory: createTagFactory(httpClient),
memberFactory: createMemberFactory(httpClient),
tierFactory: createTierFactory(httpClient),
offerFactory: createOfferFactory(httpClient),
automatedEmailFactory: createAutomatedEmailFactory(httpClient),
commentFactory: createCommentFactory(httpClient)
};
}
+25
View File
@@ -0,0 +1,25 @@
import {faker} from '@faker-js/faker';
import {randomBytes} from 'crypto';
/**
* Generate a MongoDB-style ObjectId
*/
export function generateId(): string {
const timestamp = Math.floor(Date.now() / 1000).toString(16);
const randomHex = randomBytes(8).toString('hex');
return timestamp + randomHex;
}
export function generateUuid(): string {
return faker.string.uuid();
}
export function generateSlug(text: string): string {
return text
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/--+/g, '-')
.replace(/^-+/, '')
.replace(/-+$/, '');
}