@rebasepro/client-firebase 0.0.1-canary.4d4fb3e

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 (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4 -0
  3. package/dist/components/FirebaseLoginView.d.ts +72 -0
  4. package/dist/components/RebaseFirebaseApp.d.ts +19 -0
  5. package/dist/components/RebaseFirebaseAppProps.d.ts +144 -0
  6. package/dist/components/index.d.ts +3 -0
  7. package/dist/components/social_icons.d.ts +6 -0
  8. package/dist/hooks/index.d.ts +7 -0
  9. package/dist/hooks/useAppCheck.d.ts +20 -0
  10. package/dist/hooks/useFirebaseAuthController.d.ts +15 -0
  11. package/dist/hooks/useFirebaseRealTimeDBDelegate.d.ts +5 -0
  12. package/dist/hooks/useFirebaseStorageSource.d.ts +14 -0
  13. package/dist/hooks/useFirestoreDriver.d.ts +56 -0
  14. package/dist/hooks/useInitialiseFirebase.d.ts +34 -0
  15. package/dist/hooks/useRecaptcha.d.ts +8 -0
  16. package/dist/index.d.ts +4 -0
  17. package/dist/index.es.js +2757 -0
  18. package/dist/index.es.js.map +1 -0
  19. package/dist/index.umd.js +2743 -0
  20. package/dist/index.umd.js.map +1 -0
  21. package/dist/social_icons.d.ts +6 -0
  22. package/dist/types/appcheck.d.ts +10 -0
  23. package/dist/types/auth.d.ts +41 -0
  24. package/dist/types/index.d.ts +3 -0
  25. package/dist/types/text_search.d.ts +39 -0
  26. package/dist/utils/algolia.d.ts +9 -0
  27. package/dist/utils/collections_firestore.d.ts +5 -0
  28. package/dist/utils/database.d.ts +2 -0
  29. package/dist/utils/index.d.ts +7 -0
  30. package/dist/utils/local_text_search_controller.d.ts +2 -0
  31. package/dist/utils/pinecone.d.ts +24 -0
  32. package/dist/utils/rebase_search_controller.d.ts +73 -0
  33. package/dist/utils/text_search_controller.d.ts +13 -0
  34. package/package.json +61 -0
  35. package/src/components/FirebaseLoginView.tsx +703 -0
  36. package/src/components/RebaseFirebaseApp.tsx +275 -0
  37. package/src/components/RebaseFirebaseAppProps.tsx +180 -0
  38. package/src/components/index.ts +3 -0
  39. package/src/components/social_icons.tsx +135 -0
  40. package/src/hooks/index.ts +7 -0
  41. package/src/hooks/useAppCheck.ts +101 -0
  42. package/src/hooks/useFirebaseAuthController.ts +334 -0
  43. package/src/hooks/useFirebaseRealTimeDBDelegate.ts +269 -0
  44. package/src/hooks/useFirebaseStorageSource.ts +208 -0
  45. package/src/hooks/useFirestoreDriver.ts +778 -0
  46. package/src/hooks/useInitialiseFirebase.ts +132 -0
  47. package/src/hooks/useRecaptcha.tsx +28 -0
  48. package/src/index.ts +4 -0
  49. package/src/social_icons.tsx +135 -0
  50. package/src/types/appcheck.ts +11 -0
  51. package/src/types/auth.tsx +74 -0
  52. package/src/types/index.ts +3 -0
  53. package/src/types/text_search.ts +42 -0
  54. package/src/utils/algolia.ts +27 -0
  55. package/src/utils/collections_firestore.ts +149 -0
  56. package/src/utils/database.ts +39 -0
  57. package/src/utils/index.ts +7 -0
  58. package/src/utils/local_text_search_controller.ts +143 -0
  59. package/src/utils/pinecone.ts +75 -0
  60. package/src/utils/rebase_search_controller.ts +356 -0
  61. package/src/utils/text_search_controller.ts +34 -0
@@ -0,0 +1,143 @@
1
+ import { collection, getFirestore, onSnapshot, query } from "@firebase/firestore";
2
+ import Fuse from "fuse.js"
3
+
4
+ import { FirebaseApp } from "@firebase/app";
5
+ import { EntityCollection } from "@rebasepro/types";
6
+ import { FirestoreTextSearchController, FirestoreTextSearchControllerBuilder } from "../types";
7
+
8
+ const MAX_SEARCH_RESULTS = 80;
9
+
10
+ export const localSearchControllerBuilder: FirestoreTextSearchControllerBuilder = ({
11
+ firebaseApp,
12
+ }: {
13
+
14
+ firebaseApp: FirebaseApp,
15
+ }): FirestoreTextSearchController => {
16
+
17
+ let currentPath: string | undefined;
18
+ const indexes: Record<string, Fuse<object & { id: string }>> = {};
19
+ const listeners: Record<string, () => void> = {};
20
+
21
+ const destroyListener = (path: string) => {
22
+ if (listeners[path]) {
23
+ listeners[path]();
24
+ delete listeners[path];
25
+ delete indexes[path];
26
+ }
27
+ }
28
+
29
+ const init = ({
30
+ path,
31
+ collection: collectionProp,
32
+ databaseId
33
+ }: {
34
+ path: string,
35
+ collection?: EntityCollection,
36
+ databaseId?: string
37
+ }): Promise<boolean> => {
38
+
39
+ if (currentPath && path !== currentPath) {
40
+ destroyListener(currentPath)
41
+ }
42
+
43
+ currentPath = path;
44
+
45
+ return new Promise(async (resolve, reject) => {
46
+ if (collectionProp) {
47
+ console.debug("Init local search controller", path);
48
+ const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
49
+ const col = collection(firestore, path);
50
+ listeners[path] = onSnapshot(query(col),
51
+ {
52
+ next: (snapshot) => {
53
+ if (snapshot.metadata.fromCache && snapshot.metadata.hasPendingWrites) {
54
+ return;
55
+ }
56
+ const docs = snapshot.docs.map(doc => ({
57
+ id: doc.id,
58
+ ...doc.data()
59
+ }));
60
+
61
+ indexes[path] = buildIndex(docs, collectionProp);
62
+ console.debug("Added docs to index", path, docs.length);
63
+ resolve(true);
64
+ },
65
+ error: (e) => {
66
+ console.error("Error initializing local search controller", path);
67
+ console.error(e);
68
+ reject(e);
69
+ }
70
+ }
71
+ );
72
+ }
73
+ });
74
+ }
75
+
76
+ const search = async ({
77
+ searchString,
78
+ path
79
+ }: {
80
+ searchString: string,
81
+ path: string,
82
+ databaseId?: string
83
+ }) => {
84
+ console.debug("Searching local index", path, searchString);
85
+ const index = indexes[path];
86
+ if (!index) {
87
+ throw new Error(`Index not found for path ${path}`);
88
+ }
89
+ let searchResult = index.search(searchString);
90
+ searchResult = searchResult.splice(0, MAX_SEARCH_RESULTS);
91
+ searchResult = searchResult.sort((a, b) => {
92
+ // Check if item A is an exact match
93
+ const aExactMatch = a.item.id === searchString;
94
+ // Check if item B is an exact match
95
+ const bExactMatch = b.item.id === searchString;
96
+
97
+ if (aExactMatch && !bExactMatch) {
98
+ return -1; // Prioritize item A
99
+ } else if (!aExactMatch && bExactMatch) {
100
+ return 1; // Prioritize item B
101
+ } else {
102
+ // If both are exact matches or both are not, sort by Fuse's original score
103
+ return (a.score ?? 0) - (b.score ?? 0);
104
+ }
105
+ });
106
+ return searchResult.map((e: any) => e.item.id);
107
+ };
108
+
109
+ return {
110
+ init,
111
+ search,
112
+ }
113
+ }
114
+
115
+ function buildIndex(list: (object & { id: string })[], collection: EntityCollection) {
116
+
117
+ const keys = ["id", ...Object.keys(collection.properties)];
118
+
119
+ const fuseOptions = {
120
+ // isCaseSensitive: false,
121
+ // includeScore: false,
122
+ // shouldSort: true,
123
+ // includeMatches: false,
124
+ // findAllMatches: false,
125
+ // minMatchCharLength: 1,
126
+ // location: 0,
127
+ threshold: 0.6,
128
+ // distance: 100,
129
+ // useExtendedSearch: false,
130
+ // ignoreLocation: false,
131
+ // ignoreFieldNorm: false,
132
+ // fieldNormWeight: 1,
133
+ includeScore: true,
134
+ keys: [{
135
+ name: "title",
136
+ weight: 1.0
137
+ }, ...keys.map(key => ({
138
+ name: key,
139
+ weight: 0.5
140
+ }))]
141
+ };
142
+ return new Fuse<object & { id: string }>(list, fuseOptions);
143
+ }
@@ -0,0 +1,75 @@
1
+ import { User as FirebaseUser } from "@firebase/auth";
2
+ import { FirestoreTextSearchController, FirestoreTextSearchControllerBuilder } from "../types";
3
+ import { EntityCollection } from "@rebasepro/types";
4
+
5
+ const DEFAULT_SERVER = "https://api.rebase.pro";
6
+
7
+ /**
8
+ * Utility function to perform a text search in an algolia index,
9
+ * returning the ids of the entities.
10
+ * @param index
11
+ * @param query
12
+ * @group Firebase
13
+ */
14
+ export async function performPineconeTextSearch({
15
+ host = DEFAULT_SERVER,
16
+ firebaseToken,
17
+ projectId,
18
+ collectionPath,
19
+ query
20
+ }: {
21
+ host?: string,
22
+ firebaseToken: string,
23
+ collectionPath: string,
24
+ projectId: string,
25
+ query: string
26
+ }): Promise<readonly string[]> {
27
+
28
+ console.debug("Performing Pinecone query", collectionPath, query);
29
+ const response = await fetch((host ?? DEFAULT_SERVER) + `/projects/${projectId}/search/${collectionPath}`,
30
+ {
31
+ // mode: "no-cors",
32
+ method: "POST",
33
+ headers: {
34
+ "Content-Type": "application/json",
35
+ Authorization: `Basic ${firebaseToken}`,
36
+ // "x-de-version": version
37
+ },
38
+ body: JSON.stringify({
39
+ query
40
+ })
41
+ });
42
+
43
+ const promise = await response.json();
44
+ return promise.data.ids;
45
+
46
+ }
47
+
48
+ export function buildPineconeSearchController({
49
+ isPathSupported,
50
+ search
51
+ }: {
52
+ isPathSupported: (path: string) => boolean,
53
+ search: (props: {
54
+ searchString: string,
55
+ path: string,
56
+ currentUser?: FirebaseUser
57
+ }) => Promise<readonly string[] | undefined>,
58
+ }): FirestoreTextSearchControllerBuilder {
59
+ return (props): FirestoreTextSearchController => {
60
+
61
+ const init = (props: {
62
+ path: string,
63
+ collection?: EntityCollection
64
+ }) => {
65
+ // do nothing
66
+ return Promise.resolve(isPathSupported(props.path));
67
+ }
68
+
69
+ return {
70
+ init,
71
+ search
72
+ }
73
+ }
74
+
75
+ }
@@ -0,0 +1,356 @@
1
+ import { FirestoreTextSearchController, FirestoreTextSearchControllerBuilder } from "../types";
2
+ import { FirebaseApp } from "@firebase/app";
3
+ import { getFunctions, httpsCallable } from "@firebase/functions";
4
+ import { EntityCollection } from "@rebasepro/types";
5
+
6
+ /**
7
+ * Configuration returned by the Rebase Search Extension
8
+ */
9
+ interface SearchConfig {
10
+ host: string;
11
+ port: number;
12
+ protocol: "http" | "https";
13
+ apiKey: string;
14
+ collectionsToIndex: string[];
15
+ path?: string;
16
+ }
17
+ /**
18
+ * Options for building the Rebase Search Controller
19
+ */
20
+ export interface RebaseSearchControllerOptions {
21
+ /**
22
+ * The Firebase region where the extension is deployed.
23
+ */
24
+ region: string;
25
+
26
+ /**
27
+ * The extension instance ID. Defaults to "rebase-search".
28
+ * Use this if you installed the extension with a custom instance ID.
29
+ */
30
+ extensionInstanceId?: string;
31
+
32
+ /**
33
+ * Custom Typesense configuration. If provided, skips fetching from extension.
34
+ * Use this if you want to connect to your own Typesense instance.
35
+ */
36
+ customConfig?: {
37
+ host: string;
38
+ port?: number;
39
+ protocol?: "http" | "https";
40
+ apiKey: string;
41
+ path?: string;
42
+ };
43
+
44
+ /**
45
+ * Override the collections to index returned by the extension.
46
+ * Use this if you want to restrict search to specific collections on the client side,
47
+ * regardless of what is configured in the extension.
48
+ */
49
+ collections?: string[];
50
+ }
51
+
52
+ /**
53
+ * Creates a text search controller that uses the Rebase Search Extension.
54
+ *
55
+ * This requires the `rebase-search` extension to be installed in the user's
56
+ * Firebase project. The extension automatically deploys Typesense to Cloud Run
57
+ * and syncs Firestore data.
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * import { buildRebaseSearchController } from "@rebasepro/client-firebase";
62
+ *
63
+ * // Using the extension (recommended)
64
+ * const textSearchControllerBuilder = buildRebaseSearchController();
65
+ *
66
+ * // Or with custom Typesense instance
67
+ * const textSearchControllerBuilder = buildRebaseSearchController({
68
+ * customConfig: {
69
+ * host: "your-typesense-instance.com",
70
+ * apiKey: "your-api-key"
71
+ * }
72
+ * });
73
+ *
74
+ * <RebaseApp
75
+ * textSearchControllerBuilder={textSearchControllerBuilder}
76
+ * collections={[
77
+ * {
78
+ * path: "products",
79
+ * name: "Products",
80
+ * textSearchEnabled: true, // Enable search for this collection
81
+ * properties: { ... }
82
+ * }
83
+ * ]}
84
+ * />
85
+ * ```
86
+ *
87
+ * @param options - Configuration options
88
+ * @returns A FirestoreTextSearchControllerBuilder
89
+ *
90
+ * @group Firebase
91
+ */
92
+ export function buildRebaseSearchController(
93
+ options?: RebaseSearchControllerOptions
94
+ ): FirestoreTextSearchControllerBuilder {
95
+ const region = options?.region || "us-central1";
96
+ const extensionInstanceId = options?.extensionInstanceId || "typesense-search";
97
+
98
+ let searchConfig: SearchConfig | null = null;
99
+ let typesenseClient: any = null;
100
+ let initPromise: Promise<void> | null = null;
101
+
102
+ return ({ firebaseApp }: { firebaseApp: FirebaseApp }): FirestoreTextSearchController => {
103
+
104
+ /**
105
+ * Initializes the Typesense client
106
+ */
107
+ const initializeClient = async (): Promise<void> => {
108
+ if (typesenseClient) return;
109
+
110
+ // Use custom config if provided
111
+ if (options?.customConfig) {
112
+ searchConfig = {
113
+ host: options.customConfig.host,
114
+ port: options.customConfig.port || 443,
115
+ protocol: options.customConfig.protocol || "https",
116
+ apiKey: options.customConfig.apiKey,
117
+ path: options.customConfig.path,
118
+ collectionsToIndex: ["*"],
119
+ };
120
+ } else {
121
+ // Fetch config from extension
122
+ const functions = getFunctions(firebaseApp, region);
123
+ const getConfig = httpsCallable<void, SearchConfig>(
124
+ functions,
125
+ `ext-${extensionInstanceId}-getSearchConfig`
126
+ );
127
+
128
+ try {
129
+ const result = await getConfig();
130
+ searchConfig = result.data;
131
+ if (options?.collections && options.collections.length > 0) {
132
+ searchConfig.collectionsToIndex = options.collections;
133
+ }
134
+ } catch (error: any) {
135
+ console.error("Failed to get search config from extension:", error);
136
+ throw new Error(
137
+ "Failed to initialize Rebase Search. " +
138
+ "Make sure the rebase-search extension is installed and configured. " +
139
+ `Error: ${error.message || error}`
140
+ );
141
+ }
142
+ }
143
+
144
+ if (!searchConfig) {
145
+ throw new Error("Search config not available");
146
+ }
147
+
148
+ // Dynamically import Typesense client to avoid bundling if not used
149
+ const Typesense = (await import("typesense")).default;
150
+
151
+ typesenseClient = new Typesense.Client({
152
+ nodes: [{
153
+ host: searchConfig.host,
154
+ port: searchConfig.port,
155
+ protocol: searchConfig.protocol,
156
+ path: searchConfig.path || "",
157
+ }],
158
+ apiKey: searchConfig.apiKey,
159
+ connectionTimeoutSeconds: 5,
160
+ retryIntervalSeconds: 0.5,
161
+ numRetries: 2,
162
+ });
163
+ };
164
+
165
+ /**
166
+ * Converts a Firestore path to Typesense collection name
167
+ * e.g., "users/123/orders" -> "users_orders"
168
+ */
169
+ const getTypesenseCollectionName = (path: string): string => {
170
+ const pathParts = path.split("/");
171
+ // Extract collection names (even indices) and join with underscore
172
+ const collectionNames: string[] = [];
173
+ for (let i = 0; i < pathParts.length; i += 2) {
174
+ if (pathParts[i]) {
175
+ collectionNames.push(pathParts[i]);
176
+ }
177
+ }
178
+ return collectionNames.join("_");
179
+ };
180
+
181
+ /**
182
+ * Extracts parent filter for subcollection queries
183
+ * e.g., "users/123/orders" -> { "_parent_users_id": "123" }
184
+ */
185
+ const getParentFilter = (path: string): string | null => {
186
+ const pathParts = path.split("/");
187
+ if (pathParts.length <= 1) return null;
188
+
189
+ // Build filter for parent IDs
190
+ const filters: string[] = [];
191
+ for (let i = 0; i < pathParts.length - 1; i += 2) {
192
+ const collectionName = pathParts[i];
193
+ const docId = pathParts[i + 1];
194
+ if (collectionName && docId) {
195
+ filters.push(`_parent_${collectionName}_id:=${docId}`);
196
+ }
197
+ }
198
+
199
+ return filters.length > 0 ? filters.join(" && ") : null;
200
+ };
201
+
202
+ /**
203
+ * Initializes search for a specific collection path
204
+ */
205
+ const init = async (props: {
206
+ path: string;
207
+ collection?: EntityCollection;
208
+ databaseId?: string;
209
+ }): Promise<boolean> => {
210
+ try {
211
+ // Ensure client is initialized (only once)
212
+ if (!initPromise) {
213
+ initPromise = initializeClient();
214
+ }
215
+ await initPromise;
216
+
217
+ if (!searchConfig) return false;
218
+
219
+ // Get collection pattern (e.g., "users/orders" from "users/123/orders")
220
+ const pathParts = props.path.split("/");
221
+ const collectionNames: string[] = [];
222
+ for (let i = 0; i < pathParts.length; i += 2) {
223
+ if (pathParts[i]) collectionNames.push(pathParts[i]);
224
+ }
225
+ const collectionPattern = collectionNames.join("/");
226
+ const rootCollection = collectionNames[0];
227
+
228
+ // Check if this collection is indexed
229
+ if (searchConfig.collectionsToIndex.includes("*")) {
230
+ return true;
231
+ }
232
+
233
+ // Check exact pattern or root collection
234
+ return searchConfig.collectionsToIndex.includes(collectionPattern) ||
235
+ searchConfig.collectionsToIndex.includes(rootCollection);
236
+ } catch (error) {
237
+ console.error("Failed to initialize Rebase Search:", error);
238
+ return false;
239
+ }
240
+ };
241
+
242
+ // Cache for Typesense collection schemas (field names)
243
+ const schemaCache: Map<string, string[]> = new Map();
244
+
245
+ /**
246
+ * Fetches the Typesense collection schema and returns searchable string field names.
247
+ * Results are cached to avoid repeated API calls.
248
+ */
249
+ const getSearchableFieldsFromSchema = async (collectionName: string): Promise<string[]> => {
250
+ // Check cache first
251
+ if (schemaCache.has(collectionName)) {
252
+ return schemaCache.get(collectionName)!;
253
+ }
254
+
255
+ try {
256
+ const collection = await typesenseClient.collections(collectionName).retrieve();
257
+
258
+ // Extract string fields from the schema
259
+ const stringFields = collection.fields
260
+ .filter((f: any) => {
261
+ // Include string and string[] types, exclude internal fields
262
+ const isStringType = f.type === "string" ||
263
+ f.type === "string[]" ||
264
+ f.type === "string*" ||
265
+ f.type === "auto";
266
+ const isNotInternal = !f.name.startsWith("_") && f.name !== ".*";
267
+ return isStringType && isNotInternal;
268
+ })
269
+ .map((f: any) => f.name);
270
+
271
+ schemaCache.set(collectionName, stringFields);
272
+ return stringFields;
273
+ } catch (error: any) {
274
+ if (error.httpStatus === 404) {
275
+ throw new Error(
276
+ `Collection "${collectionName}" not found in Typesense. ` +
277
+ "Make sure the collection has been indexed. Try running the backfill function."
278
+ );
279
+ }
280
+ throw error;
281
+ }
282
+ };
283
+
284
+ /**
285
+ * Performs a search and returns document IDs
286
+ * Supports subcollections by filtering on parent IDs
287
+ */
288
+ const search = async (props: {
289
+ searchString: string;
290
+ path: string;
291
+ databaseId?: string;
292
+ collection?: EntityCollection;
293
+ }): Promise<readonly string[] | undefined> => {
294
+ if (!typesenseClient) {
295
+ // Ensure client is initialized
296
+ if (!initPromise) {
297
+ initPromise = initializeClient();
298
+ }
299
+ await initPromise;
300
+ }
301
+
302
+ if (!typesenseClient) {
303
+ throw new Error("Typesense client not initialized. Check extension configuration.");
304
+ }
305
+
306
+ // Convert path to Typesense collection name
307
+ const collectionName = getTypesenseCollectionName(props.path);
308
+
309
+ // Get parent filter for subcollections
310
+ const parentFilter = getParentFilter(props.path);
311
+
312
+ // Get searchable fields from the actual Typesense schema
313
+ const searchableFields = await getSearchableFieldsFromSchema(collectionName);
314
+
315
+ if (searchableFields.length === 0) {
316
+ throw new Error(
317
+ `No searchable string fields found in Typesense collection "${collectionName}". ` +
318
+ "Make sure some documents have been indexed with string fields."
319
+ );
320
+ }
321
+
322
+ const queryBy = searchableFields.join(",");
323
+
324
+ try {
325
+ const searchParams: any = {
326
+ q: props.searchString,
327
+ query_by: queryBy,
328
+ per_page: 100,
329
+ prefix: true, // Enable prefix matching
330
+ typo_tokens_threshold: 1, // Allow some typos
331
+ };
332
+
333
+ // Add filter for subcollection queries
334
+ if (parentFilter) {
335
+ searchParams.filter_by = parentFilter;
336
+ }
337
+
338
+ const result = await typesenseClient
339
+ .collections(collectionName)
340
+ .documents()
341
+ .search(searchParams);
342
+
343
+ // Extract document IDs from hits
344
+ const ids = result.hits?.map((hit: any) => hit.document.id) ?? [];
345
+
346
+ return ids as readonly string[];
347
+ } catch (error: any) {
348
+ // Parse error message for user-friendly display
349
+ const message = error.message || error.toString();
350
+ throw new Error(`Search failed: ${message}`);
351
+ }
352
+ };
353
+
354
+ return { init, search };
355
+ };
356
+ }
@@ -0,0 +1,34 @@
1
+ import { FirestoreTextSearchController, FirestoreTextSearchControllerBuilder } from "../types";
2
+ import { EntityCollection } from "@rebasepro/types";
3
+
4
+ /**
5
+ * Utility function to perform a text search in an external index,
6
+ * returning the ids of the entities.
7
+ * @group Firebase
8
+ */
9
+ export function buildExternalSearchController({
10
+ isPathSupported,
11
+ search
12
+ }: {
13
+ isPathSupported: (path: string) => boolean,
14
+ search: (props: {
15
+ searchString: string,
16
+ path: string
17
+ }) => Promise<readonly string[] | undefined>,
18
+ }): FirestoreTextSearchControllerBuilder {
19
+ return (props): FirestoreTextSearchController => {
20
+
21
+ const init = (props: {
22
+ path: string,
23
+ collection?: EntityCollection
24
+ }) => {
25
+ return Promise.resolve(isPathSupported(props.path));
26
+ }
27
+
28
+ return {
29
+ init,
30
+ search
31
+ }
32
+ }
33
+
34
+ }