Skip to content

Commit 5e54169

Browse files
committed
fix(lint): the stored-metadata reference refusal names a relationship field, and pins the user case its collector admits
A user field's authored reference is not refused at parse, so the hop collector already admits a user naming the family; the collector's docblock said otherwise. Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
1 parent a5fd527 commit 5e54169

3 files changed

Lines changed: 20 additions & 6 deletions

File tree

‎.changeset/24677-reference-into-stored-metadata-body-refused.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ A `lookup` or `master_detail` whose `reference` names a table of the stored-meta
1515

1616
- FROM: `meta: { type: 'lookup', reference: 'sys_metadata' }` (or a `master_detail`, or `sys_metadata_history`) saved with a 200 and built with exit 0, and then only ever showed an id. TO: refused, with `422 INVALID_METADATA` at the save door and an error at `os validate` / `os build`. The message names the field, its type and the target, and ends on the family's one prescription.
1717
- A reference to any other table, platform tables such as `sys_user` included, is unchanged.
18+
- A `user` field whose authored `reference` names a family table is refused the same way: the parse accepts an authored `reference` on a `user` field, and it is the target every reader resolves.
1819
- The refusal of a formula or a validation rule that reads through such a field is unchanged, and is still reported at that expression.
1920
- A stored object row that already declares such a field keeps loading: no authoring rule runs when a stored row is rehydrated, and the engine guards keep refusing every relationship read into the family. Its next active save, or a draft's promotion, through the metadata API is refused until the field is removed or retargeted. The engine guards stay, for every path that runs no authoring rule.
2021
- `@objectstack/spec` (`patch`): the `reference` describe states the convention, so the generated field reference says it too.

‎packages/lint/src/validate-expressions.stored-metadata-body-reference.test.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,15 @@ describe('[#24677] a lookup or master_detail into a stored-metadata-body table i
8989
}
9090
}
9191

92+
it('a `user` field whose authored `reference` names the family is refused too — the collector\'s type set, not a second one', () => {
93+
// `user`'s target is implied (`sys_user`), but an authored `reference` on it
94+
// is not refused at parse and is the target every reader resolves, so the
95+
// hop refusal's collector admits it, and this refusal judges the same set.
96+
const [issue] = atReference(validateStackExpressions({ objects: [carrier('user', 'sys_metadata')] }));
97+
expect(issue?.message).toContain('`meta` is a `user`');
98+
expect(atReference(validateStackExpressions({ objects: [carrier('user', 'sys_user')] }))).toEqual([]);
99+
});
100+
92101
it('a master_detail refusal says its `parent` root stays unbound; a lookup refusal does not', () => {
93102
const [md] = atReference(validateStackExpressions({ objects: [carrier('master_detail', 'sys_metadata')] }));
94103
const [lk] = atReference(validateStackExpressions({ objects: [carrier('lookup', 'sys_metadata')] }));

‎packages/lint/src/validate-expressions.ts‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1735,9 +1735,12 @@ function singleMasterOf(obj: AnyRec): string | undefined {
17351735
* serve a hop, so the lint refuses it where it is written.
17361736
*
17371737
* The literal `.reference` read stays in the argument for the #5017 receiver
1738-
* scan, exactly as in {@link masterDetailCount}. A type whose target is implied
1739-
* (`user`, `tree`) never names the family, so the authored carrier is the only
1740-
* one read.
1738+
* scan, exactly as in {@link masterDetailCount}. Only the authored carrier is
1739+
* read: a type whose target is implied never implies the family. A `tree`
1740+
* field's authored `reference` cannot name it (the object schema refuses any
1741+
* target but the declaring object); a `user` field's authored `reference` is
1742+
* not refused at parse, so a `user` naming the family is collected too
1743+
* (#24677 measured it).
17411744
*/
17421745
function storedMetadataBodyReferencesOf(obj: AnyRec): Map<string, string> {
17431746
const out = new Map<string, string>();
@@ -1774,8 +1777,9 @@ function storedMetadataBodyHopMessage(
17741777

17751778
/**
17761779
* [#24677] The refusal of the declaration every hop above presupposes: a
1777-
* `lookup` or `master_detail` field whose `reference` is a stored-metadata-body
1778-
* table ({@link storedMetadataBodyReferencesOf}). The engine never hydrates
1780+
* `lookup` or `master_detail` field (or any other type the collector admits)
1781+
* whose `reference` is a stored-metadata-body table
1782+
* ({@link storedMetadataBodyReferencesOf}). The engine never hydrates
17791783
* such a field — `expand` keeps its stored id, and a master-detail `parent`
17801784
* root over a family header stays unbound — so the field saves and never
17811785
* works. It names the field, its type, the target and the rule, and ends on
@@ -1792,7 +1796,7 @@ function storedMetadataBodyReferenceMessage(field: string, type: string, target:
17921796
+ 'unbound, so every write a `parent` rule judges is refused'
17931797
: 'and a formula or a validation rule never reads through the field';
17941798
return `\`${field}\` is a \`${type}\` whose \`reference\` names '${target}', a table of stored metadata. `
1795-
+ 'A `lookup` or `master_detail` field may not target a stored-metadata-body table: the engine never '
1799+
+ 'A relationship field may not target a stored-metadata-body table: the engine never '
17961800
+ `hydrates one, so \`$expand\` keeps the stored id and never nests the row, ${consequence}. `
17971801
+ 'Those rows carry stored metadata bodies, served only '
17981802
+ 'through the metadata API (`GET /api/v1/meta/:type/:name`), where the body is projected; to point a '

0 commit comments

Comments
 (0)