Coverage for postrfp / ref / handlers / designers.py: 99%

77 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-12-03 01:35 +0000

1""" 

2HTTP endpoints for managing ContentSchema objects 

3""" 

4 

5from typing import Optional 

6 

7from sqlalchemy.orm import Session 

8 

9from postrfp.shared.pager import Pager 

10from postrfp.authorisation import perms 

11from postrfp.shared.decorators import http 

12from postrfp.model.humans import User 

13from postrfp.model.ref import ContentSpec 

14from postrfp.shared.serial.refmodels import ContentSchema, ListResponse 

15 

16from postrfp.shared.serial.refmodels import ( 

17 SchemaUpdate, 

18 AddOptionalFieldRequest, 

19 AddRequiredFieldRequest, 

20 DeleteFieldRequest, 

21 RenameFieldRequest, 

22 MoveFieldRequest, 

23) 

24 

25 

26@http 

27def get_specs( 

28 session: Session, 

29 user: User, 

30 q_name: Optional[str] = None, 

31 pager: Optional[Pager] = None, 

32) -> ListResponse: 

33 """ 

34 List all content schemas accessible to the user's organization 

35 

36 Content schemas define the structure, validation rules, and field definitions 

37 for content items. Schemas are organization-owned and control how structured 

38 content is created and validated within the system. 

39 

40 **Query Parameters:** 

41 - `q_name`: Filter schemas by name (case-insensitive partial match) 

42 - Standard pagination parameters supported 

43 

44 **Returns:** Paginated list of schema definitions accessible to the user 

45 

46 **Access Control:** 

47 Schemas are filtered to show only: 

48 - Schemas owned by the user's organization 

49 - Schemas shared with the user's organization (if sharing is implemented) 

50 

51 **Schema Information Includes:** 

52 - Schema identification (ID, name, description) 

53 - JSON Schema definition with validation rules 

54 - Creation and modification timestamps 

55 - Owning organization details 

56 - Current workflow status 

57 

58 **Use Cases:** 

59 - Browse available content templates 

60 - Find schemas for specific content types 

61 - Understand validation requirements before content creation 

62 - Manage organization's content structure standards 

63 

64 **Examples:** 

65 - Get all schemas: no query parameters 

66 - Search by name: `?q_name=financial report` 

67 """ 

68 if pager is None: 

69 pager = Pager(page=1, page_size=20) 

70 

71 # Start query with schemas owned by user's org or shared with them 

72 query = ( 

73 session.query(ContentSpec) 

74 .filter((ContentSpec.org_id == user.org_id)) 

75 .order_by(ContentSpec.name) 

76 ) 

77 

78 if q_name: 

79 query = query.filter(ContentSpec.name.ilike(f"%{q_name}%")) 

80 

81 total_records = query.count() 

82 records = query.slice(pager.startfrom, pager.goto).all() 

83 

84 return ListResponse( 

85 items=[ContentSchema.model_validate(schema) for schema in records], 

86 pagination=pager.as_pagination(total_records, len(records)), 

87 ) 

88 

89 

90@http 

91def get_spec(session: Session, user: User, spec_id: int) -> ContentSchema: 

92 """ 

93 Get a single content schema by ID 

94 

95 Retrieves the complete schema definition including all field specifications, 

96 validation rules, metadata, and documentation. Essential for understanding 

97 content structure requirements and implementing content creation interfaces. 

98 

99 **Path Parameters:** 

100 - `spec_id`: Unique identifier of the content schema 

101 

102 **Returns:** Complete schema definition including: 

103 - Schema identification and metadata 

104 - Full JSON Schema specification with all validation rules 

105 - Field definitions and constraints 

106 - Required vs optional field specifications 

107 - Data types and format requirements 

108 - Custom validation patterns 

109 - Creation and modification history 

110 

111 **JSON Schema Features:** 

112 The returned schema follows JSON Schema Draft 7+ specifications and may include: 

113 - Field type definitions (string, number, object, array, etc.) 

114 - Validation constraints (min/max length, patterns, enums) 

115 - Nested object structures 

116 - Array item specifications 

117 - Conditional field requirements 

118 - Custom format validators 

119 

120 **Use Cases:** 

121 - Generate content creation forms dynamically 

122 - Validate content before submission 

123 - Understand content structure for API integration 

124 - Document content requirements for users 

125 

126 **Raises:** 

127 - `NoResultFound`: If the schema ID does not exist 

128 - `AuthorizationFailure`: If the user lacks access to this schema 

129 """ 

130 content_spec = session.get_one(ContentSpec, spec_id) 

131 

132 return ContentSchema.model_validate(content_spec) 

133 

134 

135@http 

136def post_spec(session: Session, user: User, spec_doc: ContentSchema) -> ContentSchema: 

