Summary
When a delete is refused by referential integrity, the engine throws a 409 DELETE_RESTRICTED
whose message is surfaced verbatim to end users by Console's toast. That message is
English-only, embeds machine object/field names, and hands the user a developer instruction
(set deleteBehavior:'cascade' on ...) — none of which is actionable or comprehensible for a
business user of a localized app.
The error transport itself is fine (status is set, so the message is not swallowed into a
generic 500). This is purely about the user-facing copy.
Where
packages/objectql — cascadeDeleteRelations, the behavior === "restrict" branch
(observed in @objectstack/objectql@17.0.0-rc.5, dist/core.js):
const reason = fdef.deleteBehavior !== "restrict" && fdef.required === true
? ` (${fieldName} is required, so it cannot be cleared)` : "";
const err = new Error(
`Cannot delete ${object} (${id}): ${dependents.length} dependent ${childName} record(s) ` +
`reference it via ${fieldName}${reason}. Delete or reassign them first, ` +
`or set deleteBehavior:'cascade' on ${childName}.${fieldName}.`
);
err.code = "DELETE_RESTRICTED";
err.status = 409;
err.object = object;
err.dependentObject = childName;
err.dependentCount = dependents.length;
Actual behaviour
Deleting a sys_business_unit referenced by a required lookup produces this red toast in
Console, shown as-is to the operator in an otherwise fully Chinese UI:
Cannot delete sys_business_unit (<id>): 1 dependent os_tianshun_ehr_sporadic_application
record(s) reference it via apply_dept (apply_dept is required, so it cannot be cleared).
Delete or reassign them first, or set deleteBehavior:'cascade' on
os_tianshun_ehr_sporadic_application.apply_dept.
Three separate problems for the end user:
- Not localized — English string in a zh-CN app; there is no i18n key to override.
- Machine names leak —
os_tianshun_ehr_sporadic_application, apply_dept. The user
knows these as 「零星申请」and「申报部门」; the raw API names mean nothing to them.
- Developer advice shown to end users —
set deleteBehavior:'cascade' on ... is a
metadata-authoring instruction. A business user cannot act on it, and it invites
support tickets asking where that setting lives.
Expected
A business user should get something like:「该 <对象label> 正被 1 条 <子对象label> 通过
「<字段label>」引用,且该字段为必填,无法清空。请先删除或改派这些记录。」 — i.e. localized,
using labels rather than API names, and without the metadata-authoring hint.
The structured fields (code, object, dependentObject, dependentCount) are already
present and are the right channel for developers/logs — the developer guidance belongs there
(or in a separate detail/devHint property), not in the user-visible message.
Suggested direction
- Emit
message through the i18n layer with an overridable key (e.g.
errors.deleteRestricted), interpolating resolved labels for object/field where
available and falling back to API names only when no label exists;
- Move
Delete or reassign them first, or set deleteBehavior:'cascade' on ... out of
message into a developer-only field (or drop the deleteBehavior half of it entirely —
the "delete or reassign them first" half is useful to end users);
- Keep
status: 409 and the structured fields exactly as they are — that part works well.
Repro
- Object A with a lookup field on object B where
required: true (so set_null is coerced to restrict);
- Create one B record and one A record referencing it;
- Delete the B record from Console (record page, drawer, list row action, or bulk bar — all four surface the same toast);
- Observe the raw English message in the toast.
Found while building an app on 17.0.0-rc.5; reported per "platform issues go to the platform repo".
Summary
When a delete is refused by referential integrity, the engine throws a
409 DELETE_RESTRICTEDwhose
messageis surfaced verbatim to end users by Console's toast. That message isEnglish-only, embeds machine object/field names, and hands the user a developer instruction
(
set deleteBehavior:'cascade' on ...) — none of which is actionable or comprehensible for abusiness user of a localized app.
The error transport itself is fine (
statusis set, so the message is not swallowed into ageneric 500). This is purely about the user-facing copy.
Where
packages/objectql—cascadeDeleteRelations, thebehavior === "restrict"branch(observed in
@objectstack/objectql@17.0.0-rc.5,dist/core.js):Actual behaviour
Deleting a
sys_business_unitreferenced by a required lookup produces this red toast inConsole, shown as-is to the operator in an otherwise fully Chinese UI:
Three separate problems for the end user:
os_tianshun_ehr_sporadic_application,apply_dept. The userknows these as 「零星申请」and「申报部门」; the raw API names mean nothing to them.
set deleteBehavior:'cascade' on ...is ametadata-authoring instruction. A business user cannot act on it, and it invites
support tickets asking where that setting lives.
Expected
A business user should get something like:「该 <对象label> 正被 1 条 <子对象label> 通过
「<字段label>」引用,且该字段为必填,无法清空。请先删除或改派这些记录。」 — i.e. localized,
using labels rather than API names, and without the metadata-authoring hint.
The structured fields (
code,object,dependentObject,dependentCount) are alreadypresent and are the right channel for developers/logs — the developer guidance belongs there
(or in a separate
detail/devHintproperty), not in the user-visiblemessage.Suggested direction
messagethrough the i18n layer with an overridable key (e.g.errors.deleteRestricted), interpolating resolved labels for object/field whereavailable and falling back to API names only when no label exists;
Delete or reassign them first, or set deleteBehavior:'cascade' on ...out ofmessageinto a developer-only field (or drop thedeleteBehaviorhalf of it entirely —the "delete or reassign them first" half is useful to end users);
status: 409and the structured fields exactly as they are — that part works well.Repro
required: true(soset_nullis coerced torestrict);Found while building an app on 17.0.0-rc.5; reported per "platform issues go to the platform repo".