Files
komp_ac/common/proto/tables_data.proto
2026-08-22 11:14:54 +02:00

473 lines
16 KiB
Protocol Buffer
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// common/proto/tables_data.proto
syntax = "proto3";
package komp_ac.tables_data;
import "common.proto";
import "ecb.proto";
import "google/protobuf/struct.proto";
// Read and write row data for user-defined tables inside profiles (schemas).
// Operations are performed against the physical PostgreSQL table that
// corresponds to the logical table definition and are scoped by profile
// (schema). Deletions are soft (set deleted = true). Typed binding and
// script-based validation are enforced consistently.
service TablesData {
// Insert a new row into a table with strict type binding and script validation.
//
// Behavior:
// - Validates that profile (schema) exists and table is defined for it
// - Validates provided columns exist (user-defined or allowed system/FK columns)
// - For columns targeted by scripts in this table, the client MUST provide the
// value, and it MUST equal the scripts calculated value (compared type-safely)
// - Binds values with correct SQL types, rejects invalid formats/ranges
// - Inserts the row and returns the new id; queues search indexing (best effort)
// - If the physical table is missing but the definition exists, returns INTERNAL
rpc PostTableData(PostTableDataRequest) returns (PostTableDataResponse);
// Insert an ACCOUNTING-enabled source row with an explicit exchange-rate
// treatment for the journal line created from it.
rpc PostAccountingTableData(PostAccountingTableDataRequest)
returns (PostTableDataResponse);
// Starts a durable, profile-scoped import staging session. Staging never changes
// profile data; CommitTableDataImport applies every staged chunk atomically.
rpc BeginTableDataImport(BeginTableDataImportRequest)
returns (BeginTableDataImportResponse);
// Adds one table chunk to an import session. Rows retain chunk and table order.
rpc StageTableDataImport(StageTableDataImportRequest)
returns (StageTableDataImportResponse);
// Applies every staged row through the ordinary validated insert machinery in
// one PostgreSQL transaction. Any failure rolls back every imported side effect.
rpc CommitTableDataImport(CommitTableDataImportRequest)
returns (CommitTableDataImportResponse);
// Discards a staged import. Already committed imports cannot be aborted.
rpc AbortTableDataImport(AbortTableDataImportRequest)
returns (AbortTableDataImportResponse);
// Update existing row data with strict type binding and script validation.
//
// Behavior:
// - Validates profile and table, and that the record exists
// - If request data is empty, returns success without changing the row
// - For columns targeted by scripts:
// • If included in update, provided value must equal the script result
// • If not included, update must not cause the script result to differ
// from the current stored value; otherwise FAILED_PRECONDITION is returned
// - Binds values with correct SQL types; rejects invalid formats/ranges
// - Updates the row and returns the id; queues search indexing (best effort)
rpc PutTableData(PutTableDataRequest) returns (PutTableDataResponse);
rpc PutAccountingTableData(PutAccountingTableDataRequest)
returns (PutTableDataResponse);
// Performs a PUT after the user explicitly accepted changing a referenced version.
rpc PutTableDataConfirmed(PutTableDataConfirmedRequest) returns (PutTableDataResponse);
// Lists profiles whose rows currently point at this row version.
rpc GetTableUpdateImpact(TableUpdateImpactRequest) returns (TableUpdateImpactResponse);
// Snapshots the current version of a row and advances its version.
// Existing references remain immutable; new references use the new version.
rpc ArchiveTableData(ArchiveTableDataRequest) returns (ArchiveTableDataResponse);
// Soft-delete a single record (sets deleted = true) if it exists and is not already deleted.
//
// Behavior:
// - Validates profile and table definition
// - Updates only rows with deleted = false
// - success = true means a row was actually changed; false means nothing to delete
// - If the physical table is missing but the definition exists, returns INTERNAL
rpc DeleteTableData(DeleteTableDataRequest) returns (DeleteTableDataResponse);
// Fetch a single non-deleted row by id as textified values.
//
// Behavior:
// - Validates profile and table definition
// - Returns all columns as strings (COALESCE(col::TEXT, '') AS col)
// including: id, deleted, row_revision, all user-defined columns, and FK columns
// named by whoever declared each link, plus "account" on
// ACCOUNTING-enabled tables
// - Fails with NOT_FOUND if record does not exist or is soft-deleted
// - If the physical table is missing but the definition exists, returns INTERNAL
rpc GetTableData(GetTableDataRequest) returns (GetTableDataResponse);
// Fetches one exact version of a row.
rpc GetTableDataVersion(GetTableDataVersionRequest) returns (GetTableDataResponse);
// Count non-deleted rows in a table.
//
// Behavior:
// - Validates profile and table definition
// - Returns komp_ac.common.CountResponse.count with rows where deleted = FALSE
// - If the physical table is missing but the definition exists, returns INTERNAL
rpc GetTableDataCount(GetTableDataCountRequest) returns (komp_ac.common.CountResponse);
// Fetch the last non-deleted row by id. This is the efficient form-opening
// path and does not scan the table through OFFSET or COUNT(*).
rpc GetLastTableData(GetLastTableDataRequest) returns (GetLastTableDataResponse);
// Fetch the nearest visible row before or after an existing row id. This is
// the ordinary form-navigation path and skips deleted/id-gap rows.
rpc GetAdjacentTableData(GetAdjacentTableDataRequest)
returns (GetAdjacentTableDataResponse);
// Fetch the N-th non-deleted row by id order (1-based), then return its full data.
//
// Behavior:
// - position is 1-based (position = 1 → first row by id ASC with deleted = FALSE)
// - Returns NOT_FOUND if position is out of bounds
// - Otherwise identical to GetTableData for the selected id
rpc GetTableDataByPosition(GetTableDataByPositionRequest) returns (GetTableDataResponse);
}
// Insert a new row.
message PostTableDataRequest {
// Required. Profile (PostgreSQL schema) name that owns the table.
// Must exist in the schemas table.
string profile_name = 1;
// Required. Logical table (definition) name within the profile.
// Must exist in table_definitions for the given profile.
string table_name = 2;
// Required. Key-value data for columns to insert.
//
// Allowed keys:
// - User-defined columns from the table definition
// - System/FK columns:
// • "deleted" (BOOLEAN), optional; default FALSE if not provided
// • one BIGINT per link, named by whoever declared it
// • "account" (required, non-null slash-delimited account string) on
// ACCOUNTING-enabled tables
//
// Type expectations by SQL type:
// - TEXT: string value; empty string is treated as NULL
// - BOOLEAN: bool value
// - TIMESTAMPTZ: ISO 8601/RFC 3339 string (parsed to TIMESTAMPTZ)
// - INTEGER: number with no fractional part and within i32 range
// - BIGINT: canonical integer string for the full i64 range; a number is
// accepted only within protobuf NumberValue's exact integer range
// - NUMERIC(p,s): string representation only; empty string becomes NULL
// (numbers for NUMERIC are rejected to avoid precision loss)
//
// Script validation rules:
// - If a script exists for a target column, that column MUST be present here,
// and its provided value MUST equal the scripts computed value (type-aware
// comparison, e.g., decimals are compared numerically).
//
// Notes:
// - Unknown/invalid column names are rejected
// - Some application-specific validations may apply (e.g., max length for
// certain fields like "telefon")
map<string, google.protobuf.Value> data = 3;
}
message PostAccountingTableDataRequest {
PostTableDataRequest row = 1;
komp_ac.ecb.ExchangeRateSelection exchange_rate_selection = 2;
}
// Insert response.
message PostTableDataResponse {
// True if the insert succeeded.
bool success = 1;
// Human-readable message.
string message = 2;
// The id of the inserted row.
int64 inserted_id = 3;
// Revision committed for the inserted row.
int64 row_revision = 4;
// Journal created or updated by automatic accounting, when applicable.
optional int64 journal_id = 5;
}
message BeginTableDataImportRequest {
string profile_name = 1;
}
message BeginTableDataImportResponse {
string import_id = 1;
}
message StageTableDataImportRequest {
string import_id = 1;
string table_name = 2;
repeated TableDataImportRow rows = 3;
}
message TableDataImportRow {
// Required. Data payload for one staged row.
map<string, google.protobuf.Value> data = 1;
}
message StageTableDataImportResponse {
int64 staged_rows = 1;
int64 total_staged_rows = 2;
}
message CommitTableDataImportRequest {
string import_id = 1;
}
message CommitTableDataImportResponse {
bool success = 1;
int64 inserted_rows = 2;
}
message AbortTableDataImportRequest {
string import_id = 1;
}
message AbortTableDataImportResponse {
bool success = 1;
}
// Update an existing row.
message PutTableDataRequest {
// Required. Profile (schema) name.
string profile_name = 1;
// Required. Table name within the profile.
string table_name = 2;
// Required. Id of the row to update.
int64 id = 3;
// Required. Columns to update (same typing rules as PostTableDataRequest.data).
// "account" may be omitted, but cannot be null when supplied.
//
// Special script rules:
// - If a script targets column X and X is included here, the value for X must
// equal the scripts result (type-aware).
// - If X is not included here but the update would cause the scripts result
// to change compared to the current stored value, the update is rejected with
// FAILED_PRECONDITION, instructing the caller to include X explicitly.
//
// Passing an empty map results in a no-op success response.
map<string, google.protobuf.Value> data = 4;
// Required. Revision returned when this row was loaded or last saved.
int64 expected_revision = 5;
}
message PutAccountingTableDataRequest {
PutTableDataRequest update = 1;
komp_ac.ecb.ExchangeRateSelection exchange_rate_selection = 2;
bool confirmed = 3;
repeated string expected_affected_profiles = 4;
}
message PutTableDataConfirmedRequest {
PutTableDataRequest update = 1;
repeated string expected_affected_profiles = 2;
}
// Update response.
message PutTableDataResponse {
// True if the update succeeded (or no-op on empty data).
bool success = 1;
// Human-readable message.
string message = 2;
// The id of the updated row.
int64 updated_id = 3;
// Revision committed for the updated row.
int64 row_revision = 4;
// Journal updated by automatic accounting, when applicable.
optional int64 journal_id = 5;
}
message TableUpdateImpactRequest {
string profile_name = 1;
string table_name = 2;
int64 id = 3;
}
message TableUpdateImpactResponse {
repeated string affected_profiles = 1;
}
message ArchiveTableDataRequest {
string profile_name = 1;
string table_name = 2;
int64 id = 3;
int64 expected_revision = 4;
}
message ArchiveTableDataResponse {
bool success = 1;
int64 archived_version = 2;
string archived_at = 3;
repeated string affected_profiles = 4;
int64 current_version = 5;
int64 row_revision = 6;
}
// Soft-delete a single row.
message DeleteTableDataRequest {
// Required. Profile (schema) name.
string profile_name = 1;
// Required. Table name within the profile.
string table_name = 2;
// Required. Row id to soft-delete.
int64 record_id = 3;
// Required. Revision returned when this row was loaded or last saved.
int64 expected_revision = 4;
}
// Soft-delete response.
message DeleteTableDataResponse {
// True when the row was successfully marked deleted.
bool success = 1;
}
// Fetch a single non-deleted row by id.
message GetTableDataRequest {
// Required. Profile (schema) name.
string profile_name = 1;
// Required. Table name within the profile.
string table_name = 2;
// Required. Id of the row to fetch.
int64 id = 3;
}
message GetTableDataVersionRequest {
string profile_name = 1;
string table_name = 2;
int64 id = 3;
int64 version = 4;
}
// A link resolved exactly as the source row stored it, together with the
// target row's current state. The physical companion version column remains
// an internal implementation detail.
enum ResolvedTableLinkStatus {
CURRENT = 0;
SUPERSEDED = 1;
UNAVAILABLE = 2;
DELETED = 3;
// The source row may expose the FK and the target's configured display
// columns, but the caller may not read the target table itself.
RESTRICTED = 4;
}
message ResolvedTableLinkVersion {
int64 version = 1;
map<string, string> data = 2;
repeated string row_display_values = 3;
repeated string row_display_columns = 4;
}
message ResolvedTableLinkUpdate {
int64 version = 1;
map<string, string> data = 2;
repeated string row_display_values = 3;
repeated string changed_fields = 4;
}
message ResolvedTableLink {
int64 id = 1;
ResolvedTableLinkVersion saved = 2;
ResolvedTableLinkStatus status = 3;
ResolvedTableLinkUpdate newer = 4;
}
// Row payload: all columns returned as strings.
message GetTableDataResponse {
// Map of column_name → stringified value for:
// - id, deleted
// - all user-defined columns from the table definition
// - one column per link, named by whoever declared it
// - account for ACCOUNTING-enabled tables
//
// All values are returned as TEXT via col::TEXT and COALESCEed to empty string
// (NULL becomes ""). The row is returned only if deleted = FALSE.
map<string, string> data = 1;
// Configured human-readable values for this row, in display order. A value
// is empty when its column is NULL/empty; an empty list means the row is
// identified by its id alone.
repeated string row_display_values = 2;
repeated string row_display_columns = 3;
// Links keyed by their public column name. Each value contains the row as
// saved by this record and, when the caller may read the target table, its
// newest available state. Restricted links contain only the target table's
// configured display columns.
map<string, ResolvedTableLink> resolved_links = 4;
// The generation returned by this response. Zero is never returned.
int64 row_version = 5;
// Active journal containing this row's automatic accounting line, when the
// table is ACCOUNTING-enabled and the row has been posted.
optional int64 journal_id = 6;
}
// Count non-deleted rows.
message GetTableDataCountRequest {
// Required. Profile (schema) name.
string profile_name = 1;
// Required. Table name within the profile.
string table_name = 2;
}
// Fetch the last visible row by stable row id.
message GetLastTableDataRequest {
string profile_name = 1;
string table_name = 2;
}
message GetLastTableDataResponse {
// Zero when the table has no visible rows.
int64 last_id = 1;
// Absent when last_id is zero.
GetTableDataResponse row = 2;
}
enum RowIdDirection {
ROW_ID_DIRECTION_NEXT = 0;
ROW_ID_DIRECTION_PREVIOUS = 1;
}
message GetAdjacentTableDataRequest {
string profile_name = 1;
string table_name = 2;
int64 anchor_id = 3;
RowIdDirection direction = 4;
}
message GetAdjacentTableDataResponse {
// Absent when no visible row exists in the requested direction.
GetTableDataResponse row = 1;
}
// Fetch by ordinal position among non-deleted rows (1-based).
message GetTableDataByPositionRequest {
// Required. Profile (schema) name.
string profile_name = 1;
// Required. Table name within the profile.
string table_name = 2;
// Required. 1-based position by id ascending among rows with deleted = FALSE.
int32 position = 3;
}