@programisto/edrm-search 1.0.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/.eslintrc.json +13 -0
- package/CHANGELOG.md +13 -0
- package/README.md +66 -0
- package/dist/bin/www.d.ts +1 -0
- package/dist/bin/www.js +14 -0
- package/dist/lib/opensearch-provider.d.ts +2 -0
- package/dist/lib/opensearch-provider.js +70 -0
- package/dist/lib/search-provider-adapter.d.ts +5 -0
- package/dist/lib/search-provider-adapter.js +10 -0
- package/dist/lib/search-service.d.ts +13 -0
- package/dist/lib/search-service.js +100 -0
- package/dist/lib/types.d.ts +16 -0
- package/dist/lib/types.js +1 -0
- package/dist/listeners/search.listener.d.ts +1 -0
- package/dist/listeners/search.listener.js +62 -0
- package/dist/routes/search.router.d.ts +8 -0
- package/dist/routes/search.router.js +78 -0
- package/package.json +58 -0
- package/tsconfig.json +20 -0
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"env": { "node": true, "es2021": true },
|
|
3
|
+
"extends": ["standard"],
|
|
4
|
+
"parser": "@typescript-eslint/parser",
|
|
5
|
+
"parserOptions": { "ecmaVersion": 12, "sourceType": "module" },
|
|
6
|
+
"plugins": ["@typescript-eslint"],
|
|
7
|
+
"rules": {
|
|
8
|
+
"semi": ["error", "always"],
|
|
9
|
+
"indent": "off",
|
|
10
|
+
"space-before-function-paren": "off"
|
|
11
|
+
},
|
|
12
|
+
"ignorePatterns": ["dist/", "node_modules/"]
|
|
13
|
+
}
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.0.0] (unreleased)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Initial release: EDRM Search module for Endurance.
|
|
8
|
+
- Listener for `postSave` and `postDeleteOne` events to sync indexes with MongoDB collections.
|
|
9
|
+
- OpenSearch provider (index, remove, search, ensureIndex).
|
|
10
|
+
- Search service with configurable prefix and lazy index creation.
|
|
11
|
+
- Adapter implementing `EnduranceSearchProvider` and registration via `EnduranceSearchMiddleware.setInstance()`.
|
|
12
|
+
- Optional routes POST/GET `/search` (disabled by default via `EDRM_SEARCH_ROUTES_ENABLED`).
|
|
13
|
+
- Support for `q`, `filter`, `sort`, `from`, `size`, and raw `body` in search options.
|
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# EDRM Search
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
EDRM Search is an Endurance module that provides full-text search via OpenSearch (or Elasticsearch). It listens to Endurance schema events (`postSave`, `postDeleteOne`) to keep search indexes in sync with MongoDB collections. Search is exposed through the **Endurance search middleware**, so other EDRM modules can call search without depending on edrm-search.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Automatic index sync: documents are indexed on save and removed on delete.
|
|
10
|
+
- One index per MongoDB collection (with optional prefix).
|
|
11
|
+
- Search via **middleware**: use `EnduranceSearchMiddleware.getInstance().search(collection, options)` in any module.
|
|
12
|
+
- Optional unified route `POST /search` (disabled by default; enable with `EDRM_SEARCH_ROUTES_ENABLED=true`).
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @programisto/edrm-search
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- **@programisto/endurance** ≥ 1.0.x (with `EnduranceSearchMiddleware` and `postSave` / `postDeleteOne` events)
|
|
23
|
+
- Node.js ≥ 18
|
|
24
|
+
- OpenSearch (or compatible) cluster
|
|
25
|
+
|
|
26
|
+
## Environment Variables
|
|
27
|
+
|
|
28
|
+
| Variable | Required | Default | Description |
|
|
29
|
+
| -------- | -------- | ------- | ----------- |
|
|
30
|
+
| `SEARCH_ENGINE_URL` | Yes (if search enabled) | - | OpenSearch/Elasticsearch cluster URL (e.g. `https://localhost:9200`) |
|
|
31
|
+
| `SEARCH_ENGINE_ENABLED` | No | - | Set to `true` to enable indexing and the search provider |
|
|
32
|
+
| `SEARCH_ENGINE_INDEX_PREFIX` | No | `''` | Prefix for index names (e.g. `app_` → `app_users`) |
|
|
33
|
+
| `SEARCH_ENGINE_PROVIDER` | No | `opensearch` | `opensearch` or `elasticsearch` |
|
|
34
|
+
| `SEARCH_ENGINE_USERNAME` | No | - | Basic auth username |
|
|
35
|
+
| `SEARCH_ENGINE_PASSWORD` | No | - | Basic auth password |
|
|
36
|
+
| `EDRM_SEARCH_ROUTES_ENABLED` | No | `false` | Set to `true` to enable the `/search` HTTP routes (disabled by default for security) |
|
|
37
|
+
|
|
38
|
+
## Using search from another module (middleware)
|
|
39
|
+
|
|
40
|
+
In any EDRM module, use the Endurance search middleware so you do not depend on edrm-search:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { EnduranceSearchMiddleware } from '@programisto/endurance';
|
|
44
|
+
|
|
45
|
+
// In a route handler:
|
|
46
|
+
const provider = EnduranceSearchMiddleware.getInstance();
|
|
47
|
+
if (provider.isSearchEnabled()) {
|
|
48
|
+
const result = await provider.search('users', { q: req.query.q, size: 20 });
|
|
49
|
+
res.json(result);
|
|
50
|
+
} else {
|
|
51
|
+
// Fallback to Model.find() or other logic
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Routes (when enabled)
|
|
56
|
+
|
|
57
|
+
When `EDRM_SEARCH_ROUTES_ENABLED=true`:
|
|
58
|
+
|
|
59
|
+
- **POST /search** – Body: `{ collection, q?, filter?, sort?, from?, size?, body? }`
|
|
60
|
+
- **GET /search** – Query: `collection` (required), `q`, `filter`, `sort`, `from`, `size`
|
|
61
|
+
|
|
62
|
+
By default these routes return **503** with `code: "SEARCH_ROUTES_DISABLED"` so that search is used only via the middleware from your modules.
|
|
63
|
+
|
|
64
|
+
## Collection name convention
|
|
65
|
+
|
|
66
|
+
If the document does not expose a collection name via `constructor.getModel().collection.name`, the listener derives it from the event name (e.g. `User:postSave` → collection `users`).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/bin/www.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
const localAppPath = path.join(__dirname, '../lib/app.js');
|
|
8
|
+
const coreAppPath = path.join(__dirname, '../../node_modules/@programisto/endurance/dist/internal/app.js');
|
|
9
|
+
if (fs.existsSync(localAppPath)) {
|
|
10
|
+
await import(localAppPath);
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
await import(coreAppPath);
|
|
14
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Client } from '@opensearch-project/opensearch';
|
|
2
|
+
let clientInstance = null;
|
|
3
|
+
function getClient() {
|
|
4
|
+
if (!clientInstance) {
|
|
5
|
+
const node = process.env.SEARCH_ENGINE_URL ?? '';
|
|
6
|
+
const clientConfig = { node };
|
|
7
|
+
const username = process.env.SEARCH_ENGINE_USERNAME;
|
|
8
|
+
const password = process.env.SEARCH_ENGINE_PASSWORD;
|
|
9
|
+
if (username && password) {
|
|
10
|
+
clientConfig.auth = { username, password };
|
|
11
|
+
}
|
|
12
|
+
clientInstance = new Client(clientConfig);
|
|
13
|
+
}
|
|
14
|
+
return clientInstance;
|
|
15
|
+
}
|
|
16
|
+
export function createOpenSearchProvider() {
|
|
17
|
+
const client = getClient();
|
|
18
|
+
return {
|
|
19
|
+
async index(indexName, id, body) {
|
|
20
|
+
await client.index({
|
|
21
|
+
index: indexName,
|
|
22
|
+
id: String(id),
|
|
23
|
+
body: body,
|
|
24
|
+
refresh: false
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
async remove(indexName, id) {
|
|
28
|
+
await client.delete({
|
|
29
|
+
index: indexName,
|
|
30
|
+
id: String(id),
|
|
31
|
+
refresh: false
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
async search(indexName, body) {
|
|
35
|
+
const response = await client.search({
|
|
36
|
+
index: indexName,
|
|
37
|
+
body: body
|
|
38
|
+
});
|
|
39
|
+
const hits = (response.body.hits?.hits ?? []).map((h) => ({
|
|
40
|
+
_id: h._id,
|
|
41
|
+
_source: h._source ?? {}
|
|
42
|
+
}));
|
|
43
|
+
return { hits };
|
|
44
|
+
},
|
|
45
|
+
async ensureIndex(indexName) {
|
|
46
|
+
try {
|
|
47
|
+
const exists = await client.indices.exists({ index: indexName });
|
|
48
|
+
const existsBody = exists.body;
|
|
49
|
+
if (!existsBody) {
|
|
50
|
+
await client.indices.create({
|
|
51
|
+
index: indexName,
|
|
52
|
+
body: { settings: { 'index.mapping.total_fields.limit': 2000 } }
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
const status = err?.meta?.statusCode;
|
|
58
|
+
if (status === 404) {
|
|
59
|
+
await client.indices.create({
|
|
60
|
+
index: indexName,
|
|
61
|
+
body: { settings: { 'index.mapping.total_fields.limit': 2000 } }
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { EnduranceSearchProvider, type SearchOptions, type SearchResult } from '@programisto/endurance';
|
|
2
|
+
export declare class EdrmSearchProviderAdapter extends EnduranceSearchProvider {
|
|
3
|
+
isSearchEnabled(): boolean;
|
|
4
|
+
search(collection: string, options: SearchOptions): Promise<SearchResult>;
|
|
5
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { EnduranceSearchProvider } from '@programisto/endurance';
|
|
2
|
+
import { searchService } from './search-service.js';
|
|
3
|
+
export class EdrmSearchProviderAdapter extends EnduranceSearchProvider {
|
|
4
|
+
isSearchEnabled() {
|
|
5
|
+
return searchService.isEnabled();
|
|
6
|
+
}
|
|
7
|
+
async search(collection, options) {
|
|
8
|
+
return searchService.search(collection, { ...options, collection });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SearchOptions, SearchResult } from '@programisto/endurance';
|
|
2
|
+
declare class SearchService {
|
|
3
|
+
private provider;
|
|
4
|
+
private prefix;
|
|
5
|
+
private getProvider;
|
|
6
|
+
isEnabled(): boolean;
|
|
7
|
+
getIndexName(collectionName: string): string;
|
|
8
|
+
index(collectionName: string, id: string, document: object): Promise<void>;
|
|
9
|
+
remove(collectionName: string, id: string): Promise<void>;
|
|
10
|
+
search(collectionName: string, options: SearchOptions): Promise<SearchResult>;
|
|
11
|
+
}
|
|
12
|
+
export declare const searchService: SearchService;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createOpenSearchProvider } from './opensearch-provider.js';
|
|
2
|
+
const DEFAULT_SIZE = 20;
|
|
3
|
+
const MAX_SIZE = 100;
|
|
4
|
+
function buildSearchBody(options) {
|
|
5
|
+
if (options.body && typeof options.body === 'object') {
|
|
6
|
+
return options.body;
|
|
7
|
+
}
|
|
8
|
+
const body = {
|
|
9
|
+
from: options.from ?? 0,
|
|
10
|
+
size: Math.min(options.size ?? DEFAULT_SIZE, MAX_SIZE)
|
|
11
|
+
};
|
|
12
|
+
const must = [];
|
|
13
|
+
if (options.q && String(options.q).trim()) {
|
|
14
|
+
must.push({
|
|
15
|
+
query_string: {
|
|
16
|
+
query: String(options.q).trim(),
|
|
17
|
+
default_field: '*'
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
if (options.filter && typeof options.filter === 'object' && Object.keys(options.filter).length > 0) {
|
|
22
|
+
const filterClauses = Object.entries(options.filter).map(([field, value]) => ({
|
|
23
|
+
term: { [field]: value }
|
|
24
|
+
}));
|
|
25
|
+
must.push({ bool: { filter: filterClauses } });
|
|
26
|
+
}
|
|
27
|
+
if (must.length > 0) {
|
|
28
|
+
body.query = { bool: { must } };
|
|
29
|
+
}
|
|
30
|
+
else if (!body.query) {
|
|
31
|
+
body.query = { match_all: {} };
|
|
32
|
+
}
|
|
33
|
+
if (options.sort && options.sort.length > 0) {
|
|
34
|
+
body.sort = options.sort;
|
|
35
|
+
}
|
|
36
|
+
return body;
|
|
37
|
+
}
|
|
38
|
+
class SearchService {
|
|
39
|
+
provider = null;
|
|
40
|
+
prefix = process.env.SEARCH_ENGINE_INDEX_PREFIX ?? '';
|
|
41
|
+
getProvider() {
|
|
42
|
+
if (!this.provider) {
|
|
43
|
+
this.provider = createOpenSearchProvider();
|
|
44
|
+
}
|
|
45
|
+
return this.provider;
|
|
46
|
+
}
|
|
47
|
+
isEnabled() {
|
|
48
|
+
const url = process.env.SEARCH_ENGINE_URL;
|
|
49
|
+
const enabled = process.env.SEARCH_ENGINE_ENABLED;
|
|
50
|
+
return Boolean(url && (enabled === 'true' || enabled === '1'));
|
|
51
|
+
}
|
|
52
|
+
getIndexName(collectionName) {
|
|
53
|
+
return this.prefix + collectionName;
|
|
54
|
+
}
|
|
55
|
+
async index(collectionName, id, document) {
|
|
56
|
+
if (!this.isEnabled())
|
|
57
|
+
return;
|
|
58
|
+
try {
|
|
59
|
+
const indexName = this.getIndexName(collectionName);
|
|
60
|
+
const provider = this.getProvider();
|
|
61
|
+
await provider.ensureIndex(indexName);
|
|
62
|
+
await provider.index(indexName, String(id), document);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
console.error('[edrm-search] Index error:', err);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async remove(collectionName, id) {
|
|
69
|
+
if (!this.isEnabled())
|
|
70
|
+
return;
|
|
71
|
+
try {
|
|
72
|
+
const indexName = this.getIndexName(collectionName);
|
|
73
|
+
const provider = this.getProvider();
|
|
74
|
+
await provider.remove(indexName, String(id));
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
const status = err?.meta?.statusCode;
|
|
78
|
+
if (status !== 404) {
|
|
79
|
+
console.error('[edrm-search] Remove error:', err);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async search(collectionName, options) {
|
|
84
|
+
if (!this.isEnabled()) {
|
|
85
|
+
return { hits: [] };
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const indexName = this.getIndexName(collectionName);
|
|
89
|
+
const body = buildSearchBody({ ...options, collection: collectionName });
|
|
90
|
+
const provider = this.getProvider();
|
|
91
|
+
const result = await provider.search(indexName, body);
|
|
92
|
+
return { hits: result.hits };
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
console.error('[edrm-search] Search error:', err);
|
|
96
|
+
return { hits: [] };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export const searchService = new SearchService();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal provider interface for the search engine (OpenSearch/ES).
|
|
3
|
+
* Public search contract is EnduranceSearchProvider from @programisto/endurance.
|
|
4
|
+
*/
|
|
5
|
+
export interface InternalSearchProvider {
|
|
6
|
+
index(indexName: string, id: string, body: object): Promise<void>;
|
|
7
|
+
remove(indexName: string, id: string): Promise<void>;
|
|
8
|
+
search(indexName: string, body: object): Promise<InternalSearchResponse>;
|
|
9
|
+
ensureIndex(indexName: string): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export interface InternalSearchResponse {
|
|
12
|
+
hits: Array<{
|
|
13
|
+
_id: string;
|
|
14
|
+
_source: object;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { enduranceListener, EnduranceSearchMiddleware } from '@programisto/endurance';
|
|
2
|
+
import { searchService } from '../lib/search-service.js';
|
|
3
|
+
import { EdrmSearchProviderAdapter } from '../lib/search-provider-adapter.js';
|
|
4
|
+
function getCollectionNameFromDoc(doc, eventName) {
|
|
5
|
+
const ctor = doc
|
|
6
|
+
?.constructor;
|
|
7
|
+
const name = ctor?.getModel?.()?.collection?.name;
|
|
8
|
+
if (name)
|
|
9
|
+
return name;
|
|
10
|
+
const className = eventName.split(':')[0] ?? '';
|
|
11
|
+
const lower = className.toLowerCase();
|
|
12
|
+
return lower.endsWith('s') ? lower : lower + 's';
|
|
13
|
+
}
|
|
14
|
+
function docToPlainObject(doc) {
|
|
15
|
+
if (doc === null || doc === undefined)
|
|
16
|
+
return {};
|
|
17
|
+
const d = doc;
|
|
18
|
+
if (typeof d.toJSON === 'function')
|
|
19
|
+
return d.toJSON();
|
|
20
|
+
if (typeof d.toObject === 'function')
|
|
21
|
+
return d.toObject();
|
|
22
|
+
if (typeof doc === 'object' && doc !== null)
|
|
23
|
+
return { ...doc };
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
if (searchService.isEnabled()) {
|
|
27
|
+
EnduranceSearchMiddleware.setInstance(new EdrmSearchProviderAdapter());
|
|
28
|
+
}
|
|
29
|
+
enduranceListener.createAnyListener(async (...args) => {
|
|
30
|
+
if (!searchService.isEnabled())
|
|
31
|
+
return;
|
|
32
|
+
if (!args?.length || typeof args[0] !== 'string')
|
|
33
|
+
return;
|
|
34
|
+
const event = args[0];
|
|
35
|
+
const data = args.length > 1 ? args[1] : undefined;
|
|
36
|
+
try {
|
|
37
|
+
if (event.endsWith(':postSave') && data) {
|
|
38
|
+
const doc = data;
|
|
39
|
+
const id = doc._id != null ? String(doc._id) : undefined;
|
|
40
|
+
if (id == null)
|
|
41
|
+
return;
|
|
42
|
+
const collectionName = getCollectionNameFromDoc(doc, event);
|
|
43
|
+
const payload = docToPlainObject(doc);
|
|
44
|
+
searchService.index(collectionName, id, payload).catch((err) => {
|
|
45
|
+
console.error('[edrm-search] postSave index error:', err);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
else if (event.endsWith(':postDeleteOne') && data && typeof data === 'object') {
|
|
49
|
+
const payload = data;
|
|
50
|
+
const id = payload.id != null ? String(payload.id) : undefined;
|
|
51
|
+
const collectionName = payload.collectionName;
|
|
52
|
+
if (id != null && collectionName) {
|
|
53
|
+
searchService.remove(collectionName, id).catch((err) => {
|
|
54
|
+
console.error('[edrm-search] postDeleteOne remove error:', err);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
console.error('[edrm-search] Listener error:', err);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { EnduranceRouter, EnduranceSearchMiddleware } from '@programisto/endurance';
|
|
2
|
+
const ROUTES_DISABLED_MESSAGE = 'Search routes are disabled. Use the search middleware from your module.';
|
|
3
|
+
const ROUTES_DISABLED_CODE = 'SEARCH_ROUTES_DISABLED';
|
|
4
|
+
function routesEnabled() {
|
|
5
|
+
return process.env.EDRM_SEARCH_ROUTES_ENABLED === 'true';
|
|
6
|
+
}
|
|
7
|
+
class SearchRouter extends EnduranceRouter {
|
|
8
|
+
setupRoutes() {
|
|
9
|
+
this.router.use((req, res, next) => {
|
|
10
|
+
if (!routesEnabled()) {
|
|
11
|
+
res.status(503).json({ message: ROUTES_DISABLED_MESSAGE, code: ROUTES_DISABLED_CODE });
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
next();
|
|
15
|
+
});
|
|
16
|
+
const securityOptions = { requireAuth: true, permissions: [] };
|
|
17
|
+
this.post('/', securityOptions, this.searchPost.bind(this));
|
|
18
|
+
this.get('/', securityOptions, this.searchGet.bind(this));
|
|
19
|
+
}
|
|
20
|
+
async searchPost(req, res) {
|
|
21
|
+
try {
|
|
22
|
+
const collection = req.body?.collection ?? req.query?.collection;
|
|
23
|
+
if (!collection || typeof collection !== 'string') {
|
|
24
|
+
res.status(400).json({ error: 'Missing or invalid "collection" (string)' });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const provider = EnduranceSearchMiddleware.getInstance();
|
|
28
|
+
if (!provider.isSearchEnabled()) {
|
|
29
|
+
res.status(503).json({ error: 'Search is not enabled', code: 'SEARCH_NOT_ENABLED' });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const options = {
|
|
33
|
+
collection,
|
|
34
|
+
q: req.body?.q ?? req.query?.q,
|
|
35
|
+
filter: req.body?.filter ?? req.query?.filter,
|
|
36
|
+
sort: req.body?.sort ?? req.query?.sort,
|
|
37
|
+
from: req.body?.from ?? req.query?.from,
|
|
38
|
+
size: req.body?.size ?? req.query?.size,
|
|
39
|
+
body: req.body?.body
|
|
40
|
+
};
|
|
41
|
+
const result = await provider.search(collection, options);
|
|
42
|
+
res.json(result);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
console.error('[edrm-search] Search error:', err);
|
|
46
|
+
res.status(500).json({ error: 'Search failed' });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async searchGet(req, res) {
|
|
50
|
+
try {
|
|
51
|
+
const collection = req.query?.collection;
|
|
52
|
+
if (!collection || typeof collection !== 'string') {
|
|
53
|
+
res.status(400).json({ error: 'Missing or invalid "collection" query (string)' });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const provider = EnduranceSearchMiddleware.getInstance();
|
|
57
|
+
if (!provider.isSearchEnabled()) {
|
|
58
|
+
res.status(503).json({ error: 'Search is not enabled', code: 'SEARCH_NOT_ENABLED' });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const options = {
|
|
62
|
+
collection,
|
|
63
|
+
q: req.query?.q,
|
|
64
|
+
filter: req.query?.filter,
|
|
65
|
+
sort: req.query?.sort,
|
|
66
|
+
from: req.query?.from != null ? Number(req.query.from) : undefined,
|
|
67
|
+
size: req.query?.size != null ? Number(req.query.size) : undefined
|
|
68
|
+
};
|
|
69
|
+
const result = await provider.search(collection, options);
|
|
70
|
+
res.json(result);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
console.error('[edrm-search] Search error:', err);
|
|
74
|
+
res.status(500).json({ error: 'Search failed' });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export default new SearchRouter();
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@programisto/edrm-search",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/routes/search.router.js",
|
|
6
|
+
"types": "./dist/routes/search.router.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "node ./dist/bin/www.js",
|
|
9
|
+
"dev": "tsc-watch --onSuccess \"node ./dist/bin/www.js\"",
|
|
10
|
+
"test": "echo \"Tests skipped\"",
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"lint": "eslint \"**/*.ts\"",
|
|
13
|
+
"prepare": "husky install 2>/dev/null || true"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/programisto-labs/edrm-search"
|
|
18
|
+
},
|
|
19
|
+
"author": "",
|
|
20
|
+
"license": "ISC",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/programisto-labs/edrm-search/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/programisto-labs/edrm-search#readme",
|
|
25
|
+
"description": "EDRM module for full-text search via OpenSearch/Elasticsearch, synced from Endurance schema events",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@programisto/endurance": "^1.0.8",
|
|
28
|
+
"@opensearch-project/opensearch": "^2.10.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@commitlint/config-conventional": "^19.8.1",
|
|
32
|
+
"@types/express": "^4.17.21",
|
|
33
|
+
"@types/node": "^22.0.0",
|
|
34
|
+
"@typescript-eslint/eslint-plugin": "^8.35.0",
|
|
35
|
+
"@typescript-eslint/parser": "^8.35.0",
|
|
36
|
+
"eslint": "^8.57.1",
|
|
37
|
+
"eslint-config-standard": "^17.1.0",
|
|
38
|
+
"eslint-plugin-import": "^2.32.0",
|
|
39
|
+
"eslint-plugin-n": "^16.6.2",
|
|
40
|
+
"eslint-plugin-promise": "^6.6.0",
|
|
41
|
+
"commitlint": "^19.8.1",
|
|
42
|
+
"husky": "^9.0.0",
|
|
43
|
+
"reflect-metadata": "^0.2.2",
|
|
44
|
+
"tsc-watch": "^6.2.1",
|
|
45
|
+
"typescript": "^5.8.3"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"dist",
|
|
49
|
+
"README.md",
|
|
50
|
+
"CHANGELOG.md",
|
|
51
|
+
".eslintrc.json",
|
|
52
|
+
"package.json",
|
|
53
|
+
"tsconfig.json"
|
|
54
|
+
],
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"rootDir": "./src/",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"moduleResolution": "node",
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"allowSyntheticDefaultImports": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"experimentalDecorators": true,
|
|
16
|
+
"emitDecoratorMetadata": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*.ts", "src/**/*.d.ts"],
|
|
19
|
+
"exclude": ["node_modules", "dist", "__tests__", "**/*.test.ts"]
|
|
20
|
+
}
|