@strav/search 0.3.32 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/helpers.ts +50 -0
- package/src/index.ts +2 -0
- package/src/search_manager.ts +29 -4
- package/src/types.ts +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strav/search",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Full-text search for the Strav framework",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"tsconfig.json"
|
|
19
19
|
],
|
|
20
20
|
"peerDependencies": {
|
|
21
|
-
"@strav/kernel": "0.
|
|
22
|
-
"@strav/database": "0.
|
|
23
|
-
"@strav/cli": "0.
|
|
21
|
+
"@strav/kernel": "0.4.0",
|
|
22
|
+
"@strav/database": "0.4.0",
|
|
23
|
+
"@strav/cli": "0.4.0"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
26
|
"test": "bun test tests/",
|
package/src/helpers.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
SearchResult,
|
|
7
7
|
IndexSettings,
|
|
8
8
|
DriverConfig,
|
|
9
|
+
SearchScope,
|
|
9
10
|
} from './types.ts'
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -67,4 +68,53 @@ export const search = {
|
|
|
67
68
|
deleteIndex(index: string): Promise<void> {
|
|
68
69
|
return SearchManager.engine().deleteIndex(SearchManager.indexName(index))
|
|
69
70
|
},
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Return a tenant-scoped wrapper of this helper. All index names
|
|
74
|
+
* resolved through it are namespaced as `${prefix}t${tenantId}_${name}`,
|
|
75
|
+
* giving two tenants on the same shared engine independent indexes.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* await search.for({ tenantId: 42 }).upsert('articles', 1, { … })
|
|
79
|
+
* await search.for({ tenantId: 42 }).query('articles', 'lookup')
|
|
80
|
+
*
|
|
81
|
+
* Apps that don't need multi-tenant isolation skip `.for()` and call
|
|
82
|
+
* the top-level helpers directly.
|
|
83
|
+
*/
|
|
84
|
+
for(scope: SearchScope): ScopedSearch {
|
|
85
|
+
return makeScoped(scope)
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Scoped helper ────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export interface ScopedSearch {
|
|
92
|
+
query(index: string, query: string, options?: SearchOptions): Promise<SearchResult>
|
|
93
|
+
upsert(index: string, id: string | number, document: Record<string, unknown>): Promise<void>
|
|
94
|
+
upsertMany(index: string, documents: SearchDocument[]): Promise<void>
|
|
95
|
+
delete(index: string, id: string | number): Promise<void>
|
|
96
|
+
deleteMany(index: string, ids: Array<string | number>): Promise<void>
|
|
97
|
+
flush(index: string): Promise<void>
|
|
98
|
+
createIndex(index: string, options?: IndexSettings): Promise<void>
|
|
99
|
+
deleteIndex(index: string): Promise<void>
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function makeScoped(scope: SearchScope): ScopedSearch {
|
|
103
|
+
return {
|
|
104
|
+
query: (index, query, options) =>
|
|
105
|
+
SearchManager.engine().search(SearchManager.indexName(index, scope), query, options),
|
|
106
|
+
upsert: (index, id, document) =>
|
|
107
|
+
SearchManager.engine().upsert(SearchManager.indexName(index, scope), id, document),
|
|
108
|
+
upsertMany: (index, documents) =>
|
|
109
|
+
SearchManager.engine().upsertMany(SearchManager.indexName(index, scope), documents),
|
|
110
|
+
delete: (index, id) =>
|
|
111
|
+
SearchManager.engine().delete(SearchManager.indexName(index, scope), id),
|
|
112
|
+
deleteMany: (index, ids) =>
|
|
113
|
+
SearchManager.engine().deleteMany(SearchManager.indexName(index, scope), ids),
|
|
114
|
+
flush: index => SearchManager.engine().flush(SearchManager.indexName(index, scope)),
|
|
115
|
+
createIndex: (index, options) =>
|
|
116
|
+
SearchManager.engine().createIndex(SearchManager.indexName(index, scope), options),
|
|
117
|
+
deleteIndex: index =>
|
|
118
|
+
SearchManager.engine().deleteIndex(SearchManager.indexName(index, scope)),
|
|
119
|
+
}
|
|
70
120
|
}
|
package/src/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ export type { SearchableInstance, SearchableModel } from './searchable.ts'
|
|
|
30
30
|
|
|
31
31
|
// Helper
|
|
32
32
|
export { search } from './helpers.ts'
|
|
33
|
+
export type { ScopedSearch } from './helpers.ts'
|
|
33
34
|
|
|
34
35
|
// Errors
|
|
35
36
|
export { SearchError, IndexNotFoundError, SearchQueryError } from './errors.ts'
|
|
@@ -43,4 +44,5 @@ export type {
|
|
|
43
44
|
SearchResult,
|
|
44
45
|
SearchHit,
|
|
45
46
|
IndexSettings,
|
|
47
|
+
SearchScope,
|
|
46
48
|
} from './types.ts'
|
package/src/search_manager.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { inject, Configuration, ConfigurationError } from '@strav/kernel'
|
|
2
2
|
import type { SearchEngine } from './search_engine.ts'
|
|
3
|
-
import type { SearchConfig, DriverConfig } from './types.ts'
|
|
3
|
+
import type { SearchConfig, DriverConfig, SearchScope } from './types.ts'
|
|
4
4
|
import { MeilisearchDriver } from './drivers/meilisearch_driver.ts'
|
|
5
5
|
import { TypesenseDriver } from './drivers/typesense_driver.ts'
|
|
6
6
|
import { AlgoliaDriver } from './drivers/algolia_driver.ts'
|
|
@@ -53,9 +53,34 @@ export default class SearchManager {
|
|
|
53
53
|
return SearchManager._config?.prefix ?? ''
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Resolve a full index name by applying the configured prefix and
|
|
58
|
+
* optional per-tenant scope. Per-tenant scoping namespaces the index
|
|
59
|
+
* as `${prefix}t${tenantId}_${name}` so two tenants on the same
|
|
60
|
+
* shared engine cannot read or overwrite each other's documents.
|
|
61
|
+
*
|
|
62
|
+
* Apps that don't need multi-tenant isolation can omit the scope.
|
|
63
|
+
* The driver layer is unchanged — namespacing happens here at the
|
|
64
|
+
* manager boundary.
|
|
65
|
+
*/
|
|
66
|
+
static indexName(name: string, scope?: SearchScope | null): string {
|
|
67
|
+
const base = SearchManager.prefix ? `${SearchManager.prefix}${name}` : name
|
|
68
|
+
if (!scope || scope.tenantId === undefined || scope.tenantId === null) return base
|
|
69
|
+
// Validate tenant identifier — anything that ends up in an index
|
|
70
|
+
// name lands in URL paths / SQL identifiers downstream, so refuse
|
|
71
|
+
// values that could escape the namespace. Letters, digits, dashes,
|
|
72
|
+
// underscores only.
|
|
73
|
+
const tenantId = String(scope.tenantId)
|
|
74
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(tenantId)) {
|
|
75
|
+
throw new ConfigurationError(
|
|
76
|
+
`SearchManager.indexName: invalid tenantId ${JSON.stringify(tenantId)} — ` +
|
|
77
|
+
`must match /^[a-zA-Z0-9_-]+$/.`
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
if (SearchManager.prefix) {
|
|
81
|
+
return `${SearchManager.prefix}t${tenantId}_${name}`
|
|
82
|
+
}
|
|
83
|
+
return `t${tenantId}_${name}`
|
|
59
84
|
}
|
|
60
85
|
|
|
61
86
|
/** Register a custom driver factory. */
|
package/src/types.ts
CHANGED
|
@@ -5,6 +5,21 @@ export interface SearchDocument {
|
|
|
5
5
|
[key: string]: unknown
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
// ── Multi-tenant scope ────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Per-tenant scope applied at index-name resolution. Drivers don't see
|
|
12
|
+
* the scope directly — `SearchManager.indexName(name, scope)` rewrites
|
|
13
|
+
* the index name to `${prefix}t${tenantId}_${name}` so two tenants on
|
|
14
|
+
* the same shared engine read independent indexes.
|
|
15
|
+
*
|
|
16
|
+
* The tenantId must match `/^[a-zA-Z0-9_-]+$/`; anything else throws
|
|
17
|
+
* (the value lands in URL paths and SQL identifiers downstream).
|
|
18
|
+
*/
|
|
19
|
+
export interface SearchScope {
|
|
20
|
+
tenantId: string | number
|
|
21
|
+
}
|
|
22
|
+
|
|
8
23
|
// ── Index settings ────────────────────────────────────────────────────────
|
|
9
24
|
|
|
10
25
|
export interface IndexSettings {
|