From 05dd5879289cdd8caabe456f3931c0408da85c85 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Thu, 13 Mar 2025 18:24:38 -0700 Subject: [PATCH] [Manager] Add Algolia search (#3036) --- .github/workflows/release.yaml | 2 + package-lock.json | 191 ++++++++++++++++++ package.json | 1 + .../content/manager/ManagerDialogContent.vue | 23 +-- .../registrySearchBar/RegistrySearchBar.vue | 45 ++--- .../SearchFilterDropdown.vue | 20 +- src/composables/useRegistrySearch.ts | 107 +++++----- src/services/algoliaSearchService.ts | 153 ++++++++++++++ src/types/algoliasearch-lite.d.ts | 19 ++ vite.config.mts | 4 +- 10 files changed, 449 insertions(+), 116 deletions(-) create mode 100644 src/services/algoliaSearchService.ts create mode 100644 src/types/algoliasearch-lite.d.ts diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d5004d5a8..68be19441 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -27,6 +27,8 @@ jobs: - name: Build project env: SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }} + ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }} run: | npm ci npm run fetch-templates diff --git a/package-lock.json b/package-lock.json index d0cd31bc6..19aa7766f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "@xterm/addon-fit": "^0.10.0", "@xterm/addon-serialize": "^0.13.0", "@xterm/xterm": "^5.5.0", + "algoliasearch": "^5.21.0", "axios": "^1.8.2", "dotenv": "^16.4.5", "fuse.js": "^7.0.0", @@ -113,6 +114,173 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/@algolia/client-abtesting": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.21.0.tgz", + "integrity": "sha512-I239aSmXa3pXDhp3AWGaIfesqJBNFA7drUM8SIfNxMIzvQXUnHRf4rW1o77QXLI/nIClNsb8KOLaB62gO9LnlQ==", + "dependencies": { + "@algolia/client-common": "5.21.0", + "@algolia/requester-browser-xhr": "5.21.0", + "@algolia/requester-fetch": "5.21.0", + "@algolia/requester-node-http": "5.21.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.21.0.tgz", + "integrity": "sha512-OxoUfeG9G4VE4gS7B4q65KkHzdGsQsDwxQfR5J9uKB8poSGuNlHJWsF3ABqCkc5VliAR0m8KMjsQ9o/kOpEGnQ==", + "dependencies": { + "@algolia/client-common": "5.21.0", + "@algolia/requester-browser-xhr": "5.21.0", + "@algolia/requester-fetch": "5.21.0", + "@algolia/requester-node-http": "5.21.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.21.0.tgz", + "integrity": "sha512-iHLgDQFyZNe9M16vipbx6FGOA8NoMswHrfom/QlCGoyh7ntjGvfMb+J2Ss8rRsAlOWluv8h923Ku3QVaB0oWDQ==", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.21.0.tgz", + "integrity": "sha512-y7XBO9Iwb75FLDl95AYcWSLIViJTpR5SUUCyKsYhpP9DgyUqWbISqDLXc96TS9shj+H+7VsTKA9cJK8NUfVN6g==", + "dependencies": { + "@algolia/client-common": "5.21.0", + "@algolia/requester-browser-xhr": "5.21.0", + "@algolia/requester-fetch": "5.21.0", + "@algolia/requester-node-http": "5.21.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.21.0.tgz", + "integrity": "sha512-6KU658lD9Tss4oCX6c/O15tNZxw7vR+WAUG95YtZzYG/KGJHTpy2uckqbMmC2cEK4a86FAq4pH5azSJ7cGMjuw==", + "dependencies": { + "@algolia/client-common": "5.21.0", + "@algolia/requester-browser-xhr": "5.21.0", + "@algolia/requester-fetch": "5.21.0", + "@algolia/requester-node-http": "5.21.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.21.0.tgz", + "integrity": "sha512-pG6MyVh1v0X+uwrKHn3U+suHdgJ2C+gug+UGkNHfMELHMsEoWIAQhxMBOFg7hCnWBFjQnuq6qhM3X9X5QO3d9Q==", + "dependencies": { + "@algolia/client-common": "5.21.0", + "@algolia/requester-browser-xhr": "5.21.0", + "@algolia/requester-fetch": "5.21.0", + "@algolia/requester-node-http": "5.21.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.21.0.tgz", + "integrity": "sha512-nZfgJH4njBK98tFCmCW1VX/ExH4bNOl9DSboxeXGgvhoL0fG1+4DDr/mrLe21OggVCQqHwXBMh6fFInvBeyhiQ==", + "dependencies": { + "@algolia/client-common": "5.21.0", + "@algolia/requester-browser-xhr": "5.21.0", + "@algolia/requester-fetch": "5.21.0", + "@algolia/requester-node-http": "5.21.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.21.0.tgz", + "integrity": "sha512-k6MZxLbZphGN5uRri9J/krQQBjUrqNcScPh985XXEFXbSCRvOPKVtjjLdVjGVHXXPOQgKrIZHxIdRNbHS+wVuA==", + "dependencies": { + "@algolia/client-common": "5.21.0", + "@algolia/requester-browser-xhr": "5.21.0", + "@algolia/requester-fetch": "5.21.0", + "@algolia/requester-node-http": "5.21.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.21.0.tgz", + "integrity": "sha512-FiW5nnmyHvaGdorqLClw3PM6keXexAMiwbwJ9xzQr4LcNefLG3ln82NafRPgJO/z0dETAOKjds5aSmEFMiITHQ==", + "dependencies": { + "@algolia/client-common": "5.21.0", + "@algolia/requester-browser-xhr": "5.21.0", + "@algolia/requester-fetch": "5.21.0", + "@algolia/requester-node-http": "5.21.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.21.0.tgz", + "integrity": "sha512-+JXavbbliaLmah5QNgc/TDW/+r0ALa+rGhg5Y7+pF6GpNnzO0L+nlUaDNE8QbiJfz54F9BkwFUnJJeRJAuzTFw==", + "dependencies": { + "@algolia/client-common": "5.21.0", + "@algolia/requester-browser-xhr": "5.21.0", + "@algolia/requester-fetch": "5.21.0", + "@algolia/requester-node-http": "5.21.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.21.0.tgz", + "integrity": "sha512-Iw+Yj5hOmo/iixHS94vEAQ3zi5GPpJywhfxn1el/zWo4AvPIte/+1h9Ywgw/+3M7YBj4jgAkScxjxQCxzLBsjA==", + "dependencies": { + "@algolia/client-common": "5.21.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.21.0.tgz", + "integrity": "sha512-Z00SRLlIFj3SjYVfsd9Yd3kB3dUwQFAkQG18NunWP7cix2ezXpJqA+xAoEf9vc4QZHdxU3Gm8gHAtRiM2iVaTQ==", + "dependencies": { + "@algolia/client-common": "5.21.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.21.0.tgz", + "integrity": "sha512-WqU0VumUILrIeVYCTGZlyyZoC/tbvhiyPxfGRRO1cSjxN558bnJLlR2BvS0SJ5b75dRNK7HDvtXo2QoP9eLfiA==", + "dependencies": { + "@algolia/client-common": "5.21.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -3859,6 +4027,29 @@ "dev": true, "license": "MIT" }, + "node_modules/algoliasearch": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.21.0.tgz", + "integrity": "sha512-hexLq2lSO1K5SW9j21Ubc+q9Ptx7dyRTY7se19U8lhIlVMLCNXWCyQ6C22p9ez8ccX0v7QVmwkl2l1CnuGoO2Q==", + "dependencies": { + "@algolia/client-abtesting": "5.21.0", + "@algolia/client-analytics": "5.21.0", + "@algolia/client-common": "5.21.0", + "@algolia/client-insights": "5.21.0", + "@algolia/client-personalization": "5.21.0", + "@algolia/client-query-suggestions": "5.21.0", + "@algolia/client-search": "5.21.0", + "@algolia/ingestion": "1.21.0", + "@algolia/monitoring": "1.21.0", + "@algolia/recommend": "5.21.0", + "@algolia/requester-browser-xhr": "5.21.0", + "@algolia/requester-fetch": "5.21.0", + "@algolia/requester-node-http": "5.21.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/alien-signals": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-0.2.2.tgz", diff --git a/package.json b/package.json index 7fa6ec47c..f1dbf0024 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@xterm/addon-fit": "^0.10.0", "@xterm/addon-serialize": "^0.13.0", "@xterm/xterm": "^5.5.0", + "algoliasearch": "^5.21.0", "axios": "^1.8.2", "dotenv": "^16.4.5", "fuse.js": "^7.0.0", diff --git a/src/components/dialog/content/manager/ManagerDialogContent.vue b/src/components/dialog/content/manager/ManagerDialogContent.vue index e44575700..21b0e68d7 100644 --- a/src/components/dialog/content/manager/ManagerDialogContent.vue +++ b/src/components/dialog/content/manager/ManagerDialogContent.vue @@ -30,9 +30,8 @@
([ ]) const selectedTab = ref(tabs.value[0]) -const { searchQuery, pageNumber, sortField, isLoading, error, searchResults } = +const { searchQuery, pageNumber, isLoading, searchResults, searchMode } = useRegistrySearch() -pageNumber.value = 1 +pageNumber.value = 0 const isInitialLoad = computed( () => searchResults.value.length === 0 && searchQuery.value === '' @@ -216,12 +215,4 @@ const handleGridContainerClick = (event: MouseEvent) => { const showInfoPanel = computed(() => selectedNodePacks.value.length > 0) const hasMultipleSelections = computed(() => selectedNodePacks.value.length > 1) - -const currentFilterBy = ref('all') -const handleSortChange = (sortBy: PackField) => { - sortField.value = sortBy -} -const handleFilterChange = (filterBy: PackField) => { - currentFilterBy.value = filterBy -} diff --git a/src/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue b/src/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue index b7ebd5d53..027d334ce 100644 --- a/src/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue +++ b/src/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue @@ -4,8 +4,7 @@
- {{ $t('g.resultsCount', { count: searchResults.length }) }} + {{ $t('g.resultsCount', { count: searchResults?.length || 0 }) }}
@@ -40,34 +37,25 @@ import IconField from 'primevue/iconfield' import InputIcon from 'primevue/inputicon' import InputText from 'primevue/inputtext' -import { computed, ref } from 'vue' +import { computed } from 'vue' import { useI18n } from 'vue-i18n' import SearchFilterDropdown from '@/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue' import type { PackField, SearchOption } from '@/types/comfyManagerTypes' import { components } from '@/types/comfyRegistryTypes' -const DEFAULT_SORT: PackField = 'downloads' -const DEFAULT_FILTER = 'nodePack' - const props = defineProps<{ - searchQuery: string searchResults?: components['schemas']['Node'][] }>() +const searchQuery = defineModel('searchQuery') +const searchMode = defineModel('searchMode', { default: 'packs' }) +const sortField = defineModel('sortField', { default: 'downloads' }) + const { t } = useI18n() -const currentSort = ref(DEFAULT_SORT) -const currentFilter = ref(DEFAULT_FILTER) - -const emit = defineEmits<{ - 'update:searchQuery': [value: string] - 'update:sortBy': [value: PackField] - 'update:filterBy': [value: string] -}>() - const hasResults = computed( - () => props.searchQuery.trim() && props.searchResults?.length + () => searchQuery.value?.trim() && props.searchResults?.length ) const sortOptions: SearchOption[] = [ @@ -77,16 +65,7 @@ const sortOptions: SearchOption[] = [ { id: 'category', label: t('g.category') } ] const filterOptions: SearchOption[] = [ - { id: 'nodePack', label: t('manager.filter.nodePack') }, - { id: 'node', label: t('g.nodes') } + { id: 'packs', label: t('manager.filter.nodePack') }, + { id: 'nodes', label: t('g.nodes') } ] - -const handleSortChange = () => { - // TODO: emit to Algolia service - emit('update:sortBy', currentSort.value) -} -const handleFilterChange = () => { - // TODO: emit to Algolia service - emit('update:filterBy', currentFilter.value) -} diff --git a/src/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue b/src/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue index 3032aaa09..392ed6b6b 100644 --- a/src/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue +++ b/src/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue @@ -2,12 +2,12 @@
{{ label }}: import Dropdown from 'primevue/dropdown' -import { computed } from 'vue' import type { SearchOption } from '@/types/comfyManagerTypes' -const { modelValue, options, label } = defineProps<{ - modelValue: T +defineProps<{ options: SearchOption[] label: string + modelValue: T }>() -const emit = defineEmits<{ +defineEmits<{ 'update:modelValue': [value: T] }>() - -const selectedValue = computed({ - get: () => modelValue, - set: (value) => emit('update:modelValue', value) -}) - -const handleChange = () => { - emit('update:modelValue', selectedValue.value) -} diff --git a/src/composables/useRegistrySearch.ts b/src/composables/useRegistrySearch.ts index 1b4d70c68..7c5750654 100644 --- a/src/composables/useRegistrySearch.ts +++ b/src/composables/useRegistrySearch.ts @@ -1,72 +1,77 @@ -import { debounce } from 'lodash' -import { onUnmounted, ref, watch } from 'vue' +import { watchDebounced } from '@vueuse/core' +import { memoize } from 'lodash' +import { computed, ref, watch } from 'vue' -import { useComfyRegistryService } from '@/services/comfyRegistryService' -import { useComfyRegistryStore } from '@/stores/comfyRegistryStore' -import type { components } from '@/types/comfyRegistryTypes' +import { + AlgoliaNodePack, + SearchAttribute, + useAlgoliaSearchService +} from '@/services/algoliaSearchService' +import { PackField } from '@/types/comfyManagerTypes' -const SEARCH_DEBOUNCE_TIME = 256 -const DEFAULT_PAGE_SIZE = 60 -const DEFAULT_SORT_FIELD: keyof components['schemas']['Node'] = 'downloads' +const SEARCH_DEBOUNCE_TIME = 16 +const DEFAULT_PAGE_SIZE = 64 /** * Composable for managing UI state of Comfy Node Registry search. */ export function useRegistrySearch() { - const registryStore = useComfyRegistryStore() - const registryService = useComfyRegistryService() - - const searchQuery = ref('') - const pageNumber = ref(1) + const isLoading = ref(false) + const sortField = ref('downloads') + const searchMode = ref<'nodes' | 'packs'>('packs') const pageSize = ref(DEFAULT_PAGE_SIZE) - const sortField = ref(DEFAULT_SORT_FIELD) - const searchResults = ref([]) + const pageNumber = ref(0) + const searchQuery = ref('') + const results = ref([]) - const search = async () => { - try { - const isEmptySearch = searchQuery.value === '' - const result = isEmptySearch - ? await registryStore.listAllPacks({ - page: pageNumber.value, - limit: pageSize.value, - sort: [sortField.value] - }) - : await registryService.search({ - search: searchQuery.value, - page: pageNumber.value, - limit: pageSize.value - }) + const searchAttributes = computed(() => + searchMode.value === 'nodes' ? ['comfy_nodes'] : ['name', 'description'] + ) - if (result) { - searchResults.value = result.nodes || [] - } else { - searchResults.value = [] - } - } catch (err) { - console.error('Error loading packs:', err) - searchResults.value = [] - } + const resultsAsRegistryPacks = computed(() => + results.value ? results.value.map(algoliaToRegistry) : [] + ) + const resultsAsNodes = computed(() => + results.value + ? results.value.reduce( + (acc, hit) => acc.concat(hit.comfy_nodes), + [] as string[] + ) + : [] + ) + + const { searchPacks, toRegistryPack } = useAlgoliaSearchService() + + const algoliaToRegistry = memoize( + toRegistryPack, + (algoliaNode: AlgoliaNodePack) => algoliaNode.id + ) + + const onQueryChange = async () => { + isLoading.value = true + results.value = await searchPacks(searchQuery.value, { + pageSize: pageSize.value, + pageNumber: pageNumber.value, + restrictSearchableAttributes: searchAttributes.value + }) + isLoading.value = false } - const debouncedSearch = debounce(search, SEARCH_DEBOUNCE_TIME) - - // Debounce search when query changes - watch(() => searchQuery.value, debouncedSearch) - - watch(() => [pageNumber.value, sortField.value], search, { immediate: true }) - - onUnmounted(() => { - debouncedSearch.cancel() // Cancel debounced searches - registryStore.cancelRequests() // Cancel in-flight requests + watch([pageNumber, sortField, searchMode], onQueryChange, { + immediate: true + }) + watchDebounced(searchQuery, onQueryChange, { + debounce: SEARCH_DEBOUNCE_TIME }) return { + isLoading, pageNumber, pageSize, sortField, + searchMode, searchQuery, - searchResults, - isLoading: registryService.isLoading, - error: registryService.error + searchResults: resultsAsRegistryPacks, + nodeSearchResults: resultsAsNodes } } diff --git a/src/services/algoliaSearchService.ts b/src/services/algoliaSearchService.ts new file mode 100644 index 000000000..e3268205d --- /dev/null +++ b/src/services/algoliaSearchService.ts @@ -0,0 +1,153 @@ +import type { + BaseSearchParamsWithoutQuery, + Hit +} from 'algoliasearch/dist/lite/browser' +import { liteClient as algoliasearch } from 'algoliasearch/dist/lite/builds/browser' +import { omit } from 'lodash' + +import { components } from '@/types/comfyRegistryTypes' + +declare const __ALGOLIA_APP_ID__: string +declare const __ALGOLIA_API_KEY__: string + +type SafeNestedProperty< + T, + K1 extends keyof T, + K2 extends keyof NonNullable +> = T[K1] extends undefined | null ? undefined : NonNullable[K2] + +type RegistryNodePack = components['schemas']['Node'] + +export interface AlgoliaNodePack { + objectID: RegistryNodePack['id'] + name: RegistryNodePack['name'] + publisher_id: SafeNestedProperty + description: RegistryNodePack['description'] + comfy_nodes: string[] + total_install: RegistryNodePack['downloads'] + id: RegistryNodePack['id'] + create_time: string + update_time: SafeNestedProperty< + RegistryNodePack, + 'latest_version', + 'createdAt' + > + license: RegistryNodePack['license'] + repository_url: RegistryNodePack['repository'] + status: RegistryNodePack['status'] + latest_version: SafeNestedProperty< + RegistryNodePack, + 'latest_version', + 'version' + > + latest_version_status: SafeNestedProperty< + RegistryNodePack, + 'latest_version', + 'status' + > + comfy_node_extract_status: SafeNestedProperty< + RegistryNodePack, + 'latest_version', + 'comfy_node_extract_status' + > +} + +export type SearchAttribute = keyof AlgoliaNodePack + +const RETRIEVE_ATTRIBUTES: SearchAttribute[] = [ + 'comfy_nodes', + 'name', + 'description', + 'latest_version', + 'status', + 'publisher_id', + 'total_install', + 'update_time', + 'license', + 'repository_url', + 'latest_version_status', + 'comfy_node_extract_status', + 'id' +] + +type SearchNodePacksParams = BaseSearchParamsWithoutQuery & { + pageSize: number + pageNumber: number + restrictSearchableAttributes: SearchAttribute[] +} + +export const useAlgoliaSearchService = () => { + const searchClient = algoliasearch(__ALGOLIA_APP_ID__, __ALGOLIA_API_KEY__) + + const toRegistryLatestVersion = ( + algoliaNode: AlgoliaNodePack + ): RegistryNodePack['latest_version'] => { + return { + version: algoliaNode.latest_version, + createdAt: algoliaNode.update_time, + status: algoliaNode.latest_version_status, + comfy_node_extract_status: + algoliaNode.comfy_node_extract_status ?? undefined + } + } + + const toRegistryPublisher = ( + algoliaNode: AlgoliaNodePack + ): RegistryNodePack['publisher'] => { + return { + id: algoliaNode.publisher_id, + name: algoliaNode.publisher_id + } + } + + /** + * Convert from node pack in Algolia format to Comfy Registry format + */ + function toRegistryPack(algoliaNode: AlgoliaNodePack): RegistryNodePack { + return { + id: algoliaNode.id ?? algoliaNode.objectID, + name: algoliaNode.name, + description: algoliaNode.description, + repository: algoliaNode.repository_url, + license: algoliaNode.license, + downloads: algoliaNode.total_install, + status: algoliaNode.status, + latest_version: toRegistryLatestVersion(algoliaNode), + publisher: toRegistryPublisher(algoliaNode) + } + } + + /** + * Search for node packs in Algolia + */ + const searchPacks = async ( + query: string, + params: SearchNodePacksParams + ): Promise[]> => { + const { pageSize, pageNumber } = params + const rest = omit(params, ['pageSize', 'pageNumber']) + + const { results } = await searchClient.search({ + requests: [ + { + query, + indexName: 'nodes_index', + attributesToRetrieve: RETRIEVE_ATTRIBUTES, + ...rest, + hitsPerPage: pageSize, + length: pageSize, + page: pageNumber + } + ], + strategy: 'none' + }) + + // Narrow from `SearchResponse | SearchForFacetValuesResponse` to `SearchResponse` + return 'hits' in results[0] ? results[0].hits : [] // Only querying a single index for now + } + + return { + searchPacks, + toRegistryPack + } +} diff --git a/src/types/algoliasearch-lite.d.ts b/src/types/algoliasearch-lite.d.ts new file mode 100644 index 000000000..1514d272a --- /dev/null +++ b/src/types/algoliasearch-lite.d.ts @@ -0,0 +1,19 @@ +declare module 'algoliasearch/dist/lite/builds/browser' { + import { LiteClient, ClientOptions } from 'algoliasearch/dist/lite/browser' + + /** + * Creates a new Algolia Search client that uses the Lite API Client (Browser version) + * + * @param appId - Your Algolia Application ID + * @param apiKey - Your Algolia API Key + * @param options - Options for the client + * @returns An Algolia Search client instance + */ + export function liteClient( + appId: string, + apiKey: string, + options?: ClientOptions + ): LiteClient + + export const apiClientVersion: string +} diff --git a/vite.config.mts b/vite.config.mts index 2cbb7280e..3678c70f4 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -182,7 +182,9 @@ export default defineConfig({ __SENTRY_ENABLED__: JSON.stringify( !(process.env.NODE_ENV === 'development' || !process.env.SENTRY_DSN) ), - __SENTRY_DSN__: JSON.stringify(process.env.SENTRY_DSN || '') + __SENTRY_DSN__: JSON.stringify(process.env.SENTRY_DSN || ''), + __ALGOLIA_APP_ID__: JSON.stringify(process.env.VITE_ALGOLIA_APP_ID || ''), + __ALGOLIA_API_KEY__: JSON.stringify(process.env.VITE_ALGOLIA_API_KEY || '') }, resolve: {