@magicpages/ghost-typesense-core 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,48 @@
1
+ import { Config } from '@magicpages/ghost-typesense-config';
2
+
3
+ interface Post {
4
+ id: string;
5
+ title: string;
6
+ slug: string;
7
+ html: string;
8
+ excerpt: string;
9
+ feature_image?: string;
10
+ published_at: number;
11
+ updated_at: number;
12
+ tags?: string[];
13
+ authors?: string[];
14
+ [key: string]: unknown;
15
+ }
16
+ declare class GhostTypesenseManager {
17
+ private ghost;
18
+ private typesense;
19
+ private config;
20
+ private collectionName;
21
+ constructor(config: Config);
22
+ /**
23
+ * Initialize the Typesense collection with the specified schema
24
+ */
25
+ initializeCollection(): Promise<void>;
26
+ /**
27
+ * Transform a Ghost post into the format expected by Typesense
28
+ */
29
+ private transformPost;
30
+ /**
31
+ * Fetch all posts from Ghost and index them in Typesense
32
+ */
33
+ indexAllPosts(): Promise<void>;
34
+ /**
35
+ * Index a single post in Typesense
36
+ */
37
+ indexPost(postId: string): Promise<void>;
38
+ /**
39
+ * Delete a post from Typesense
40
+ */
41
+ deletePost(postId: string): Promise<void>;
42
+ /**
43
+ * Clear all documents from the collection
44
+ */
45
+ clearCollection(): Promise<void>;
46
+ }
47
+
48
+ export { GhostTypesenseManager, type Post };
@@ -0,0 +1,48 @@
1
+ import { Config } from '@magicpages/ghost-typesense-config';
2
+
3
+ interface Post {
4
+ id: string;
5
+ title: string;
6
+ slug: string;
7
+ html: string;
8
+ excerpt: string;
9
+ feature_image?: string;
10
+ published_at: number;
11
+ updated_at: number;
12
+ tags?: string[];
13
+ authors?: string[];
14
+ [key: string]: unknown;
15
+ }
16
+ declare class GhostTypesenseManager {
17
+ private ghost;
18
+ private typesense;
19
+ private config;
20
+ private collectionName;
21
+ constructor(config: Config);
22
+ /**
23
+ * Initialize the Typesense collection with the specified schema
24
+ */
25
+ initializeCollection(): Promise<void>;
26
+ /**
27
+ * Transform a Ghost post into the format expected by Typesense
28
+ */
29
+ private transformPost;
30
+ /**
31
+ * Fetch all posts from Ghost and index them in Typesense
32
+ */
33
+ indexAllPosts(): Promise<void>;
34
+ /**
35
+ * Index a single post in Typesense
36
+ */
37
+ indexPost(postId: string): Promise<void>;
38
+ /**
39
+ * Delete a post from Typesense
40
+ */
41
+ deletePost(postId: string): Promise<void>;
42
+ /**
43
+ * Clear all documents from the collection
44
+ */
45
+ clearCollection(): Promise<void>;
46
+ }
47
+
48
+ export { GhostTypesenseManager, type Post };
package/dist/index.js ADDED
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ GhostTypesenseManager: () => GhostTypesenseManager
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+ var import_content_api = require("@ts-ghost/content-api");
27
+ var import_typesense = require("typesense");
28
+ var GhostTypesenseManager = class {
29
+ ghost;
30
+ typesense;
31
+ config;
32
+ collectionName;
33
+ constructor(config) {
34
+ this.config = config;
35
+ this.collectionName = config.collection.name;
36
+ this.ghost = new import_content_api.TSGhostContentAPI(
37
+ config.ghost.url,
38
+ config.ghost.key,
39
+ "v5.0"
40
+ );
41
+ this.typesense = new import_typesense.Client({
42
+ nodes: config.typesense.nodes,
43
+ apiKey: config.typesense.apiKey,
44
+ connectionTimeoutSeconds: config.typesense.connectionTimeoutSeconds,
45
+ retryIntervalSeconds: config.typesense.retryIntervalSeconds,
46
+ numRetries: 3
47
+ });
48
+ }
49
+ /**
50
+ * Initialize the Typesense collection with the specified schema
51
+ */
52
+ async initializeCollection() {
53
+ const collections = await this.typesense.collections().retrieve();
54
+ const existingCollection = collections.find((c) => c.name === this.collectionName);
55
+ if (existingCollection) {
56
+ const collection = this.typesense.collections(this.collectionName);
57
+ await collection.delete();
58
+ }
59
+ const schema = {
60
+ name: this.collectionName,
61
+ fields: this.config.collection.fields.map((field) => ({
62
+ name: field.name,
63
+ type: field.type,
64
+ facet: field.facet,
65
+ index: field.index,
66
+ optional: field.optional,
67
+ sort: field.sort
68
+ }))
69
+ };
70
+ await this.typesense.collections().create(schema);
71
+ }
72
+ /**
73
+ * Transform a Ghost post into the format expected by Typesense
74
+ */
75
+ transformPost(post) {
76
+ console.log("Transforming post:", post.id, post.title);
77
+ const transformed = {
78
+ id: post.id,
79
+ title: post.title,
80
+ slug: post.slug,
81
+ html: post.html,
82
+ excerpt: post.excerpt || "",
83
+ published_at: new Date(post.published_at || Date.now()).getTime(),
84
+ updated_at: new Date(post.updated_at || Date.now()).getTime()
85
+ };
86
+ if (post.feature_image) {
87
+ transformed.feature_image = post.feature_image;
88
+ }
89
+ const tags = post.tags;
90
+ if (tags && Array.isArray(tags) && tags.length > 0) {
91
+ transformed.tags = tags.map((tag) => tag.name);
92
+ }
93
+ const authors = post.authors;
94
+ if (authors && Array.isArray(authors) && authors.length > 0) {
95
+ transformed.authors = authors.map((author) => author.name);
96
+ }
97
+ this.config.collection.fields.forEach((field) => {
98
+ const value = post[field.name];
99
+ if (!transformed[field.name] && value !== void 0 && value !== null) {
100
+ transformed[field.name] = value;
101
+ }
102
+ });
103
+ console.log("Transformed document:", transformed);
104
+ return transformed;
105
+ }
106
+ /**
107
+ * Fetch all posts from Ghost and index them in Typesense
108
+ */
109
+ async indexAllPosts() {
110
+ let allPosts = [];
111
+ const posts = this.ghost.posts.browse({
112
+ limit: 15
113
+ // Default limit in Ghost
114
+ }).include({ tags: true, authors: true });
115
+ const response = await posts.fetch();
116
+ if (!response.success) {
117
+ throw new Error("Failed to fetch posts from Ghost");
118
+ }
119
+ allPosts = allPosts.concat(response.data);
120
+ const total = response.meta.pagination.total;
121
+ const limit = response.meta.pagination.limit;
122
+ const totalPages = Math.ceil(total / limit);
123
+ for (let page = 2; page <= totalPages; page++) {
124
+ const pageResponse = await this.ghost.posts.browse({
125
+ limit,
126
+ page
127
+ }).include({ tags: true, authors: true }).fetch();
128
+ if (!pageResponse.success) {
129
+ throw new Error(`Failed to fetch page ${page} from Ghost`);
130
+ }
131
+ allPosts = allPosts.concat(pageResponse.data);
132
+ }
133
+ console.log(`Found ${allPosts.length} posts to index`);
134
+ const documents = allPosts.map((post) => this.transformPost(post));
135
+ try {
136
+ const collection = this.typesense.collections(this.collectionName);
137
+ const results = await Promise.all(
138
+ documents.map(
139
+ (doc) => collection.documents().upsert(doc).then(() => ({ success: true, id: doc.id })).catch((error) => ({ success: false, id: doc.id, error: error.message }))
140
+ )
141
+ );
142
+ const succeeded = results.filter((r) => r.success).length;
143
+ const failed = results.filter((r) => !r.success).length;
144
+ console.log(`Indexing complete: ${succeeded} succeeded, ${failed} failed`);
145
+ if (failed > 0) {
146
+ console.log("Failed documents:", results.filter((r) => !r.success));
147
+ }
148
+ } catch (error) {
149
+ console.error("Indexing error:", error);
150
+ throw error;
151
+ }
152
+ }
153
+ /**
154
+ * Index a single post in Typesense
155
+ */
156
+ async indexPost(postId) {
157
+ const post = await this.ghost.posts.read({ id: postId }).include({ tags: true, authors: true }).fetch();
158
+ if (!post.success) {
159
+ throw new Error(`Failed to fetch post ${postId} from Ghost`);
160
+ }
161
+ const document = this.transformPost(post.data);
162
+ const collection = this.typesense.collections(this.collectionName);
163
+ await collection.documents().upsert(document);
164
+ }
165
+ /**
166
+ * Delete a post from Typesense
167
+ */
168
+ async deletePost(postId) {
169
+ const collection = this.typesense.collections(this.collectionName);
170
+ await collection.documents().delete({ filter_by: `id:${postId}` });
171
+ }
172
+ /**
173
+ * Clear all documents from the collection
174
+ */
175
+ async clearCollection() {
176
+ const collection = this.typesense.collections(this.collectionName);
177
+ await collection.documents().delete();
178
+ }
179
+ };
180
+ // Annotate the CommonJS export names for ESM import in node:
181
+ 0 && (module.exports = {
182
+ GhostTypesenseManager
183
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,158 @@
1
+ // src/index.ts
2
+ import { TSGhostContentAPI } from "@ts-ghost/content-api";
3
+ import { Client } from "typesense";
4
+ var GhostTypesenseManager = class {
5
+ ghost;
6
+ typesense;
7
+ config;
8
+ collectionName;
9
+ constructor(config) {
10
+ this.config = config;
11
+ this.collectionName = config.collection.name;
12
+ this.ghost = new TSGhostContentAPI(
13
+ config.ghost.url,
14
+ config.ghost.key,
15
+ "v5.0"
16
+ );
17
+ this.typesense = new Client({
18
+ nodes: config.typesense.nodes,
19
+ apiKey: config.typesense.apiKey,
20
+ connectionTimeoutSeconds: config.typesense.connectionTimeoutSeconds,
21
+ retryIntervalSeconds: config.typesense.retryIntervalSeconds,
22
+ numRetries: 3
23
+ });
24
+ }
25
+ /**
26
+ * Initialize the Typesense collection with the specified schema
27
+ */
28
+ async initializeCollection() {
29
+ const collections = await this.typesense.collections().retrieve();
30
+ const existingCollection = collections.find((c) => c.name === this.collectionName);
31
+ if (existingCollection) {
32
+ const collection = this.typesense.collections(this.collectionName);
33
+ await collection.delete();
34
+ }
35
+ const schema = {
36
+ name: this.collectionName,
37
+ fields: this.config.collection.fields.map((field) => ({
38
+ name: field.name,
39
+ type: field.type,
40
+ facet: field.facet,
41
+ index: field.index,
42
+ optional: field.optional,
43
+ sort: field.sort
44
+ }))
45
+ };
46
+ await this.typesense.collections().create(schema);
47
+ }
48
+ /**
49
+ * Transform a Ghost post into the format expected by Typesense
50
+ */
51
+ transformPost(post) {
52
+ console.log("Transforming post:", post.id, post.title);
53
+ const transformed = {
54
+ id: post.id,
55
+ title: post.title,
56
+ slug: post.slug,
57
+ html: post.html,
58
+ excerpt: post.excerpt || "",
59
+ published_at: new Date(post.published_at || Date.now()).getTime(),
60
+ updated_at: new Date(post.updated_at || Date.now()).getTime()
61
+ };
62
+ if (post.feature_image) {
63
+ transformed.feature_image = post.feature_image;
64
+ }
65
+ const tags = post.tags;
66
+ if (tags && Array.isArray(tags) && tags.length > 0) {
67
+ transformed.tags = tags.map((tag) => tag.name);
68
+ }
69
+ const authors = post.authors;
70
+ if (authors && Array.isArray(authors) && authors.length > 0) {
71
+ transformed.authors = authors.map((author) => author.name);
72
+ }
73
+ this.config.collection.fields.forEach((field) => {
74
+ const value = post[field.name];
75
+ if (!transformed[field.name] && value !== void 0 && value !== null) {
76
+ transformed[field.name] = value;
77
+ }
78
+ });
79
+ console.log("Transformed document:", transformed);
80
+ return transformed;
81
+ }
82
+ /**
83
+ * Fetch all posts from Ghost and index them in Typesense
84
+ */
85
+ async indexAllPosts() {
86
+ let allPosts = [];
87
+ const posts = this.ghost.posts.browse({
88
+ limit: 15
89
+ // Default limit in Ghost
90
+ }).include({ tags: true, authors: true });
91
+ const response = await posts.fetch();
92
+ if (!response.success) {
93
+ throw new Error("Failed to fetch posts from Ghost");
94
+ }
95
+ allPosts = allPosts.concat(response.data);
96
+ const total = response.meta.pagination.total;
97
+ const limit = response.meta.pagination.limit;
98
+ const totalPages = Math.ceil(total / limit);
99
+ for (let page = 2; page <= totalPages; page++) {
100
+ const pageResponse = await this.ghost.posts.browse({
101
+ limit,
102
+ page
103
+ }).include({ tags: true, authors: true }).fetch();
104
+ if (!pageResponse.success) {
105
+ throw new Error(`Failed to fetch page ${page} from Ghost`);
106
+ }
107
+ allPosts = allPosts.concat(pageResponse.data);
108
+ }
109
+ console.log(`Found ${allPosts.length} posts to index`);
110
+ const documents = allPosts.map((post) => this.transformPost(post));
111
+ try {
112
+ const collection = this.typesense.collections(this.collectionName);
113
+ const results = await Promise.all(
114
+ documents.map(
115
+ (doc) => collection.documents().upsert(doc).then(() => ({ success: true, id: doc.id })).catch((error) => ({ success: false, id: doc.id, error: error.message }))
116
+ )
117
+ );
118
+ const succeeded = results.filter((r) => r.success).length;
119
+ const failed = results.filter((r) => !r.success).length;
120
+ console.log(`Indexing complete: ${succeeded} succeeded, ${failed} failed`);
121
+ if (failed > 0) {
122
+ console.log("Failed documents:", results.filter((r) => !r.success));
123
+ }
124
+ } catch (error) {
125
+ console.error("Indexing error:", error);
126
+ throw error;
127
+ }
128
+ }
129
+ /**
130
+ * Index a single post in Typesense
131
+ */
132
+ async indexPost(postId) {
133
+ const post = await this.ghost.posts.read({ id: postId }).include({ tags: true, authors: true }).fetch();
134
+ if (!post.success) {
135
+ throw new Error(`Failed to fetch post ${postId} from Ghost`);
136
+ }
137
+ const document = this.transformPost(post.data);
138
+ const collection = this.typesense.collections(this.collectionName);
139
+ await collection.documents().upsert(document);
140
+ }
141
+ /**
142
+ * Delete a post from Typesense
143
+ */
144
+ async deletePost(postId) {
145
+ const collection = this.typesense.collections(this.collectionName);
146
+ await collection.documents().delete({ filter_by: `id:${postId}` });
147
+ }
148
+ /**
149
+ * Clear all documents from the collection
150
+ */
151
+ async clearCollection() {
152
+ const collection = this.typesense.collections(this.collectionName);
153
+ await collection.documents().delete();
154
+ }
155
+ };
156
+ export {
157
+ GhostTypesenseManager
158
+ };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@magicpages/ghost-typesense-core",
3
+ "version": "0.0.0",
4
+ "description": "Core functionality for Ghost-Typesense integration",
5
+ "author": "MagicPages",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/magicpages/ghost-typesense.git",
10
+ "directory": "packages/core"
11
+ },
12
+ "main": "./dist/index.js",
13
+ "module": "./dist/index.mjs",
14
+ "types": "./dist/index.d.ts",
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format cjs,esm --dts",
20
+ "clean": "rimraf dist",
21
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
22
+ "lint": "eslint src --ext .ts",
23
+ "test": "vitest run",
24
+ "typecheck": "tsc --noEmit"
25
+ },
26
+ "dependencies": {
27
+ "@magicpages/ghost-typesense-config": "*",
28
+ "@ts-ghost/content-api": "^4.0.6",
29
+ "@ts-ghost/core-api": "^4.0.6",
30
+ "typesense": "^1.7.2",
31
+ "zod": "^3.22.4"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^20.11.17",
35
+ "tsup": "^8.0.1",
36
+ "rimraf": "^5.0.5",
37
+ "vitest": "^1.2.2",
38
+ "typescript": "^5.3.3"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ }
43
+ }