Skip to content
Merged
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Details and migration steps: [docs/en/changelogs/1.13.0.md](docs/en/changelogs/1
- A table with a Binary column reads back over the JSON body codec and the Plain format again, so such a form can be created and saved from a JSON client.
- A `DateTime` filter value, parameter value or message property arrives over the JSON body codec as the same UTC instant MessagePack delivers, instead of keeping an Unspecified or Local kind; a JSON response now marks such a value with `Z`.
- A table with a read-only column and a modified row reads back over the JSON body codec and the Plain format, as it already did over MessagePack.
- A form's list or count filter converts a string value to the type of its field, so a date, a `Guid` or a quoted number sent in a Plain body filters a typed column on PostgreSQL and Oracle, and no longer returns no rows on SQL Server; text a database used to convert on its own, such as `""` for a number or `1` for a boolean, is now refused.
- A form's list or count filter converts a string value to the type of its field, so a date, a `Guid` or a quoted number sent in a Plain body filters a typed column on PostgreSQL and Oracle, and no longer returns no rows on SQL Server; text a database used to convert on its own, such as `""` for a number or `1` for a boolean, is now refused with `-32602 InvalidParams` and a message naming the field.
- An Avalonia editor bound to a detail row no longer throws when the form replaces its dataset, for example on New; it is left alone, as in Blazor. The rules both heads share for this, for time write-back, lookup display fields, menu rows, check-box cells and sort marks now live once in `Polhem.UI.Core`.
- A MudBlazor or Fluent UI grid releases its data object subscriptions when it is disposed; before, the data object kept every grid a page had shown. The rules both Blazor UI packages repeated (the column filter, the menu drawer, card clicks, rejected numeric and time entries, the grid's editor script and the dialog parameters) now live in `Polhem.Web.Blazor`, and the column filter has behavior tests.

Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Polhem 套件的重要變更,每個版本只列幾項重點。每個版本都
- 含 Binary 欄位的資料表又能經由 JSON body codec 與 Plain 格式讀回,因此 JSON 用戶端可以新增並儲存這類表單。
- `DateTime` 型別的篩選值、參數值與訊息屬性經由 JSON body codec 傳遞時,會與 MessagePack 一樣以同一個 UTC 時間點送達,不再保留 Unspecified 或 Local 的 kind;JSON 回應中這類值現在帶有 `Z`。
- 含唯讀欄位且有修改過的列的資料表,可經由 JSON body codec 與 Plain 格式讀回,與 MessagePack 原本的行為相同。
- 表單清單與筆數查詢的過濾條件會把字串值轉成其欄位的型別,因此 Plain body 送出的日期、`Guid` 或加了引號的數字,可以在 PostgreSQL 與 Oracle 上過濾有型別的欄位,在 SQL Server 上也不再查無資料;資料庫過去會自行轉換的文字(例如數字欄位的 `""`、布林欄位的 `1`)現在會被拒絕。
- 表單清單與筆數查詢的過濾條件會把字串值轉成其欄位的型別,因此 Plain body 送出的日期、`Guid` 或加了引號的數字,可以在 PostgreSQL 與 Oracle 上過濾有型別的欄位,在 SQL Server 上也不再查無資料;資料庫過去會自行轉換的文字(例如數字欄位的 `""`、布林欄位的 `1`)現在會以 `-32602 InvalidParams` 拒絕,訊息會指出欄位。
- Avalonia 綁在明細列上的編輯器,在表單替換資料集時(例如新增)不再擲出例外,改為與 Blazor 一樣保持不動。兩個 head 共用的這項規則,以及時間欄寫回、lookup 顯示欄位、選單列、勾選欄與排序標記的規則,現在只在 `Polhem.UI.Core` 寫一份。
- MudBlazor 與 Fluent UI 的 grid 在釋放時會取消對 data object 的訂閱;原本 data object 會一直留著頁面顯示過的每個 grid。兩個 Blazor UI 套件重複的規則(欄位篩選、選單抽屜、卡片點擊、被拒的數值與時間輸入、grid 的編輯器腳本、對話框參數)現在放在 `Polhem.Web.Blazor`,欄位篩選也有了行為測試。

Expand Down
6 changes: 3 additions & 3 deletions docs/en/api/jsonrpc-frontend-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,8 @@ filters a typed column on every database (`GetListFilterValueDbTests` runs this
`StartsWith` and `EndsWith` operators, since they match text.

A relation field converts by the `DbType` it declares, so declare the type of the column it reads.
A string that is not valid for its field's type is refused, including text a database used to
convert on its own, such as `""` for a number or `1` for a boolean. `parameters` entries and other `object`
A string that is not valid for its field's type is refused with `-32602 InvalidParams` and a message naming the
field, including text a database used to convert on its own, such as `""` for a number or `1` for a boolean. `parameters` entries and other `object`
members are not converted. A client that needs typed values there uses the JSON body codec
(`"codec": "json"` with `Encoded` or `Encrypted`), whose `[code, value]` envelope carries the type —
that envelope is what `WireValueEnvelope` in `wire-contracts/messages.d.ts` describes; a Plain body
Expand Down Expand Up @@ -290,7 +290,7 @@ larger than it accepts. Up to 1.1.0 a response also echoed the method name in a
| `-32700` | `ParseError` | Malformed JSON in the request body | Fix client serialization |
| `-32600` | `InvalidRequest` | Not a valid JSON-RPC request (no `method`, wrong `jsonrpc`), a rejected API key, or a malformed `Authorization` header | Inspect headers and body |
| `-32601` | `MethodNotFound` | The method name is not `progId.action`, or names no action the business object exposes; the message is fixed and does not repeat the name | Check method name / casing |
| `-32602` | `InvalidParams` | The parameters carry no value to bind (no `params`, no `value`, or a `null` one); a `Plain` body could not be read into the type the method takes; or, while the host requires the wire frame, a signed-in session called a [replay-protected](api-method-reference.md#replay-protection) method with a `Plain` or `Encoded` payload. The message is always "Invalid params.". Arguments that are readable but invalid answer `-32099` | Fix the `params` shape, or call that method `Encrypted` |
| `-32602` | `InvalidParams` | The parameters carry no value to bind (no `params`, no `value`, or a `null` one); a `Plain` body could not be read into the type the method takes; or, while the host requires the wire frame, a signed-in session called a [replay-protected](api-method-reference.md#replay-protection) method with a `Plain` or `Encoded` payload; the message of these is always "Invalid params.". A filter value that cannot be read as the type of its field answers this code too, with a message naming the field. Other arguments that are readable but invalid answer `-32099` | Fix the `params` shape or the named filter value, or call that method `Encrypted` |
| `-32603` | `InternalError` | Unhandled server-side exception (`-32000` up to 1.1.0). An `Encrypted` payload that fails authentication lands here too, so a client whose payload version does not match the server's sees it on every `Encrypted` call | Not user-facing. The message is "Internal server error" unless the server runs in debug mode. When every `Encrypted` call fails this way and other formats work, check that the client and the server are on matching versions |
| `-32001` | `Unauthorized` | The method needs a signed-in caller and the access token is missing, unknown or expired | Sign in again |
| `-32002` | `CompanyNotEntered` | Method needs company context | Call `System.EnterCompany` first |
Expand Down
6 changes: 4 additions & 2 deletions docs/en/changelogs/1.13.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ count and delete now convert a filter's string values to the type of the field t
query is built: a `DateTime` field reads a UTC instant (text without an offset is taken to be UTC), a `Date` field
reads the date written, and `Guid`, numeric and `Boolean` fields read their invariant text; dates are read in their
ISO 8601 forms only. `String`, `Text` and `Time` fields and the `LIKE` operators keep the string. A string that is not
valid for its field's type is refused before the query reaches the database, including text a database used to
convert on its own, such as `""` for a number or `1` for a boolean.
valid for its field's type is refused before the query reaches the database with `-32602 InvalidParams` and a
message naming the field (code that calls the business object or the repository directly catches
`InvalidRequestValueException`, an `ArgumentException`),
including text a database used to convert on its own, such as `""` for a number or `1` for a boolean.

The conversion applies to a string value on any codec; values of any other type are unchanged. A relation field
converts by the `DbType` it declares. See
Expand Down
8 changes: 4 additions & 4 deletions docs/zh-TW/api/jsonrpc-frontend-integration.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!-- source: en/api/jsonrpc-frontend-integration.md blob: a7b8981d3b31eb373455810d6aad115228ada388 -->
<!-- source: en/api/jsonrpc-frontend-integration.md blob: c2fc6e25c3c575d61b69d5d451256cb5d062dad5 -->
# JSON-RPC 前端整合指引

[English](../../en/api/jsonrpc-frontend-integration.md) · [← 文件索引](../README.md)
Expand Down Expand Up @@ -143,8 +143,8 @@ Encoded / Encrypted 的 body 是不透明的位元組。即使在那條路徑上
- `String`、`Text` 與 `Time` 欄位保留字串,`Like`、`Contains`、`StartsWith` 與 `EndsWith` 運算子也是,
因為它們比對的是文字。

relation 欄位依其宣告的 `DbType` 轉換,因此請宣告它所讀取欄位的型別。不符合欄位型別的字串會被拒絕,包括資料庫
過去會自行轉換的文字,例如數字欄位的 `""`、布林欄位的 `1`。`parameters` 的項目與其他 `object` 成員不會轉換;需要在那裡帶型別的值時,
relation 欄位依其宣告的 `DbType` 轉換,因此請宣告它所讀取欄位的型別。不符合欄位型別的字串會以 `-32602 InvalidParams`
拒絕,訊息會指出欄位,包括資料庫過去會自行轉換的文字,例如數字欄位的 `""`、布林欄位的 `1`。`parameters` 的項目與其他 `object` 成員不會轉換;需要在那裡帶型別的值時,
client 改用 JSON body codec(`"codec": "json"` 搭配 `Encoded` 或 `Encrypted`),它的 `[code, value]`
信封會帶上型別 —— `wire-contracts/messages.d.ts` 裡的 `WireValueEnvelope` 描述的正是這個信封;Plain body
帶的則是裸值。
Expand Down Expand Up @@ -262,7 +262,7 @@ XML 相同。定義在每條路徑上都以 XML 傳輸,因為它們的巢狀
| `-32700` | `ParseError` | request body 不是合法 JSON | 修 client 序列化 |
| `-32600` | `InvalidRequest` | 不是有效的 JSON-RPC 請求(缺 `method`、`jsonrpc` 不對)、金鑰被拒,或 `Authorization` header 格式錯誤 | 檢查 headers 與 body |
| `-32601` | `MethodNotFound` | 方法名稱不是 `progId.action` 格式,或沒有對應業務物件公開的 action;訊息固定,不會回帶名稱 | 檢查方法名稱 / 大小寫 |
| `-32602` | `InvalidParams` | 參數沒有可繫結的值(沒有 `params`、沒有 `value`,或 `value` 為 `null`);`Plain` 本文無法讀成方法所接受的型別;或 host 要求 wire frame 時,已登入 session 以 `Plain` 或 `Encoded` payload 呼叫[有重放防護](api-method-reference.md#重放防護)的方法。訊息固定為「Invalid params.」。讀得進來但內容不合法的參數回 `-32099` | 修正 `params` 的結構,或改以 `Encrypted` 呼叫該方法 |
| `-32602` | `InvalidParams` | 參數沒有可繫結的值(沒有 `params`、沒有 `value`,或 `value` 為 `null`);`Plain` 本文無法讀成方法所接受的型別;或 host 要求 wire frame 時,已登入 session 以 `Plain` 或 `Encoded` payload 呼叫[有重放防護](api-method-reference.md#重放防護)的方法;這幾種的訊息固定為「Invalid params.」。過濾值無法讀成其欄位的型別時也回這個代碼,訊息會指出欄位。其他讀得進來但內容不合法的參數回 `-32099` | 修正 `params` 的結構或訊息指出的過濾值,或改以 `Encrypted` 呼叫該方法 |
| `-32603` | `InternalError` | 未處理的 server 端例外(1.1.0 以前為 `-32000`)。驗證失敗的 `Encrypted` payload 也會落在這裡,所以 payload 版本與伺服端不相符的用戶端,每個 `Encrypted` 呼叫都會收到它 | 訊息不適合對使用者顯示。除非伺服端在 debug 模式,訊息一律為「Internal server error」。若所有 `Encrypted` 呼叫都這樣失敗、其他格式正常,請確認用戶端與伺服端的版本相符 |
| `-32001` | `Unauthorized` | 方法需要已登入的呼叫者,而 access token 缺漏、未知或已過期 | 重新登入 |
| `-32002` | `CompanyNotEntered` | 方法需要公司 context | 先呼叫 `System.EnterCompany` |
Expand Down
5 changes: 3 additions & 2 deletions docs/zh-TW/changelogs/1.13.0.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!-- source: en/changelogs/1.13.0.md blob: 3232fc27961d1e52080fc73119f7ad105ef11731 -->
<!-- source: en/changelogs/1.13.0.md blob: 8dec0d6cffc48cba150077700e00cd2f0a4f9e12 -->
# Polhem 1.13.0 — 發佈說明

[English](../../en/changelogs/1.13.0.md)
Expand Down Expand Up @@ -54,7 +54,8 @@ PostgreSQL 拒絕拿它與有型別的欄位比較(`operator does not exist: d
則把加了引號的小數轉成欄位本身的精度,`'10.5'` 對整數精度的欄位會以 11 比較,不報錯就查無資料。表單的清單、筆數與刪除,現在會在組查詢之前,把過濾條件中的字串值轉成所比對欄位的型別:
`DateTime` 欄位讀成 UTC 時間點(沒帶時差的文字視為 UTC),`Date` 欄位讀取寫出的日期,`Guid`、數值與 `Boolean`
欄位讀取與文化無關的文字形式;日期只接受 ISO 8601 形式。`String`、`Text` 與 `Time` 欄位以及 `LIKE` 系列運算子
保留字串。不符合欄位型別的字串,會在查詢送到資料庫之前被拒絕,包括資料庫過去會自行轉換的文字,例如數字欄位的
保留字串。不符合欄位型別的字串,會在查詢送到資料庫之前以 `-32602 InvalidParams` 拒絕,訊息會指出欄位(直接呼叫 business object 或
repository 的程式碼捕捉的是 `InvalidRequestValueException`,它是 `ArgumentException`),包括資料庫過去會自行轉換的文字,例如數字欄位的
`""`、布林欄位的 `1`。

這個轉換適用於任何 codec 送來的字串值;其他型別的值不受影響。relation 欄位依其宣告的 `DbType` 轉換。見
Expand Down
59 changes: 49 additions & 10 deletions src/Polhem.Api.Client/Definitions/FormDefinitionLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,20 @@ public async Task<FormLayout> GetRuntimeLayoutAsync(string progId, FormSchema lo
ArgumentNullException.ThrowIfNull(localizedSchema);

string effectiveLayoutId = string.IsNullOrWhiteSpace(layoutId) ? progId : layoutId;
var customize = await _defineAccess.GetCustomizeFormLayoutAsync(progId, effectiveLayoutId, cancellationToken).ConfigureAwait(false);
var @base = await _defineAccess.GetFormLayoutAsync(effectiveLayoutId, cancellationToken).ConfigureAwait(false);
FormLayout? customize, @base;
if (FetchConcurrently)
{
var customizeTask = _defineAccess.GetCustomizeFormLayoutAsync(progId, effectiveLayoutId, cancellationToken);
var baseTask = _defineAccess.GetFormLayoutAsync(effectiveLayoutId, cancellationToken);
await Task.WhenAll((Task)customizeTask, baseTask).ConfigureAwait(false);
customize = await customizeTask.ConfigureAwait(false);
@base = await baseTask.ConfigureAwait(false);
}
else
{
customize = await _defineAccess.GetCustomizeFormLayoutAsync(progId, effectiveLayoutId, cancellationToken).ConfigureAwait(false);
@base = await _defineAccess.GetFormLayoutAsync(effectiveLayoutId, cancellationToken).ConfigureAwait(false);
}

var definition = CustomizeOverlay.PickFormLayout(customize, @base)
?? throw new InvalidOperationException(
Expand Down Expand Up @@ -200,19 +212,46 @@ private async Task<SnapshotLanguageService> BuildLanguageServiceAsync(FormSchema
private async Task<Dictionary<string, LanguageLayers>> FetchLayersAsync(IEnumerable<string> namespaces,
string lang, string defaultLanguage, CancellationToken cancellationToken)
{
var snapshot = new Dictionary<string, LanguageLayers>(StringComparer.OrdinalIgnoreCase);
foreach (string language in LanguageFallback.GetChain(lang, defaultLanguage))
var pairs = LanguageFallback.GetChain(lang, defaultLanguage)
.SelectMany(language => namespaces.Select(ns => (Language: language, Namespace: ns)))
.ToList();
var layers = new LanguageLayers[pairs.Count];
if (FetchConcurrently)
{
foreach (string ns in namespaces)
{
var customize = await _defineAccess.GetCustomizeLanguageAsync(language, ns, cancellationToken).ConfigureAwait(false);
var @base = await SafeGetLanguageAsync(language, ns, cancellationToken).ConfigureAwait(false);
snapshot[SnapshotLanguageService.BuildKey(language, ns)] = new LanguageLayers(@base, customize);
}
await Task.WhenAll(pairs.Select(async (pair, i) =>
layers[i] = await FetchLayersAsync(pair.Language, pair.Namespace, cancellationToken).ConfigureAwait(false)))
.ConfigureAwait(false);
}
else
{
for (int i = 0; i < pairs.Count; i++)
layers[i] = await FetchLayersAsync(pairs[i].Language, pairs[i].Namespace, cancellationToken).ConfigureAwait(false);
}

var snapshot = new Dictionary<string, LanguageLayers>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < pairs.Count; i++)
snapshot[SnapshotLanguageService.BuildKey(pairs[i].Language, pairs[i].Namespace)] = layers[i];
return snapshot;
}

private async Task<LanguageLayers> FetchLayersAsync(string language, string ns, CancellationToken cancellationToken)
{
var customize = await _defineAccess.GetCustomizeLanguageAsync(language, ns, cancellationToken).ConfigureAwait(false);
var @base = await SafeGetLanguageAsync(language, ns, cancellationToken).ConfigureAwait(false);
return new LanguageLayers(@base, customize);
}

/// <summary>
/// Gets a value indicating whether the definitions a form needs are fetched at once rather than one after
/// another.
/// </summary>
/// <remarks>
/// Only a remote client gains: each fetch is a round trip, and on a cold cache a form needs one per layer,
/// namespace and culture of the fall-back chain. An in-process client has no round trip to save, and its calls
/// run on the caller's services, which are not meant to be used by several calls at once.
/// </remarks>
private bool FetchConcurrently => !_defineAccess.Client.IsLocal;

private static HashSet<string> CollectNamespaces(FormSchema schema)
{
var namespaces = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
Expand Down
Loading
Loading