Make practice title optional on create.

POST /practices and exam-prep practice create accept missing or null title; persist as empty string. Refresh OpenAPI and document the behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Yared Yemane 2026-05-20 04:11:09 -07:00
parent bd1767d2a6
commit 71bc09a638
8 changed files with 187 additions and 26 deletions

View File

@ -415,6 +415,8 @@ This creates the practice record scoped to lesson.
### Request ### Request
`title` is optional; omit it or use an empty string to create a practice without a display title (stored as empty).
Include `publish_status`: `DRAFT` to hide the practice from subscribed learners until you set it to `PUBLISHED` (via create or `PUT /practices/:id`). Omit the field or send `PUBLISHED` to go live immediately (backward compatible). Include `publish_status`: `DRAFT` to hide the practice from subscribed learners until you set it to `PUBLISHED` (via create or `PUT /practices/:id`). Omit the field or send `PUBLISHED` to go live immediately (backward compatible).
```json ```json

View File

@ -10434,13 +10434,21 @@ const docTemplate = `{
"domain.CreateExamPrepPracticeInput": { "domain.CreateExamPrepPracticeInput": {
"type": "object", "type": "object",
"required": [ "required": [
"question_set_id", "question_set_id"
"title"
], ],
"properties": { "properties": {
"persona_id": { "persona_id": {
"type": "integer" "type": "integer"
}, },
"publish_status": {
"type": "string",
"enum": [
"DRAFT",
"draft",
"PUBLISHED",
"published"
]
},
"question_set_id": { "question_set_id": {
"type": "integer" "type": "integer"
}, },
@ -10485,11 +10493,19 @@ const docTemplate = `{
"type": "string" "type": "string"
}, },
"publish_status": { "publish_status": {
"description": "Omit or DRAFT (default) for drafts; PUBLISHED for learner-visible lessons.", "description": "Omit or empty defaults to DRAFT; set PUBLISHED to make visible to learners immediately.",
"type": "string" "type": "string",
"enum": [
"DRAFT",
"draft",
"PUBLISHED",
"published"
]
}, },
"sort_order": { "sort_order": {
"type": "integer" "description": "SortOrder within the module when set; omit to append after current max within module_id.",
"type": "integer",
"minimum": 0
}, },
"thumbnail": { "thumbnail": {
"type": "string" "type": "string"
@ -10516,6 +10532,11 @@ const docTemplate = `{
}, },
"name": { "name": {
"type": "string" "type": "string"
},
"sort_order": {
"description": "SortOrder within the course when set; omit to append after current max within course_id (uniqueness is per-course).",
"type": "integer",
"minimum": 0
} }
} }
}, },
@ -10524,8 +10545,7 @@ const docTemplate = `{
"required": [ "required": [
"parent_id", "parent_id",
"parent_kind", "parent_kind",
"question_set_id", "question_set_id"
"title"
], ],
"properties": { "properties": {
"parent_id": { "parent_id": {
@ -10546,6 +10566,16 @@ const docTemplate = `{
"persona_id": { "persona_id": {
"type": "integer" "type": "integer"
}, },
"publish_status": {
"description": "Omit or empty for backward compatibility defaults to PUBLISHED; set DRAFT to save hidden from learners until published.",
"type": "string",
"enum": [
"DRAFT",
"draft",
"PUBLISHED",
"published"
]
},
"question_set_id": { "question_set_id": {
"type": "integer" "type": "integer"
}, },
@ -11327,6 +11357,15 @@ const docTemplate = `{
"persona_id": { "persona_id": {
"type": "integer" "type": "integer"
}, },
"publish_status": {
"type": "string",
"enum": [
"DRAFT",
"draft",
"PUBLISHED",
"published"
]
},
"question_set_id": { "question_set_id": {
"type": "integer" "type": "integer"
}, },
@ -11380,8 +11419,13 @@ const docTemplate = `{
"type": "string" "type": "string"
}, },
"publish_status": { "publish_status": {
"description": "DRAFT or PUBLISHED", "type": "string",
"type": "string" "enum": [
"DRAFT",
"draft",
"PUBLISHED",
"published"
]
}, },
"sort_order": { "sort_order": {
"type": "integer" "type": "integer"
@ -11420,6 +11464,15 @@ const docTemplate = `{
"persona_id": { "persona_id": {
"type": "integer" "type": "integer"
}, },
"publish_status": {
"type": "string",
"enum": [
"DRAFT",
"draft",
"PUBLISHED",
"published"
]
},
"question_set_id": { "question_set_id": {
"type": "integer" "type": "integer"
}, },

View File

@ -10426,13 +10426,21 @@
"domain.CreateExamPrepPracticeInput": { "domain.CreateExamPrepPracticeInput": {
"type": "object", "type": "object",
"required": [ "required": [
"question_set_id", "question_set_id"
"title"
], ],
"properties": { "properties": {
"persona_id": { "persona_id": {
"type": "integer" "type": "integer"
}, },
"publish_status": {
"type": "string",
"enum": [
"DRAFT",
"draft",
"PUBLISHED",
"published"
]
},
"question_set_id": { "question_set_id": {
"type": "integer" "type": "integer"
}, },
@ -10477,11 +10485,19 @@
"type": "string" "type": "string"
}, },
"publish_status": { "publish_status": {
"description": "Omit or DRAFT (default) for drafts; PUBLISHED for learner-visible lessons.", "description": "Omit or empty defaults to DRAFT; set PUBLISHED to make visible to learners immediately.",
"type": "string" "type": "string",
"enum": [
"DRAFT",
"draft",
"PUBLISHED",
"published"
]
}, },
"sort_order": { "sort_order": {
"type": "integer" "description": "SortOrder within the module when set; omit to append after current max within module_id.",
"type": "integer",
"minimum": 0
}, },
"thumbnail": { "thumbnail": {
"type": "string" "type": "string"
@ -10508,6 +10524,11 @@
}, },
"name": { "name": {
"type": "string" "type": "string"
},
"sort_order": {
"description": "SortOrder within the course when set; omit to append after current max within course_id (uniqueness is per-course).",
"type": "integer",
"minimum": 0
} }
} }
}, },
@ -10516,8 +10537,7 @@
"required": [ "required": [
"parent_id", "parent_id",
"parent_kind", "parent_kind",
"question_set_id", "question_set_id"
"title"
], ],
"properties": { "properties": {
"parent_id": { "parent_id": {
@ -10538,6 +10558,16 @@
"persona_id": { "persona_id": {
"type": "integer" "type": "integer"
}, },
"publish_status": {
"description": "Omit or empty for backward compatibility defaults to PUBLISHED; set DRAFT to save hidden from learners until published.",
"type": "string",
"enum": [
"DRAFT",
"draft",
"PUBLISHED",
"published"
]
},
"question_set_id": { "question_set_id": {
"type": "integer" "type": "integer"
}, },
@ -11319,6 +11349,15 @@
"persona_id": { "persona_id": {
"type": "integer" "type": "integer"
}, },
"publish_status": {
"type": "string",
"enum": [
"DRAFT",
"draft",
"PUBLISHED",
"published"
]
},
"question_set_id": { "question_set_id": {
"type": "integer" "type": "integer"
}, },
@ -11372,8 +11411,13 @@
"type": "string" "type": "string"
}, },
"publish_status": { "publish_status": {
"description": "DRAFT or PUBLISHED", "type": "string",
"type": "string" "enum": [
"DRAFT",
"draft",
"PUBLISHED",
"published"
]
}, },
"sort_order": { "sort_order": {
"type": "integer" "type": "integer"
@ -11412,6 +11456,15 @@
"persona_id": { "persona_id": {
"type": "integer" "type": "integer"
}, },
"publish_status": {
"type": "string",
"enum": [
"DRAFT",
"draft",
"PUBLISHED",
"published"
]
},
"question_set_id": { "question_set_id": {
"type": "integer" "type": "integer"
}, },

View File

@ -389,6 +389,13 @@ definitions:
properties: properties:
persona_id: persona_id:
type: integer type: integer
publish_status:
enum:
- DRAFT
- draft
- PUBLISHED
- published
type: string
question_set_id: question_set_id:
type: integer type: integer
quick_tips: quick_tips:
@ -401,7 +408,6 @@ definitions:
type: string type: string
required: required:
- question_set_id - question_set_id
- title
type: object type: object
domain.CreateExamPrepUnitInput: domain.CreateExamPrepUnitInput:
properties: properties:
@ -419,9 +425,18 @@ definitions:
description: description:
type: string type: string
publish_status: publish_status:
description: Omit or DRAFT (default) for drafts; PUBLISHED for learner-visible lessons. description: Omit or empty defaults to DRAFT; set PUBLISHED to make visible
to learners immediately.
enum:
- DRAFT
- draft
- PUBLISHED
- published
type: string type: string
sort_order: sort_order:
description: SortOrder within the module when set; omit to append after current
max within module_id.
minimum: 0
type: integer type: integer
thumbnail: thumbnail:
type: string type: string
@ -440,6 +455,11 @@ definitions:
type: string type: string
name: name:
type: string type: string
sort_order:
description: SortOrder within the course when set; omit to append after current
max within course_id (uniqueness is per-course).
minimum: 0
type: integer
required: required:
- name - name
type: object type: object
@ -456,6 +476,15 @@ definitions:
- LESSON - LESSON
persona_id: persona_id:
type: integer type: integer
publish_status:
description: Omit or empty for backward compatibility defaults to PUBLISHED;
set DRAFT to save hidden from learners until published.
enum:
- DRAFT
- draft
- PUBLISHED
- published
type: string
question_set_id: question_set_id:
type: integer type: integer
quick_tips: quick_tips:
@ -470,7 +499,6 @@ definitions:
- parent_id - parent_id
- parent_kind - parent_kind
- question_set_id - question_set_id
- title
type: object type: object
domain.CreateProgramInput: domain.CreateProgramInput:
properties: properties:
@ -996,6 +1024,13 @@ definitions:
properties: properties:
persona_id: persona_id:
type: integer type: integer
publish_status:
enum:
- DRAFT
- draft
- PUBLISHED
- published
type: string
question_set_id: question_set_id:
type: integer type: integer
quick_tips: quick_tips:
@ -1031,7 +1066,11 @@ definitions:
description: description:
type: string type: string
publish_status: publish_status:
description: DRAFT or PUBLISHED enum:
- DRAFT
- draft
- PUBLISHED
- published
type: string type: string
sort_order: sort_order:
type: integer type: integer
@ -1057,6 +1096,13 @@ definitions:
properties: properties:
persona_id: persona_id:
type: integer type: integer
publish_status:
enum:
- DRAFT
- draft
- PUBLISHED
- published
type: string
question_set_id: question_set_id:
type: integer type: integer
quick_tips: quick_tips:

View File

@ -24,7 +24,7 @@ func (p ExamPrepPractice) VisibleToLearners() bool {
// CreateExamPrepPracticeInput is the body for POST .../exam-prep/lessons/{lessonId}/practices (lesson from path). // CreateExamPrepPracticeInput is the body for POST .../exam-prep/lessons/{lessonId}/practices (lesson from path).
type CreateExamPrepPracticeInput struct { type CreateExamPrepPracticeInput struct {
Title string `json:"title" validate:"required"` Title *string `json:"title,omitempty"`
StoryDescription *string `json:"story_description,omitempty"` StoryDescription *string `json:"story_description,omitempty"`
StoryImage *string `json:"story_image,omitempty"` StoryImage *string `json:"story_image,omitempty"`
PersonaID *int64 `json:"persona_id,omitempty"` PersonaID *int64 `json:"persona_id,omitempty"`

View File

@ -63,7 +63,7 @@ func (p Practice) VisibleToLearners() bool {
type CreatePracticeInput struct { type CreatePracticeInput struct {
ParentKind ParentKind `json:"parent_kind" validate:"required,oneof=COURSE MODULE LESSON"` ParentKind ParentKind `json:"parent_kind" validate:"required,oneof=COURSE MODULE LESSON"`
ParentID int64 `json:"parent_id" validate:"required,gt=0"` ParentID int64 `json:"parent_id" validate:"required,gt=0"`
Title string `json:"title" validate:"required"` Title *string `json:"title,omitempty"`
StoryDescription *string `json:"story_description,omitempty"` StoryDescription *string `json:"story_description,omitempty"`
StoryImage *string `json:"story_image,omitempty"` StoryImage *string `json:"story_image,omitempty"`
PersonaID *int64 `json:"persona_id,omitempty"` PersonaID *int64 `json:"persona_id,omitempty"`

View File

@ -51,7 +51,7 @@ func (s *Store) CreateExamPrepLessonPractice(ctx context.Context, lessonID int64
ps := domain.ParsePracticePublishStatusInput(in.PublishStatus) ps := domain.ParsePracticePublishStatusInput(in.PublishStatus)
p, err := s.queries.ExamPrepCreateLessonPractice(ctx, dbgen.ExamPrepCreateLessonPracticeParams{ p, err := s.queries.ExamPrepCreateLessonPractice(ctx, dbgen.ExamPrepCreateLessonPracticeParams{
UnitModuleLessonID: lessonID, UnitModuleLessonID: lessonID,
Title: in.Title, Title: derefString(in.Title),
StoryDescription: toPgText(in.StoryDescription), StoryDescription: toPgText(in.StoryDescription),
StoryImage: toPgText(in.StoryImage), StoryImage: toPgText(in.StoryImage),
PersonaID: int64PtrToPg8(in.PersonaID), PersonaID: int64PtrToPg8(in.PersonaID),

View File

@ -33,6 +33,13 @@ func optionalInt8UpdateID(val *int64) pgtype.Int8 {
return pgtype.Int8{Int64: *val, Valid: true} return pgtype.Int8{Int64: *val, Valid: true}
} }
func derefString(p *string) string {
if p == nil {
return ""
}
return *p
}
func lmsPracticeToDomain(p dbgen.LmsPractice) domain.Practice { func lmsPracticeToDomain(p dbgen.LmsPractice) domain.Practice {
out := domain.Practice{ out := domain.Practice{
ID: p.ID, ID: p.ID,
@ -98,7 +105,7 @@ func (s *Store) CreateLmsPractice(
CourseID: int64PtrToPg8(courseID), CourseID: int64PtrToPg8(courseID),
ModuleID: int64PtrToPg8(moduleID), ModuleID: int64PtrToPg8(moduleID),
LessonID: int64PtrToPg8(lessonID), LessonID: int64PtrToPg8(lessonID),
Title: in.Title, Title: derefString(in.Title),
StoryDescription: toPgText(in.StoryDescription), StoryDescription: toPgText(in.StoryDescription),
StoryImage: toPgText(in.StoryImage), StoryImage: toPgText(in.StoryImage),
PersonaID: int64PtrToPg8(in.PersonaID), PersonaID: int64PtrToPg8(in.PersonaID),