fix(smartfuzzy): handle empty search strings safely and update tests for stricter TypeScript compatibility

This commit is contained in:
2026-05-01 16:02:29 +00:00
parent 58109bd7e0
commit 31c7865d88
12 changed files with 3676 additions and 4989 deletions
+40
View File
@@ -0,0 +1,40 @@
{
"@git.zone/cli": {
"projectType": "npm",
"module": {
"githost": "code.foss.global",
"gitscope": "push.rocks",
"gitrepo": "smartfuzzy",
"shortDescription": "search things easily",
"npmPackagename": "@push.rocks/smartfuzzy",
"license": "MIT",
"description": "A library for fuzzy matching strings against word dictionaries or arrays, with support for object and article searching.",
"keywords": [
"fuzzy matching",
"string matching",
"dictionary matching",
"search",
"text analysis",
"object sorting",
"article search",
"text similarity",
"keyword matching",
"data filtering"
]
},
"release": {
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public"
}
},
"@git.zone/tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
},
"@ship.zone/szci": {
"npmGlobalTools": [],
"npmRegistryUrl": "registry.npmjs.org"
}
}
+7
View File
@@ -1,5 +1,12 @@
# Changelog # Changelog
## 2026-05-01 - 2.0.1 - fix(smartfuzzy)
handle empty search strings safely and update tests for stricter TypeScript compatibility
- Return null when closestStringMatch receives an empty search string to avoid unnecessary fuzzy matching
- Update article search tests to satisfy current @tsclass/tsclass typings by providing an author object and using undefined for optional URLs
- Migrate tests to @git.zone/tstest/tapbundle and refresh build/test tooling configuration
## 2025-08-05 - 2.0.0 - BREAKING_CHANGE(api) ## 2025-08-05 - 2.0.0 - BREAKING_CHANGE(api)
Major API cleanup and comprehensive documentation overhaul Major API cleanup and comprehensive documentation overhaul
+3 -1
View File
@@ -1,4 +1,6 @@
Copyright (c) 2014 Lossless GmbH (hello@lossless.com) The MIT License (MIT)
Copyright (c) 2026 Task Venture Capital GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+13 -6
View File
@@ -1,5 +1,5 @@
{ {
"gitzone": { "@git.zone/cli": {
"projectType": "npm", "projectType": "npm",
"module": { "module": {
"githost": "code.foss.global", "githost": "code.foss.global",
@@ -21,13 +21,20 @@
"keyword matching", "keyword matching",
"data filtering" "data filtering"
] ]
},
"release": {
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public"
} }
}, },
"npmci": { "@git.zone/tsdoc": {
"npmGlobalTools": [],
"npmAccessLevel": "public"
},
"tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n" "legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
},
"@ship.zone/szci": {
"npmGlobalTools": [],
"npmRegistryUrl": "registry.npmjs.org"
} }
} }
+13 -15
View File
@@ -5,26 +5,25 @@
"description": "A library for fuzzy matching strings against word dictionaries or arrays, with support for object and article searching.", "description": "A library for fuzzy matching strings against word dictionaries or arrays, with support for object and article searching.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts", "typings": "dist_ts/index.d.ts",
"author": "Lossless GmbH", "author": "Task Venture Capital GmbH <hello@task.vc>",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"test": "(tstest test/)", "test": "tstest test/ --verbose --timeout 20",
"format": "(gitzone format)", "format": "gitzone format",
"build": "(tsbuild tsfolders --allowimplicitany)", "build": "tsbuild tsfolders",
"buildDocs": "tsdoc" "buildDocs": "tsdoc"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.6.4", "@git.zone/tsbuild": "^4.4.0",
"@git.zone/tsrun": "^1.3.3", "@git.zone/tsrun": "^2.0.3",
"@git.zone/tstest": "^2.3.2", "@git.zone/tstest": "^3.6.3",
"@push.rocks/tapbundle": "^6.0.3", "@types/node": "^25.6.0"
"@types/node": "^22.15.17"
}, },
"dependencies": { "dependencies": {
"@push.rocks/smartpromise": "^4.0.2", "@push.rocks/smartpromise": "^4.0.2",
"@tsclass/tsclass": "^9.2.0", "@tsclass/tsclass": "^9.5.1",
"fuse.js": "^7.1.0", "fuse.js": "^7.3.0",
"leven": "^4.0.0" "leven": "^4.1.0"
}, },
"browserslist": [ "browserslist": [
"last 1 chrome versions" "last 1 chrome versions"
@@ -38,6 +37,8 @@
"dist_ts_web/**/*", "dist_ts_web/**/*",
"assets/**/*", "assets/**/*",
"cli.js", "cli.js",
".smartconfig.json",
"license",
"npmextra.json", "npmextra.json",
"readme.md" "readme.md"
], ],
@@ -62,8 +63,5 @@
"url": "https://code.foss.global/push.rocks/smartfuzzy/issues" "url": "https://code.foss.global/push.rocks/smartfuzzy/issues"
}, },
"type": "module", "type": "module",
"pnpm": {
"overrides": {}
},
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39" "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
} }
+3547 -4931
View File
File diff suppressed because it is too large Load Diff
+37 -22
View File
@@ -1,39 +1,49 @@
import { expect, tap } from '@push.rocks/tapbundle'; import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as tsclass from '@tsclass/tsclass'; import * as tsclass from '@tsclass/tsclass';
import * as smartfuzzy from '../ts/index.js'; import * as smartfuzzy from '../ts/index.js';
// Create fixed timestamps for consistent test results // Create fixed timestamps for consistent test results
const timestamp1 = 1620000000000; // May 2021 const timestamp1 = 1620000000000; // May 2021
const timestamp2 = 1620086400000; // May 2021 + 1 day const timestamp2 = 1620086400000; // May 2021 + 1 day
const testAuthor: tsclass.content.IAuthor = {
firstName: 'Test',
surName: 'Author',
birthday: {
day: 1,
month: 1,
year: 1980,
},
articles: [],
};
// Test articles with known content // Test articles with known content
const testArticles: tsclass.content.IArticle[] = [ const testArticles: tsclass.content.IArticle[] = [
{ {
title: 'Berlin has a ambivalent history', title: 'Berlin has a ambivalent history',
content: 'it is known that Berlin has an interesting history', content: 'it is known that Berlin has an interesting history',
author: null, author: testAuthor,
tags: ['city', 'Europe', 'history', 'travel'], tags: ['city', 'Europe', 'history', 'travel'],
timestamp: timestamp1, timestamp: timestamp1,
featuredImageUrl: null, featuredImageUrl: undefined,
url: null, url: undefined,
}, },
{ {
title: 'Washington is a great city', title: 'Washington is a great city',
content: 'it is known that Washington is one of the greatest cities in the world', content: 'it is known that Washington is one of the greatest cities in the world',
author: null, author: testAuthor,
tags: ['city', 'USA', 'travel', 'politics'], tags: ['city', 'USA', 'travel', 'politics'],
timestamp: timestamp2, timestamp: timestamp2,
featuredImageUrl: null, featuredImageUrl: undefined,
url: null, url: undefined,
}, },
{ {
title: 'Travel tips for European cities', title: 'Travel tips for European cities',
content: 'Here are some travel tips for European cities including Berlin and Paris', content: 'Here are some travel tips for European cities including Berlin and Paris',
author: null, author: testAuthor,
tags: ['travel', 'Europe', 'tips'], tags: ['travel', 'Europe', 'tips'],
timestamp: timestamp2, timestamp: timestamp2,
featuredImageUrl: null, featuredImageUrl: undefined,
url: null, url: undefined,
} }
]; ];
@@ -59,14 +69,19 @@ tap.test('should search by exact tag match', async () => {
expect(result.length).toBeGreaterThan(0); expect(result.length).toBeGreaterThan(0);
// First result should be the Washington article (contains USA tag) // First result should be the Washington article (contains USA tag)
expect(result[0].item.title).toInclude('Washington'); const firstResult = result[0];
if (!firstResult) {
throw new Error('Expected at least one result');
}
expect(firstResult.item.title).toInclude('Washington');
// Should include match information // Should include match information
expect(result[0].matches).toBeDefined(); const matches = firstResult.matches ?? [];
expect(result[0].matches.length).toBeGreaterThan(0); expect(matches).toBeDefined();
expect(matches.length).toBeGreaterThan(0);
// At least one match should be for the 'USA' tag // At least one match should be for the 'USA' tag
const tagMatch = result[0].matches.find(m => m.key === 'tags' && m.value === 'USA'); const tagMatch = matches.find((match) => match.key === 'tags' && match.value === 'USA');
expect(tagMatch).toBeDefined(); expect(tagMatch).toBeDefined();
}); });
@@ -79,8 +94,8 @@ tap.test('should search by title and content', async () => {
// The Travel article mentions Berlin in content, so it should be included // The Travel article mentions Berlin in content, so it should be included
// but ranked lower // but ranked lower
const berlinArticleIndex = result.findIndex(r => r.item.title.includes('Berlin')); const berlinArticleIndex = result.findIndex((searchResult) => searchResult.item.title.includes('Berlin'));
const travelArticleIndex = result.findIndex(r => r.item.title.includes('Travel')); const travelArticleIndex = result.findIndex((searchResult) => searchResult.item.title.includes('Travel'));
expect(berlinArticleIndex).toBeLessThan(travelArticleIndex); expect(berlinArticleIndex).toBeLessThan(travelArticleIndex);
}); });
@@ -93,11 +108,11 @@ tap.test('should add articles incrementally', async () => {
const newArticle: tsclass.content.IArticle = { const newArticle: tsclass.content.IArticle = {
title: 'New Article', title: 'New Article',
content: 'This is a new article about technology', content: 'This is a new article about technology',
author: null, author: testAuthor,
tags: ['technology', 'new'], tags: ['technology', 'new'],
timestamp: Date.now(), timestamp: Date.now(),
featuredImageUrl: null, featuredImageUrl: undefined,
url: null, url: undefined,
}; };
newSearch.addArticle(newArticle); newSearch.addArticle(newArticle);
@@ -113,11 +128,11 @@ tap.test('should add articles incrementally', async () => {
const anotherArticle: tsclass.content.IArticle = { const anotherArticle: tsclass.content.IArticle = {
title: 'Another Tech Article', title: 'Another Tech Article',
content: 'Another article about technology innovations', content: 'Another article about technology innovations',
author: null, author: testAuthor,
tags: ['technology', 'innovation'], tags: ['technology', 'innovation'],
timestamp: Date.now(), timestamp: Date.now(),
featuredImageUrl: null, featuredImageUrl: undefined,
url: null, url: undefined,
}; };
newSearch.addArticle(anotherArticle); newSearch.addArticle(anotherArticle);
+3 -3
View File
@@ -1,4 +1,4 @@
import { expect, tap } from '@push.rocks/tapbundle'; import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartfuzzy from '../ts/index.js'; import * as smartfuzzy from '../ts/index.js';
class Car { class Car {
@@ -73,11 +73,11 @@ tap.test('should sort objects by multiple field search', async () => {
// fuzzy matching algorithm's threshold setting // fuzzy matching algorithm's threshold setting
// BMW should be the first result // BMW should be the first result
const bmwIndex = result.findIndex(r => r.item.brand === 'BMW'); const bmwIndex = result.findIndex((fuzzyResult) => fuzzyResult.item.brand === 'BMW');
expect(bmwIndex).toEqual(0); expect(bmwIndex).toEqual(0);
// If Toyota is in results, it should be ranked lower than BMW // If Toyota is in results, it should be ranked lower than BMW
const toyotaIndex = result.findIndex(r => r.item.brand === 'Toyota'); const toyotaIndex = result.findIndex((fuzzyResult) => fuzzyResult.item.brand === 'Toyota');
if (toyotaIndex !== -1) { if (toyotaIndex !== -1) {
expect(bmwIndex).toBeLessThan(toyotaIndex); expect(bmwIndex).toBeLessThan(toyotaIndex);
} }
+1 -1
View File
@@ -1,4 +1,4 @@
import { expect, tap } from '@push.rocks/tapbundle'; import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartfuzzy from '../ts/index.js'; import * as smartfuzzy from '../ts/index.js';
let testSmartfuzzy: smartfuzzy.Smartfuzzy; let testSmartfuzzy: smartfuzzy.Smartfuzzy;
+1 -1
View File
@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartfuzzy', name: '@push.rocks/smartfuzzy',
version: '1.1.10', version: '2.0.1',
description: 'A library for fuzzy matching strings against word dictionaries or arrays, with support for object and article searching.' description: 'A library for fuzzy matching strings against word dictionaries or arrays, with support for object and article searching.'
} }
+5 -1
View File
@@ -118,6 +118,10 @@ export class Smartfuzzy {
return null; // Return null for empty dictionary instead of throwing error return null; // Return null for empty dictionary instead of throwing error
} }
if (stringArg.length === 0) {
return null;
}
const fuseDictionary: { name: string }[] = []; const fuseDictionary: { name: string }[] = [];
for (const wordArg of this.dictionary) { for (const wordArg of this.dictionary) {
fuseDictionary.push({ fuseDictionary.push({
@@ -135,7 +139,7 @@ export class Smartfuzzy {
}; };
const fuse = new plugins.fuseJs(fuseDictionary, fuseOptions); const fuse = new plugins.fuseJs(fuseDictionary, fuseOptions);
const fuzzyResult = fuse.search(stringArg); const fuzzyResult = fuse.search(stringArg);
let closestMatch: string = null; let closestMatch: string | null = null;
if (fuzzyResult.length > 0) { if (fuzzyResult.length > 0) {
closestMatch = fuzzyResult[0].item.name; closestMatch = fuzzyResult[0].item.name;
} }
+3 -5
View File
@@ -5,12 +5,10 @@
"target": "ES2022", "target": "ES2022",
"module": "NodeNext", "module": "NodeNext",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
"noImplicitAny": true,
"esModuleInterop": true, "esModuleInterop": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"baseUrl": ".", "types": ["node"]
"paths": {}
}, },
"exclude": [ "exclude": ["dist_*/**/*.d.ts"]
"dist_*/**/*.d.ts"
]
} }