Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/api/V2App.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Factory+ RDF store
* App endpoints
* Copyright 2026 University of Sheffield AMRC
*/

package uk.co.amrc.factoryplus.metadb.api;

import java.util.Date;
import java.util.UUID;
import java.util.function.Supplier;

import jakarta.inject.*;
import jakarta.json.*;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.*;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.jena.rdf.model.*;
import org.apache.jena.vocabulary.*;

import io.vavr.Lazy;
import io.vavr.collection.*;
import io.vavr.control.*;

import uk.co.amrc.factoryplus.metadb.db.*;

@Path("v2/app/{app}")
@Consumes("application/json")
public class V2App {
private static final Logger log = LoggerFactory.getLogger(V2App.class);

@Inject private RdfStore db;
@Inject private SecurityContext auth;

/* Because we are pulling from dataflow we do not need our own txns.
* Dataflow will open a txn if it needs to do an initial fetch and
* doesn't have cached results already. This means we don't want to
* use requestRead and need to build our own RequestHandler. */
private Lazy<RequestHandler> req = Lazy.of(() ->
new RequestHandler(db, auth)
.fetchACL());

@PathParam("app") private UUID app;

private void checkACL (UUID perm, UUID targ)
{
req.get().checkACL(perm, targ);
}

private JsonValue uuidSet (Set<UUID> uuids)
{
var strs = uuids.map(UUID::toString)
.toJavaSet();
return Json.createArrayBuilder(strs)
.build();
}

private Map<UUID, ConfigEntry.Value> appValues ()
{
return db.dataflow()
.appValues(app)
.blockingFirst();
}

@GET @Path("object")
public JsonValue list ()
{
checkACL(Vocab.Perm.ReadApp, app);
var objs = appValues().keySet();
return uuidSet(objs);
}

private Set<UUID> findAll (JsonValue config)
{
log.info("Searching for config {}", config);
return appValues()
.filterValues(e -> e.value().equals(config))
.keySet();
}

@POST @Path("find")
public JsonValue find (JsonValue config)
{
checkACL(Vocab.Perm.ReadApp, app);
return uuidSet(findAll(config));
}

@POST @Path("class/{class}/find")
public JsonValue findInClass (@PathParam("class") UUID klass, JsonValue config)
{
checkACL(Vocab.Perm.ReadApp, app);
var rel = Relation.of("member").bind(klass, true, false);
checkACL(rel.perm(), klass);

var objs = findAll(config);
var mems = db.dataflow()
.relation(rel)
.blockingFirst();
return uuidSet(objs.intersect(mems));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ private Map<UUID, ConfigEntry.Value> appState (UUID app)
.withReplay()
.build();

/* XXX This returns an empty Map for UUIDs which aren't Apps. This
* is incorrect, we should probably have an Option return type. */
private Observable<Map<UUID, ConfigEntry.Value>> _buildAppValues (UUID app)
{
return appUpdates(app)
Expand Down
7 changes: 6 additions & 1 deletion acs-service-setup/dumps/clusters.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#-REQUIRE: auth git helm
#-REQUIRE: auth git helm metadb
---
service: !u UUIDs.Service.ConfigDB
version: 2
Expand Down Expand Up @@ -59,6 +59,11 @@ objects:
!u Clusters.Class.Cluster:
name: "Edge cluster"
iri: edge/Cluster
subclassOf:
# Edge cluster is not the only possibility for a Sparkplug
# Group, so probably we should have a SparkplugGroup
# intermediate class here.
- !u MetaDB.Class.SparkplugEntity

!u UUIDs.Class.App:
!u Clusters.App.Bootstrap:
Expand Down
7 changes: 6 additions & 1 deletion acs-service-setup/dumps/sparkplug.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# ACS-specific deployment decisions
#-REQUIRE: auth
#-REQUIRE: auth metadb
---
service: !u UUIDs.Service.ConfigDB
version: 2
Expand All @@ -19,10 +19,15 @@ objects:
- !u UUIDs.Permission.CmdEsc.Rebirth

!u Auth.Class.ClientRole:
# XXX There is some confusion here between 'an object with a
# Sparkplug Node address' and 'an object which is authorised to use
# its Node address to publish data'. The former is ⊂ Principal and ⊂
# SparkplugEntity but only the latter is ∈ ClientRole.
!u ACS.Group.SparkplugNode:
name: "Sparkplug Node"
subclassOf:
- !u Auth.Class.Principal
- !u MetaDB.Class.SparkplugEntity
# _Active Edge Agent_ is created by helm.js.
!u ACS.Group.SparkplugReader:
name: "Sparkplug global reader"
Expand Down
81 changes: 67 additions & 14 deletions lib/js-service-client/lib/service/configdb.js
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,8 @@ export class ConfigDB extends ServiceInterface {
*
* The arguments are either `(app, query, results)` or an object
* containing these properties.
*
* @deprecated The MetaDB will not implement this.
* @arg app The application UUID
* @arg klass (optional) Restrict to objects in this class
* @arg query The query object
Expand Down Expand Up @@ -344,27 +346,78 @@ export class ConfigDB extends ServiceInterface {
return await res.json();
}

/** Use `search` to locate a single object.
* This calls `search` with the provided options. The `results` key
* must be omitted. If we get more than one result this throws an
* exception.
* @returns The UUID of the single object which matches the query.
/** List objects by config entry.
*
* Finds all objects with a config entry exactly matching the one
* supplied. Results may be restricted by class.
*
* @arg app The App to search
* @arg config The config entry to match against
* @arg klass (optional) Restrict results to this class
* @returns An array of UUIDs
*/
async resolve (opts) {
if ("results" in opts)
throw new RangeError("resolve doesn't return results");
async find (app, config, klass) {
const kpath = klass ? `/class/${klass}` : "";

const uuids = await this.search(opts);
const [st, uuids] = await this.fetch({
method: "POST",
url: `v2/app/${app}${kpath}/find`,
body: config,
});

if (!Array.isArray(uuids))
this.throw("search didn't return an array");
if (uuids.length > 1)
this.throw(format("More than one result resolving %s with %o",
opts.app, opts.query));
if (st != 200) {
const kmsg = klass ? ` in ${klass}` : "";
this.throw(`Find for ${app}${kmsg} failed`, st);
}
return uuids;
}

/** Find a single object by config entry.
*
* Calls `find` and expects the result to contain no more than one
* object. Throws if we get multiple results.
*
* @arg app The App to search
* @arg config The config entry to match against
* @arg klass (optional) Restrict results to this class
* @returns A single UUID, or undefined
*/
async find_unique (app, config, klass) {
const uuids = await this.find(app, config, klass);
if (uuids.length > 1) {
const kmsg = klass ? ` in ${klass}` : "";
this.throw(`Find for ${app}${kmsg} returned multiple results`);
}
return uuids[0];
}

/** Find a single object by config entry.
*
* This is a compatibility method that calls `find_unique`.
*
* This used to call `search` but that endpoint is deprecated and
* not implemented by the MetaDB. The `query` parameter will be used
* directly as the config entry to match. This is only compatible
* with `search` as long as only top-level properties are used; the
* JSONpath feature of `search` is no longer supported. Properties
* explicitly mapped to `undefined` will not be included in the
* request; this matches the `search` behaviour of requiring these
* to not be present. Properties omitted from the query must also be
* not present in the config; this is a change from the `search`
* behaviour, which would ignore additional properties.
*
* Arguments are in an object.
*
* @deprecated Use find_unique for new code
* @arg app The App to search
* @arg query The config entry to search for
* @arg klass Restrict search to objects of this class
* @returns The UUID of the single object which matches the query.
*/
resolve (opts) {
return this.find_unique(opts.app, opts.query, opts.klass);
}

/** Create a ConfigDBWatcher object.
* This returns a Promise to a ConfigDBWatcher object for this
* ConfigDB. This allows watching config entry changes over
Expand Down
Loading