Skip to content

Commit 0fdc632

Browse files
committed
feat: introduced report-centric schema and abstraction layer
1 parent 51c8a83 commit 0fdc632

6 files changed

Lines changed: 747 additions & 13 deletions

File tree

api/db/database.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from sqlmodel import create_engine, Session
2+
import os
23

3-
DATABASE_URL = "sqlite:///./fireform.db"
4+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
5+
DATABASE_URL = f"sqlite:///{os.path.join(BASE_DIR, 'fireform.db')}"
46

57
engine = create_engine(
68
DATABASE_URL,
7-
echo=True,
9+
echo=False,
810
connect_args={"check_same_thread": False},
911
)
1012

api/db/init_db.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from sqlmodel import SQLModel
2-
from api.db.database import engine
3-
from api.db import models
2+
from database import engine
3+
import models
44

55
def init_db():
66
SQLModel.metadata.create_all(engine)

api/db/models.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
from sqlmodel import SQLModel, Field
1+
from sqlmodel import SQLModel, Field, UniqueConstraint
22
from sqlalchemy import Column, JSON
33
from datetime import datetime
4+
from enum import Enum
45

56
class Template(SQLModel, table=True):
67
id: int | None = Field(default=None, primary_key=True)
7-
name: str
8+
name: str = Field(unique=True)
89
fields: dict = Field(sa_column=Column(JSON))
910
pdf_path: str
1011
created_at: datetime = Field(default_factory=datetime.utcnow)
@@ -15,4 +16,39 @@ class FormSubmission(SQLModel, table=True):
1516
template_id: int
1617
input_text: str
1718
output_pdf_path: str
18-
created_at: datetime = Field(default_factory=datetime.utcnow)
19+
created_at: datetime = Field(default_factory=datetime.utcnow)
20+
21+
22+
class ReportSchema(SQLModel, table=True):
23+
id: int | None = Field(default=None, primary_key=True)
24+
name: str = Field(unique=True)
25+
description: str
26+
use_case: str
27+
created_at: datetime = Field(default_factory=datetime.utcnow)
28+
29+
class ReportSchemaTemplate(SQLModel, table=True):
30+
id: int | None = Field(default=None, primary_key=True)
31+
template_id: int
32+
report_schema_id: int
33+
field_mapping: dict = Field(default={}, sa_column=Column(JSON))
34+
35+
__table_args__ = (UniqueConstraint("template_id", "report_schema_id"),)
36+
37+
class Datatype(str, Enum):
38+
STRING = "string"
39+
INT = "int"
40+
DATE = "date"
41+
ENUM = 'enum'
42+
43+
44+
class SchemaField(SQLModel, table=True):
45+
id: int | None = Field(default=None, primary_key=True)
46+
report_schema_id: int
47+
field_name: str
48+
source_template_id: int
49+
description: str = Field(default="")
50+
data_type: Datatype = Field(default=Datatype.STRING)
51+
word_limit: int | None = Field(default=None)
52+
required: bool = Field(default=False)
53+
allowed_values: dict | None = Field(sa_column=Column(JSON))
54+
canonical_name: str | None = Field(default=None)

api/db/repositories.py

Lines changed: 272 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,285 @@
1+
from ast import For
2+
from collections import defaultdict
3+
from sqlalchemy.exc import IntegrityError
14
from sqlmodel import Session, select
2-
from api.db.models import Template, FormSubmission
5+
from api.db.models import (
6+
Template,
7+
FormSubmission,
8+
ReportSchema,
9+
ReportSchemaTemplate,
10+
SchemaField,
11+
)
312

4-
# Templates
513
def create_template(session: Session, template: Template) -> Template:
14+
try:
15+
session.add(template)
16+
session.commit()
17+
session.refresh(template)
18+
return template
19+
except IntegrityError:
20+
raise
21+
22+
def get_template(session: Session, template_id: int) -> Template | None:
23+
return session.get(Template, template_id)
24+
25+
def update_template(session: Session, template_id: int, updates: dict) -> Template | None:
26+
template = session.get(Template, template_id)
27+
if not template:
28+
return None
29+
for key, value in updates.items():
30+
setattr(template, key, value)
631
session.add(template)
732
session.commit()
833
session.refresh(template)
934
return template
1035

