Found while measuring whether crm_product.tax_rate could be stamped onto line items (#1198). Out of that card's scope — it is platform behaviour, not this app's metadata — but it changes what a typo costs in every hook, action and flow in this repo, so it is worth measuring properly.
The measurement
Shipped stack, memory driver, @objectstack/* 17.0.0. crm_opportunity_line_item declares no tax_rate (its twin crm_quote_line_item does):
LEG 4 — crm_opportunity_line_item declares tax_rate? false
insert with tax_rate accepted; stored value = 10
stored keys: id, crm_opportunity, crm_product, quantity, tax_rate, discount,
created_at, updated_at, list_price, unit_price, total_price
The insert succeeded, the undeclared key was written, and a subsequent read handed it back. No error, no warning, no drop.
Why it matters here
Every hook and action in this app assigns to input.<field> by name, and flows write fields: { … } maps keyed by name. Under this behaviour a misspelled key — input.close_data, billing_frequncy — is a silent successful write to a column nobody declared. Nothing fails: not the write, not objectstack validate (which reads metadata, not rows), not the schema, not a later read of the correctly-spelled field, which simply stays at its old value. The record grows a shadow key that no view, formula, index or permission knows about, and the only symptom is a field that never seems to update.
This also removes one guardrail the repo currently reasons as if it had. test/hook-write-shape.test.ts pins the shape hooks write; it cannot pin the shape they could write. And the field-level permission surface is declared per object — a key the object does not declare cannot be governed by it, so an undeclared write also sits outside allowEdit / field-level security by construction.
What is not yet known
- whether this is memory-driver-specific or holds on
driver-sql / driver-sqlite-wasm (a relational driver has a fixed column set, so it may reject or silently drop instead — which would make the behaviour differ by driver, arguably worse than either); - whether some
strict-style option exists on the object or the datasource to refuse unknown keys; - whether this is intended (schemaless-by-design) or a gap. ADR-0104 D3 wave 2 moved
accept/maxSize from browser-only to server-enforced on the grounds that a constraint that exists only client-side is not a constraint — the same argument applies to a field list.
Answering the first two is cheap; the third is an upstream question for objectstack-ai/objectstack.
Reproduce
insert('crm_opportunity_line_item', {
crm_opportunity: <id>, crm_product: <id>, quantity: 1, tax_rate: 10,
})
against a kernel booted with DefaultDatasourcePlugin({ driver: 'memory' }) + AppPlugin(stack, undefined, { skipSeedData: true }), then read the row back. test/decorative-field-sweep.test.ts has the boot boilerplate.
Filed unassigned; no work started.
Found while measuring whether
crm_product.tax_ratecould be stamped onto line items (#1198). Out of that card's scope — it is platform behaviour, not this app's metadata — but it changes what a typo costs in every hook, action and flow in this repo, so it is worth measuring properly.The measurement
Shipped stack, memory driver,
@objectstack/*17.0.0.crm_opportunity_line_itemdeclares notax_rate(its twincrm_quote_line_itemdoes):The insert succeeded, the undeclared key was written, and a subsequent read handed it back. No error, no warning, no drop.
Why it matters here
Every hook and action in this app assigns to
input.<field>by name, and flows writefields: { … }maps keyed by name. Under this behaviour a misspelled key —input.close_data,billing_frequncy— is a silent successful write to a column nobody declared. Nothing fails: not the write, notobjectstack validate(which reads metadata, not rows), not the schema, not a later read of the correctly-spelled field, which simply stays at its old value. The record grows a shadow key that no view, formula, index or permission knows about, and the only symptom is a field that never seems to update.This also removes one guardrail the repo currently reasons as if it had.
test/hook-write-shape.test.tspins the shape hooks write; it cannot pin the shape they could write. And the field-level permission surface is declared per object — a key the object does not declare cannot be governed by it, so an undeclared write also sits outsideallowEdit/ field-level security by construction.What is not yet known
driver-sql/driver-sqlite-wasm(a relational driver has a fixed column set, so it may reject or silently drop instead — which would make the behaviour differ by driver, arguably worse than either);strict-style option exists on the object or the datasource to refuse unknown keys;accept/maxSizefrom browser-only to server-enforced on the grounds that a constraint that exists only client-side is not a constraint — the same argument applies to a field list.Answering the first two is cheap; the third is an upstream question for
objectstack-ai/objectstack.Reproduce
against a kernel booted with
DefaultDatasourcePlugin({ driver: 'memory' })+AppPlugin(stack, undefined, { skipSeedData: true }), then read the row back.test/decorative-field-sweep.test.tshas the boot boilerplate.Filed unassigned; no work started.