137 """ 

138 Create a new content schema 

139 

140 Creates a new JSON Schema definition that will control the structure and 

141 validation of content items. The schema becomes available for content creation 

142 within the user's organization and can define complex data structures. 

143 

144 **Request Body:** Schema document including: 

145 - `name`: Descriptive name for the schema (required) 

146 - `description`: Detailed explanation of the schema purpose 

147 - `spec_doc`: Complete JSON Schema definition (required) 

148 

149 **Returns:** Created schema with assigned ID and metadata 

150 

151 **JSON Schema Requirements:** 

152 The `spec_doc` must be a valid JSON Schema (Draft 7+ recommended) including: 

153 - Root `type` definition (typically "object") 

154 - `properties` defining all available fields 

155 - `required` array listing mandatory fields 

156 - Field-level validation constraints 

157 - Proper data type specifications 

158 

159 **Schema Validation:** 

160 The system validates that: 

161 - The JSON Schema syntax is correct 

162 - Required fields are properly defined 

163 - Type definitions are valid 

164 - Constraints are logically consistent 

165 

166 **Ownership:** 

167 - Schema is owned by the user's organization 

168 - Only the owning organization can modify the schema 

169 - Content created with this schema will reference the organization 

170 

171 **Best Practices:** 

172 - Use descriptive field names and documentation 

173 - Define appropriate validation constraints 

174 - Consider backward compatibility for schema updates 

175 - Include examples in the schema description 

176 

177 **@permissions REF_SPEC_SAVE** 

178 

179 **Examples:** 

180 ```json 

181 { 

182 "name": "Financial Report Schema", 

183 "description": "Quarterly financial reporting template", 

184 "spec_doc": { 

185 "type": "object", 

186 "properties": { 

187 "report_period": {"type": "string", "format": "date"}, 

188 "revenue": {"type": "number", "minimum": 0}, 

189 "expenses": {"type": "number", "minimum": 0} 

190 }, 

191 "required": ["report_period", "revenue", "expenses"] 

192 } 

193 } 

194 ``` 

195 """ 

196 user.check_permission(perms.REF_SPEC_SAVE) 

197 

198 # Create new schema 

199 new_content_spec = ContentSpec( 

200 name=spec_doc.name, 

201 description=spec_doc.description, 

202 spec_doc=spec_doc.spec_doc, 

203 auth_policy=spec_doc.auth_policy, 

204 org_id=user.org_id, # Always use the current user's org ID for ownership 

205 ) 

206 session.add(new_content_spec) 

207 

208 session.flush() 

209 return ContentSchema.model_validate(new_content_spec) 

210 

211 

212@http 

213def put_spec( 

214 session: Session, user: User, spec_id: int, spec_doc: ContentSchema 

215) -> ContentSchema: 

216 """ 

217 Update content schema metadata (name, description, auth_policy) 

218 

219 Updates the metadata fields of a schema without modifying the schema definition 

220 itself. This is safe to perform at any time as it doesn't affect content 

221 validation or structure. 

222 

223 **Path Parameters:** 

224 - `spec_id`: ID of the schema to update 

225 

226 **Request Body:** Schema document with updated metadata 

227 - `name`: Updated descriptive name for the schema 

228 - `description`: Updated explanation of schema purpose 

229 - `auth_policy`: Updated authorization policy (CEL expression) 

230 - `spec_doc`: Schema definition (ignored - use PATCH for schema changes) 

231 

232 **Returns:** Updated schema with new metadata 

233 

234 **Note on Schema Definition Changes:** 

235 The `spec_doc` field is only updated by this endpoint when the schema is still in draft mode (is_draft=True). 

236 To modify a non-draft schema, use: 

237 - `PATCH /specs/{id}` - For controlled migrations with data updates 

238 - Draft mode (when implemented) - For unrestricted edits before publishing 

239 

240 **Why Separate Metadata from Schema Changes:** 

241 - Schema changes require careful migration of all content documents 

242 - Metadata changes are purely informational and have no impact on content 

243 - This separation prevents accidental breaking changes to content validation 

244 

245 **@permissions REF_SPEC_SAVE** 

246 

247 **Raises:** 

248 - `NoResultFound`: If the schema ID does not exist 

249 - `AuthorizationFailure`: If the user's organization doesn't own this schema 

250 """ 

251 user.check_permission(perms.REF_SPEC_SAVE) 

252 

253 content_spec = session.get_one(ContentSpec, spec_id) 

254 

255 # Update metadata fields only (not spec_doc) 

256 content_spec.name = spec_doc.name 

257 content_spec.description = spec_doc.description 

258 content_spec.auth_policy = spec_doc.auth_policy 

259 if spec_doc.is_draft is not None: 

260 content_spec.is_draft = spec_doc.is_draft 

261 

262 # NOTE: spec_doc is only updated if the schema is still in draft mode 

263 if content_spec.is_draft: 

264 content_spec.spec_doc = spec_doc.spec_doc 

265 

266 return ContentSchema.model_validate(content_spec) 

267 

268 

269@http 

270def patch_spec( 

271 session: Session, user: User, spec_id: int, schema_update: SchemaUpdate 

272) -> ContentSchema: 