11-
def get_template(session: Session, template_id: int) -> Template | None:
12-
return session.get(Template, template_id)
36+
def list_templates(session: Session) -> list[Template]:
37+
return session.exec(select(Template)).all()
38+
39+
def delete_template(session: Session, template_id: int) -> bool:
40+
"""Remove template and dependent rows (form submissions, schema links, schema fields)."""
41+
template = session.get(Template, template_id)
42+
if not template:
43+
return False
44+
45+
for form in session.exec(
46+
select(FormSubmission).where(FormSubmission.template_id == template_id)
47+
).all():
48+
session.delete(form)
49+
50+
for junction in session.exec(
51+
select(ReportSchemaTemplate).where(
52+
ReportSchemaTemplate.template_id == template_id
53+
)
54+
).all():
55+
for field in session.exec(
56+
select(SchemaField).where(
57+
SchemaField.report_schema_id == junction.report_schema_id,
58+
SchemaField.source_template_id == template_id,
59+
)
60+
).all():
61+
session.delete(field)
62+
session.delete(junction)
63+
64+
session.delete(template)
65+
session.commit()
66+
return True
1367

14-
# Forms
1568
def create_form(session: Session, form: FormSubmission) -> FormSubmission:
1669
session.add(form)
1770
session.commit()
1871
session.refresh(form)
19-
return form
72+
return form
73+
74+
def get_form(session: Session, form_id: int) -> FormSubmission:
75+
return session.get(FormSubmission, form_id)
76+
77+
def update_form(session: Session, form_id: int, updates: dict) -> FormSubmission | None:
78+
form = session.get(FormSubmission, form_id)
79+
if not form:
80+
return None
81+
for key, value in updates.items():
82+
setattr(form, key, value)
83+
session.add(form)
84+
session.commit()
85+
session.refresh(form)
86+
return form
87+
88+
def delete_form(session: Session, form_id: int) -> FormSubmission:
89+
form_submission = session.get(FormSubmission, form_id)
90+
if form_submission:
91+
session.delete(form_submission)
92+
session.commit()
93+
return True
94+
return False
95+
96+
def create_report_schema(session: Session, schema: ReportSchema) -> ReportSchema:
97+
try:
98+
session.add(schema)
99+
session.commit()
100+
session.refresh(schema)
101+
return schema
102+
except IntegrityError:
103+
raise
104+
105+
def get_report_schema(session: Session, schema_id: int) -> ReportSchema | None:
106+
return session.get(ReportSchema, schema_id)
107+
108+
def list_report_schemas(session: Session) -> list[ReportSchema]:
109+
return session.exec(select(ReportSchema)).all()
110+
111+
def update_report_schema(session: Session, schema_id: int, updates: dict) -> ReportSchema | None:
112+
schema = session.get(ReportSchema, schema_id)
113+
if not schema:
114+
return None
115+
for key, value in updates.items():
116+
setattr(schema, key, value)
117+
session.add(schema)
118+
session.commit()
119+
session.refresh(schema)
120+
return schema
121+
122+
def delete_report_schema(session: Session, schema_id: int) -> bool:
123+
schema = session.get(ReportSchema, schema_id)
124+
if not schema:
125+
return False
126+
127+
fields = session.exec(
128+
select(SchemaField).where(SchemaField.report_schema_id == schema_id)
129+
).all()
130+
for field in fields:
131+
session.delete(field)
132+
133+
junctions = session.exec(
134+
select(ReportSchemaTemplate).where(
135+
ReportSchemaTemplate.report_schema_id == schema_id
136+
)
137+
).all()
138+
for junction in junctions:
139+
session.delete(junction)
140+
141+
session.delete(schema)
142+
session.commit()
143+
return True
144+
145+
146+
def add_template_to_schema(
147+
session: Session, schema_id: int, template_id: int
148+
) -> ReportSchemaTemplate:
149+
"""Associate a template with a schema.
150+
151+
Looks up `template.fields` and auto-creates a SchemaField for each field,
152+
pre-populated with `field_name` and `source_template_id`.
153+
Other metadata is left as defaults for the user to fill in later.
154+
"""
155+
template = session.get(Template, template_id)
156+
if not template:
157+
raise ValueError(f"Template {template_id} not found")
158+
159+
schema = session.get(ReportSchema, schema_id)
160+
if not schema:
161+
raise ValueError(f"ReportSchema {schema_id} not found")
162+
163+
# exists = session.exec(select(ReportSchemaTemplate).where(ReportSchemaTemplate.report_schema_id == schema_id, ReportSchemaTemplate.template_id == template_id)).first()
164+
# if exists:
165+
# raise IntegrityError
166+
167+
# Create the junction record (field_mapping starts empty, populated during canonization)
168+
junction = ReportSchemaTemplate(
169+
report_schema_id=schema_id,
170+
template_id=template_id,
171+
)
172+
173+
session.add(junction)
174+
175+
# Auto-create a SchemaField for each field in the template
176+
for field_name in template.fields:
177+
schema_field = SchemaField(
178+
report_schema_id=schema_id,
179+
field_name=field_name,
180+
source_template_id=template_id,
181+
)
182+
session.add(schema_field)
183+
184+
session.commit()
185+
session.refresh(junction)
186+
return junction
187+
188+
def remove_template_from_schema(
189+
session: Session, schema_id: int, template_id: int
190+
) -> bool:
191+
"""Disassociate a template from a schema and remove its SchemaField entries."""
192+
junction = session.exec(
193+
select(ReportSchemaTemplate).where(
194+
ReportSchemaTemplate.report_schema_id == schema_id,
195+
ReportSchemaTemplate.template_id == template_id,
196+
)
197+
).first()
198+
if not junction:
199+
return False
200+
201+
fields = session.exec(
202+
select(SchemaField).where(
203+
SchemaField.report_schema_id == schema_id,
204+
SchemaField.source_template_id == template_id,
205+
)
206+
).all()
207+
for field in fields:
208+
session.delete(field)
209+
210+
session.delete(junction)
211+
session.commit()
212+
return True
213+
214+
215+
def get_schema_fields(session: Session, schema_id: int) -> list[SchemaField]:
216+
return session.exec(
217+
select(SchemaField).where(SchemaField.report_schema_id == schema_id)
218+
).all()
219+
220+
def get_schema_field(session: Session, field_id: int) -> SchemaField:
221+
return session.get(SchemaField, field_id)
222+
223+
def update_schema_field(session: Session, schema_id: int, field_id: int, updates: dict) -> SchemaField | None:
224+
"""Update field metadata: description, data_type, word_limit, required, allowed_values.
225+
226+
Validates that the field belongs to the given schema before updating,
227+
so the same template field in different schemas can have independent metadata.
228+
"""
229+
field = session.get(SchemaField, field_id)
230+
if not field or field.report_schema_id != schema_id:
231+
return None
232+
for key, value in updates.items():
233+
setattr(field, key, value)
234+
session.add(field)
235+
session.commit()
236+
session.refresh(field)
237+
return field
238+
239+
240+
# ── Template mapping (post-canonization) ─────────────────────────────────────
241+
242+
def update_template_mapping(
243+
session: Session, schema_id: int, template_id: int
244+
) -> ReportSchemaTemplate | None:
245+
"""Auto-generate and store the canonical → PDF field mapping after canonization.
246+
247+
Builds the mapping by looking up all SchemaFields for this schema+template pair
248+
and mapping each field's canonical_name → field_name.
249+
"""
250+
junction = session.exec(
251+
select(ReportSchemaTemplate).where(
252+
ReportSchemaTemplate.report_schema_id == schema_id,
253+
ReportSchemaTemplate.template_id == template_id,
254+
)
255+
).first()
256+
if not junction:
257+
return None
258+
259+
# Build mapping from SchemaFields that have been canonized
260+
fields = session.exec(
261+
select(SchemaField).where(
262+
SchemaField.report_schema_id == schema_id,
263+
SchemaField.source_template_id == template_id,
264+
)
265+
).all()
266+
267+
grouped: defaultdict[str, list[str]] = defaultdict(list)
268+
for field in sorted(fields, key=lambda f: f.field_name):
269+
key = field.canonical_name if field.canonical_name else field.field_name
270+
grouped[key].append(field.field_name)
271+
272+
# One PDF field -> store str; several sharing a canonical -> list (distribute handles both).
273+
field_mapping: dict = {}
274+
for key, names in grouped.items():
275+
field_mapping[key] = names[0] if len(names) == 1 else names
276+
277+
junction.field_mapping = field_mapping
278+
session.add(junction)
279+
session.commit()
280+
session.refresh(junction)
281+
return junction
282+
283+
def get_field_mapping(session: Session, schema_id: int, template_id: int) -> ReportSchemaTemplate:
284+
junction = session.exec(select(ReportSchemaTemplate).where(ReportSchemaTemplate.report_schema_id == schema_id, ReportSchemaTemplate.template_id == template_id)).first()
285+
return junction.field_mapping

0 commit comments

Comments
 (0)