@rubixstudios/payload-typesense 1.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.
Files changed (60) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +182 -0
  3. package/dist/components/HeadlessSearchInput.d.ts +86 -0
  4. package/dist/components/HeadlessSearchInput.d.ts.map +1 -0
  5. package/dist/components/HeadlessSearchInput.js +602 -0
  6. package/dist/components/ThemeProvider.d.ts +10 -0
  7. package/dist/components/ThemeProvider.d.ts.map +1 -0
  8. package/dist/components/ThemeProvider.js +17 -0
  9. package/dist/components/index.d.ts +6 -0
  10. package/dist/components/index.d.ts.map +1 -0
  11. package/dist/components/index.js +3 -0
  12. package/dist/components/themes/hooks.d.ts +52 -0
  13. package/dist/components/themes/hooks.d.ts.map +1 -0
  14. package/dist/components/themes/hooks.js +177 -0
  15. package/dist/components/themes/index.d.ts +5 -0
  16. package/dist/components/themes/index.d.ts.map +1 -0
  17. package/dist/components/themes/index.js +4 -0
  18. package/dist/components/themes/themes.d.ts +6 -0
  19. package/dist/components/themes/themes.d.ts.map +1 -0
  20. package/dist/components/themes/themes.js +156 -0
  21. package/dist/components/themes/types.d.ts +147 -0
  22. package/dist/components/themes/types.d.ts.map +1 -0
  23. package/dist/components/themes/types.js +1 -0
  24. package/dist/components/themes/utils.d.ts +30 -0
  25. package/dist/components/themes/utils.d.ts.map +1 -0
  26. package/dist/components/themes/utils.js +397 -0
  27. package/dist/endpoints/customEndpointHandler.d.ts +3 -0
  28. package/dist/endpoints/customEndpointHandler.d.ts.map +1 -0
  29. package/dist/endpoints/customEndpointHandler.js +5 -0
  30. package/dist/endpoints/health.d.ts +12 -0
  31. package/dist/endpoints/health.d.ts.map +1 -0
  32. package/dist/endpoints/health.js +174 -0
  33. package/dist/endpoints/search.d.ts +13 -0
  34. package/dist/endpoints/search.d.ts.map +1 -0
  35. package/dist/endpoints/search.js +375 -0
  36. package/dist/index.d.ts +39 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +148 -0
  39. package/dist/lib/cache.d.ts +41 -0
  40. package/dist/lib/cache.d.ts.map +1 -0
  41. package/dist/lib/cache.js +96 -0
  42. package/dist/lib/config-validation.d.ts +75 -0
  43. package/dist/lib/config-validation.d.ts.map +1 -0
  44. package/dist/lib/config-validation.js +174 -0
  45. package/dist/lib/hooks.d.ts +4 -0
  46. package/dist/lib/hooks.d.ts.map +1 -0
  47. package/dist/lib/hooks.js +54 -0
  48. package/dist/lib/initialization.d.ts +5 -0
  49. package/dist/lib/initialization.d.ts.map +1 -0
  50. package/dist/lib/initialization.js +102 -0
  51. package/dist/lib/schema-mapper.d.ts +14 -0
  52. package/dist/lib/schema-mapper.d.ts.map +1 -0
  53. package/dist/lib/schema-mapper.js +137 -0
  54. package/dist/lib/types.d.ts +183 -0
  55. package/dist/lib/types.d.ts.map +1 -0
  56. package/dist/lib/types.js +2 -0
  57. package/dist/lib/typesense-client.d.ts +5 -0
  58. package/dist/lib/typesense-client.d.ts.map +1 -0
  59. package/dist/lib/typesense-client.js +20 -0
  60. package/package.json +92 -0
