feat(search): recherche intelligente — parsing backend + pg_trgm fuzzy

- parseSearchTokens() côté backend : extrait type (async/openapi),
  version (v1, 1.2.3) et latestOnly depuis la chaîne brute
- Recherche fuzzy via word_similarity (pg_trgm, seuil 0.5) + ILIKE
- latestOnly automatique quand champ non vide sans version explicite
- Suppression de parseSearch() dans BrowseComponent et CatalogComponent
- Suppression de version/latestOnly de ApiListQuery (géré par le backend)
- Activation de pg_trgm via CREATE EXTENSION dans SeederService
- Tests unitaires Vitest pour parseSearchTokens (24 cas)

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
z3n
2026-06-17 08:57:45 +00:00
parent e1a3dbc85b
commit 498c6577e4
6 changed files with 262 additions and 26 deletions

View File

@@ -0,0 +1,156 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { ApisService } from './apis.service';
import { ApiType } from '@datacat/shared';
/** Accès à la méthode privée pour les tests unitaires */
function parse(raw: string) {
return (ApisService.prototype as any).parseSearchTokens.call({}, raw) as {
text: string;
type?: ApiType;
version?: string;
latestOnly: boolean;
};
}
describe('ApisService.parseSearchTokens()', () => {
describe('chaîne vide', () => {
it('retourne text vide, pas de type, pas de version, latestOnly=false', () => {
expect(parse('')).toEqual({ text: '', type: undefined, version: undefined, latestOnly: false });
});
it('gère les espaces seuls', () => {
expect(parse(' ')).toEqual({ text: '', type: undefined, version: undefined, latestOnly: false });
});
});
describe('texte libre seul (sans version ni type)', () => {
it('"auth" → latestOnly=true automatique', () => {
expect(parse('auth')).toEqual({ text: 'auth', type: undefined, version: undefined, latestOnly: true });
});
it('"paiement commande" → texte libre multi-mots, latestOnly=true', () => {
expect(parse('paiement commande')).toEqual({
text: 'paiement commande',
type: undefined,
version: undefined,
latestOnly: true,
});
});
});
describe('détection du type', () => {
it('"async" → ASYNCAPI', () => {
const r = parse('async');
expect(r.type).toBe('ASYNCAPI');
});
it('"asyncapi" → ASYNCAPI', () => {
expect(parse('asyncapi').type).toBe('ASYNCAPI');
});
it('"openapi" → OPENAPI', () => {
expect(parse('openapi').type).toBe('OPENAPI');
});
it('"open" → OPENAPI', () => {
expect(parse('open').type).toBe('OPENAPI');
});
it('type seul → latestOnly=true (champ non vide, pas de version)', () => {
expect(parse('async').latestOnly).toBe(true);
});
it('mots de type ne font pas partie du texte libre', () => {
expect(parse('auth async').text).toBe('auth');
});
});
describe('détection de version', () => {
it('"v1" → version="1", latestOnly=false', () => {
expect(parse('v1')).toEqual({ text: '', type: undefined, version: '1', latestOnly: false });
});
it('"1.2" → version="1.2"', () => {
expect(parse('1.2').version).toBe('1.2');
});
it('"v1.2.3" → version="1.2.3" (préfixe v supprimé)', () => {
expect(parse('v1.2.3').version).toBe('1.2.3');
});
it('"1.0.0" → version exacte', () => {
expect(parse('1.0.0').version).toBe('1.0.0');
});
it('version présente → latestOnly=false', () => {
expect(parse('auth v1').latestOnly).toBe(false);
});
it('numéro de version ne fait pas partie du texte libre', () => {
expect(parse('auth v1').text).toBe('auth');
});
});
describe('mot-clé "latest"', () => {
it('"latest" seul → latestOnly=true, pas de texte', () => {
expect(parse('latest')).toEqual({ text: '', type: undefined, version: undefined, latestOnly: true });
});
it('"latest" + type → latestOnly=true + type détecté', () => {
const r = parse('latest async');
expect(r.latestOnly).toBe(true);
expect(r.type).toBe('ASYNCAPI');
});
it('"latest" + version → les deux flags actifs', () => {
const r = parse('latest v1');
expect(r.latestOnly).toBe(true);
expect(r.version).toBe('1');
});
});
describe('combinaisons', () => {
it('"auth async" → texte=auth, type=ASYNCAPI, latestOnly=true', () => {
expect(parse('auth async')).toEqual({
text: 'auth',
type: 'ASYNCAPI',
version: undefined,
latestOnly: true,
});
});
it('"auth async v1" → texte=auth, type=ASYNCAPI, version=1, latestOnly=false', () => {
expect(parse('auth async v1')).toEqual({
text: 'auth',
type: 'ASYNCAPI',
version: '1',
latestOnly: false,
});
});
it('"auth 1.0.0" → texte=auth, version=1.0.0, latestOnly=false', () => {
expect(parse('auth 1.0.0')).toEqual({
text: 'auth',
type: undefined,
version: '1.0.0',
latestOnly: false,
});
});
it('"openapi latest" → type=OPENAPI, latestOnly=true', () => {
expect(parse('openapi latest')).toEqual({
text: '',
type: 'OPENAPI',
version: undefined,
latestOnly: true,
});
});
it('insensible à la casse pour les mots-clés', () => {
const r = parse('ASYNC Auth LATEST');
expect(r.type).toBe('ASYNCAPI');
expect(r.latestOnly).toBe(true);
expect(r.text).toBe('Auth');
});
});
});