feat(sdk): HubClient.submit() with JSON inputs and outputs
Some checks failed
Security / Security check (push) Failing after 1s
Some checks failed
Security / Security check (push) Failing after 1s
Wrap the Submit RPC so callers can run a flow inline with typed inputs and read its outputs. jsonInputs (each value a Dart Map) are converted to a google.protobuf.Struct via a recursive builder and sent as Payload.json — the only path that can feed a json-typed module input (e.g. econ.skm_score, law.benefit_score). The returned SubmitResponse.outputs carry the flow outputs, which the audit log does not persist. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
d07a0a9777
commit
597f6bec4f
1 changed files with 78 additions and 0 deletions
|
|
@ -4,6 +4,7 @@
|
||||||
// public method maps onto exactly one RPC on Hub or HubAdmin, so
|
// public method maps onto exactly one RPC on Hub or HubAdmin, so
|
||||||
// callers don't have to know which service hosts which call.
|
// callers don't have to know which service hosts which call.
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
|
|
@ -17,6 +18,8 @@ import 'generated/chain/v1/hub.pb.dart' as pb;
|
||||||
import 'generated/chain/v1/hub.pbgrpc.dart' as grpc_hub;
|
import 'generated/chain/v1/hub.pbgrpc.dart' as grpc_hub;
|
||||||
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart'
|
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart'
|
||||||
show Empty;
|
show Empty;
|
||||||
|
import 'package:protobuf/well_known_types/google/protobuf/struct.pb.dart'
|
||||||
|
show Struct, Value, ListValue, NullValue;
|
||||||
|
|
||||||
/// Connection coordinates for a hub endpoint.
|
/// Connection coordinates for a hub endpoint.
|
||||||
class HubEndpoint {
|
class HubEndpoint {
|
||||||
|
|
@ -752,12 +755,87 @@ class HubClient {
|
||||||
return _admin.runSavedFlow(req);
|
return _admin.runSavedFlow(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Submit a flow YAML to the hub for a one-shot run, with typed
|
||||||
|
/// inputs, and read its outputs from the response.
|
||||||
|
///
|
||||||
|
/// Unlike [runSavedFlow] (which runs a flow already saved in the
|
||||||
|
/// hub by name), this sends the flow definition inline and exposes
|
||||||
|
/// the full [Submit] RPC. Two things only this path can do:
|
||||||
|
/// * pass **JSON-typed inputs** ([jsonInputs]) — for modules that
|
||||||
|
/// read `inputs.require_json(...)` (e.g. `econ.skm_score`,
|
||||||
|
/// `law.benefit_score`). Each value must be a JSON object (a
|
||||||
|
/// Dart `Map`); it is converted to a `google.protobuf.Struct`.
|
||||||
|
/// * read the flow's **outputs** off the response. The audit log
|
||||||
|
/// does not persist outputs — they come back only here.
|
||||||
|
///
|
||||||
|
/// [textInputs] → text Payloads, [fileInputs] → bytes Payloads
|
||||||
|
/// (MIME from [fileMimeTypes], default `application/octet-stream`).
|
||||||
|
/// All maps default to empty.
|
||||||
|
Future<SubmitResponse> submit({
|
||||||
|
required String flowYaml,
|
||||||
|
Map<String, String> textInputs = const {},
|
||||||
|
Map<String, Uint8List> fileInputs = const {},
|
||||||
|
Map<String, Object?> jsonInputs = const {},
|
||||||
|
Map<String, String> fileMimeTypes = const {},
|
||||||
|
}) async {
|
||||||
|
final req = pb.SubmitRequest()..flowYaml = utf8.encode(flowYaml);
|
||||||
|
for (final entry in textInputs.entries) {
|
||||||
|
req.inputs[entry.key] = pb_common.Payload()..text = entry.value;
|
||||||
|
}
|
||||||
|
for (final entry in fileInputs.entries) {
|
||||||
|
final mime = fileMimeTypes[entry.key] ?? 'application/octet-stream';
|
||||||
|
req.inputs[entry.key] = pb_common.Payload()
|
||||||
|
..bytes = (pb_common.Bytes()
|
||||||
|
..mimeType = mime
|
||||||
|
..data = entry.value);
|
||||||
|
}
|
||||||
|
for (final entry in jsonInputs.entries) {
|
||||||
|
final value = entry.value;
|
||||||
|
if (value is! Map) {
|
||||||
|
throw ArgumentError(
|
||||||
|
'jsonInputs["${entry.key}"] must be a JSON object (Map); '
|
||||||
|
'got ${value.runtimeType}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
req.inputs[entry.key] = pb_common.Payload()..json = _structFromMap(value);
|
||||||
|
}
|
||||||
|
return _hub.submit(req);
|
||||||
|
}
|
||||||
|
|
||||||
/// Closes the gRPC channel. Idempotent.
|
/// Closes the gRPC channel. Idempotent.
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
await _channel.shutdown();
|
await _channel.shutdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert a Dart JSON object into a `google.protobuf.Struct`.
|
||||||
|
/// Mirrors the proto3-JSON mapping the hub expects for a
|
||||||
|
/// `Payload.json` input. Keys are stringified; values recurse via
|
||||||
|
/// [_structValue].
|
||||||
|
Struct _structFromMap(Map<dynamic, dynamic> map) {
|
||||||
|
final s = Struct();
|
||||||
|
map.forEach((k, v) => s.fields[k.toString()] = _structValue(v));
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a single Dart JSON value into a `google.protobuf.Value`.
|
||||||
|
Value _structValue(Object? v) {
|
||||||
|
if (v == null) return Value()..nullValue = NullValue.NULL_VALUE;
|
||||||
|
if (v is bool) return Value()..boolValue = v;
|
||||||
|
if (v is num) return Value()..numberValue = v.toDouble();
|
||||||
|
if (v is String) return Value()..stringValue = v;
|
||||||
|
if (v is Map) return Value()..structValue = _structFromMap(v);
|
||||||
|
if (v is Iterable) {
|
||||||
|
final list = ListValue();
|
||||||
|
for (final e in v) {
|
||||||
|
list.values.add(_structValue(e));
|
||||||
|
}
|
||||||
|
return Value()..listValue = list;
|
||||||
|
}
|
||||||
|
// Unknown type — stringify so one stray value can't fail the run.
|
||||||
|
return Value()..stringValue = v.toString();
|
||||||
|
}
|
||||||
|
|
||||||
/// One declared input in a saved flow's `inputs:` section.
|
/// One declared input in a saved flow's `inputs:` section.
|
||||||
/// Returned by [HubClient.getFlowDefinition] so GUI clients
|
/// Returned by [HubClient.getFlowDefinition] so GUI clients
|
||||||
/// can build a typed run-flow form. The [type] field carries
|
/// can build a typed run-flow form. The [type] field carries
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue