Skip to content

Commit 877ae03

Browse files
authored
feat: Implement ApplyResource RPC for granular manifest mutations (#1695)
* feat: Implement ApplyResource RPC for granular manifest mutations Add ApplyResource RPC to ApplyManifestService that enables targeted single-resource updates without requiring a full manifest reapply. The endpoint reads the current manifest from the version store, patches the specified resource into it, validates the full dependency graph, computes a diff against the current state, and executes only the affected phases. A new manifest version is created on success. Key additions: - ApplyResourceRequest/Response proto messages with ManifestResourceType enum - Resource patching logic (upsert-or-append for all 12 resource types) - diffAgainst() for explicit base comparison (vs loading from store) - Full dry_run support and optimistic locking via expected_sequence_number - Post-apply hooks and version store persistence * fix: Address review feedback on ApplyResource - Fix valRuleKey to apply strings.ToUpper normalization matching differ.valRuleKey (prevents key mismatch and spurious diffs) - Add step result warning when versionStore.Save fails so callers see stale-baseline risk instead of silent failure --------- Co-authored-by: Ben Coombs <bjcoombs@users.noreply.github.com>
1 parent 94df1e0 commit 877ae03

4 files changed

Lines changed: 1288 additions & 0 deletions

File tree

api/proto/meridian/control_plane/v1/apply_manifest_service.proto

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import "buf/validate/validate.proto";
66
import "google/api/annotations.proto";
77
import "meridian/control_plane/v1/manifest.proto";
88
import "meridian/control_plane/v1/manifest_history_service.proto";
9+
import "meridian/mapping/v1/mapping.proto";
10+
import "meridian/party/v1/party.proto";
911

1012
option go_package = "github.com/meridianhub/meridian/api/proto/meridian/control_plane/v1;controlplanev1";
1113

@@ -20,6 +22,18 @@ service ApplyManifestService {
2022
body: "*"
2123
};
2224
}
25+
26+
// ApplyResource applies a granular mutation to a single resource within the manifest.
27+
// It reads the current manifest from the database, patches the specified resource into
28+
// the composite manifest, validates the full dependency graph, computes a diff, and
29+
// executes only the affected phases. A new manifest version is created on success.
30+
// In dry_run mode, returns the diff without applying changes.
31+
rpc ApplyResource(ApplyResourceRequest) returns (ApplyResourceResponse) {
32+
option (google.api.http) = {
33+
post: "/v1/manifests/apply-resource"
34+
body: "*"
35+
};
36+
}
2337
}
2438

2539
// ApplyManifestRequest contains the manifest to apply and apply options.
@@ -160,3 +174,138 @@ message ValidationError {
160174
// (e.g., the instrument code, account type code, or saga name).
161175
string resource_id = 7;
162176
}
177+
178+
// ManifestResourceType identifies a category of resource within the manifest.
179+
// Used by ApplyResource to specify which resource type is being mutated.
180+
enum ManifestResourceType {
181+
// MANIFEST_RESOURCE_TYPE_UNSPECIFIED is the default zero value.
182+
MANIFEST_RESOURCE_TYPE_UNSPECIFIED = 0;
183+
184+
// MANIFEST_RESOURCE_TYPE_INSTRUMENT targets an InstrumentDefinition.
185+
MANIFEST_RESOURCE_TYPE_INSTRUMENT = 1;
186+
187+
// MANIFEST_RESOURCE_TYPE_ACCOUNT_TYPE targets an AccountTypeDefinition.
188+
MANIFEST_RESOURCE_TYPE_ACCOUNT_TYPE = 2;
189+
190+
// MANIFEST_RESOURCE_TYPE_VALUATION_RULE targets a ValuationRule.
191+
MANIFEST_RESOURCE_TYPE_VALUATION_RULE = 3;
192+
193+
// MANIFEST_RESOURCE_TYPE_SAGA targets a SagaDefinition.
194+
MANIFEST_RESOURCE_TYPE_SAGA = 4;
195+
196+
// MANIFEST_RESOURCE_TYPE_PARTY_TYPE targets a PartyTypeDefinition.
197+
MANIFEST_RESOURCE_TYPE_PARTY_TYPE = 5;
198+
199+
// MANIFEST_RESOURCE_TYPE_MAPPING targets a MappingDefinition.
200+
MANIFEST_RESOURCE_TYPE_MAPPING = 6;
201+
202+
// MANIFEST_RESOURCE_TYPE_PROVIDER_CONNECTION targets a ProviderConnectionConfig.
203+
MANIFEST_RESOURCE_TYPE_PROVIDER_CONNECTION = 7;
204+
205+
// MANIFEST_RESOURCE_TYPE_INSTRUCTION_ROUTE targets an InstructionRouteConfig.
206+
MANIFEST_RESOURCE_TYPE_INSTRUCTION_ROUTE = 8;
207+
208+
// MANIFEST_RESOURCE_TYPE_MARKET_DATA_SOURCE targets a MarketDataSourceDefinition.
209+
MANIFEST_RESOURCE_TYPE_MARKET_DATA_SOURCE = 9;
210+
211+
// MANIFEST_RESOURCE_TYPE_MARKET_DATA_SET targets a MarketDataSetDefinition.
212+
MANIFEST_RESOURCE_TYPE_MARKET_DATA_SET = 10;
213+
214+
// MANIFEST_RESOURCE_TYPE_ORGANIZATION targets an OrganizationDefinition.
215+
MANIFEST_RESOURCE_TYPE_ORGANIZATION = 11;
216+
217+
// MANIFEST_RESOURCE_TYPE_INTERNAL_ACCOUNT targets an InternalAccountDefinition.
218+
MANIFEST_RESOURCE_TYPE_INTERNAL_ACCOUNT = 12;
219+
}
220+
221+
// ApplyResourceRequest contains the resource to apply and apply options.
222+
// The caller specifies the resource type and provides the resource payload
223+
// as a oneof. The server reads the current manifest, patches the resource
224+
// into it, validates the full dependency graph, diffs, and executes only
225+
// the affected phases.
226+
message ApplyResourceRequest {
227+
// resource_type identifies which manifest section is being mutated.
228+
ManifestResourceType resource_type = 1 [(buf.validate.field).enum = {
229+
defined_only: true
230+
not_in: [0]
231+
}];
232+
233+
// resource is the resource payload to apply. Exactly one must be set,
234+
// and it must match the resource_type field.
235+
oneof resource {
236+
option (buf.validate.oneof).required = true;
237+
238+
// instrument is an InstrumentDefinition to create or update.
239+
InstrumentDefinition instrument = 2;
240+
241+
// account_type is an AccountTypeDefinition to create or update.
242+
AccountTypeDefinition account_type = 3;
243+
244+
// valuation_rule is a ValuationRule to create or update.
245+
ValuationRule valuation_rule = 4;
246+
247+
// saga is a SagaDefinition to create or update.
248+
SagaDefinition saga = 5;
249+
250+
// party_type is a PartyTypeDefinition to create or update.
251+
meridian.party.v1.PartyTypeDefinition party_type = 6;
252+
253+
// mapping is a MappingDefinition to create or update.
254+
meridian.mapping.v1.MappingDefinition mapping = 7;
255+
256+
// provider_connection is a ProviderConnectionConfig to create or update.
257+
ProviderConnectionConfig provider_connection = 8;
258+
259+
// instruction_route is an InstructionRouteConfig to create or update.
260+
InstructionRouteConfig instruction_route = 9;
261+
262+
// market_data_source is a MarketDataSourceDefinition to create or update.
263+
MarketDataSourceDefinition market_data_source = 10;
264+
265+
// market_data_set is a MarketDataSetDefinition to create or update.
266+
MarketDataSetDefinition market_data_set = 11;
267+
268+
// organization is an OrganizationDefinition to create or update.
269+
OrganizationDefinition organization = 12;
270+
271+
// internal_account is an InternalAccountDefinition to create or update.
272+
InternalAccountDefinition internal_account = 13;
273+
}
274+
275+
// dry_run when true performs validation and planning without committing changes.
276+
bool dry_run = 14;
277+
278+
// applied_by identifies who is applying this resource (e.g., user email, API key ID).
279+
string applied_by = 15 [(buf.validate.field).string = {
280+
min_len: 1
281+
max_len: 255
282+
}];
283+
284+
// expected_sequence_number is the sequence number the caller expects the
285+
// current manifest to have. If it does not match the actual current sequence
286+
// number, the apply is rejected with ABORTED status (ETag semantics).
287+
// Set to 0 to skip the check (first apply or "overwrite" mode).
288+
int64 expected_sequence_number = 16;
289+
}
290+
291+
// ApplyResourceResponse contains the result of a single-resource application.
292+
message ApplyResourceResponse {
293+
// status is the outcome of the apply operation.
294+
ApplyManifestStatus status = 1;
295+
296+
// step_results contains the result of each execution step.
297+
repeated StepResult step_results = 2;
298+
299+
// validation_errors contains structured validation errors if the manifest is invalid.
300+
repeated ValidationError validation_errors = 3;
301+
302+
// diff_summary is a human-readable summary of changes detected.
303+
string diff_summary = 4;
304+
305+
// sequence_number is the sequence number assigned to the new manifest version.
306+
// Callers should use this value as expected_sequence_number in subsequent applies.
307+
int64 sequence_number = 5;
308+
309+
// snapshot is the manifest version record created by this apply, if successful.
310+
ManifestVersion snapshot = 6;
311+
}

0 commit comments

Comments
 (0)