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
12 changes: 12 additions & 0 deletions .changeset/25071-composite-title-stored.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@objectstack/lint': patch
---

fix(lint): the record-title diagnostics no longer steer a composite title that must be queried into a formula field

Clause-②: no (wording only: no verdict, severity, code, key or export moves)
Metadata-protocol: none

The `title-format-retired` and `title-unresolvable` messages and fix lines, the `object/missing-name-field` fix line, and the `os explain title-format-retired` text now say the same thing: a composite record title that an import, a sort or a search must match is a stored computed field, a non-formula field (such as a `text` field) that declares an `expression`, which the engine computes and stores on every write, designated as `nameField`. A `formula` field with `returnType: 'text'` stays a legal `nameField` for a title shown for display only: its value is computed on read and stored nowhere, so a sort by it is refused, search never reaches it, and an import cannot match a lookup cell against it.

No finding fires on a different object than before, and no severity changes. Only the text an author reads changes.
2 changes: 1 addition & 1 deletion content/docs/data-modeling/formulas.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
export const Invoice = ObjectSchema.create({
name: 'invoice',
sharingModel: 'private',
nameField: 'display_title', // ADR-0079 — the record title is a designated field; composite titles migrate off the deprecated `titleFormat` to a text formula
nameField: 'display_title', // ADR-0079 — the record title is a designated field; a composite title migrates off the deprecated `titleFormat` to a text formula for display only, or to a stored computed field (a non-formula field with an `expression`, written on write) when import, sort or search must match it
fields: {
// Composite record title as a text formula, surfaced via `nameField` above.
display_title: Field.formula({
Expand Down
8 changes: 6 additions & 2 deletions docs/adr/0079-record-display-name.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,12 @@ the diagnostic is advisory, not an error:
> consumed by the release)

**Migration is stated, not left to the author**: a single-field title becomes
`nameField: '<field>'`; a **composite** title becomes a `formula` field with
`returnType: 'text'`, designated as the `nameField`
`nameField: '<field>'`; a **composite** title that must be queried (matched by
an import, sorted, searched) becomes a stored computed field (a non-formula
field with an `expression`, written on write), designated as the `nameField`,
the remedy addendum item 9 states for sort and search and ruling record
6308445871 extends to import; a `formula` field with `returnType: 'text'`
designated as the `nameField` stays legal for a title shown for display only
(`packages/lint/src/validate-record-title.ts`; the worked example is
`content/docs/data-modeling/formulas.mdx`).

Expand Down
4 changes: 2 additions & 2 deletions packages/lint/src/data-model-rules.fix-lines.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ describe('[#22161] data-model-rules.ts — one remedy line per fix', () => {
it('object/missing-name-field: the pointer, and where the titleFormat reasoning lives', () => {
const fix = fixOf(record(lintDataModel([{ name: LONG_OBJECT, fields: { amount: { type: 'number' } } }])), MISSING_NAME_FIELD);
expect(fix).toBe(
"Set `nameField: '<field>'` (ADR-0079's title pointer) to a stored text/autonumber field or a " +
"`returnType: 'text'` formula; `titleFormat` does not count (`os explain title-format-retired`).",
"Set `nameField` (ADR-0079), never `titleFormat` (`os explain title-format-retired`); a composite title " +
"to import/sort/search by is a text field with an `expression` (a text formula is display-only).",
);
// The command the line names answers, and says why `titleFormat` does not count.
expect(explainRule('title-format-retired')!.paragraphs.join('\n')).toContain('render-only');
Expand Down
4 changes: 2 additions & 2 deletions packages/lint/src/data-model-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -738,8 +738,8 @@ export function lintDataModel(objects: any[]): LintIssue[] {
// [#22161] One remedy line; why `titleFormat` does not count is
// `title-format-retired`'s `os explain` text.
fix:
"Set `nameField: '<field>'` (ADR-0079's title pointer) to a stored text/autonumber field or a " +
"`returnType: 'text'` formula; `titleFormat` does not count (`os explain title-format-retired`).",
"Set `nameField` (ADR-0079), never `titleFormat` (`os explain title-format-retired`); a composite title " +
"to import/sort/search by is a text field with an `expression` (a text formula is display-only).",
});
}

Expand Down
10 changes: 7 additions & 3 deletions packages/lint/src/rule-explanations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2644,9 +2644,13 @@ const TITLE_FORMAT_RETIRED_EXPLANATION: RuleExplanation = {
'alias), or one derived from the object\'s fields. `titleFormat` is a render-only template: the ' +
'server can neither return the title it draws nor query by it, the shared title predicate ' +
'(`objectTitleCompleteness`) never reads it, and an explicit `nameField` takes precedence over it.',
'For a single-field title, set `nameField` to that field. For a composite title, add a formula ' +
'field with `returnType: \'text\'` and point `nameField` at it: a formula whose result is text is ' +
'title-eligible. Then drop `titleFormat`, which this warning reports for as long as it is declared.',
'For a single-field title, set `nameField` to that field. A composite title that must be queried ' +
'(matched by an import, sorted, searched) is a stored computed field: a non-formula field, such as ' +
'a `text` field, that declares an `expression`, which the engine computes and stores on every ' +
'write; point `nameField` at it. A formula with `returnType: \'text\'` is title-eligible too and ' +
'stays legal for a title shown for display only: its value is computed on read and stored nowhere, ' +
'so a sort by it is refused, search never reaches it, and an import cannot match a lookup cell ' +
'against it. Then drop `titleFormat`, which this warning reports for as long as it is declared.',
'A warning, not an error: the schema still parses the key, so existing metadata keeps loading.',
],
};
Expand Down
8 changes: 4 additions & 4 deletions packages/lint/src/validate-record-title.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,12 @@ describe('[#22161] one-line fixes — title-format-retired and title-unresolvabl
const findings = validateRecordTitle(objStack({ titleFormat: '{{record.amount}}', fields: { amount: { type: 'currency' } } }));
const byRule = new Map(findings.map((f) => [f.rule, f.hint]));
expect(byRule.get(TITLE_FORMAT_RETIRED)).toBe(
"Set nameField: '<field>' for a single-field title, or add a formula field (returnType: 'text') and " +
'designate it via nameField; then drop titleFormat',
"Set nameField: '<field>' for one field; a composite title to import, sort or search by is a text " +
'field with an expression, stored on write (a text formula is display-only); drop titleFormat',
);
expect(byRule.get(TITLE_UNRESOLVABLE)).toBe(
"Set nameField to a text/email field (or a formula field with returnType: 'text'), or add a text " +
'field named "name"/"title"',
'Set nameField to a text/email field (a composite title to import, sort or search by: a text field ' +
'with an expression; a text formula is display-only), or add a text field named "name"/"title"',
);
});

Expand Down
11 changes: 6 additions & 5 deletions packages/lint/src/validate-record-title.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,13 @@ export function validateRecordTitle(stack: AnyRec): RecordTitleFinding[] {
path,
message:
`${objName}: titleFormat is retired (ADR-0079) — migrate to nameField ` +
`(single field) or a formula field designated nameField`,
`(single field); a composite title that import, sort or search must match is a stored ` +
`field with an expression designated nameField (a formula title is for display only)`,
// [#22161] One remedy line; why the template is retired (render-only,
// neither returned nor queryable) is `os explain title-format-retired`.
hint:
`Set nameField: '<field>' for a single-field title, or add a formula field (returnType: 'text') ` +
`and designate it via nameField; then drop titleFormat`,
`Set nameField: '<field>' for one field; a composite title to import, sort or search by is a ` +
`text field with an expression, stored on write (a text formula is display-only); drop titleFormat`,
});
}

Expand All @@ -104,8 +105,8 @@ export function validateRecordTitle(stack: AnyRec): RecordTitleFinding[] {
// [#22161] One remedy line; the runtime's fallback title, and why this
// is only a warning, are `os explain title-unresolvable`.
hint:
`Set nameField to a text/email field (or a formula field with returnType: 'text'), or add a ` +
`text field named "name"/"title"`,
`Set nameField to a text/email field (a composite title to import, sort or search by: a text ` +
`field with an expression; a text formula is display-only), or add a text field named "name"/"title"`,
});
}
}
Expand Down
Loading