@@ -0,0 +1,96 @@
1
+ export class SearchCache {
2
+ cache = new Map();
3
+ defaultTTL;
4
+ maxSize;
5
+ constructor(options = {}){
6
+ this.defaultTTL = options.ttl || 5 * 60 * 1000; // 5 minutes default
7
+ this.maxSize = options.maxSize || 1000; // 1000 entries default
8
+ }
9
+ /**
10
+ * Generate cache key from search parameters
11
+ */ generateKey(query, collection, params) {
12
+ const baseKey = `${collection || 'universal'}:${query}`;
13
+ if (params) {
14
+ const sortedParams = Object.keys(params).sort().map((key)=>`${key}=${params[key]}`).join('&');
15
+ return `${baseKey}:${sortedParams}`;
16
+ }
17
+ return baseKey;
18
+ }
19
+ /**
20
+ * Clear expired entries
21
+ */ cleanup() {
22
+ const now = Date.now();
23
+ for (const [key, entry] of this.cache.entries()){
24
+ if (now - entry.timestamp > entry.ttl) {
25
+ this.cache.delete(key);
26
+ }
27
+ }
28
+ }
29
+ /**
30
+ * Clear cache entries matching pattern
31
+ */ clear(pattern) {
32
+ if (!pattern) {
33
+ this.cache.clear();
34
+ return;
35
+ }
36
+ for (const key of this.cache.keys()){
37
+ if (key.includes(pattern)) {
38
+ this.cache.delete(key);
39
+ }
40
+ }
41
+ }
42
+ /**
43
+ * Get cached search result
44
+ */ get(query, collection, params) {
45
+ const key = this.generateKey(query, collection || '', params);
46
+ const entry = this.cache.get(key);
47
+ if (!entry) {
48
+ return null;
49
+ }
50
+ // Check if entry has expired
51
+ if (Date.now() - entry.timestamp > entry.ttl) {
52
+ this.cache.delete(key);
53
+ return null;
54
+ }
55
+ return entry.data;
56
+ }
57
+ /**
58
+ * Get cache statistics
59
+ */ getStats() {
60
+ return {
61
+ maxSize: this.maxSize,
62
+ size: this.cache.size
63
+ };
64
+ }
65
+ /**
66
+ * Check if cache has valid entry
67
+ */ has(query, collection, params) {
68
+ return this.get(query, collection, params) !== null;
69
+ }
70
+ /**
71
+ * Set cached search result
72
+ */ set(query, data, collection, params, ttl) {
73
+ const key = this.generateKey(query, collection || '', params);
74
+ // Enforce max size by removing oldest entries
75
+ if (this.cache.size >= this.maxSize) {
76
+ const oldestKey = this.cache.keys().next().value;
77
+ if (oldestKey) {
78
+ this.cache.delete(oldestKey);
79
+ }
80
+ }
81
+ this.cache.set(key, {
82
+ data,
83
+ timestamp: Date.now(),
84
+ ttl: ttl || this.defaultTTL
85
+ });
86
+ }
87
+ }
88
+ // Global cache instance
89
+ export const searchCache = new SearchCache({
90
+ maxSize: 1000,
91
+ ttl: 5 * 60 * 1000 // 5 minutes
92
+ });
93
+ // Cleanup expired entries every 10 minutes
94
+ setInterval(()=>{
95
+ searchCache.cleanup();
96
+ }, 10 * 60 * 1000);
@@ -0,0 +1,75 @@
1
+ import { z } from 'zod';
2
+ export declare const TypesenseSearchConfigSchema: z.ZodObject<{
3
+ collections: z.ZodRecord<z.ZodString, z.ZodObject<{
4
+ displayName: z.ZodOptional<z.ZodString>;
5
+ enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
6
+ facetFields: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
7
+ fieldMapping: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
8
+ icon: z.ZodOptional<z.ZodString>;
9
+ searchFields: z.ZodArray<z.ZodString>;
10
+ }, z.core.$strip>>;
11
+ settings: z.ZodOptional<z.ZodObject<{
12
+ cache: z.ZodOptional<z.ZodObject<{
13
+ maxSize: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
14
+ ttl: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
15
+ }, z.core.$strip>>;
16
+ categorized: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
17
+ }, z.core.$strip>>;
18
+ typesense: z.ZodObject<{
19
+ apiKey: z.ZodString;
20
+ connectionTimeoutSeconds: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
21
+ nodes: z.ZodArray<z.ZodObject<{
22
+ host: z.ZodString;
23
+ port: z.ZodNumber;
24
+ protocol: z.ZodEnum<{
25
+ http: "http";
26
+ https: "https";
27
+ }>;
28
+ }, z.core.$strip>>;
29
+ numRetries: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
30
+ retryIntervalSeconds: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
31
+ }, z.core.$strip>;
32
+ }, z.core.$strip>;
33
+ export type ValidatedTypesenseSearchConfig = z.infer<typeof TypesenseSearchConfigSchema>;
34
+ export interface ValidationResult {
35
+ data?: ValidatedTypesenseSearchConfig;
36
+ errors?: string[];
37
+ success: boolean;
38
+ }
39
+ /**
40
+ * Validate plugin configuration
41
+ */
42
+ export declare function validateConfig(config: unknown): ValidationResult;
43
+ /**
44
+ * Validate and transform configuration with defaults
45
+ */
46
+ export declare function validateAndTransformConfig(config: unknown): ValidationResult;
47
+ /**
48
+ * Validate individual collection configuration
49
+ */
50
+ export declare function validateCollectionConfig(collectionSlug: string, config: unknown): ValidationResult;
51
+ /**
52
+ * Get configuration validation errors in a user-friendly format
53
+ */
54
+ export declare function getValidationErrors(errors: string[]): string;
55
+ /**
56
+ * Validate search parameters
57
+ */
58
+ export declare const SearchParamsSchema: z.ZodObject<{
59
+ facets: z.ZodOptional<z.ZodArray<z.ZodString>>;
60
+ filters: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
61
+ highlight_fields: z.ZodOptional<z.ZodArray<z.ZodString>>;
62
+ num_typos: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
63
+ page: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
64
+ per_page: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
65
+ q: z.ZodString;
66
+ snippet_threshold: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
67
+ sort_by: z.ZodOptional<z.ZodString>;
68
+ typo_tokens_threshold: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
69
+ }, z.core.$strip>;
70
+ export type ValidatedSearchParams = z.infer<typeof SearchParamsSchema>;
71
+ /**
72
+ * Validate search parameters
73
+ */
74
+ export declare function validateSearchParams(params: unknown): ValidationResult;
75
+ //# sourceMappingURL=config-validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-validation.d.ts","sourceRoot":"","sources":["../../src/lib/config-validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAgCvB,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAUtC,CAAA;AAGF,MAAM,MAAM,8BAA8B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAA;AAGxF,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,8BAA8B,CAAA;IACrC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;CACjB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,gBAAgB,CAyBhE;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,OAAO,GAAG,gBAAgB,CAyB5E;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,gBAAgB,CAyBlG;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAE5D;AAED;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;iBAW7B,CAAA;AAEF,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAEtE;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,GAAG,gBAAgB,CAyBtE"}
@@ -0,0 +1,174 @@
1
+ import { z } from 'zod';
2
+ // Typesense node configuration schema
3
+ const TypesenseNodeSchema = z.object({
4
+ host: z.string().min(1, 'Host is required'),
5
+ port: z.number().int().min(1).max(65535, 'Port must be between 1 and 65535'),
6
+ protocol: z.enum([
7
+ 'http',
8
+ 'https'
9
+ ])
10
+ });
11
+ // Collection configuration schema
12
+ const CollectionConfigSchema = z.object({
13
+ displayName: z.string().optional(),
14
+ enabled: z.boolean().optional().default(true),
15
+ facetFields: z.array(z.string()).optional().default([]),
16
+ fieldMapping: z.record(z.string(), z.string()).optional(),
17
+ icon: z.string().optional(),
18
+ searchFields: z.array(z.string()).min(1, 'At least one search field is required')
19
+ });
20
+ // Cache configuration schema
21
+ const CacheConfigSchema = z.object({
22
+ maxSize: z.number().int().min(1, 'Max size must be at least 1').optional().default(1000),
23
+ ttl: z.number().int().min(1000, 'TTL must be at least 1000ms').optional().default(300000) // 5 minutes
24
+ });
25
+ // Settings configuration schema
26
+ const SettingsConfigSchema = z.object({
27
+ cache: CacheConfigSchema.optional(),
28
+ categorized: z.boolean().optional().default(false)
29
+ });
30
+ // Main plugin configuration schema
31
+ export const TypesenseSearchConfigSchema = z.object({
32
+ collections: z.record(z.string(), CollectionConfigSchema),
33
+ settings: SettingsConfigSchema.optional(),
34
+ typesense: z.object({
35
+ apiKey: z.string().min(1, 'API key is required'),
36
+ connectionTimeoutSeconds: z.number().int().min(1).optional().default(10),
37
+ nodes: z.array(TypesenseNodeSchema).min(1, 'At least one Typesense node is required'),
38
+ numRetries: z.number().int().min(0).optional().default(3),
39
+ retryIntervalSeconds: z.number().int().min(1).optional().default(1)
40
+ })
41
+ });
42
+ /**
43
+ * Validate plugin configuration
44
+ */ export function validateConfig(config) {
45
+ try {
46
+ const validatedConfig = TypesenseSearchConfigSchema.parse(config);
47
+ return {
48
+ data: validatedConfig,
49
+ success: true
50
+ };
51
+ } catch (error) {
52
+ if (error instanceof z.ZodError) {
53
+ const errors = error.issues.map((err)=>{
54
+ const path = err.path.length > 0 ? `${err.path.join('.')}: ` : '';
55
+ return `${path}${err.message}`;
56
+ });
57
+ return {
58
+ errors,
59
+ success: false
60
+ };
61
+ }
62
+ return {
63
+ errors: [
64
+ 'Invalid configuration format'
65
+ ],
66
+ success: false
67
+ };
68
+ }
69
+ }
70
+ /**
71
+ * Validate and transform configuration with defaults
72
+ */ export function validateAndTransformConfig(config) {
73
+ try {
74
+ const validatedConfig = TypesenseSearchConfigSchema.parse(config);
75
+ return {
76
+ data: validatedConfig,
77
+ success: true
78
+ };
79
+ } catch (error) {
80
+ if (error instanceof z.ZodError) {
81
+ const errors = error.issues.map((err)=>{
82
+ const path = err.path.length > 0 ? `${err.path.join('.')}: ` : '';
83
+ return `${path}${err.message}`;
84
+ });
85
+ return {
86
+ errors,
87
+ success: false
88
+ };
89
+ }
90
+ return {
91
+ errors: [
92
+ 'Invalid configuration format'
93
+ ],
94
+ success: false
95
+ };
96
+ }
97
+ }
98
+ /**
99
+ * Validate individual collection configuration
100
+ */ export function validateCollectionConfig(collectionSlug, config) {
101
+ try {
102
+ const validatedConfig = CollectionConfigSchema.parse(config);
103
+ return {
104
+ data: {
105
+ [collectionSlug]: validatedConfig
106
+ },
107
+ success: true
108
+ };
109
+ } catch (error) {
110
+ if (error instanceof z.ZodError) {
111
+ const errors = error.issues.map((err)=>{
112
+ const path = err.path.length > 0 ? `${err.path.join('.')}: ` : '';
113
+ return `Collection '${collectionSlug}' - ${path}${err.message}`;
114
+ });
115
+ return {
116
+ errors,
117
+ success: false
118
+ };
119
+ }
120
+ return {
121
+ errors: [
122
+ `Collection '${collectionSlug}': Invalid configuration format`
123
+ ],
124
+ success: false
125
+ };
126
+ }
127
+ }
128
+ /**
129
+ * Get configuration validation errors in a user-friendly format
130
+ */ export function getValidationErrors(errors) {
131
+ return errors.map((error, index)=>`${index + 1}. ${error}`).join('\n');
132
+ }
133
+ /**
134
+ * Validate search parameters
135
+ */ export const SearchParamsSchema = z.object({
136
+ facets: z.array(z.string()).optional(),
137
+ filters: z.record(z.string(), z.any()).optional(),
138
+ highlight_fields: z.array(z.string()).optional(),
139
+ num_typos: z.number().int().min(0).max(4).optional().default(0),
140
+ page: z.number().int().min(1).optional().default(1),
141
+ per_page: z.number().int().min(1).max(250).optional().default(10),
142
+ q: z.string().min(1, 'Query parameter "q" is required'),
143
+ snippet_threshold: z.number().int().min(0).max(100).optional().default(30),
144
+ sort_by: z.string().optional(),
145
+ typo_tokens_threshold: z.number().int().min(1).optional().default(1)
146
+ });
147
+ /**
148
+ * Validate search parameters
149
+ */ export function validateSearchParams(params) {
150
+ try {
151
+ const validatedParams = SearchParamsSchema.parse(params);
152
+ return {
153
+ data: validatedParams,
154
+ success: true
155
+ };
156
+ } catch (error) {
157
+ if (error instanceof z.ZodError) {
158
+ const errors = error.issues.map((err)=>{
159
+ const path = err.path.length > 0 ? `${err.path.join('.')}: ` : '';
160
+ return `${path}${err.message}`;
161
+ });
162
+ return {
163
+ errors,
164
+ success: false
165
+ };
166
+ }
167
+ return {
168
+ errors: [
169
+ 'Invalid search parameters format'
170
+ ],
171
+ success: false
172
+ };
173
+ }
174
+ }
@@ -0,0 +1,4 @@
1
+ import type Typesense from 'typesense';
2
+ import type { TypesenseSearchConfig } from '../index.js';
3
+ export declare const setupHooks: (typesenseClient: Typesense.Client, pluginOptions: TypesenseSearchConfig, existingHooks?: any) => any;
4
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/lib/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,SAAS,MAAM,WAAW,CAAA;AAEtC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAIxD,eAAO,MAAM,UAAU,GACrB,iBAAiB,SAAS,CAAC,MAAM,EACjC,eAAe,qBAAqB,EACpC,gBAAgB,GAAG,QAiCpB,CAAA"}
@@ -0,0 +1,54 @@
1
+ import { mapPayloadDocumentToTypesense } from './schema-mapper.js';
2
+ export const setupHooks = (typesenseClient, pluginOptions, existingHooks)=>{
3
+ const hooks = {
4
+ ...existingHooks
5
+ };
6
+ if (pluginOptions.collections) {
7
+ for (const [collectionSlug, config] of Object.entries(pluginOptions.collections)){
8
+ if (config?.enabled) {
9
+ // After create/update hook
10
+ hooks.afterChange = {
11
+ ...hooks.afterChange,
12
+ [collectionSlug]: [
13
+ ...hooks.afterChange?.[collectionSlug] || [],
14
+ async ({ doc, operation, req: _req })=>{
15
+ await syncDocumentToTypesense(typesenseClient, collectionSlug, doc, operation, config);
16
+ }
17
+ ]
18
+ };
19
+ // After delete hook
20
+ hooks.afterDelete = {
21
+ ...hooks.afterDelete,
22
+ [collectionSlug]: [
23
+ ...hooks.afterDelete?.[collectionSlug] || [],
24
+ async ({ doc, req: _req })=>{
25
+ await deleteDocumentFromTypesense(typesenseClient, collectionSlug, doc.id);
26
+ }
27
+ ]
28
+ };
29
+ }
30
+ }
31
+ }
32
+ return hooks;
33
+ };
34
+ const syncDocumentToTypesense = async (typesenseClient, collectionSlug, doc, operation, config)=>{
35
+ try {
36
+ const typesenseDoc = mapPayloadDocumentToTypesense(doc, collectionSlug, config);
37
+ await typesenseClient.collections(collectionSlug).documents().upsert(typesenseDoc);
38
+ // Document synced successfully
39
+ } catch (error) {
40
+ // Handle document sync error
41
+ // Log the problematic document for debugging
42
+ if (error.message.includes('validation')) {
43
+ // Log problematic document details
44
+ }
45
+ }
46
+ };
47
+ const deleteDocumentFromTypesense = async (typesenseClient, collectionSlug, docId)=>{
48
+ try {
49
+ await typesenseClient.collections(collectionSlug).documents(docId).delete();
50
+ // Document deleted successfully
51
+ } catch (_error) {
52
+ // Handle document deletion error
53
+ }
54
+ };
@@ -0,0 +1,5 @@
1
+ import type { Payload } from 'payload';
2
+ import type Typesense from 'typesense';
3
+ import type { TypesenseSearchConfig } from '../index.js';
4
+ export declare const initializeTypesenseCollections: (payload: Payload, typesenseClient: Typesense.Client, pluginOptions: TypesenseSearchConfig) => Promise<void>;
5
+ //# sourceMappingURL=initialization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"initialization.d.ts","sourceRoot":"","sources":["../../src/lib/initialization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,KAAK,SAAS,MAAM,WAAW,CAAA;AAEtC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAMxD,eAAO,MAAM,8BAA8B,GACzC,SAAS,OAAO,EAChB,iBAAiB,SAAS,CAAC,MAAM,EACjC,eAAe,qBAAqB,kBAiCrC,CAAA"}
@@ -0,0 +1,102 @@
1
+ import { validateConfig } from './config-validation.js';
2
+ import { mapCollectionToTypesenseSchema, mapPayloadDocumentToTypesense } from './schema-mapper.js';
3
+ import { testTypesenseConnection } from './typesense-client.js';
4
+ export const initializeTypesenseCollections = async (payload, typesenseClient, pluginOptions)=>{
5
+ // Validate configuration first
6
+ const validation = validateConfig(pluginOptions);
7
+ if (!validation.success) {
8
+ // Handle configuration validation error
9
+ throw new Error('Invalid plugin configuration');
10
+ }
11
+ // Configuration validated successfully
12
+ // Test Typesense connection first
13
+ const isConnected = await testTypesenseConnection(typesenseClient);
14
+ if (!isConnected) {
15
+ // Typesense connection failed
16
+ return;
17
+ }
18
+ // Initialize Typesense collections
19
+ if (pluginOptions.collections) {
20
+ for (const [collectionSlug, config] of Object.entries(pluginOptions.collections)){
21
+ if (config?.enabled) {
22
+ try {
23
+ await initializeCollection(payload, typesenseClient, collectionSlug, config);
24
+ } catch (_error) {
25
+ // Handle collection initialization error
26
+ }
27
+ }
28
+ }
29
+ }
30
+ // Collections initialized successfully
31
+ };
32
+ const initializeCollection = async (payload, typesenseClient, collectionSlug, config)=>{
33
+ // Get the collection config from Payload
34
+ const collection = payload.collections[collectionSlug];
35
+ if (!collection) {
36
+ // Collection not found in Payload
37
+ return;
38
+ }
39
+ // Create Typesense schema
40
+ const schema = mapCollectionToTypesenseSchema(collection, collectionSlug, config);
41
+ // Create schema for collection
42
+ try {
43
+ // Check if collection exists
44
+ await typesenseClient.collections(collectionSlug).retrieve();
45
+ // Collection already exists
46
+ } catch (_error) {
47
+ // Collection doesn't exist, create it
48
+ try {
49
+ await typesenseClient.collections().create(schema);
50
+ // Collection created successfully
51
+ } catch (_createError) {
52
+ // Handle collection creation error
53
+ return;
54
+ }
55
+ }
56
+ // Sync existing documents
57
+ await syncExistingDocuments(payload, typesenseClient, collectionSlug, config);
58
+ };
59
+ const syncExistingDocuments = async (payload, typesenseClient, collectionSlug, config)=>{
60
+ try {
61
+ const { docs } = await payload.find({
62
+ collection: collectionSlug,
63
+ depth: 0,
64
+ limit: 1000
65
+ });
66
+ if (docs.length === 0) {
67
+ // No documents to sync
68
+ return;
69
+ }
70
+ // Batch sync documents
71
+ const batchSize = 100;
72
+ for(let i = 0; i < docs.length; i += batchSize){
73
+ const batch = docs.slice(i, i + batchSize);
74
+ const typesenseDocs = batch.map((doc)=>mapPayloadDocumentToTypesense(doc, collectionSlug, config));
75
+ try {
76
+ const _importResult = await typesenseClient.collections(collectionSlug).documents().import(typesenseDocs, {
77
+ action: 'upsert'
78
+ });
79
+ // Documents synced successfully
80
+ } catch (batchError) {
81
+ // Handle batch sync error
82
+ // Log detailed import results if available
83
+ if (batchError.importResults) {
84
+ // Handle import results error
85
+ // Try to sync documents individually to identify problematic ones
86
+ // Attempt individual document sync
87
+ for(let j = 0; j < typesenseDocs.length; j++){
88
+ try {
89
+ await typesenseClient.collections(collectionSlug).documents().upsert(typesenseDocs[j]);
90
+ // Individual sync successful
91
+ } catch (_individualError) {
92
+ // Handle individual sync error
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ // Successfully synced documents
99
+ } catch (_error) {
100
+ // Handle document sync error
101
+ }
102
+ };
@@ -0,0 +1,14 @@
1
+ import type { Collection } from 'payload';
2
+ import type { TypesenseSearchConfig } from '../index.js';
3
+ export declare const mapCollectionToTypesenseSchema: (collection: Collection, collectionSlug: string, config: NonNullable<TypesenseSearchConfig["collections"]>[string] | undefined) => {
4
+ name: string;
5
+ fields: ({
6
+ name: string;
7
+ type: "string";
8
+ } | {
9
+ name: string;
10
+ type: "int64";
11
+ })[];
12
+ };
13
+ export declare const mapPayloadDocumentToTypesense: (doc: any, collectionSlug: string, config: NonNullable<TypesenseSearchConfig["collections"]>[string] | undefined) => any;
14
+ //# sourceMappingURL=schema-mapper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-mapper.d.ts","sourceRoot":"","sources":["../../src/lib/schema-mapper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAEzC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AA6BxD,eAAO,MAAM,8BAA8B,GACzC,YAAY,UAAU,EACtB,gBAAgB,MAAM,EACtB,QAAQ,WAAW,CAAC,qBAAqB,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,SAAS;;;;;;;;;CAyC9E,CAAA;AAED,eAAO,MAAM,6BAA6B,GACxC,KAAK,GAAG,EACR,gBAAgB,MAAM,EACtB,QAAQ,WAAW,CAAC,qBAAqB,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,SAAS,QA+E9E,CAAA"}
@@ -0,0 +1,137 @@
1
+ // Helper function to extract text content from richText structure
2
+ const extractTextFromRichText = (richText)=>{
3
+ if (!richText || !richText.root) {
4
+ return '';
5
+ }
6
+ const extractText = (node)=>{
7
+ if (typeof node === 'string') {
8
+ return node;
9
+ }
10
+ if (node && typeof node === 'object') {
11
+ if (node.text) {
12
+ return node.text;
13
+ }
14
+ if (node.children && Array.isArray(node.children)) {
15
+ return node.children.map(extractText).join('');
16
+ }
17
+ }
18
+ return '';
19
+ };
20
+ return extractText(richText.root);
21
+ };
22
+ export const mapCollectionToTypesenseSchema = (collection, collectionSlug, config)=>{
23
+ const searchableFields = config?.searchFields || [
24
+ 'title',
25
+ 'content',
26
+ 'description'
27
+ ];
28
+ const facetFields = config?.facetFields || [];
29
+ // Map schema for collection
30
+ // Base fields that every collection should have
31
+ // Note: 'id' field is automatically created by Typesense, so we don't include it
32
+ const baseFields = [
33
+ {
34
+ name: 'slug',
35
+ type: 'string'
36
+ },
37
+ {
38
+ name: 'createdAt',
39
+ type: 'int64'
40
+ },
41
+ {
42
+ name: 'updatedAt',
43
+ type: 'int64'
44
+ }
45
+ ];
46
+ // Map searchable fields
47
+ const searchFields = searchableFields.map((field)=>({
48
+ name: field,
49
+ type: 'string',
50
+ facet: facetFields.includes(field)
51
+ }));
52
+ // Process search fields
53
+ // Map facet-only fields (not in searchable fields)
54
+ const facetOnlyFields = facetFields.filter((field)=>!searchableFields.includes(field)).map((field)=>({
55
+ name: field,
56
+ type: 'string',
57
+ facet: true
58
+ }));
59
+ const finalSchema = {
60
+ name: collectionSlug,
61
+ fields: [
62
+ ...baseFields,
63
+ ...searchFields,
64
+ ...facetOnlyFields
65
+ ]
66
+ };
67
+ // Return final schema
68
+ return finalSchema;
69
+ };
70
+ export const mapPayloadDocumentToTypesense = (doc, collectionSlug, config)=>{
71
+ const searchableFields = config?.searchFields || [
72
+ 'title',
73
+ 'content',
74
+ 'description'
75
+ ];
76
+ const facetFields = config?.facetFields || [];
77
+ // Validate required fields
78
+ if (!doc.id) {
79
+ throw new Error(`Document missing required 'id' field: ${JSON.stringify(doc)}`);
80
+ }
81
+ if (!doc.createdAt) {
82
+ // Use current time for missing createdAt
83
+ doc.createdAt = new Date();
84
+ }
85
+ if (!doc.updatedAt) {
86
+ // Use current time for missing updatedAt
87
+ doc.updatedAt = new Date();
88
+ }
89
+ // Base document structure with safe date handling
90
+ const typesenseDoc = {
91
+ id: String(doc.id),
92
+ slug: doc.slug || '',
93
+ createdAt: new Date(doc.createdAt).getTime(),
94
+ updatedAt: new Date(doc.updatedAt).getTime()
95
+ };
96
+ // Add searchable fields with validation
97
+ searchableFields.forEach((field)=>{
98
+ // Handle array fields with dot notation (e.g., 'technologies.name', 'tags.tag')
99
+ if (field.includes('.')) {
100
+ const [arrayField, subField] = field.split('.', 2);
101
+ if (arrayField && subField && Array.isArray(doc[arrayField]) && doc[arrayField].length > 0) {
102
+ typesenseDoc[field] = doc[arrayField].map((item)=>item?.[subField] || '').join(' ');
103
+ } else {
104
+ typesenseDoc[field] = '';
105
+ }
106
+ } else if (doc[field] !== undefined && doc[field] !== null) {
107
+ // Handle richText fields specially
108
+ if ((field === 'content' || field === 'description') && typeof doc[field] === 'object' && doc[field].root) {
109
+ // Extract text from richText structure
110
+ typesenseDoc[field] = extractTextFromRichText(doc[field]);
111
+ } else {
112
+ // Convert to string for other fields
113
+ typesenseDoc[field] = String(doc[field]);
114
+ }
115
+ } else {
116
+ // Set empty string for missing fields
117
+ typesenseDoc[field] = '';
118
+ }
119
+ });
120
+ // Add facet fields with validation - ensure all facet fields are present
121
+ facetFields.forEach((field)=>{
122
+ if (doc[field] !== undefined && doc[field] !== null) {
123
+ // Convert to string for facet fields
124
+ typesenseDoc[field] = String(doc[field]);
125
+ } else {
126
+ // Add default value for missing facet fields
127
+ typesenseDoc[field] = 'unknown';
128
+ }
129
+ });
130
+ // Validate that we have at least one searchable field
131
+ const hasSearchableContent = searchableFields.some((field)=>typesenseDoc[field] && typesenseDoc[field].trim().length > 0);
132
+ if (!hasSearchableContent) {
133
+ // Add placeholder for missing content
134
+ typesenseDoc.title = typesenseDoc.title || `Document ${doc.id}`;
135
+ }
136
+ return typesenseDoc;
137
+ };