diff --git a/.changeset/lint-delete-behavior-suggestion-drops-set-null.md b/.changeset/lint-delete-behavior-suggestion-drops-set-null.md new file mode 100644 index 0000000000..749ea7d1c3 --- /dev/null +++ b/.changeset/lint-delete-behavior-suggestion-drops-set-null.md @@ -0,0 +1,23 @@ +--- +"@objectstack/lint": patch +--- + +fix(lint): `relationship/delete-behavior` suggestion no longer names `set_null` as declarable on a `master_detail` + +`lintDataModel`'s `relationship/delete-behavior` suggestion told an author an +undeclared `master_detail.deleteBehavior` could be `cascade`, `restrict`, or +`set_null`. Since #9689 (PR #11406, maintainer ruling 2026-08-19), an authored +`deleteBehavior: 'set_null'` on a `master_detail` field is a named parse-time +rejection — a detail row cannot outlive its master, so the engine resolves +every value except `restrict` to `cascade` on this type. Following the +suggestion's own `set_null` mention literally walked an author into that +rejection at publish time. + +The message now enumerates only the two values `FieldSchema` actually accepts +on a `master_detail` (`cascade`/`restrict` — matching the vocabulary already +offered by the metadata-admin field form, `object.form.ts`'s `master_detail` +`deleteBehavior` options), and keeps the same outcome-naming courtesy as the +parse-time rejection message: it still names `set_null` to say plainly that it +is not honored on this type, and points to `lookup` for the case where +children must survive the parent. The `fix` payload (`deleteBehavior: +'cascade'`) was already correct and is unchanged. diff --git a/packages/cli/test/data-model-rules.test.ts b/packages/cli/test/data-model-rules.test.ts index c78d29795b..c6019a3610 100644 --- a/packages/cli/test/data-model-rules.test.ts +++ b/packages/cli/test/data-model-rules.test.ts @@ -40,6 +40,14 @@ describe('lintDataModel — relationships', () => { ]); const db = issues.find((i) => i.rule === 'relationship/delete-behavior'); expect(db?.severity).toBe('suggestion'); + // #9689 (PR #11406) made an authored `deleteBehavior: 'set_null'` on a + // master_detail a named parse-time rejection — the DECLARABLE enumeration + // this suggestion names must not walk an author into that rejection. The + // message may still MENTION `set_null` to explain why it is excluded (the + // same outcome-naming courtesy as the field.zod.ts rejection message), so + // assert the declarable set directly rather than a bare substring-absence. + expect(db?.message).not.toContain('cascade/restrict/set_null'); + expect(db?.message).toContain('(cascade/restrict'); }); it('suggests inlineEdit on master_detail line-item children', () => { diff --git a/packages/lint/src/data-model-rules.ts b/packages/lint/src/data-model-rules.ts index f71636000c..4b3c419928 100644 --- a/packages/lint/src/data-model-rules.ts +++ b/packages/lint/src/data-model-rules.ts @@ -604,7 +604,7 @@ export function lintDataModel(objects: any[]): LintIssue[] { issues.push({ severity: 'suggestion', rule: 'relationship/delete-behavior', - message: `master_detail "${obj.name}.${fieldName}" → ${parent} should declare deleteBehavior (cascade/restrict/set_null)`, + message: `master_detail "${obj.name}.${fieldName}" → ${parent} should declare deleteBehavior (cascade/restrict — set_null is not honored on master_detail; use a lookup field if children must survive the parent)`, path: `${fieldPath}.deleteBehavior`, fix: "deleteBehavior: 'cascade'", });