273 """ 

274 Apply a schema migration to a ContentSpec and all associated Content. 

275 

276 This performs a coordinated migration that: 

277 1. Generates JSON Patch operations for schema and data 

278 2. Applies schema patches to ContentSpec.spec_doc 

279 3. Applies data patches to all Content.content_doc (with full audit trail) 

280 4. Validates all changes 

281 

282 Each Content modification creates a ContentRevision and AuditEvent for 

283 complete audit trail compliance. 

284 """ 

285 # TODO: Performance considerations for large Content sets 

286 user.check_permission(perms.REF_SPEC_SAVE) 

287 

288 import jsonpatch # type: ignore[import-untyped] 

289 from sqlalchemy.orm.attributes import flag_modified 

290 

291 from postrfp.ref.service.content_service import patch_content_doc 

292 from postrfp.ref.json_migration.patches import PatchBuilder 

293 

294 content_spec = session.get_one(ContentSpec, spec_id) 

295 

296 # 1. Generate patches based on request type 

297 builder = PatchBuilder(content_spec.spec_doc) 

298 

299 spec_change = schema_update.request 

300 

301 match spec_change: 

302 case AddOptionalFieldRequest(): 

303 patches = builder.add_optional_field(spec_change) 

304 case AddRequiredFieldRequest(): 

305 patches = builder.add_required_field(spec_change) 

306 case DeleteFieldRequest(): 

307 patches = builder.delete_field(spec_change) 

308 case RenameFieldRequest(): 

309 patches = builder.rename_field(spec_change) 

310 case MoveFieldRequest(): 

311 patches = builder.move_field(spec_change) 

312 case _: # pragma: no cover (not possible with Pydantic validation) 

313 raise ValueError(f"Unsupported schema change type: {type(spec_change)}") 

314 

315 # 2. Apply schema patches to ContentSpec.spec_doc 

316 schema_patch_dicts = [ 

317 p.model_dump(mode="json", by_alias=True) for p in patches.schema_patches 

318 ] 

319 jsonpatch.apply_patch(content_spec.spec_doc, schema_patch_dicts, in_place=True) 

320 flag_modified(content_spec, "spec_doc") 

321 

322 # 3. Validate the new schema is still valid JSON Schema 

323 content_spec.validate_spec("spec_doc", content_spec.spec_doc) 

324 

325 # 4. Apply data patches to all Content items (if schema change requires it) 

326 if patches.data_patches: 

327 migration_comment = f"Schema migration: {spec_change.__class__.__name__}" 

328 

329 # Apply patches to each Content with full audit trail 

330 # This will create ContentRevision and AuditEvent objects for each modification 

331 for content in content_spec.contents: 

332 patch_content_doc( 

333 session=session, 

334 content=content, 

335 json_patch_dicts=patches.data_patches, 

336 if_match=None, # No ETag in migration context 

337 updated_by_user=user, 

338 comment=migration_comment, 

339 skip_etag_check=True, # Skip ETag for administrative migrations 

340 ) 

341 

342 return ContentSchema.model_validate(content_spec) 

343 

344 

345@http 

346def delete_spec(session: Session, user: User, spec_id: int) -> None: 

347 """ 

348 Delete a content schema 

349 

350 Permanently removes a schema definition from the system. This is a destructive 

351 operation that will affect all content items using this schema and should be 

352 used with extreme caution. 

353 

354 **Path Parameters:** 

355 - `spec_id`: ID of the schema to delete 

356 

357 **Returns:** None (204 No Content status) 

358 

359 **Critical Impact:** 

360 - **All content using this schema will become invalid** 

361 - Content items will no longer pass validation 

362 - Existing content becomes uneditable (validation failures) 

363 - Content creation with this schema becomes impossible 

364 - API responses may include validation warnings 

365 

366 **What Gets Deleted:** 

367 - The schema definition itself 

368 - All validation rules and field specifications 

369 - Schema metadata and documentation 

370 - Workflow state information for the schema 

371 

372 **Content Impact:** 

373 Content items using this schema will: 

374 - Remain accessible for viewing (existing data preserved) 

375 - Fail validation checks during editing attempts 

376 - Display warnings about missing schema 

377 - Require schema reassignment or recreation for continued use 

378 

379 **Permission Requirements:** 

380 - User's organization must own the schema 

381 - Only the schema owner can delete it 

382 - Cannot be deleted if it would violate system constraints 

383 

384 **Recommended Alternatives:** 

385 Instead of deletion, consider: 

386 - Deprecating the schema (mark as inactive) 

387 - Creating a migration path to a new schema 

388 - Archiving content using the schema first 

389 - Updating the schema to a minimal valid state 

390 

391 **@permissions REF_SPEC_SAVE** 

392 

393 **Raises:** 

394 - `NoResultFound`: If the schema ID does not exist 

395 - `AuthorizationFailure`: If the user's organization doesn't own this schema 

396 - `IntegrityError`: If content still depends on this schema (depending on DB constraints) 

397 

398 **⚠️ Warning:** This action cannot be undone. Ensure all content using this schema 

399 has been migrated or archived before proceeding with deletion. 

400 """ 

401 user.check_permission(perms.REF_SPEC_SAVE) 

402 

403 schema = session.get_one(ContentSpec, spec_id) 

404 

405 session.delete(schema)