// common/proto/tables_data.proto syntax = "proto3"; package komp_ac.tables_data; import "common.proto"; import "exchange_rates.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 script’s 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 script’s 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 data = 3; } message PostAccountingTableDataRequest { PostTableDataRequest row = 1; komp_ac.exchange_rates.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; // Import-contract revision returned by GetTableImportDescriptor. A zero // value is accepted for older clients and snapshots the current revision. int64 expected_table_revision = 4; } message TableDataImportRow { // Required. Data payload for one staged row. map 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 script’s result (type-aware). // - If X is not included here but the update would cause the script’s 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 data = 4; // Required. Revision returned when this row was loaded or last saved. int64 expected_revision = 5; } message PutAccountingTableDataRequest { PutTableDataRequest update = 1; komp_ac.exchange_rates.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 data = 2; repeated string row_display_values = 3; repeated string row_display_columns = 4; } message ResolvedTableLinkUpdate { int64 version = 1; map 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 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 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; // Present when the row was loaded through a form-navigation RPC. The row and // its numbering bounds come from the same server operation. RowNavigation navigation = 7; } // 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; RowNavigationMode navigation_mode = 3; } // Controls how an unfiltered form numbers rows. enum RowNavigationMode { // Actual database row id / highest visible row id. Deleted ids leave gaps. ROW_NAVIGATION_MODE_ID = 0; // Ordinal position / exact visible-row count. Deleted rows are omitted. ROW_NAVIGATION_MODE_POSITION = 1; } // Complete numbering state returned with a navigated row. message RowNavigation { RowNavigationMode mode = 1; // Actual row id in ID mode; 1-based ordinal in POSITION mode. For an empty // table this is the first new-row slot (1). uint64 current = 2; // Highest visible id in ID mode; exact visible-row count in POSITION mode. uint64 total = 3; } message GetLastTableDataResponse { // Zero when the table has no visible rows. Retained as row identity; clients // should use navigation for display bounds. int64 last_id = 1; // Absent when last_id is zero. GetTableDataResponse row = 2; RowNavigation navigation = 3; } 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; RowNavigationMode navigation_mode = 5; // Current ordinal position when navigation_mode is POSITION. Ignored in ID mode. uint64 anchor_position = 6; } message GetAdjacentTableDataResponse { // Absent when no visible row exists in the requested direction. GetTableDataResponse row = 1; RowNavigation navigation = 2; } // 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; // Set this or record_id, never both. This is the 1-based position by id // ascending among rows with deleted = FALSE. int32 position = 3; // Direct row lookup that also returns its ID or ordinal navigation bounds. int64 record_id = 4; RowNavigationMode navigation_mode = 5; }