-
Notifications
You must be signed in to change notification settings - Fork 92
Improve Settings
#1864
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Improve Settings
#1864
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #1864 +/- ##
==========================================
- Coverage 99.03% 98.29% -0.74%
==========================================
Files 18 19 +1
Lines 1449 1175 -274
Branches 305 253 -52
==========================================
- Hits 1435 1155 -280
- Misses 14 20 +6 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot reviewed 30 out of 30 changed files in this pull request and generated no comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR refactors and improves the handling of Settings by separating and documenting settings types, centralizing settings update methods under a new "setting" property on the Index class, and significantly refactoring the tests related to settings. Key changes include:
- Moving and redefining Settings types into a dedicated settings module.
- Updating API calls to use the new client.index(...).setting.* method pattern.
- Removing or refactoring a number of tests to align with the new settings updates and experimental features support.
Reviewed Changes
Copilot reviewed 46 out of 46 changed files in this pull request and generated no comments.
File | Description |
---|---|
tests/*.test.ts | Updates to test files to use new settings methods and removal of redundant tests |
src/types/*.ts | Refactoring of settings types and removal of duplicated definitions |
src/settings.ts, src/meilisearch.ts | Introduction of new settings functions and experimental features endpoints |
README.md, .code-samples.meilisearch.yaml | Documentation updates to reflect new settings API usage |
Comments suppressed due to low confidence (3)
tests/faceting.test.ts:1
- Several test files (e.g. faceting, filterable_attributes, facet_search_settings, distinct_attribute, displayed_attributes, dictionary) have been removed. Please ensure that the new or remaining tests fully cover all the settings functionalities previously verified to avoid gaps in test coverage.
Entire file removed
src/meilisearch.ts:459
- The new getExperimentalFeatures and updateExperimentalFeatures endpoints have been added using the 'patch' method for updates. Verify that these endpoints are fully documented, have clear versioning implications, and that there are adequate tests for their behavior.
New experimental features endpoints added
README.md:262
- Make sure that all usage examples and code samples in the documentation and YAML files accurately reflect the new settings API changes and provide guidance for users during migration.
Documentation examples updated to use client.index('myIndex').setting.*
WalkthroughThe codebase underwent a major refactor of index settings management. All individual getter, updater, and resetter methods for index settings were consolidated into a unified Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Index
participant SettingFns
Client->>Index: get setting
Index->>SettingFns: return settings interface
Client->>SettingFns: updateSynonyms(data)
SettingFns->>Index: HTTP PUT/PATCH to /settings/synonyms
Client->>SettingFns: getSynonyms()
SettingFns->>Index: HTTP GET to /settings/synonyms
Client->>SettingFns: resetSynonyms()
SettingFns->>Index: HTTP DELETE to /settings/synonyms
Note over Client,SettingFns: Same pattern applies for all other settings (filterableAttributes, rankingRules, etc.)
Assessment against linked issues
Poem
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (8)
.code-samples.meilisearch.yaml (1)
6-10
: Documentation samples now rely on the newsetting
property – double-check the promise handling.All updated snippets still use the fire-and-forget style (
client.index(...).setting.updateXxx(...)
) without awaiting the returnedEnqueuedTaskPromise
.
While brevity is fine for docs, omitting eitherawait
or.then()
can mislead users who need the taskUid for subsequent polling or error handling.Consider adding a short comment or one minimal example showing the idiomatic pattern:
const task = await client.index('movies') .setting.updateSynonyms({ ... }); await task.waitTask(); // <- most common real-world needThis helps newcomers understand that a task object is produced, not an immediate success result.
(No change required in every snippet—one prominent example in the “settings” section is enough.)Also applies to: 14-16, 20-24
src/settings.ts (2)
31-33
:camelToKebabCase
misses consecutive upper-case segments.Example:
"URLPath"
➜"u-r-lpath"
instead of"url-path"
.
A simple fix:-return str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`); +return str + .replace(/([a-z0-9])([A-Z])/g, "$1-$2") // word boundary + .toLowerCase();Not blocking, but improves future-proofing.
48-63
: Minor optimisation & readability
return await httpRequest.get({ path });
→ justreturn httpRequest.get({ path });
(await
in areturn
adds an unnecessary micro-task).Using
for..of Object.entries
already provides the strongly-typed key (name: string
),
but ifopts
is frozen the loop could instead iterate overObject.keys(opts)
to avoid constructing entry tuples. Marginal win, non-blocking.tests/settings.test.ts (3)
269-273
:delete
inside tight test loops – replace withundefined
to satisfy linter and avoid shape-deopt.The Biome linter flags the
delete output.sortFacetValuesBy["*"];
pattern.
In hot code this can de-optimise objects; in tests the perf hit is negligible, but you can appease the linter and keep intent clear:-const star = output.sortFacetValuesBy["*"]; -delete output.sortFacetValuesBy["*"]; +const star = output.sortFacetValuesBy["*"]; +output.sortFacetValuesBy["*"] = undefined;(or clone the object before mutation).
🧰 Tools
🪛 Biome (1.9.4)
[error] 271-272: Avoid the delete operator which can impact performance.
Unsafe fix: Use an undefined assignment instead.
(lint/performance/noDelete)
503-507
: Reassigning the loop variable shadows immutable data – prefer a new name.
mappedSetting = mappedSetting.map(...)
mutates the parameter captured from
describe.for
. While legal (function parameters are mutable), it can confuse
readers becausemappedSetting
was perceived as an input, not a working copy.- mappedSetting = mappedSetting.map(...) + const mappedSettingWithLabel = mappedSetting.map(...)This improves clarity and prevents accidental reuse of the transformed value
elsewhere in the block.
510-521
: Race-condition risk: relying onwaitTask({ timeout: 30_000 })
in serial loops slows CI.Each individual setting test waits up to 30 s. With ~40 settings this can add 20 min worst-case.
If the Meilisearch instance supports batch updates, consider:
- Parallelising the per-setting tasks with
Promise.allSettled
.- Lowering timeout via server-side per-cluster config.
Not a blocker, but may considerably speed up CI.
src/indexes.ts (1)
574-583
: Expose thesetting
facade asreadonly
at type levelThe getter returns a fresh reference each call; consumers could still re-assign it (
index.setting = …
) unless TS is told it’s immutable.- get setting() { + get setting(): Readonly<SettingFns> { return this.#setting; }Small addition but it prevents accidental mutation and documents intent.
src/types/settings.ts (1)
133-139
:EmbeddingSettings
double-inherits fromSubEmbeddingSettings
EmbeddingSettings
already containssearchEmbedder
andindexingEmbedder
fields typed asSubEmbeddingSettings
, but the final intersection
& SubEmbeddingSettings
merges the entire parent again, creating duplicate
properties and making some keys (source
,model
, …) ambiguous in the
resulting type.Unless a “root-level shortcut” is explicitly required, consider removing the
intersection to avoid confusion:-export type EmbeddingSettings = PartialAndNullable<{ … }> & SubEmbeddingSettings; +export type EmbeddingSettings = PartialAndNullable<{ … }>;If both levels are intentional, a code comment explaining why will help future
maintainers.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting
⛔ Files ignored due to path filters (1)
tests/__snapshots__/settings.test.ts.snap
is excluded by!**/*.snap
📒 Files selected for processing (43)
.code-samples.meilisearch.yaml
(17 hunks)README.md
(3 hunks)src/indexes.ts
(4 hunks)src/settings.ts
(1 hunks)src/types/index.ts
(1 hunks)src/types/settings.ts
(1 hunks)src/types/shared.ts
(1 hunks)src/types/task_and_batch.ts
(2 hunks)src/types/types.ts
(0 hunks)tests/dictionary.test.ts
(0 hunks)tests/displayed_attributes.test.ts
(0 hunks)tests/distinct_attribute.test.ts
(0 hunks)tests/documents.test.ts
(4 hunks)tests/dump.test.ts
(1 hunks)tests/embedders.test.ts
(0 hunks)tests/env/node/getting_started.cjs
(1 hunks)tests/facet_search_settings.test.ts
(0 hunks)tests/faceting.test.ts
(0 hunks)tests/filterable_attributes.test.ts
(0 hunks)tests/get_search.test.ts
(1 hunks)tests/index.test.ts
(1 hunks)tests/keys.test.ts
(1 hunks)tests/localized_attributes.test.ts
(0 hunks)tests/non_separator_tokens.test.ts
(0 hunks)tests/pagination.test.ts
(0 hunks)tests/prefix_search_settings.test.ts
(0 hunks)tests/proximity_precision.test.ts
(0 hunks)tests/ranking_rules.test.ts
(0 hunks)tests/raw_document.test.ts
(1 hunks)tests/search.test.ts
(3 hunks)tests/search_cutoff_ms.test.ts
(0 hunks)tests/searchable_attributes.test.ts
(0 hunks)tests/separator_tokens.test.ts
(0 hunks)tests/settings.test.ts
(1 hunks)tests/snapshots.test.ts
(1 hunks)tests/sortable_attributes.test.ts
(0 hunks)tests/stop_words.test.ts
(0 hunks)tests/synonyms.test.ts
(0 hunks)tests/task.test.ts
(1 hunks)tests/token.test.ts
(1 hunks)tests/typed_search.test.ts
(2 hunks)tests/typo_tolerance.test.ts
(0 hunks)tests/utils/meilisearch-test-utils.ts
(3 hunks)
💤 Files with no reviewable changes (21)
- tests/separator_tokens.test.ts
- tests/non_separator_tokens.test.ts
- tests/localized_attributes.test.ts
- tests/search_cutoff_ms.test.ts
- tests/typo_tolerance.test.ts
- tests/faceting.test.ts
- tests/sortable_attributes.test.ts
- tests/displayed_attributes.test.ts
- tests/embedders.test.ts
- tests/pagination.test.ts
- tests/searchable_attributes.test.ts
- tests/prefix_search_settings.test.ts
- tests/filterable_attributes.test.ts
- tests/facet_search_settings.test.ts
- tests/proximity_precision.test.ts
- tests/synonyms.test.ts
- tests/dictionary.test.ts
- src/types/types.ts
- tests/ranking_rules.test.ts
- tests/distinct_attribute.test.ts
- tests/stop_words.test.ts
🧰 Additional context used
🧬 Code Graph Analysis (7)
src/types/task_and_batch.ts (1)
src/types/settings.ts (1)
UpdatableSettings
(170-211)
tests/typed_search.test.ts (1)
src/meilisearch.ts (1)
index
(86-88)
tests/utils/meilisearch-test-utils.ts (2)
src/types/task_and_batch.ts (1)
Task
(122-131)src/http-requests.ts (1)
T
(199-264)
tests/documents.test.ts (2)
tests/env/esm/src/index.js (1)
client
(4-4)src/meilisearch.ts (1)
index
(86-88)
src/settings.ts (5)
src/types/settings.ts (1)
SingleUpdatableSettings
(217-217)src/types/task_and_batch.ts (1)
EnqueuedTaskPromise
(137-143)src/http-requests.ts (1)
HttpRequests
(140-290)src/task.ts (1)
HttpRequestsWithEnqueuedTaskPromise
(192-196)src/types/types.ts (1)
RecordAny
(10-10)
src/types/settings.ts (1)
src/types/shared.ts (1)
PascalToCamelCase
(10-10)
src/indexes.ts (4)
src/settings.ts (2)
makeSettingFns
(36-66)SettingFns
(15-25)src/errors/meilisearch-error.ts (1)
MeiliSearchError
(1-3)src/types/settings.ts (2)
Settings
(224-224)UpdatableSettings
(170-211)src/types/task_and_batch.ts (1)
EnqueuedTaskPromise
(137-143)
🪛 Biome (1.9.4)
tests/settings.test.ts
[error] 271-272: Avoid the delete operator which can impact performance.
Unsafe fix: Use an undefined assignment instead.
(lint/performance/noDelete)
🔇 Additional comments (46)
tests/keys.test.ts (1)
3-3
: Consistent import path update
The import forErrorStatusCode
has been correctly updated from the old types entrypoint to the new consolidated../src/index.js
. This aligns with the refactoring of type exports.tests/snapshots.test.ts (1)
2-2
: Consistent import path update
TheErrorStatusCode
import path has been updated to../src/index.js
, matching the centralized exports. No test logic was affected.tests/dump.test.ts (1)
2-2
: Consistent import path update
TheErrorStatusCode
import was correctly changed to the unified../src/index.js
module. Test assertions remain valid.tests/task.test.ts (1)
2-2
: Consistent import path update
The import forErrorStatusCode
now points to the rootindex.js
entrypoint, in line with the refactoring. All task tests continue to reference the correct error codes.tests/index.test.ts (1)
2-2
: Consistent import path update
UpdatedErrorStatusCode
import to the new../src/index.js
entrypoint. This keeps type imports in sync with the consolidated exports.tests/raw_document.test.ts (1)
7-7
: Import path updated to reflect refactored type exports.The import path for
ContentTypeEnum
has been updated from"../src/types/index.js"
to"../src/index.js"
to align with the consolidated export structure. This change is part of the broader settings refactoring described in the PR objectives.tests/get_search.test.ts (1)
2-2
: Import path updated to reflect refactored type exports.The import path for
ErrorStatusCode
has been updated from"../src/types/index.js"
to"../src/index.js"
to align with the consolidated type export structure. This change is consistent with similar updates across other files in the PR.tests/env/node/getting_started.cjs (1)
22-22
: API usage updated to use new settings interface.This line changes the method call from
index.updateFilterableAttributes
toindex.setting.updateFilterableAttributes
, implementing the new API pattern where all settings methods are accessed via thesetting
property. This change aligns with the PR objective to consolidate setting manipulation methods under a dedicated property.src/types/index.ts (1)
2-2
: Added export for new settings types.This change exports all types from the settings module, making them available throughout the codebase. This is a crucial part of the refactoring described in the PR objectives, where setting types have been reorganized and improved.
tests/token.test.ts (1)
302-302
: API pattern change is correctThe change to use
setting.updateFilterableAttributes
instead of directly callingupdateFilterableAttributes
follows the new API pattern, where all index settings methods are now accessed through the unifiedsetting
property.tests/search.test.ts (3)
10-10
: Import paths updated correctlyThe import paths for
ErrorStatusCode
,MatchingStrategies
, and related types have been consolidated to use the main entry point from"../src/index.js"
instead of specific file paths, which is a cleaner approach.Also applies to: 14-14
1217-1219
: API pattern change is correctThe change to use
setting.updateLocalizedAttributes
instead of directly callingupdateLocalizedAttributes
aligns with the new API pattern, where all index settings methods are now accessed through the unifiedsetting
property.
1425-1425
: Consider addressing the TODO commentThere's a new TODO comment about an assertion error with promising not resolving. This might indicate a test issue that should be addressed.
Could you provide more details about this assertion error or check if this is related to the API changes in this PR?
src/types/task_and_batch.ts (1)
1-1
: Type import and extension updated correctlyThe change from importing
Settings
toUpdatableSettings
and updating theTaskDetails
type to extendUpdatableSettings
instead is aligned with the PR's objective to make settings types more generic and better match the Rust source code types.This is a breaking change that requires consumers to use
UpdatableSettings
instead ofSettings
when working with tasks, but it provides a more accurate type definition reflecting what fields can actually be updated.Also applies to: 100-100
tests/typed_search.test.ts (2)
9-9
: Import paths updated correctlyThe import paths have been consolidated to use the main entry point from
"../src/index.js"
instead of specific file paths, which improves maintainability.
121-123
: API pattern change and type parameter update are correctTwo important changes have been made:
The generic type parameter
<Movie>
has been removed from theindex()
call, which aligns with the implementation insrc/meilisearch.ts
where theindex()
method is generic.The method call now uses
.setting.updateFilterableAttributes()
instead of calling it directly on the index object, following the new API pattern where all settings methods are accessed through the unifiedsetting
property.These changes are part of the broader refactoring to improve the organization and clarity of the settings API.
tests/documents.test.ts (4)
143-146
: API update for FilterableAttributes methodThe method for updating filterable attributes has been changed to use the new
setting
property on the index, aligning with the PR's goal of consolidating all settings methods under a unified interface.
494-496
: API update for FilterableAttributes methodConsistent with the earlier change, this code now uses
setting.updateFilterableAttributes
instead of directly calling the method on the index.
512-514
: API update for FilterableAttributes methodAnother instance of the updated API pattern for setting filterable attributes through the
setting
property.
688-688
: API update for FilterableAttributes methodThis change completes the migration to the new settings API pattern within this test file.
src/types/shared.ts (1)
9-10
: New type utility for Pascal-to-camel case conversionAdded a utility type that converts PascalCase to camelCase using TypeScript's built-in
Uncapitalize
utility. This supports the settings refactoring by providing proper type conversions when generating setting method names.The source attribution via StackOverflow comment is good practice for referencing external solutions.
tests/utils/meilisearch-test-utils.ts (4)
3-3
: Added Task type importUpdated the import to include the
Task
type, which is needed for the new task validation helper function.
130-133
: Added utility for task status verificationNew helper method that validates if a task completed successfully by checking for null error and "succeeded" status. This simplifies testing the outcome of settings operations that return tasks.
251-253
: Added type-safe object keys extraction utilityNew helper function that returns an array of keys from an object with proper typing, useful for working with settings in a type-safe manner.
This function ensures the returned keys array maintains the type information from the input object, allowing for better type checking when working with settings keys.
256-256
: Added ObjectKeys to exportsMakes the new utility function available for use in other test files.
README.md (20)
265-268
: Updated code example to use new settings APIExample for updating filterable attributes now uses the new
setting
property, matching the implementation changes.
713-726
: Updated pagination settings documentationDocumentation for pagination settings methods now reflects the new pattern of accessing these methods through the
setting
property.
733-746
: Updated synonyms settings documentationDocumentation for synonyms settings now reflects the new API pattern.
753-766
: Updated stop words settings documentationDocumentation for stop words settings now uses the new
setting
property pattern.
773-786
: Updated ranking rules settings documentationDocumentation for ranking rules settings now uses the new
setting
property pattern.
793-806
: Updated distinct attribute settings documentationDocumentation for distinct attribute settings now reflects the new API pattern.
813-826
: Updated searchable attributes settings documentationDocumentation for searchable attributes settings now uses the new
setting
property pattern.
833-846
: Updated displayed attributes settings documentationDocumentation for displayed attributes settings now reflects the new API pattern.
853-866
: Updated filterable attributes settings documentationDocumentation for filterable attributes settings now uses the new
setting
property pattern.
873-886
: Updated sortable attributes settings documentationDocumentation for sortable attributes settings now reflects the new API pattern.
893-906
: Updated faceting settings documentationDocumentation for faceting settings now uses the new
setting
property pattern.
913-926
: Updated typo tolerance settings documentationDocumentation for typo tolerance settings now reflects the new API pattern.
934-947
: Updated separator tokens settings documentationDocumentation for separator tokens settings now uses the new
setting
property pattern.
954-967
: Updated non-separator tokens settings documentationDocumentation for non-separator tokens settings now reflects the new API pattern.
974-987
: Updated dictionary settings documentationDocumentation for dictionary settings now uses the new
setting
property pattern.
994-1007
: Updated proximity precision settings documentationDocumentation for proximity precision settings now reflects the new API pattern.
1014-1027
: Updated facet search settings documentationDocumentation for facet search settings now uses the new
setting
property pattern.
1034-1047
: Updated prefix search settings documentationDocumentation for prefix search settings now reflects the new API pattern.
1054-1067
: Updated embedders settings documentationDocumentation for embedders settings now uses the new
setting
property pattern.
1074-1087
: Updated search cutoff settings documentationDocumentation for search cutoff settings now reflects the new API pattern.
src/types/settings.ts (1)
103-106
: Potential typo:"OpenAi"
should be"OpenAI"
In
EmbedderSource
the variant is spelled"OpenAi"
, but the public
documentation (and most provider identifiers) use"OpenAI"
.
A mismatch would make type-level validation pass yet fail at runtime if the
backend expects the canonical spelling.Please verify the exact casing used by the API and update the literal to avoid
hard-to-trace 400 responses.
/** Each setting property mapped to its get, update and reset functions. */ | ||
export type SettingFns = { | ||
[TKey in keyof SingleUpdatableSettings as `get${Capitalize<TKey>}`]: () => Promise< | ||
SingleUpdatableSettings[TKey] | ||
>; | ||
} & { | ||
[TKey in keyof SingleUpdatableSettings as `update${Capitalize<TKey>}`]: ( | ||
body: SingleUpdatableSettings[TKey], | ||
) => EnqueuedTaskPromise; | ||
} & { | ||
[TKey in keyof SingleUpdatableSettings as `reset${Capitalize<TKey>}`]: () => EnqueuedTaskPromise; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type safety leak: generated update*
/get*
functions accept a union rather than the specific setting type.
Inside makeSettingFns
each callback is currently typed with
(body: SingleUpdatableSettings[keyof typeof opts]) => EnqueuedTaskPromise
That union makes all updateXxx
methods accept any setting payload, defeating the
compile-time guarantees exposed by the SettingFns
public type.
Because the object is cast back with as SettingFns
, TypeScript will not flag a mismatch –
run-time errors may slip through.
Patch:
- settingFns[`update${uppercaseName}`] = function (
- body: SingleUpdatableSettings[keyof typeof opts],
+ settingFns[`update${uppercaseName}`] = function <
+ K extends keyof SingleUpdatableSettings & typeof name
+ >(body: SingleUpdatableSettings[K]),
(or create an inner const fn = … as SettingFns["update${Capitalize<typeof name>}"]
)
so that each generated method is strictly typed to its own payload.
🤖 Prompt for AI Agents
In src/settings.ts around lines 14 to 25, the generated update and get functions
are currently typed to accept a union of all possible setting payloads instead
of their specific types, causing a type safety leak. To fix this, ensure that
each generated update and get function is explicitly typed to accept only the
payload type corresponding to its specific setting key. This can be done by
typing each function individually using the exact key's type from
SingleUpdatableSettings or by casting each function to the appropriate
SettingFns method type during creation, rather than casting the entire object at
once.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is done because there's simply no type safe way to do this is in such a generic fashion. It is safe enough, and the tests prove that it works. And this code probably won't have to be touched for the foreseeable future again because of how generic it is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@flevi29 I understand your reasoning. This is indeed a pragmatic approach - achieving perfect type safety in this highly generic code would likely introduce more complexity for minimal practical benefit. Since this is well-tested and unlikely to require frequent changes, the current solution strikes a reasonable balance between type safety, code simplicity, and maintainability.
const parseFilter = (filter?: Filter): string | undefined => { | ||
if (typeof filter === "string") return filter; | ||
else if (Array.isArray(filter)) | ||
else if (Array.isArray(filter)) { | ||
throw new MeiliSearchError( | ||
"The filter query parameter should be in string format when using searchGet", | ||
); | ||
else return undefined; | ||
} else return undefined; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
searchGet
silently ignores non-string filters
parseFilter()
now throws for arrays (👍) but other non-string filter shapes (e.g. the object syntax supported by recent releases) are quietly turned into undefined
, causing the caller’s criteria to be dropped without warning.
- else if (Array.isArray(filter)) {
+ if (Array.isArray(filter)) {
throw new MeiliSearchError(
"The filter query parameter should be in string format when using searchGet",
);
- } else return undefined;
+ }
+ if (filter != null) {
+ throw new MeiliSearchError(
+ `Unsupported filter type for searchGet: ${typeof filter}. ` +
+ "Only string filters are allowed."
+ );
+ }
+ return undefined;
Failing fast for every unsupported shape prevents subtle data-loss bugs.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const parseFilter = (filter?: Filter): string | undefined => { | |
if (typeof filter === "string") return filter; | |
else if (Array.isArray(filter)) | |
else if (Array.isArray(filter)) { | |
throw new MeiliSearchError( | |
"The filter query parameter should be in string format when using searchGet", | |
); | |
else return undefined; | |
} else return undefined; | |
}; | |
const parseFilter = (filter?: Filter): string | undefined => { | |
if (typeof filter === "string") return filter; | |
if (Array.isArray(filter)) { | |
throw new MeiliSearchError( | |
"The filter query parameter should be in string format when using searchGet", | |
); | |
} | |
if (filter != null) { | |
throw new MeiliSearchError( | |
`Unsupported filter type for searchGet: ${typeof filter}. ` + | |
"Only string filters are allowed." | |
); | |
} | |
return undefined; | |
}; |
🤖 Prompt for AI Agents
In src/indexes.ts around lines 138 to 145, the parseFilter function currently
throws an error for array filters but silently returns undefined for other
non-string filter types, which can cause unnoticed data loss. Modify the
function to throw an error for any filter type that is not a string, including
objects or other unsupported shapes, to ensure it fails fast and prevents subtle
bugs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will be addressed by #1925
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
@flevi29 I understand that you're planning to address this in PR #1925. I'll make a note of that and look forward to seeing the implementation there. Thanks for clarifying!
🏁 Script executed:
#!/bin/bash
# Fetch information about PR #1925
gh pr view 1925 --repo meilisearch/meilisearch-js --json title,body,state
Length of output: 4514
@flevi29 That makes perfect sense. I see PR #1925 is focused on improving search functionality with comprehensive refactoring of search-related types and methods. Since this issue is about filter handling in searchGet()
, it fits better in that PR than in this settings-focused one. Thanks for clarifying!
🧬 Code Graph Analysis Results
Relevant snippets for understanding src/indexes.ts
and the user comment:
File: src/errors/meilisearch-error.ts
(lines 1-3)
export class MeiliSearchError extends Error {
override name = "MeiliSearchError";
}
- Defines the base error class
MeiliSearchError
used insrc/indexes.ts
for error handling (e.g., insearchGet
method).
File: src/task.ts
(lines 58-186)
export class TaskClient {
readonly #httpRequest: HttpRequests;
readonly #defaultTimeout: number;
readonly #defaultInterval: number;
readonly #applyWaitTask: ReturnType<typeof getWaitTaskApplier>;
constructor(httpRequest: HttpRequests, defaultWaitOptions?: WaitOptions) {
this.#httpRequest = httpRequest;
this.#defaultTimeout = defaultWaitOptions?.timeout ?? 5_000;
this.#defaultInterval = defaultWaitOptions?.interval ?? 50;
this.#applyWaitTask = getWaitTaskApplier(this);
}
/** {@link https://www.meilisearch.com/docs/reference/api/tasks#get-one-task} */
async getTask(
uid: number,
extraRequestInit?: ExtraRequestInit,
): Promise<Task> {
const task = await this.#httpRequest.get<Task>({
path: `tasks/${uid}`,
extraRequestInit,
});
return task;
}
/** {@link https://www.meilisearch.com/docs/reference/api/tasks#get-tasks} */
async getTasks(params?: TasksOrBatchesQuery): Promise<TasksResults> {
const tasks = await this.#httpRequest.get<TasksResults>({
path: "tasks",
params,
});
return tasks;
}
/**
* Wait for an enqueued task to be processed.
*
* @remarks
* If an {@link EnqueuedTask} needs to be awaited instantly, it is recommended
* to instead use {@link EnqueuedTaskPromise.waitTask}, which is available on
* any method that returns an {@link EnqueuedTaskPromise}.
*/
async waitForTask(
taskUidOrEnqueuedTask: TaskUidOrEnqueuedTask,
options?: WaitOptions,
): Promise<Task> {
const taskUid = getTaskUid(taskUidOrEnqueuedTask);
const timeout = options?.timeout ?? this.#defaultTimeout;
const interval = options?.interval ?? this.#defaultInterval;
const ac = timeout > 0 ? new AbortController() : null;
const toId =
ac !== null
? setTimeout(() => void ac.abort(TIMEOUT_ID), timeout)
: undefined;
try {
for (;;) {
const task = await this.getTask(taskUid, { signal: ac?.signal });
if (task.status !== "enqueued" && task.status !== "processing") {
clearTimeout(toId);
return task;
}
if (interval > 0) {
await new Promise((resolve) => setTimeout(resolve, interval));
}
}
} catch (error) {
throw Object.is((error as Error).cause, TIMEOUT_ID)
? new MeiliSearchTaskTimeOutError(taskUid, timeout)
: error;
}
}
/**
* Lazily wait for multiple enqueued tasks to be processed.
*
* @remarks
* In this case {@link WaitOptions.timeout} is the maximum time to wait for any
* one task, not for all of the tasks to complete.
*/
async *waitForTasksIter(
taskUidsOrEnqueuedTasks:
| Iterable<TaskUidOrEnqueuedTask>
| AsyncIterable<TaskUidOrEnqueuedTask>,
options?: WaitOptions,
): AsyncGenerator<Task, void, undefined> {
for await (const taskUidOrEnqueuedTask of taskUidsOrEnqueuedTasks) {
yield await this.waitForTask(taskUidOrEnqueuedTask, options);
}
}
/** Wait for multiple enqueued tasks to be processed. */
async waitForTasks(
...params: Parameters<typeof this.waitForTasksIter>
): Promise<Task[]> {
const tasks: Task[] = [];
for await (const task of this.waitForTasksIter(...params)) {
tasks.push(task);
}
return tasks;
}
/** {@link https://www.meilisearch.com/docs/reference/api/tasks#cancel-tasks} */
cancelTasks(params: DeleteOrCancelTasksQuery): EnqueuedTaskPromise {
return this.#applyWaitTask(
this.#httpRequest.post({
path: "tasks/cancel",
params,
}),
);
}
/** {@link https://www.meilisearch.com/docs/reference/api/tasks#delete-tasks} */
deleteTasks(params: DeleteOrCancelTasksQuery): EnqueuedTaskPromise {
return this.#applyWaitTask(
this.#httpRequest.delete({
path: "tasks",
params,
}),
);
}
}
- Implements
TaskClient
used insrc/indexes.ts
to manage asynchronous tasks related to index operations. - Provides methods to get task status, wait for task completion, cancel, and delete tasks.
- Used internally in
Index
class for task-related operations.
File: src/task.ts
(lines 192-210)
export type HttpRequestsWithEnqueuedTaskPromise = {
[TKey in keyof PickedHttpRequestMethods]: (
...params: Parameters<PickedHttpRequestMethods[TKey]>
) => EnqueuedTaskPromise;
};
export function getHttpRequestsWithEnqueuedTaskPromise(
httpRequest: HttpRequests,
taskClient: TaskClient,
): HttpRequestsWithEnqueuedTaskPromise {
const applyWaitTask = getWaitTaskApplier(taskClient);
return {
post: (...params) => applyWaitTask(httpRequest.post(...params)),
put: (...params) => applyWaitTask(httpRequest.put(...params)),
patch: (...params) => applyWaitTask(httpRequest.patch(...params)),
delete: (...params) => applyWaitTask(httpRequest.delete(...params)),
};
}
- Defines a wrapper around HTTP request methods that returns
EnqueuedTaskPromise
which includes a.waitTask()
method to await task completion. - Used in
Index
class private field#httpRequestsWithTask
for methods that enqueue tasks.
File: src/types/task_and_batch.ts
(lines 137-143)
export type EnqueuedTaskPromise = Promise<EnqueuedTask> & {
/**
* Function that, through polling, awaits the {@link EnqueuedTask} resolved by
* {@link EnqueuedTaskPromise}.
*/
waitTask: (waitOptions?: WaitOptions) => Promise<Task>;
};
- Defines the type of promises returned by methods that enqueue tasks.
- These promises have an additional
waitTask
method to wait for task completion.
File: src/settings.ts
(lines 15-25)
export type SettingFns = {
[TKey in keyof SingleUpdatableSettings as `get${Capitalize<TKey>}`]: () => Promise<
SingleUpdatableSettings[TKey]
>;
} & {
[TKey in keyof SingleUpdatableSettings as `update${Capitalize<TKey>}`]: (
body: SingleUpdatableSettings[TKey],
) => EnqueuedTaskPromise;
} & {
[TKey in keyof SingleUpdatableSettings as `reset${Capitalize<TKey>}`]: () => EnqueuedTaskPromise;
};
- Defines the type for setting functions generated by
makeSettingFns
. - Each setting has
get
,update
, andreset
methods with appropriate return types.
File: src/settings.ts
(lines 36-66)
export function makeSettingFns(
httpRequest: HttpRequests,
httpRequestsWithTask: HttpRequestsWithEnqueuedTaskPromise,
basePath: string,
opts: MakeSettingsRecord,
): SettingFns {
const settingFns = {} as RecordAny;
for (const [name, method] of Object.entries(opts)) {
const uppercaseName = capitalize(name);
const path = `${basePath}/${camelToKebabCase(name)}`;
settingFns[`get${uppercaseName}`] = async function (): Promise<
SingleUpdatableSettings[keyof typeof opts]
> {
return await httpRequest.get({ path });
};
settingFns[`update${uppercaseName}`] = function (
body: SingleUpdatableSettings[keyof typeof opts],
): EnqueuedTaskPromise {
return httpRequestsWithTask[method]({ path, body });
};
settingFns[`reset${uppercaseName}`] = function (): EnqueuedTaskPromise {
return httpRequestsWithTask.delete({ path });
};
}
return settingFns as SettingFns;
}
- Factory function used in
Index
constructor to create setting-related methods. - Uses
httpRequest
forget
andhttpRequestsWithTask
forupdate
andreset
methods. - Generates methods for each setting type with appropriate HTTP methods.
File: src/types/types.ts
(selected relevant types)
RecordAny
(line 10):
export type RecordAny = Record<string, any>;
Config
(lines 47-80):
export type Config = {
host: string;
apiKey?: string;
clientAgents?: string[];
requestInit?: BaseRequestInit;
httpClient?: (...args: Parameters<typeof fetch>) => Promise<unknown>;
timeout?: number;
defaultWaitOptions?: WaitOptions;
};
SearchParams
(lines 212-235):
export type SearchParams = Query &
Pagination &
Highlight &
Crop & {
filter?: Filter;
sort?: string[];
facets?: string[];
attributesToRetrieve?: string[];
showMatchesPosition?: boolean;
matchingStrategy?: MatchingStrategies;
hitsPerPage?: number;
page?: number;
facetName?: string;
facetQuery?: string;
vector?: number[] | null;
showRankingScore?: boolean;
showRankingScoreDetails?: boolean;
rankingScoreThreshold?: number;
attributesToSearchOn?: string[] | null;
hybrid?: HybridSearch;
distinct?: string;
retrieveVectors?: boolean;
locales?: Locale[];
};
SearchRequestGET
(lines 239-258):
export type SearchRequestGET = Pagination &
Query &
Omit<Highlight, "attributesToHighlight"> &
Omit<Crop, "attributesToCrop"> & {
filter?: string;
sort?: string;
facets?: string;
attributesToRetrieve?: string;
attributesToHighlight?: string;
attributesToCrop?: string;
showMatchesPosition?: boolean;
vector?: string | null;
attributesToSearchOn?: string | null;
hybridEmbedder?: string;
hybridSemanticRatio?: number;
rankingScoreThreshold?: number;
distinct?: string;
retrieveVectors?: boolean;
locales?: Locale[];
};
Filter
(line 165):
export type Filter = string | (string | string[])[];
EnqueuedTaskPromise
(see above snippet fromsrc/types/task_and_batch.ts
).
File: src/http-requests.ts
(lines 140-290)
export class HttpRequests {
#url: URL;
#requestInit: HttpRequestsRequestInit;
#customRequestFn?: Config["httpClient"];
#requestTimeout?: Config["timeout"];
constructor(config: Config) {
const host = addTrailingSlash(addProtocolIfNotPresent(config.host));
try {
this.#url = new URL(host);
} catch (error) {
throw new MeiliSearchError("The provided host is not valid", {
cause: error,
});
}
this.#requestInit = {
...config.requestInit,
headers: getHeaders(config, config.requestInit?.headers),
};
this.#customRequestFn = config.httpClient;
this.#requestTimeout = config.timeout;
}
/**
* Combines provided extra {@link RequestInit} headers, provided content type
* and class instance RequestInit headers, prioritizing them in this order.
*
* @returns A new Headers object or the main headers of this class if no
* headers are provided
*/
#getHeaders(extraHeaders?: HeadersInit, contentType?: string): Headers {
if (extraHeaders === undefined && contentType === undefined) {
return this.#requestInit.headers;
}
const headers = new Headers(extraHeaders);
if (contentType !== undefined && !headers.has("Content-Type")) {
headers.set("Content-Type", contentType);
}
for (const [key, val] of this.#requestInit.headers) {
if (!headers.has(key)) {
headers.set(key, val);
}
}
return headers;
}
/**
* Sends a request with {@link fetch} or a custom HTTP client, combining
* parameters and class properties.
*
* @returns A promise containing the response
*/
async #request<T = unknown>({
path,
method,
params,
contentType,
body,
extraRequestInit,
}: MainRequestOptions): Promise<T> {
const url = new URL(path, this.#url);
if (params !== undefined) {
appendRecordToURLSearchParams(url.searchParams, params);
}
const init: RequestInit = {
method,
body:
contentType === undefined || typeof body !== "string"
? JSON.stringify(body)
: body,
...extraRequestInit,
...this.#requestInit,
headers: this.#getHeaders(extraRequestInit?.headers, contentType),
};
const startTimeout =
this.#requestTimeout !== undefined
? getTimeoutFn(init, this.#requestTimeout)
: null;
const stopTimeout = startTimeout?.();
let response: Response;
let responseBody: string;
try {
if (this.#customRequestFn !== undefined) {
// When using a custom HTTP client, the response should already be handled and ready to be returned
return (await this.#customRequestFn(url, init)) as T;
}
response = await fetch(url, init);
responseBody = await response.text();
} catch (error) {
throw new MeiliSearchRequestError(
url.toString(),
Object.is(error, TIMEOUT_ID)
? new MeiliSearchRequestTimeOutError(this.#requestTimeout!, init)
: error,
);
} finally {
stopTimeout?.();
}
const parsedResponse =
responseBody === ""
? undefined
: (JSON.parse(responseBody) as T | MeiliSearchErrorResponse);
if (!response.ok) {
throw new MeiliSearchApiError(
response,
parsedResponse as MeiliSearchErrorResponse | undefined,
);
}
return parsedResponse as T;
}
/** Request with GET. */
get<T = unknown>(options: RequestOptions): Promise<T> {
return this.#request<T>(options);
}
/** Request with POST. */
post<T = unknown>(options: RequestOptions): Promise<T> {
return this.#request<T>({ ...options, method: "POST" });
}
/** Request with PUT. */
put<T = unknown>(options: RequestOptions): Promise<T> {
return this.#request<T>({ ...options, method: "PUT" });
}
/** Request with PATCH. */
patch<T = unknown>(options: RequestOptions): Promise<T> {
return this.#request<T>({ ...options, method: "PATCH" });
}
/** Request with DELETE. */
delete<T = unknown>(options: RequestOptions): Promise<T> {
return this.#request<T>({ ...options, method: "DELETE" });
}
}
- Implements the low-level HTTP request handling used by
Index
class. - Supports GET, POST, PUT, PATCH, DELETE with JSON body serialization and error handling.
- Supports custom HTTP client and request timeout.
These snippets provide context on:
- How
Index
usesHttpRequests
for HTTP calls. - How
TaskClient
andHttpRequestsWithEnqueuedTaskPromise
wrap requests that enqueue tasks and provide.waitTask()
functionality. - The error class used for runtime errors.
- The factory function
makeSettingFns
used to generate setting-related methods onIndex
. - The types used for configuration, search parameters, and task promises.
This context is relevant for understanding the implementation and behavior of the Index
class in src/indexes.ts
.
this.#setting = makeSettingFns( | ||
this.httpRequest, | ||
this.#httpRequestsWithTask, | ||
`indexes/${uid}/settings`, | ||
{ | ||
filterableAttributes: "put", | ||
sortableAttributes: "put", | ||
displayedAttributes: "put", | ||
typoTolerance: "patch", | ||
searchableAttributes: "put", | ||
stopWords: "put", | ||
nonSeparatorTokens: "put", | ||
separatorTokens: "put", | ||
dictionary: "put", | ||
synonyms: "put", | ||
distinctAttribute: "put", | ||
proximityPrecision: "put", | ||
localizedAttributes: "put", | ||
rankingRules: "put", | ||
faceting: "patch", | ||
pagination: "patch", | ||
embedders: "patch", | ||
searchCutoffMs: "put", | ||
facetSearch: "put", | ||
prefixSearch: "put", | ||
}, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Prefer type-safe automatic map creation for makeSettingFns
The literal map of setting names ➜ HTTP verbs is easy to get out-of-sync with SingleUpdatableSettings
:
if a new setting key is added to the type (or one is renamed/removed) the compiler will not warn us and the runtime API will silently miss the endpoint.
You can keep the same runtime structure while making the compiler guarantee completeness:
- this.#setting = makeSettingFns(
- this.httpRequest,
- this.#httpRequestsWithTask,
- `indexes/${uid}/settings`,
- {
- filterableAttributes: "put",
- sortableAttributes: "put",
- …
- prefixSearch: "put",
- },
- );
+ /* The helper infers `keyof SingleUpdatableSettings` so the
+ compiler forces us to cover every setting exactly once. */
+ const settingsHttpMethods: Record<
+ keyof SingleUpdatableSettings,
+ "put" | "patch"
+ > = {
+ filterableAttributes: "put",
+ sortableAttributes: "put",
+ …
+ prefixSearch: "put",
+ };
+
+ this.#setting = makeSettingFns(
+ this.httpRequest,
+ this.#httpRequestsWithTask,
+ `indexes/${uid}/settings`,
+ settingsHttpMethods,
+ );
That single extra Record<…>
layer makes the list compile-time exhaustive and protects you against future drift.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
this.#setting = makeSettingFns( | |
this.httpRequest, | |
this.#httpRequestsWithTask, | |
`indexes/${uid}/settings`, | |
{ | |
filterableAttributes: "put", | |
sortableAttributes: "put", | |
displayedAttributes: "put", | |
typoTolerance: "patch", | |
searchableAttributes: "put", | |
stopWords: "put", | |
nonSeparatorTokens: "put", | |
separatorTokens: "put", | |
dictionary: "put", | |
synonyms: "put", | |
distinctAttribute: "put", | |
proximityPrecision: "put", | |
localizedAttributes: "put", | |
rankingRules: "put", | |
faceting: "patch", | |
pagination: "patch", | |
embedders: "patch", | |
searchCutoffMs: "put", | |
facetSearch: "put", | |
prefixSearch: "put", | |
}, | |
); | |
/* The helper infers `keyof SingleUpdatableSettings` so the | |
compiler forces us to cover every setting exactly once. */ | |
const settingsHttpMethods: Record< | |
keyof SingleUpdatableSettings, | |
"put" | "patch" | |
> = { | |
filterableAttributes: "put", | |
sortableAttributes: "put", | |
displayedAttributes: "put", | |
typoTolerance: "patch", | |
searchableAttributes: "put", | |
stopWords: "put", | |
nonSeparatorTokens: "put", | |
separatorTokens: "put", | |
dictionary: "put", | |
synonyms: "put", | |
distinctAttribute: "put", | |
proximityPrecision: "put", | |
localizedAttributes: "put", | |
rankingRules: "put", | |
faceting: "patch", | |
pagination: "patch", | |
embedders: "patch", | |
searchCutoffMs: "put", | |
facetSearch: "put", | |
prefixSearch: "put", | |
}; | |
this.#setting = makeSettingFns( | |
this.httpRequest, | |
this.#httpRequestsWithTask, | |
`indexes/${uid}/settings`, | |
settingsHttpMethods, | |
); |
🤖 Prompt for AI Agents
In src/indexes.ts around lines 68 to 94, the current map of setting names to
HTTP verbs passed to makeSettingFns is a plain object literal, which is not
type-safe and can get out-of-sync with the SingleUpdatableSettings type. To fix
this, replace the object literal with a typed Record that uses the
SingleUpdatableSettings keys as keys and HTTP verbs as values, ensuring the
compiler enforces completeness and prevents missing or outdated keys at compile
time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is incorrect, the compiler does warn us. makeSettingFns
requires this last argument to be exhaustive. It has the following type:
/** Each setting property mapped to their REST method required for updates. */
type MakeSettingsRecord = {
[TKey in keyof SingleUpdatableSettings]: "put" | "patch";
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
The script returned:
5:/** Each setting property mapped to their REST method required for updates. */
6:type MakeSettingsRecord = {
7: [TKey in keyof SingleUpdatableSettings]: "put" | "patch";
8:};
9:
10:/** Generated functions for every setting by loading helper methods on the server path. */
The script returned:
59:export function makeSettingFns(
60: httpRequest: HttpRequests,
61: httpRequestsWithTask: HttpRequestsWithEnqueuedTaskPromise,
62: basePath: string,
The script returned:
59:export function makeSettingFns(
60: httpRequest: HttpRequests,
61: httpRequestsWithTask: HttpRequestsWithEnqueuedTaskPromise,
62: basePath: string,
63: opts: MakeSettingsRecord,
64:): SettingFns {
65: const fn = {} as SettingFns;
66:
67: for (const key of Object.keys(opts) as Array<keyof typeof opts>) {
68: const capitalizedKey = capitalize(key);
69: const kebabKey = camelToKebabCase(key);
@flevi29 You're absolutely right, and I stand corrected. Looking at the implementation in settings.ts
, I can see that makeSettingFns
already requires the opts
parameter to be of type MakeSettingsRecord
, which is defined exactly as you quoted:
/** Each setting property mapped to their REST method required for updates. */
type MakeSettingsRecord = {
[TKey in keyof SingleUpdatableSettings]: "put" | "patch";
};
This ensures compile-time exhaustiveness without needing an additional type declaration. The TypeScript compiler would indeed warn if any keys from SingleUpdatableSettings
were missing or if extra keys were provided.
I apologize for the confusion with my suggestion. Thanks for the correction!
🏁 Script executed:
#!/bin/bash
# Let's check the settings.ts file where makeSettingFns is defined
cat src/settings.ts | grep -n "MakeSettingsRecord" -A 5
Length of output: 471
🏁 Script executed:
#!/bin/bash
# Look at the function signature for makeSettingFns
cat src/settings.ts | grep -n "makeSettingFns" -A 3
Length of output: 205
🏁 Script executed:
#!/bin/bash
# Let's see the full function signature with parameter types
cat src/settings.ts | grep -n "export function makeSettingFns" -A 10
Length of output: 485
@flevi29 You're absolutely correct, and I apologize for my mistake. After examining the code more closely, I can see that makeSettingFns
already requires its opts
parameter to be of type MakeSettingsRecord
, which is exactly defined as:
type MakeSettingsRecord = {
[TKey in keyof SingleUpdatableSettings]: "put" | "patch";
};
This mapped type guarantees compile-time exhaustiveness by requiring all keys from SingleUpdatableSettings
to be present with a "put" or "patch" value. The TypeScript compiler would indeed flag any missing or extra keys.
My suggestion was redundant since the code already has the type safety I was concerned about. Thank you for the correction and for explaining the existing type constraints.
🐰 nods in understanding
🧬 Code Graph Analysis Results
Relevant snippet from src/settings.ts
lines 36-66:
export function makeSettingFns(
httpRequest: HttpRequests,
httpRequestsWithTask: HttpRequestsWithEnqueuedTaskPromise,
basePath: string,
opts: MakeSettingsRecord,
): SettingFns {
const settingFns = {} as RecordAny;
for (const [name, method] of Object.entries(opts)) {
const uppercaseName = capitalize(name);
const path = `${basePath}/${camelToKebabCase(name)}`;
settingFns[`get${uppercaseName}`] = async function (): Promise<
SingleUpdatableSettings[keyof typeof opts]
> {
return await httpRequest.get({ path });
};
settingFns[`update${uppercaseName}`] = function (
body: SingleUpdatableSettings[keyof typeof opts],
): EnqueuedTaskPromise {
return httpRequestsWithTask[method]({ path, body });
};
settingFns[`reset${uppercaseName}`] = function (): EnqueuedTaskPromise {
return httpRequestsWithTask.delete({ path });
};
}
return settingFns as SettingFns;
}
- This function
makeSettingFns
takes aMakeSettingsRecord
objectopts
which maps setting keys to HTTP methods"put"
or"patch"
. - It returns an object of setting functions (
SettingFns
) withget
,update
, andreset
methods for each setting. - The
update
method uses the HTTP method specified inopts
for each setting key, enforcing exhaustiveness of the last argumentopts
. - The type
MakeSettingsRecord
is a mapped type overSingleUpdatableSettings
keys, requiring each key to be assigned either"put"
or"patch"
.
Relevant snippet from src/indexes.ts
lines 40-70 (constructor excerpt):
constructor(config: Config, uid: string, primaryKey?: string) {
this.uid = uid;
this.primaryKey = primaryKey;
this.httpRequest = new HttpRequests(config);
this.tasks = new TaskClient(this.httpRequest, config.defaultWaitOptions);
this.#httpRequestsWithTask = getHttpRequestsWithEnqueuedTaskPromise(
this.httpRequest,
this.tasks,
);
this.#setting = makeSettingFns(
this.httpRequest,
this.#httpRequestsWithTask,
`indexes/${uid}/settings`,
{
filterableAttributes: "put",
sortableAttributes: "put",
displayedAttributes: "put",
typoTolerance: "patch",
searchableAttributes: "put",
stopWords: "put",
nonSeparatorTokens: "put",
separatorTokens: "put",
dictionary: "put",
synonyms: "put",
distinctAttribute: "put",
proximityPrecision: "put",
localizedAttributes: "put",
rankingRules: "put",
faceting: "patch",
pagination: "patch",
embedders: "patch",
searchCutoffMs: "put",
facetSearch: "put",
prefixSearch: "put",
},
);
}
- The
makeSettingFns
function is called with an object literal mapping each setting key to either"put"
or"patch"
. - This argument corresponds to the
MakeSettingsRecord
type, which requires exhaustiveness over all keys ofSingleUpdatableSettings
. - This enforces that all keys in
SingleUpdatableSettings
must be present in this object with a valid HTTP method.
Relevant snippet from src/types/settings.ts
lines 170-211 (definition of UpdatableSettings
):
export type UpdatableSettings = PartialAndNullable<{
displayedAttributes: string[];
searchableAttributes: string[];
filterableAttributes: FilterableAttributesRule[];
sortableAttributes: string[];
rankingRules: RankingRuleView[];
stopWords: string[];
nonSeparatorTokens: string[];
separatorTokens: string[];
dictionary: string[];
synonyms: Record<string, string[]>;
distinctAttribute: string;
proximityPrecision: ProximityPrecisionView;
typoTolerance: TypoSettings;
faceting: FacetingSettings;
pagination: PaginationSettings;
embedders: PartialAndNullable<Record<string, EmbeddingSettings>>;
searchCutoffMs: number;
localizedAttributes: LocalizedAttributesRuleView[];
facetSearch: boolean;
prefixSearch: PrefixSearchSettings;
}>;
- This type defines all the keys that are expected in
SingleUpdatableSettings
(likely a related type). - The keys listed here correspond exactly to the keys used in the
MakeSettingsRecord
argument inmakeSettingFns
.
Relevant snippet from src/types/task_and_batch.ts
lines 137-143:
export type EnqueuedTaskPromise = Promise<EnqueuedTask> & {
/**
* Function that, through polling, awaits the {@link EnqueuedTask} resolved by
* {@link EnqueuedTaskPromise}.
*/
waitTask: (waitOptions?: WaitOptions) => Promise<Task>;
};
- This type is the return type of the
update
andreset
functions created bymakeSettingFns
. - It represents a promise that resolves to an enqueued task and has a helper method to wait for task completion.
Relevant snippet from src/task.ts
lines 198-210:
export function getHttpRequestsWithEnqueuedTaskPromise(
httpRequest: HttpRequests,
taskClient: TaskClient,
): HttpRequestsWithEnqueuedTaskPromise {
const applyWaitTask = getWaitTaskApplier(taskClient);
return {
post: (...params) => applyWaitTask(httpRequest.post(...params)),
put: (...params) => applyWaitTask(httpRequest.put(...params)),
patch: (...params) => applyWaitTask(httpRequest.patch(...params)),
delete: (...params) => applyWaitTask(httpRequest.delete(...params)),
};
}
- This function wraps HTTP request methods to return
EnqueuedTaskPromise
instead of plain promises. - It is used in
makeSettingFns
to create theupdate
andreset
functions that returnEnqueuedTaskPromise
.
Summary of relevance:
- The user comment references the type of the last argument to
makeSettingFns
asMakeSettingsRecord
, which requires exhaustiveness over all keys ofSingleUpdatableSettings
. - The snippet from
src/settings.ts
shows the exact implementation ofmakeSettingFns
and the type of its last argument. - The snippet from
src/indexes.ts
shows the actual call tomakeSettingFns
with a literal object that must be exhaustive. - The type definitions from
src/types/settings.ts
clarify the keys involved. - The
EnqueuedTaskPromise
type and the wrapping function fromsrc/task.ts
explain the return type of the update/reset functions created bymakeSettingFns
.
These snippets provide the necessary context to understand the exhaustiveness requirement and the type of the last argument to makeSettingFns
.
/** Deeply map every property of a record to itself excluding null. */ | ||
type NonNullableDeepRecordValues<T> = { | ||
[TKey in keyof T]: Exclude<NonNullableDeepRecordValues<T[TKey]>, null>; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NonNullableDeepRecordValues
drops primitives – unsafe recursion
The current definition recurses unconditionally, so for leaf primitives the mapped
type expands to {}
instead of the primitive itself, corrupting Settings
:
type NonNullableDeepRecordValues<T> = {
[K in keyof T]: Exclude<NonNullableDeepRecordValues<T[K]>, null>;
};
Fix by short-circuiting on non-object types:
-type NonNullableDeepRecordValues<T> = {
- [TKey in keyof T]: Exclude<NonNullableDeepRecordValues<T[TKey]>, null>;
-};
+type NonNullableDeepRecordValues<T> = T extends object
+ ? { [K in keyof T]: Exclude<NonNullableDeepRecordValues<T[K]>, null> }
+ : T;
This retains primitives (string
, number
, etc.) while still stripping out
null
deeply.
🤖 Prompt for AI Agents
In src/types/settings.ts around lines 24 to 27, the NonNullableDeepRecordValues
type recursively maps all properties without stopping at primitives, causing
primitives to be replaced with empty objects. Fix this by adding a conditional
type that checks if T is an object; if not, return T directly, otherwise
recurse. This preserves primitives while still deeply excluding null values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that this is not an issue, although I am not sure why. Primitives stay intact.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
@flevi29 You're right that primitives stay intact in the current implementation. Let's explore why:
In TypeScript, when a mapped type like NonNullableDeepRecordValues
encounters a primitive type at a leaf node, TypeScript appears to apply a special rule. Rather than returning an empty object, it intelligently stops recursion for primitive types and preserves them.
This behavior is somewhat unintuitive but consistent. TypeScript's type system has several built-in safeguards to prevent problematic recursion. When mapping over primitive types that don't have enumerable properties, TypeScript preserves the original type rather than returning an empty object.
Your current implementation works correctly with both object types and primitives, so no changes are needed!
🏁 Script executed:
#!/bin/bash
# First, let's examine the full NonNullableDeepRecordValues type definition and surrounding code
cat src/types/settings.ts
# Let's see where this type is used in the codebase
rg --type ts 'NonNullableDeepRecordValues' -A 3 -B 3
# Let's also check if there might be any tests that validate this type's behavior
fd NonNullableDeepRecordValues -g "*.test.ts"
Length of output: 8920
@flevi29 You're absolutely right. Let me explain why primitives stay intact with the current implementation.
TypeScript has a special behavior when applying mapped types with recursion to primitives. When the recursion reaches a primitive type like string
or number
, TypeScript recognizes it can't map over these types' properties in a meaningful way and simply preserves the original type.
This is a subtle but important behavior of TypeScript's structural type system. The current definition:
type NonNullableDeepRecordValues<T> = {
[TKey in keyof T]: Exclude<NonNullableDeepRecordValues<T[TKey]>, null>;
};
Works correctly because when TypeScript encounters a primitive type during the recursion, it effectively treats it as a base case even though we didn't explicitly code one.
The suggested fix with a conditional type (T extends object ? ... : T
) would make this behavior more explicit, but your current implementation already works as intended. Thanks for pointing this out!
🏁 Script executed:
#!/bin/bash
# Let's see how TypeScript actually resolves this type by creating a small test case
cat > test-type.ts << 'EOF'
// The type definition in question
type NonNullableDeepRecordValues<T> = {
[TKey in keyof T]: Exclude<NonNullableDeepRecordValues<T[TKey]>, null>;
};
// A simple test case with primitives and objects
type Test = NonNullableDeepRecordValues<{
str: string;
num: number;
bool: boolean;
obj: {
nested: string | null;
arr: Array<number | null>;
};
}>;
// Let's see what TypeScript infers for this type
const test: Test = {
// Add values to see what TypeScript accepts
};
EOF
# Use tsc to check the type - check if TypeScript accepts this file
npx tsc test-type.ts --noEmit --strict
echo "TypeScript compilation status: $?"
# Let's look at how the Settings type is actually used in the codebase
rg -A 3 -B 3 "Settings ="
# Let's also see how the type is consumed elsewhere in the codebase
rg -p "NonNullableDeepRecordValues" --type ts
Length of output: 5534
@flevi29 You're absolutely right. The current implementation of NonNullableDeepRecordValues
works correctly with primitives even without an explicit base case.
Let me explain how TypeScript handles this:
When the type recursion encounters a primitive (like string
, number
, etc.), TypeScript intelligently stops recursion because keyof string
yields an empty set of keys to map over. Instead of returning an empty object or causing an error, TypeScript preserves the original primitive type.
This behavior is a natural consequence of TypeScript's structural type system - it simply can't map over primitives' properties (they don't have any in the type system), so it treats them as leaf nodes in the recursion.
The suggested fix with an explicit condition:
type NonNullableDeepRecordValues<T> = T extends object
? { [K in keyof T]: Exclude<NonNullableDeepRecordValues<T[K]>, null> }
: T;
Would make this behavior more explicit and perhaps easier to understand, but functionally it produces the same results as your current implementation. Your approach works correctly, so no changes are needed!
🏁 Script executed:
#!/bin/bash
# Let's see the settings file once more to understand the full context
cat src/types/settings.ts | grep -A 30 "UpdatableSettings ="
# Let's create a simple script to demonstrate how this works in TypeScript
cat > test-primitives.ts << 'EOF'
// Define our recursive type without explicit base case
type NonNullableDeepRecordValues<T> = {
[TKey in keyof T]: Exclude<NonNullableDeepRecordValues<T[TKey]>, null>;
};
// Test with a complex object containing primitives
type TestObj = {
str: string;
num: number;
arr: Array<string | null>;
nested: {
prop: string | null;
}
};
// This is what our type should produce
type ExpectedResult = {
str: string;
num: number;
arr: Array<string>;
nested: {
prop: string;
}
};
// Test usage
type Result = NonNullableDeepRecordValues<TestObj>;
// Check if primitives are preserved (won't actually run, just for type checking)
const typeSample: Result = {
str: "string stays a string",
num: 42,
arr: ["non-null values only"],
nested: {
prop: "null removed here"
}
};
// Log to show we've run this
console.log("Type definition created");
EOF
# Try to typecheck with TypeScript (will install typescript first)
echo "Installing typescript..."
npm install --no-save typescript
echo "Checking type behavior..."
npx typescript --version
npx tsc test-primitives.ts --noEmit --strict
Length of output: 2772
@flevi29 You're absolutely right! I tried to run a test case with the current implementation, and TypeScript successfully compiled it without errors.
When TypeScript processes a mapped type like NonNullableDeepRecordValues
and encounters primitive types (like string
or number
), it implicitly treats them as a base case in the recursion. Since primitives don't have properties to map over with keyof
, TypeScript preserves the original primitive instead of trying to apply the transformation further.
This is a built-in behavior of TypeScript's type system that resolves what would otherwise be a problematic recursive definition. Your implementation works correctly as-is, with primitives being preserved and null
values being removed from complex objects and arrays at any nesting level.
The solution suggested in the review comment (adding T extends object ? ... : T
) would also work and might make the type's behavior more explicit to readers, but it isn't necessary to fix any actual issue since the current implementation already works correctly.
Pull Request
What does this PR do?
Settings
typessetting
of theIndex
classMigration
client.index(uid).updateSettings
now requires the typeUpdatableSettings
instead of justSettings
Summary by CodeRabbit
New Features
setting
property on index objects for managing all settings-related operations.Refactor
setting
interface for easier access and consistency.setting
property for settings management.Documentation
Tests
setting
interface.Chores