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
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-03 01:35 +0000
1"""
2HTTP endpoints for managing ContentSchema objects
3"""
5from typing import Optional
7from sqlalchemy.orm import Session
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
16from postrfp.shared.serial.refmodels import (
17 SchemaUpdate,
18 AddOptionalFieldRequest,
19 AddRequiredFieldRequest,
20 DeleteFieldRequest,
21 RenameFieldRequest,
22 MoveFieldRequest,
23)
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
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.
40 **Query Parameters:**
41 - `q_name`: Filter schemas by name (case-insensitive partial match)
42 - Standard pagination parameters supported
44 **Returns:** Paginated list of schema definitions accessible to the user
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)
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
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
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)
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 )
78 if q_name:
79 query = query.filter(ContentSpec.name.ilike(f"%{q_name}%"))
81 total_records = query.count()
82 records = query.slice(pager.startfrom, pager.goto).all()
84 return ListResponse(
85 items=[ContentSchema.model_validate(schema) for schema in records],
86 pagination=pager.as_pagination(total_records, len(records)),
87 )
90@http
91def get_spec(session: Session, user: User, spec_id: int) -> ContentSchema:
92 """
93 Get a single content schema by ID
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.
99 **Path Parameters:**
100 - `spec_id`: Unique identifier of the content schema
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
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
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
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)
132 return ContentSchema.model_validate(content_spec)
135@http
136def post_spec(session: Session, user: User, spec_doc: ContentSchema) -> ContentSchema:
137 """
138 Create a new content schema
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.
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)
149 **Returns:** Created schema with assigned ID and metadata
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
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
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
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
177 **@permissions REF_SPEC_SAVE**
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)
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)
208 session.flush()
209 return ContentSchema.model_validate(new_content_spec)
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)
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.
223 **Path Parameters:**
224 - `spec_id`: ID of the schema to update
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)
232 **Returns:** Updated schema with new metadata
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
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
245 **@permissions REF_SPEC_SAVE**
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)
253 content_spec = session.get_one(ContentSpec, spec_id)
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
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
266 return ContentSchema.model_validate(content_spec)
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.
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
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)
288 import jsonpatch # type: ignore[import-untyped]
289 from sqlalchemy.orm.attributes import flag_modified
291 from postrfp.ref.service.content_service import patch_content_doc
292 from postrfp.ref.json_migration.patches import PatchBuilder
294 content_spec = session.get_one(ContentSpec, spec_id)
296 # 1. Generate patches based on request type
297 builder = PatchBuilder(content_spec.spec_doc)
299 spec_change = schema_update.request
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)}")
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")
322 # 3. Validate the new schema is still valid JSON Schema
323 content_spec.validate_spec("spec_doc", content_spec.spec_doc)
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__}"
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 )
342 return ContentSchema.model_validate(content_spec)
345@http
346def delete_spec(session: Session, user: User, spec_id: int) -> None:
347 """
348 Delete a content schema
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.
354 **Path Parameters:**
355 - `spec_id`: ID of the schema to delete
357 **Returns:** None (204 No Content status)
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
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
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
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
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
391 **@permissions REF_SPEC_SAVE**
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)
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)
403 schema = session.get_one(ContentSpec, spec_id)
405 session.delete(schema)