@basaltkit/search 1.3.1 → 1.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/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/plugin.d.ts +3 -3
- package/dist/plugin.js +5 -2
- package/dist/search.d.ts +35 -4
- package/dist/search.js +40 -11
- package/dist/types.d.ts +15 -1
- package/package.json +6 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { defineIndex, type SearchDocument, type IndexDefinition, type SearchQuery, type SearchHit, type SearchResult, type SearchDriver, } from './types.js';
|
|
1
|
+
export { defineIndex, type SearchDocument, type SearchInput, type IndexDefinition, type SearchQuery, type SearchHit, type SearchResult, type SearchDriver, } from './types.js';
|
|
2
2
|
export { MemorySearchDriver } from './memory.js';
|
|
3
|
-
export { Search, TenantRequiredError, type SearchOptions } from './search.js';
|
|
3
|
+
export { Search, SINGLE_TENANT_SCOPE, TenantRequiredError, type SearchOptions } from './search.js';
|
|
4
4
|
export { MeilisearchDriver, MeilisearchError, SearchFilterFieldError, SearchIndexNameError, type MeilisearchDriverOptions, } from './drivers/meilisearch.js';
|
|
5
5
|
export { searchPlugin, syncRule, SEARCH, type SearchPluginOptions, type SyncRule, } from './plugin.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { defineIndex, } from './types.js';
|
|
2
2
|
export { MemorySearchDriver } from './memory.js';
|
|
3
|
-
export { Search, TenantRequiredError } from './search.js';
|
|
3
|
+
export { Search, SINGLE_TENANT_SCOPE, TenantRequiredError } from './search.js';
|
|
4
4
|
export { MeilisearchDriver, MeilisearchError, SearchFilterFieldError, SearchIndexNameError, } from './drivers/meilisearch.js';
|
|
5
5
|
export { searchPlugin, syncRule, SEARCH, } from './plugin.js';
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BasaltHooks } from '@basaltkit/core';
|
|
2
2
|
import { Search } from './search.js';
|
|
3
|
-
import type { IndexDefinition,
|
|
3
|
+
import type { IndexDefinition, SearchDriver, SearchInput } from './types.js';
|
|
4
4
|
export declare const SEARCH: import("@basaltkit/core").Token<Search>;
|
|
5
5
|
/**
|
|
6
6
|
* Keeps an index in sync with domain events: on the given hook, either upsert a
|
|
@@ -14,10 +14,10 @@ export interface SyncRule<K extends keyof BasaltHooks & string = keyof BasaltHoo
|
|
|
14
14
|
hook: K;
|
|
15
15
|
index: string;
|
|
16
16
|
/** Build the document to upsert. Return null to skip. */
|
|
17
|
-
document?: (payload: BasaltHooks[K]) =>
|
|
17
|
+
document?: (payload: BasaltHooks[K]) => SearchInput | null;
|
|
18
18
|
/** Or the identifiers to remove. Return null to skip. */
|
|
19
19
|
remove?: (payload: BasaltHooks[K]) => {
|
|
20
|
-
tenantId
|
|
20
|
+
tenantId?: string;
|
|
21
21
|
id: string;
|
|
22
22
|
} | null;
|
|
23
23
|
}
|
package/dist/plugin.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createToken, definePlugin } from '@basaltkit/core';
|
|
1
|
+
import { createToken, definePlugin, ensureMetadata } from '@basaltkit/core';
|
|
2
2
|
import { MemorySearchDriver } from './memory.js';
|
|
3
3
|
import { Search } from './search.js';
|
|
4
4
|
export const SEARCH = createToken('search');
|
|
@@ -11,7 +11,10 @@ export function searchPlugin(options = {}) {
|
|
|
11
11
|
return definePlugin({
|
|
12
12
|
name: 'basalt:search',
|
|
13
13
|
register({ container }) {
|
|
14
|
-
|
|
14
|
+
// 'tenancy:active' is tenancyPlugin's marker: how a generic package
|
|
15
|
+
// learns the app is multi-tenant without importing @basaltkit/tenancy.
|
|
16
|
+
const metadata = ensureMetadata(container);
|
|
17
|
+
container.singleton(SEARCH, () => new Search({ driver }, () => metadata.get('tenancy:active').length > 0));
|
|
15
18
|
},
|
|
16
19
|
async boot({ container, hooks }) {
|
|
17
20
|
const search = container.get(SEARCH);
|
package/dist/search.d.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { BasaltError } from '@basaltkit/core';
|
|
2
|
-
import type {
|
|
2
|
+
import type { SearchDriver, SearchInput, SearchResult } from './types.js';
|
|
3
3
|
/** Search was asked for a tenant it couldn't determine. */
|
|
4
4
|
export declare class TenantRequiredError extends BasaltError {
|
|
5
5
|
readonly status = 400;
|
|
6
6
|
constructor();
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* The scope every document lands in when the app has no tenancy at all. The
|
|
10
|
+
* driver contract is tenant-keyed, so a single-tenant app still needs one
|
|
11
|
+
* stable key — it just shouldn't have to invent (and remember) it.
|
|
12
|
+
*/
|
|
13
|
+
export declare const SINGLE_TENANT_SCOPE = "default";
|
|
8
14
|
export interface SearchOptions {
|
|
9
15
|
/** Defaults to the current tenant (`ctx().tenant.id`). */
|
|
10
16
|
tenantId?: string;
|
|
@@ -17,12 +23,37 @@ export interface SearchOptions {
|
|
|
17
23
|
* querying takes it from `options.tenantId` or the current request context.
|
|
18
24
|
*/
|
|
19
25
|
export declare class Search {
|
|
26
|
+
/**
|
|
27
|
+
* Whether the host app registered `@basaltkit/tenancy`. `searchPlugin`
|
|
28
|
+
* wires this to the container's `'tenancy:active'` metadata marker — a
|
|
29
|
+
* signal, not an import, so this generic package never depends on the
|
|
30
|
+
* opt-in SaaS layer. Defaults to `false` (single-tenant).
|
|
31
|
+
*/
|
|
32
|
+
private readonly tenancyActive;
|
|
20
33
|
private readonly driver;
|
|
21
34
|
constructor(options?: {
|
|
22
35
|
driver?: SearchDriver;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
36
|
+
},
|
|
37
|
+
/**
|
|
38
|
+
* Whether the host app registered `@basaltkit/tenancy`. `searchPlugin`
|
|
39
|
+
* wires this to the container's `'tenancy:active'` metadata marker — a
|
|
40
|
+
* signal, not an import, so this generic package never depends on the
|
|
41
|
+
* opt-in SaaS layer. Defaults to `false` (single-tenant).
|
|
42
|
+
*/
|
|
43
|
+
tenancyActive?: () => boolean);
|
|
44
|
+
index(indexName: string, document: SearchInput): Promise<void>;
|
|
45
|
+
bulk(indexName: string, documents: SearchInput[]): Promise<void>;
|
|
26
46
|
remove(indexName: string, id: string, tenantId?: string): Promise<void>;
|
|
27
47
|
search(indexName: string, q: string, options?: SearchOptions): Promise<SearchResult>;
|
|
48
|
+
/**
|
|
49
|
+
* The tenant a call is scoped to.
|
|
50
|
+
*
|
|
51
|
+
* With `@basaltkit/tenancy` registered an unresolvable tenant is an error:
|
|
52
|
+
* indexing or querying unscoped would cross tenants. Without it there is no
|
|
53
|
+
* tenant dimension, so every document shares {@link SINGLE_TENANT_SCOPE} and
|
|
54
|
+
* index/query always agree.
|
|
55
|
+
*/
|
|
56
|
+
private tenant;
|
|
57
|
+
/** Fills in the document's tenant with the same rule the read path uses. */
|
|
58
|
+
private resolveDocument;
|
|
28
59
|
}
|
package/dist/search.js
CHANGED
|
@@ -7,37 +7,66 @@ export class TenantRequiredError extends BasaltError {
|
|
|
7
7
|
super('SEARCH_TENANT_REQUIRED', 'A tenant is required — pass tenantId or run inside a tenant context.');
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The scope every document lands in when the app has no tenancy at all. The
|
|
12
|
+
* driver contract is tenant-keyed, so a single-tenant app still needs one
|
|
13
|
+
* stable key — it just shouldn't have to invent (and remember) it.
|
|
14
|
+
*/
|
|
15
|
+
export const SINGLE_TENANT_SCOPE = 'default';
|
|
16
16
|
/**
|
|
17
17
|
* Tenant-scoped full-text search. Indexing takes the tenant from the document;
|
|
18
18
|
* querying takes it from `options.tenantId` or the current request context.
|
|
19
19
|
*/
|
|
20
20
|
export class Search {
|
|
21
|
+
tenancyActive;
|
|
21
22
|
driver;
|
|
22
|
-
constructor(options = {}
|
|
23
|
+
constructor(options = {},
|
|
24
|
+
/**
|
|
25
|
+
* Whether the host app registered `@basaltkit/tenancy`. `searchPlugin`
|
|
26
|
+
* wires this to the container's `'tenancy:active'` metadata marker — a
|
|
27
|
+
* signal, not an import, so this generic package never depends on the
|
|
28
|
+
* opt-in SaaS layer. Defaults to `false` (single-tenant).
|
|
29
|
+
*/
|
|
30
|
+
tenancyActive = () => false) {
|
|
31
|
+
this.tenancyActive = tenancyActive;
|
|
23
32
|
this.driver = options.driver ?? new MemorySearchDriver();
|
|
24
33
|
}
|
|
25
34
|
index(indexName, document) {
|
|
26
|
-
return this.driver.index(indexName, document);
|
|
35
|
+
return this.driver.index(indexName, this.resolveDocument(document));
|
|
27
36
|
}
|
|
28
37
|
bulk(indexName, documents) {
|
|
29
|
-
return this.driver.bulk(indexName, documents);
|
|
38
|
+
return this.driver.bulk(indexName, documents.map((document) => this.resolveDocument(document)));
|
|
30
39
|
}
|
|
31
40
|
async remove(indexName, id, tenantId) {
|
|
32
|
-
return this.driver.remove(indexName,
|
|
41
|
+
return this.driver.remove(indexName, this.tenant(tenantId), id);
|
|
33
42
|
}
|
|
34
43
|
async search(indexName, q, options = {}) {
|
|
35
44
|
return this.driver.search(indexName, {
|
|
36
|
-
tenantId:
|
|
45
|
+
tenantId: this.tenant(options.tenantId),
|
|
37
46
|
q,
|
|
38
47
|
...(options.filters ? { filters: options.filters } : {}),
|
|
39
48
|
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
|
40
49
|
...(options.offset !== undefined ? { offset: options.offset } : {}),
|
|
41
50
|
});
|
|
42
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* The tenant a call is scoped to.
|
|
54
|
+
*
|
|
55
|
+
* With `@basaltkit/tenancy` registered an unresolvable tenant is an error:
|
|
56
|
+
* indexing or querying unscoped would cross tenants. Without it there is no
|
|
57
|
+
* tenant dimension, so every document shares {@link SINGLE_TENANT_SCOPE} and
|
|
58
|
+
* index/query always agree.
|
|
59
|
+
*/
|
|
60
|
+
tenant(explicit) {
|
|
61
|
+
const id = explicit ?? tryCtx()?.['tenant']?.id;
|
|
62
|
+
if (id)
|
|
63
|
+
return id;
|
|
64
|
+
if (this.tenancyActive())
|
|
65
|
+
throw new TenantRequiredError();
|
|
66
|
+
return SINGLE_TENANT_SCOPE;
|
|
67
|
+
}
|
|
68
|
+
/** Fills in the document's tenant with the same rule the read path uses. */
|
|
69
|
+
resolveDocument(document) {
|
|
70
|
+
return { ...document, tenantId: this.tenant(document.tenantId) };
|
|
71
|
+
}
|
|
43
72
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* A document as the driver sees it: `tenantId` is always resolved by the time
|
|
3
|
+
* it reaches the driver. Callers pass a {@link SearchInput}.
|
|
4
|
+
*/
|
|
2
5
|
export interface SearchDocument {
|
|
3
6
|
id: string;
|
|
4
7
|
tenantId: string;
|
|
5
8
|
[field: string]: unknown;
|
|
6
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* A document as callers provide it. `tenantId` is optional: `Search` resolves
|
|
12
|
+
* it from the ambient tenant, and in an app with no `tenancyPlugin` falls back
|
|
13
|
+
* to {@link SINGLE_TENANT_SCOPE} — so a non-SaaS app indexes and queries the
|
|
14
|
+
* same scope without inventing a tenant id.
|
|
15
|
+
*/
|
|
16
|
+
export interface SearchInput {
|
|
17
|
+
id: string;
|
|
18
|
+
tenantId?: string;
|
|
19
|
+
[field: string]: unknown;
|
|
20
|
+
}
|
|
7
21
|
/** Declares an index: which fields are searchable (full-text) and filterable. */
|
|
8
22
|
export interface IndexDefinition {
|
|
9
23
|
name: string;
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basaltkit/search",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"engines": {
|
|
5
|
+
"node": ">=22.5.0"
|
|
6
|
+
},
|
|
4
7
|
"description": "Full-text search for Basalt: tenant-scoped indexing and querying with a pluggable driver (in-memory for dev/test, Meilisearch for production) and automatic sync from domain events.",
|
|
5
8
|
"license": "MIT",
|
|
6
9
|
"type": "module",
|
|
10
|
+
"sideEffects": false,
|
|
7
11
|
"exports": {
|
|
8
12
|
".": {
|
|
9
13
|
"types": "./dist/index.d.ts",
|
|
@@ -14,7 +18,7 @@
|
|
|
14
18
|
"dist"
|
|
15
19
|
],
|
|
16
20
|
"dependencies": {
|
|
17
|
-
"@basaltkit/core": "^1.1
|
|
21
|
+
"@basaltkit/core": "^1.3.1"
|
|
18
22
|
},
|
|
19
23
|
"devDependencies": {
|
|
20
24
|
"@types/node": "^26.3.0",
|