@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.
- package/LICENSE +22 -0
- package/README.md +182 -0
- package/dist/components/HeadlessSearchInput.d.ts +86 -0
- package/dist/components/HeadlessSearchInput.d.ts.map +1 -0
- package/dist/components/HeadlessSearchInput.js +602 -0
- package/dist/components/ThemeProvider.d.ts +10 -0
- package/dist/components/ThemeProvider.d.ts.map +1 -0
- package/dist/components/ThemeProvider.js +17 -0
- package/dist/components/index.d.ts +6 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +3 -0
- package/dist/components/themes/hooks.d.ts +52 -0
- package/dist/components/themes/hooks.d.ts.map +1 -0
- package/dist/components/themes/hooks.js +177 -0
- package/dist/components/themes/index.d.ts +5 -0
- package/dist/components/themes/index.d.ts.map +1 -0
- package/dist/components/themes/index.js +4 -0
- package/dist/components/themes/themes.d.ts +6 -0
- package/dist/components/themes/themes.d.ts.map +1 -0
- package/dist/components/themes/themes.js +156 -0
- package/dist/components/themes/types.d.ts +147 -0
- package/dist/components/themes/types.d.ts.map +1 -0
- package/dist/components/themes/types.js +1 -0
- package/dist/components/themes/utils.d.ts +30 -0
- package/dist/components/themes/utils.d.ts.map +1 -0
- package/dist/components/themes/utils.js +397 -0
- package/dist/endpoints/customEndpointHandler.d.ts +3 -0
- package/dist/endpoints/customEndpointHandler.d.ts.map +1 -0
- package/dist/endpoints/customEndpointHandler.js +5 -0
- package/dist/endpoints/health.d.ts +12 -0
- package/dist/endpoints/health.d.ts.map +1 -0
- package/dist/endpoints/health.js +174 -0
- package/dist/endpoints/search.d.ts +13 -0
- package/dist/endpoints/search.d.ts.map +1 -0
- package/dist/endpoints/search.js +375 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +148 -0
- package/dist/lib/cache.d.ts +41 -0
- package/dist/lib/cache.d.ts.map +1 -0
- package/dist/lib/cache.js +96 -0
- package/dist/lib/config-validation.d.ts +75 -0
- package/dist/lib/config-validation.d.ts.map +1 -0
- package/dist/lib/config-validation.js +174 -0
- package/dist/lib/hooks.d.ts +4 -0
- package/dist/lib/hooks.d.ts.map +1 -0
- package/dist/lib/hooks.js +54 -0
- package/dist/lib/initialization.d.ts +5 -0
- package/dist/lib/initialization.d.ts.map +1 -0
- package/dist/lib/initialization.js +102 -0
- package/dist/lib/schema-mapper.d.ts +14 -0
- package/dist/lib/schema-mapper.d.ts.map +1 -0
- package/dist/lib/schema-mapper.js +137 -0
- package/dist/lib/types.d.ts +183 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/typesense-client.d.ts +5 -0
- package/dist/lib/typesense-client.d.ts.map +1 -0
- package/dist/lib/typesense-client.js +20 -0
- 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
|
+
};
|