Skip to content

Commit 3552ed2

Browse files
committed
feat(search): improve search feature and displaying results in dashboard
feat(mcp): add search feature
1 parent c0bb34d commit 3552ed2

31 files changed

Lines changed: 988 additions & 167 deletions

acceptance/petstore_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ func (suite *PetStoreSuite) TestSearch_Paging() {
389389
assert.Len(t, items, 10)
390390
evt := items[0].(map[string]interface{})
391391
assert.Equal(t, "HTTP", evt["type"])
392-
assert.Equal(t, "GET /pet/{petId}", evt["title"])
392+
assert.Equal(t, "/pet/{petId}", evt["title"])
393393
assert.Equal(t, "Swagger Petstore", evt["domain"])
394394
}),
395395
)

engine/common/host.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,20 @@ func (e *JobExecution) AppendLog(level, message string) {
176176
}
177177

178178
func (e *JobExecution) Title() string {
179+
if e.Error != nil {
180+
return fmt.Sprintf("Error in: %v", e.Tags["name"])
181+
}
179182
return e.Tags["name"]
180183
}
181184

185+
func (e *JobExecution) Metadata() map[string]string {
186+
return nil
187+
}
188+
189+
func (e *JobExecution) Domain() string {
190+
return "Job Execution"
191+
}
192+
182193
type FakerNode interface {
183194
Name() string
184195
Fake(r *generator.Request) (interface{}, error)

engine/host.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,9 @@ func (sh *scriptHost) Cron(expr string, handler func(), opt common.JobOptions) (
102102
}
103103

104104
func (sh *scriptHost) newJobFunc(handler func(), opt common.JobOptions, schedule string, id int) func() {
105+
name := filepath.Base(sh.name)
105106
tags := map[string]string{
106-
"name": sh.name,
107+
"name": name,
107108
"file": sh.name,
108109
"fileKey": sh.file.Info.Key(),
109110
}

mcp/data/automation-core.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,35 @@ interface Mokapi {
4444
* getEvents({ apiType: 'http', name: 'Swagger Petstore', path: '/pets' })
4545
*/
4646
getEvents(traits: HttpTraits | KafkaTraits, limit?: number): Event[];
47+
48+
/**
49+
* Returns a specific event from Mokapi based on the id
50+
* @param id The id of the event
51+
* @example
52+
* getEvent('6cbca4f8-ea25-4f66-861c-44d8b15321b1')
53+
*/
54+
getEvent(id: string): Event[];
55+
56+
/**
57+
* Advanced Search across all APIs, Topics, and Documentation.
58+
* By default, multiple terms are combined with OR (results contain at least one term). Use prefixes to enforce stricter matches.
59+
*
60+
* QUERY SYNTAX:
61+
* - Simple: `petstore` (Search in all fields)
62+
* Type: `type:kafka` (Search only Kafka)
63+
* - Exact: `"Swagger Petstore"` (Use quotes for phrases)
64+
* - Exclude: `petstore -kafka` (Exclude terms with '-')
65+
* - Wildcard: `pet*` (Matches "pet", "pets", "petstore")
66+
* - Fuzy: `pet~` (Matches "pets", "pest" or slight typos)
67+
* - Fields: `name:petstore`, `path:/pets`, `description:dog`
68+
* - Boosting: `path:/pets^2` (Make path matches more important)
69+
* - Numeric Ranges: `statusCode:>=200
70+
*
71+
* @param queryText The search term or complex query string.
72+
* @param index Page index for pagination (starts at 0).
73+
* @param limit Number of results (default 10).
74+
*/
75+
search(queryText: string, index?: number, limit?: number): SearchResult
4776
}
4877

4978
type ApiType = 'http' | 'kafka' | 'ldap' | 'mail'
@@ -53,6 +82,33 @@ interface ApiSummary {
5382
type: ApiType;
5483
}
5584

85+
interface SearchResult {
86+
items: SearchResultItem[]
87+
total: number
88+
}
89+
90+
interface SearchResultItem {
91+
/** The type of the resource (e.g., 'http', 'kafka', 'event') */
92+
type: string
93+
/** The name of the API */
94+
domain: string
95+
/** A human-readable title or summary */
96+
title: string
97+
/**
98+
* Relevant text snippets showing where the match was found.
99+
* Useful to see context like 'GET /pets' or 'Topic: orders'.
100+
*/
101+
fragments: string[]
102+
/**
103+
* Additional context specific to the result type.
104+
* - HTTP: includes 'method', 'path', 'service'
105+
* - Kafka: includes 'topic', 'service'
106+
*/
107+
metadata: Record<string, string>
108+
109+
time?: string
110+
}
111+
56112
/**
57113
* JSON Schema defines a JSON-based format for describing the structure of JSON data
58114
* @example
@@ -201,4 +257,5 @@ interface Schema {
201257
}
202258

203259
type SchemaType = "object" | "array" | "number" | "integer" | "string" | "boolean" | "null";
260+
204261
```

mcp/data/automation.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,32 @@ mokapi.getEvents()
3939
Get latest HTTP events for a specific API
4040
```typescript
4141
mokapi.getEvents({ type: 'http', name: 'Petstore' })
42-
```
42+
```
43+
44+
Search for all POST endpoints related to "payments".
45+
```typescript
46+
// 1. Search for a specific capability
47+
const searchResponse = mokapi.search("method:POST AND path:*payments*");
48+
let op = null
49+
if (searchResponse.items.length > 0) {
50+
const match = searchResponse.items[0];
51+
52+
// 2. Use metadata to load the full API specification
53+
const api = mokapi.getApi(match.metadata.service);
54+
55+
// 3. Find the specific operation to see allowed status codes/schemas
56+
// Tip: Use the path from metadata to identify the correct operation
57+
const opSummary = api.getOperations().find(x => x.path === match.metadata.path && x.method === match.metadata.method);
58+
59+
if (opSummary) {
60+
op = api.getOperation(opSummary.id);
61+
}
62+
}
63+
op
64+
```
65+
66+
Search for errors (404 or 500) related to the Petstore
67+
```typescript
68+
const errors = mokapi.search('+api:"Petstore" +event.data.response.statusCode:>=400 +type:event')
69+
errors.items.map(x => mokapi.getEvent(x.metadata.id))
70+
```

mcp/run.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"mokapi/js/faker"
1010
"mokapi/providers/openapi"
1111
"mokapi/runtime"
12+
"mokapi/runtime/search"
1213
"mokapi/schema/json/generator"
1314
"reflect"
1415
"slices"
@@ -136,6 +137,8 @@ func (m *mokapi) init(obj *goja.Object) {
136137
_ = obj.Set("getApi", m.getApi)
137138
_ = obj.Set("fake", m.fake)
138139
_ = obj.Set("getEvents", m.getEvents)
140+
_ = obj.Set("getEvent", m.getEvent)
141+
_ = obj.Set("search", m.search)
139142
}
140143

141144
func (m *mokapi) getApis() []ApiSummary {
@@ -184,6 +187,50 @@ func (m *mokapi) fake(v goja.Value) (any, error) {
184187
return generator.New(&generator.Request{Schema: js})
185188
}
186189

190+
type SearchResult struct {
191+
Items []SearchResultItem `json:"items"`
192+
Total uint64 `json:"total"`
193+
}
194+
195+
type SearchResultItem struct {
196+
Type string `json:"type"`
197+
Domain string `json:"domain,omitempty"`
198+
Title string `json:"title"`
199+
Fragments []string `json:"fragments,omitempty"`
200+
Metadata map[string]string `json:"metadata"`
201+
Time string `json:"time,omitempty"`
202+
}
203+
204+
func (m *mokapi) search(queryText string, index int, limit int) (SearchResult, error) {
205+
if limit == 0 {
206+
limit = 10
207+
}
208+
r := search.Request{
209+
QueryText: queryText,
210+
Index: index,
211+
Limit: limit,
212+
}
213+
214+
sr, err := m.app.Search(r)
215+
if err != nil {
216+
return SearchResult{}, err
217+
}
218+
result := SearchResult{
219+
Total: sr.Total,
220+
}
221+
for _, item := range sr.Results {
222+
result.Items = append(result.Items, SearchResultItem{
223+
Type: item.Type,
224+
Domain: item.Domain,
225+
Title: item.Title,
226+
Fragments: item.Fragments,
227+
Metadata: item.Params,
228+
Time: item.Time,
229+
})
230+
}
231+
return result, nil
232+
}
233+
187234
type customFieldNameMapper struct {
188235
}
189236

mcp/run_events.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@ func (m *mokapi) getEvents(vTraits goja.Value, vLimit goja.Value) ([]events.Even
3232
}
3333
}
3434

35+
func (m *mokapi) getEvent(id string) (events.Event, error) {
36+
if id == "" {
37+
return events.Event{}, fmt.Errorf("expected id parameter in GUID format, got '%v'", id)
38+
}
39+
40+
e := m.app.Events.GetEvent(id)
41+
if e.Id == "" {
42+
return e, fmt.Errorf("event %s not found. Use `mokapi.search('type:event ...')` to search for existing events", id)
43+
}
44+
return e, nil
45+
}
46+
3547
func parseTraits(v goja.Value, vm *goja.Runtime) (events.Traits, error) {
3648
traits := events.Traits{}
3749

mcp/run_events_test.go

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,25 @@ func (t *testEvent) Title() string {
1919
return t.Name
2020
}
2121

22+
func (t *testEvent) Metadata() map[string]string { return nil }
23+
2224
func TestEvents(t *testing.T) {
2325
testcases := []struct {
2426
name string
2527
app *runtime.App
2628
code string
27-
test func(t *testing.T, evts []events.Event, err error)
29+
test func(t *testing.T, result any, err error)
2830
}{
2931
{
3032
name: "without params should not error",
3133
code: "mokapi.getEvents()",
3234
app: runtimetest.NewApp(
3335
runtimetest.WithEvent(events.NewTraits().WithNamespace("http"), &testEvent{Name: "test-1"}),
3436
),
35-
test: func(t *testing.T, evts []events.Event, err error) {
37+
test: func(t *testing.T, result any, err error) {
3638
require.NoError(t, err)
39+
require.IsType(t, []events.Event{}, result)
40+
evts := result.([]events.Event)
3741
require.Len(t, evts, 1)
3842
require.Equal(t, &testEvent{Name: "test-1"}, evts[0].Data)
3943
},
@@ -45,8 +49,10 @@ func TestEvents(t *testing.T) {
4549
runtimetest.WithEvent(events.NewTraits().WithNamespace("kafka"), &testEvent{Name: "test-1"}),
4650
runtimetest.WithEvent(events.NewTraits().WithNamespace("http"), &testEvent{Name: "test-2"}),
4751
),
48-
test: func(t *testing.T, evts []events.Event, err error) {
52+
test: func(t *testing.T, result any, err error) {
4953
require.NoError(t, err)
54+
require.IsType(t, []events.Event{}, result)
55+
evts := result.([]events.Event)
5056
require.Len(t, evts, 1)
5157
require.Equal(t, &testEvent{Name: "test-2"}, evts[0].Data)
5258
},
@@ -58,8 +64,10 @@ func TestEvents(t *testing.T) {
5864
runtimetest.WithEvent(events.NewTraits().WithNamespace("http").WithName("foo"), &testEvent{Name: "test-1"}),
5965
runtimetest.WithEvent(events.NewTraits().WithNamespace("http").WithName("bar"), &testEvent{Name: "test-2"}),
6066
),
61-
test: func(t *testing.T, evts []events.Event, err error) {
67+
test: func(t *testing.T, result any, err error) {
6268
require.NoError(t, err)
69+
require.IsType(t, []events.Event{}, result)
70+
evts := result.([]events.Event)
6371
require.Len(t, evts, 1)
6472
require.Equal(t, &testEvent{Name: "test-2"}, evts[0].Data)
6573
},
@@ -71,8 +79,10 @@ func TestEvents(t *testing.T) {
7179
runtimetest.WithEvent(events.NewTraits().WithNamespace("http").With("path", "/users"), &testEvent{Name: "test-1"}),
7280
runtimetest.WithEvent(events.NewTraits().WithNamespace("http").With("path", "/pets"), &testEvent{Name: "test-2"}),
7381
),
74-
test: func(t *testing.T, evts []events.Event, err error) {
82+
test: func(t *testing.T, result any, err error) {
7583
require.NoError(t, err)
84+
require.IsType(t, []events.Event{}, result)
85+
evts := result.([]events.Event)
7686
require.Len(t, evts, 1)
7787
require.Equal(t, &testEvent{Name: "test-2"}, evts[0].Data)
7888
},
@@ -84,12 +94,27 @@ func TestEvents(t *testing.T) {
8494
runtimetest.WithEvent(events.NewTraits().WithNamespace("http").With("method", "GET"), &testEvent{Name: "test-1"}),
8595
runtimetest.WithEvent(events.NewTraits().WithNamespace("http").With("method", "POST"), &testEvent{Name: "test-2"}),
8696
),
87-
test: func(t *testing.T, evts []events.Event, err error) {
97+
test: func(t *testing.T, result any, err error) {
8898
require.NoError(t, err)
99+
require.IsType(t, []events.Event{}, result)
100+
evts := result.([]events.Event)
89101
require.Len(t, evts, 1)
90102
require.Equal(t, &testEvent{Name: "test-2"}, evts[0].Data)
91103
},
92104
},
105+
{
106+
name: "get specific event",
107+
code: "mokapi.getEvent(mokapi.getEvents({ method: 'GET' })[0].id)",
108+
app: runtimetest.NewApp(
109+
runtimetest.WithEvent(events.NewTraits().WithNamespace("http").With("method", "GET"), &testEvent{Name: "test-1"}),
110+
),
111+
test: func(t *testing.T, result any, err error) {
112+
require.NoError(t, err)
113+
require.IsType(t, events.Event{}, result)
114+
evt := result.(events.Event)
115+
require.Equal(t, &testEvent{Name: "test-1"}, evt.Data)
116+
},
117+
},
93118
}
94119

95120
for _, tc := range testcases {
@@ -100,9 +125,8 @@ func TestEvents(t *testing.T) {
100125
context.Background(),
101126
mcp.RunInput{Code: tc.code},
102127
)
103-
require.IsType(t, []events.Event{}, r.Result)
104128

105-
tc.test(t, r.Result.([]events.Event), err)
129+
tc.test(t, r.Result, err)
106130
})
107131
}
108132
}

0 commit comments

Comments
 (0)