Skip to content

Commit a1b6279

Browse files
committed
api/rofl_apps: Filter by multiple name fragments
1 parent 355a68b commit a1b6279

6 files changed

Lines changed: 193 additions & 78 deletions

File tree

api/spec/v1.yaml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,9 +1266,14 @@ paths:
12661266
- *runtime
12671267
- in: query
12681268
name: name
1269+
style: form
1270+
explode: false
12691271
schema:
1270-
type: string
1271-
description: A filter on the name of the ROFL app.
1272+
type: array
1273+
items:
1274+
type: string
1275+
maxItems: 8
1276+
description: A filter on the name of the ROFL app. If multiple names are provided, the ROFL App must match all of them.
12721277
responses:
12731278
'200':
12741279
description: A JSON object containing a list of ROFL apps.

storage/client/client.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2490,15 +2490,20 @@ func (c *StorageClient) RuntimeRoflApps(ctx context.Context, runtime common.Runt
24902490
if *params.Limit > 100 {
24912491
*params.Limit = 100
24922492
}
2493+
if params.Name != nil && len(*params.Name) > 8 {
2494+
return nil, fmt.Errorf("too many names in the name filter: %w", apiCommon.ErrBadRequest)
2495+
}
2496+
2497+
args := []interface{}{runtime, id}
2498+
query, nameArgs := queries.RuntimeRoflApps(params.Name)
2499+
args = append(args, nameArgs...)
2500+
args = append(args, params.Limit, params.Offset)
2501+
24932502
res, err := c.withTotalCount(
24942503
ctx,
2495-
queries.RuntimeRoflApps,
2504+
query,
24962505
100,
2497-
runtime,
2498-
id,
2499-
params.Name,
2500-
params.Limit,
2501-
params.Offset,
2506+
args...,
25022507
)
25032508
if err != nil {
25042509
return nil, wrapError(err)

storage/client/queries/queries.go

Lines changed: 112 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package queries
22

33
import (
44
"fmt"
5+
"strings"
56
)
67

78
func TotalCountQuery(inner string) string {
@@ -1030,76 +1031,6 @@ const (
10301031
FROM chain.runtime_nodes
10311032
WHERE runtime_id = $1::text`
10321033

1033-
RuntimeRoflApps = `
1034-
WITH
1035-
-- Latest epoch, to identify active instances.
1036-
max_epoch AS (
1037-
SELECT id FROM chain.epochs ORDER BY id DESC LIMIT 1
1038-
),
1039-
1040-
-- Aggregate active instance data per app.
1041-
active_instances AS (
1042-
SELECT
1043-
ri.app_id,
1044-
COUNT(*) AS num_active_instances,
1045-
jsonb_agg(
1046-
jsonb_build_object(
1047-
'rak', ri.rak,
1048-
'endorsing_node_id', ri.endorsing_node_id,
1049-
'endorsing_entity_id', ri.endorsing_entity_id,
1050-
'rek', ri.rek,
1051-
'expiration_epoch', ri.expiration_epoch,
1052-
'extra_keys', ri.extra_keys
1053-
) ORDER BY ri.expiration_epoch DESC
1054-
) AS instance_json
1055-
FROM chain.rofl_instances ri
1056-
JOIN max_epoch ON true
1057-
WHERE ri.expiration_epoch > max_epoch.id
1058-
GROUP BY ri.app_id
1059-
)
1060-
1061-
SELECT
1062-
ra.id,
1063-
ra.admin,
1064-
preimages.context_identifier,
1065-
preimages.context_version,
1066-
preimages.address_data,
1067-
ra.stake,
1068-
ra.policy,
1069-
ra.sek,
1070-
ra.metadata,
1071-
ra.secrets,
1072-
ra.removed,
1073-
COALESCE(ai.num_active_instances, 0) as num_active_instances,
1074-
COALESCE(ai.instance_json, '[]'::jsonb) AS active_instances
1075-
FROM chain.rofl_apps AS ra
1076-
1077-
-- Resolve admin address preimage.
1078-
LEFT JOIN chain.address_preimages AS preimages ON (
1079-
preimages.address = ra.admin AND
1080-
-- For now, the only user is the explorer, where we only care
1081-
-- about Ethereum-compatible addresses, so only get those. Can
1082-
-- easily enable for other address types though.
1083-
preimages.context_identifier = 'oasis-runtime-sdk/address: secp256k1eth' AND
1084-
preimages.context_version = 0
1085-
)
1086-
1087-
-- Join aggregated active instance data.
1088-
LEFT JOIN active_instances ai ON ai.app_id = ra.id
1089-
1090-
LEFT JOIN chain.accounts AS a ON a.address = ra.admin
1091-
1092-
WHERE
1093-
ra.runtime = $1::runtime AND
1094-
($2::text IS NULL OR ra.id = $2::text) AND
1095-
($3::text IS NULL OR ra.metadata_name ILIKE '%' || $3::text || '%') AND
1096-
-- Exclude not yet processed apps.
1097-
ra.last_processed_round IS NOT NULL
1098-
1099-
ORDER BY num_active_instances DESC, ra.num_transactions DESC, ra.id DESC
1100-
LIMIT $4::bigint
1101-
OFFSET $5::bigint`
1102-
11031034
RuntimeRoflAppTransactionsFirstLast = `
11041035
WITH
11051036
app_ids AS (
@@ -1329,3 +1260,114 @@ const (
13291260
LIMIT $2::bigint
13301261
OFFSET $3::bigint`
13311262
)
1263+
1264+
// RuntimeRoflApps returns a SQL query and argument list for selecting ROFL apps,
1265+
// optionally filtered by a list of metadata name substrings.
1266+
//
1267+
// The query is dynamically constructed to support efficient AND-based substring
1268+
// filtering using ILIKE '%term%' per name fragment.
1269+
//
1270+
// Dynamic query generation is necessary here to allow PostgreSQL to utilize the
1271+
// pg_trgm GIN index on `metadata_name` — index usage is only possible when each
1272+
// ILIKE condition is written explicitly.
1273+
//
1274+
// Parameters:
1275+
//
1276+
// $1: runtime (required)
1277+
// $2: optional app ID for exact match
1278+
// $3...$N: optional name substrings (one per term, matched via ILIKE with AND logic)
1279+
// $N+1: limit
1280+
// $N+2: offset
1281+
func RuntimeRoflApps(rawNames *[]string) (string, []interface{}) {
1282+
var clauses []string
1283+
var args []interface{}
1284+
argOffset := 3 // first 2 args are $1 = runtime, $2 = optional id.
1285+
1286+
if rawNames != nil {
1287+
for i, name := range *rawNames {
1288+
// Escape the name for the LIKE operator.
1289+
escaped := strings.ReplaceAll(name, `\`, `\\`)
1290+
escaped = strings.ReplaceAll(escaped, `%`, `\%`)
1291+
escaped = strings.ReplaceAll(escaped, `_`, `\_`)
1292+
1293+
clauses = append(clauses, fmt.Sprintf("ra.metadata_name ILIKE $%d ESCAPE '\\'", argOffset+i))
1294+
args = append(args, "%"+escaped+"%")
1295+
}
1296+
}
1297+
1298+
nameCondition := "TRUE"
1299+
if len(clauses) > 0 {
1300+
nameCondition = "(" + strings.Join(clauses, " AND ") + ")"
1301+
}
1302+
query := fmt.Sprintf(`
1303+
WITH
1304+
-- Latest epoch, to identify active instances.
1305+
max_epoch AS (
1306+
SELECT id FROM chain.epochs ORDER BY id DESC LIMIT 1
1307+
),
1308+
1309+
-- Aggregate active instance data per app.
1310+
active_instances AS (
1311+
SELECT
1312+
ri.app_id,
1313+
COUNT(*) AS num_active_instances,
1314+
jsonb_agg(
1315+
jsonb_build_object(
1316+
'rak', ri.rak,
1317+
'endorsing_node_id', ri.endorsing_node_id,
1318+
'endorsing_entity_id', ri.endorsing_entity_id,
1319+
'rek', ri.rek,
1320+
'expiration_epoch', ri.expiration_epoch,
1321+
'extra_keys', ri.extra_keys
1322+
) ORDER BY ri.expiration_epoch DESC
1323+
) AS instance_json
1324+
FROM chain.rofl_instances ri
1325+
JOIN max_epoch ON true
1326+
WHERE ri.expiration_epoch > max_epoch.id
1327+
GROUP BY ri.app_id
1328+
)
1329+
1330+
SELECT
1331+
ra.id,
1332+
ra.admin,
1333+
preimages.context_identifier,
1334+
preimages.context_version,
1335+
preimages.address_data,
1336+
ra.stake,
1337+
ra.policy,
1338+
ra.sek,
1339+
ra.metadata,
1340+
ra.secrets,
1341+
ra.removed,
1342+
COALESCE(ai.num_active_instances, 0) as num_active_instances,
1343+
COALESCE(ai.instance_json, '[]'::jsonb) AS active_instances
1344+
FROM chain.rofl_apps AS ra
1345+
1346+
-- Resolve admin address preimage.
1347+
LEFT JOIN chain.address_preimages AS preimages ON (
1348+
preimages.address = ra.admin AND
1349+
-- For now, the only user is the explorer, where we only care
1350+
-- about Ethereum-compatible addresses, so only get those. Can
1351+
-- easily enable for other address types though.
1352+
preimages.context_identifier = 'oasis-runtime-sdk/address: secp256k1eth' AND
1353+
preimages.context_version = 0
1354+
)
1355+
1356+
-- Join aggregated active instance data.
1357+
LEFT JOIN active_instances ai ON ai.app_id = ra.id
1358+
1359+
LEFT JOIN chain.accounts AS a ON a.address = ra.admin
1360+
1361+
WHERE
1362+
ra.runtime = $1::runtime AND
1363+
($2::text IS NULL OR ra.id = $2::text) AND
1364+
%s AND
1365+
-- Exclude not yet processed apps.
1366+
ra.last_processed_round IS NOT NULL
1367+
1368+
ORDER BY num_active_instances DESC, ra.num_transactions DESC, ra.id DESC
1369+
LIMIT $%d::bigint
1370+
OFFSET $%d::bigint`, nameCondition, argOffset+len(clauses), argOffset+len(clauses)+1)
1371+
1372+
return query, args
1373+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"is_total_count_clipped": false,
3+
"rofl_apps": [
4+
{
5+
"active_instances": [
6+
{
7+
"endorsing_entity_id": "hp8sjG2cJPncrEYWZKqisUNiMYzUC/QomhrJwoLQVjc=",
8+
"endorsing_node_id": "5MsgQwijUlpH9+0Hbyors5jwmx7tTmKMA4c9leV3prI=",
9+
"expiration_epoch": 44111,
10+
"extra_keys": [
11+
"{\"secp256k1\":\"Ax7ww8ukRTiqn8X2gSQTwcDM9oW2kWF3RByqfdpiT7gf\"}"
12+
],
13+
"rak": "tnvWUYb/SGtHSlDxoGENW0BzSD8t+VEJU4umH9RzVFw=",
14+
"rek": "cpw+0KhZudVM9YtdgPeOUTyI/pyp564hDzWsnqiZd0c="
15+
}
16+
],
17+
"admin": "oasis1qpupfu7e2n6pkezeaw0yhj8mcem8anj64ytrayne",
18+
"date_created": "2025-05-06T15:54:15Z",
19+
"id": "rofl1qrtetspnld9efpeasxmryl6nw9mgllr0euls3dwn",
20+
"last_activity": "2025-05-06T15:54:15Z",
21+
"metadata": {
22+
"net.oasis.rofl.author": "Matevž Jekovec <matevz@oasisprotocol.org>",
23+
"net.oasis.rofl.license": "Apache-2.0",
24+
"net.oasis.rofl.name": "demo-rofl-chatbot",
25+
"net.oasis.rofl.repository": "https://github.com/oasisprotocol/demo-rofl-chatbot",
26+
"net.oasis.rofl.version": "0.1.0"
27+
},
28+
"num_active_instances": 1,
29+
"policy": {
30+
"enclaves": [
31+
"YjeeznQx9i/UQKVoPcOVT3P5oxOHGJ6r1VwMBetuSREAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
32+
"IyVddKd5AtSGmo7FHnZrj0vIzfIOp3DMBFHwUbb0qVQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
33+
],
34+
"endorsements": [
35+
{
36+
"any": {}
37+
}
38+
],
39+
"fees": 2,
40+
"max_expiration": 3,
41+
"quotes": {
42+
"pcs": {
43+
"min_tcb_evaluation_data_number": 17,
44+
"tcb_validity_period": 30,
45+
"tdx": {}
46+
}
47+
}
48+
},
49+
"removed": false,
50+
"secrets": null,
51+
"sek": "2L9Jcwa+cfGbhfv24x3TWEn6UekjfKlvEL6qY3PLnwo=",
52+
"stake": "10000000000000000000000"
53+
}
54+
],
55+
"total_count": 1
56+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
HTTP/1.1 200 OK
2+
Content-Type: application/json
3+
Vary: Origin
4+
Date: UNINTERESTING
5+
Content-Length: UNINTERESTING
6+

tests/e2e_regression/eden_testnet_2025/test_cases.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ testCases=(
3434
'sapphire_account_with_evm_token /v1/sapphire/accounts/0xeDA395666E56dd9E2Ef3Bdc76eee373b738640DD'
3535
'sapphire_rofl_app /v1/sapphire/rofl_apps/rofl1qp55evqls4qg6cjw5fnlv4al9ptc0fsakvxvd9uw'
3636
'sapphire_rofl_app_by_name /v1/sapphire/rofl_apps?name=demo-rofl'
37+
'sapphire_rofl_app_by_multiple_names /v1/sapphire/rofl_apps?name=chatbot,demo'
3738
'sapphire_rofl_app_instances /v1/sapphire/rofl_apps/rofl1qp55evqls4qg6cjw5fnlv4al9ptc0fsakvxvd9uw/instances'
3839
'sapphire_rofl_app_instance /v1/sapphire/rofl_apps/rofl1qp55evqls4qg6cjw5fnlv4al9ptc0fsakvxvd9uw/instances/8EUGnz+hAqblEMWh+ZHKyZU7CSItm1wrJqK15dAjlfI='
3940
'sapphire_rofl_app_transactions /v1/sapphire/rofl_apps/rofl1qr5s5r9pkdkhmj7l5z4nuncjkc05p62m9uqa2svc/transactions'

0 commit comments

Comments
 (0)