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
27 changes: 27 additions & 0 deletions .changeset/20400-datasource-waiver-missing-only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@objectstack/lint': patch
---

`component-props-invalid` no longer hides a wrong `object` prop on a component that also carries a `dataSource` binding

Clause-②: no

A page component whose `dataSource.object` names the object may leave the flat
`properties.object` shorthand out: the binding supplies it, so the rule does not
report the props schema's required `object` as missing. That waiver was matched
on the issue's path alone, so it also swallowed every other issue the props
schema raised at `object`. A present but wrong value, such as `object: 7` or
`object: null`, was reported without a binding and silently passed with one.

The waiver now covers what its contract says: a missing `object`, meaning no
key or an explicit `undefined`. A value the author did write is judged as
written, and it is reported at `properties.object` exactly as it is on the same
component without a binding.

Effect on `os validate`, `os lint` and `os build`: a document that sets both a
`dataSource` binding and a wrong-typed `properties.object` now gets one
`component-props-invalid` warning it did not get before. The rule stays
advisory: without `--strict` nothing that validated before is refused; under
`os validate --strict` or `os lint --strict` the new warning fails the run, as
every warning does. A component that binds
through `dataSource` and omits `properties.object` is still clean.
52 changes: 52 additions & 0 deletions packages/lint/src/validate-component-props.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,58 @@ describe('validateComponentProps — value verdicts', () => {
]);
});

/**
* The waiver above covers a MISSING `object` only — no key, or `undefined`.
* A present value the row rejects is the author's own, and the binding
* supplies nothing in its place, so the row's verdict on it must reach the
* author exactly as it does with no binding at all. Each case is judged
* twice, beside the binding and without it, and the two answers must be the
* same finding: equality rather than wording, so the pin measures that the
* waiver lets the row's issue through unchanged.
*/
it.each([
['a number', 7],
['null', null],
])('reports a present-but-wrong `object` (%s) beside a `dataSource` binding, as it does without one', (_label, value) => {
const component = { type: 'element:number', properties: { object: value, aggregate: 'count' } };
const withBinding = validateComponentProps(
stackWith([{ ...component, dataSource: { object: 'contact' } }]),
);
const without = validateComponentProps(stackWith([component]));

// Control first: with nothing supplying `object`, the row reports it.
expect(without.map((f) => [f.rule, f.path])).toEqual([
[COMPONENT_PROPS_INVALID, 'pages[0].regions[0].components[0].properties.object'],
]);
// The binding does not silence it.
expect(withBinding.map((f) => [f.rule, f.path])).toEqual([
[COMPONENT_PROPS_INVALID, 'pages[0].regions[0].components[0].properties.object'],
]);
expect(withBinding).toEqual(without);
});

it('still waives `object: undefined` beside a binding — an explicit undefined is a missing value', () => {
const findings = validateComponentProps(
stackWith([
{
type: 'element:number',
dataSource: { object: 'contact' },
properties: { object: undefined, aggregate: 'count' },
},
]),
);
expect(findings).toEqual([]);

// …and the same bag with no binding is reported, so the silence above is
// the waiver and not the row accepting `undefined`.
const without = validateComponentProps(
stackWith([{ type: 'element:number', properties: { object: undefined, aggregate: 'count' } }]),
);
expect(invalid(without).map((f) => f.path)).toEqual([
'pages[0].regions[0].components[0].properties.object',
]);
});

/**
* #5775 — the retirement's author-facing channel. `displayField` was the
* picker's REQUIRED prop and no renderer ever read it, so the tombstone's
Expand Down
8 changes: 7 additions & 1 deletion packages/lint/src/validate-component-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,13 @@ const DATASOURCE_SUPPLIED_PROP = 'object';
function suppliedByDataSource(issue: LintZodIssue, component: AnyRec): boolean {
if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
const dataSource = isRec(component.dataSource) ? component.dataSource : undefined;
return strName(dataSource?.object) !== undefined;
if (strName(dataSource?.object) === undefined) return false;
// "Missing" is read off the component, never off the issue: the path alone
// also matches a PRESENT value the row rejects (`object: 7`, `object: null`),
// and the binding supplies nothing there — the author wrote that value and
// the row's own verdict on it stands. Only no key, or `undefined`, is waived.
const props = isRec(component.properties) ? component.properties : undefined;
return props?.[DATASOURCE_SUPPLIED_PROP] === undefined;
}

/**
Expand Down
Loading