Skip to content

Commit 845af05

Browse files
committed
update(Frontend v2): Set metadata on timestamp fields
1 parent f1d4fb6 commit 845af05

16 files changed

Lines changed: 343 additions & 96 deletions

File tree

fastapi_forge/api/main.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
from fastapi import FastAPI
66
from fastapi.staticfiles import StaticFiles
77

8+
from fastapi_forge.core.build import build_fastapi_project
9+
from fastapi_forge.schemas import ProjectSpec
10+
811
app = FastAPI()
912

1013

@@ -22,5 +25,13 @@ def start_forge_api() -> None:
2225
uvicorn.run(app, host="localhost", port=8000)
2326

2427

28+
@app.post("/generate")
29+
async def generate_project(project_spec: ProjectSpec) -> None:
30+
try:
31+
await build_fastapi_project(project_spec, dry_run=False)
32+
except Exception as e:
33+
print(e)
34+
35+
2536
if __name__ == "__main__":
2637
start_forge_api()

fastapi_forge/static/assets/index-BN-dc6bv.css

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fastapi_forge/static/assets/index-DWiWI9aR.js

Lines changed: 46 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fastapi_forge/static/assets/index-DuOkOUCX.js

Lines changed: 0 additions & 46 deletions
This file was deleted.

fastapi_forge/static/assets/index-oV0aoT2I.css

Lines changed: 0 additions & 1 deletion
This file was deleted.

fastapi_forge/static/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
<link rel="icon" href="/favicon.ico">
99
<meta name="viewport" content="width=device-width, initial-scale=1.0">
1010
<title>Vite App</title>
11-
<script type="module" crossorigin src="/assets/index-DuOkOUCX.js"></script>
12-
<link rel="stylesheet" crossorigin href="/assets/index-oV0aoT2I.css">
11+
<script type="module" crossorigin src="/assets/index-DWiWI9aR.js"></script>
12+
<link rel="stylesheet" crossorigin href="/assets/index-BN-dc6bv.css">
1313
</head>
1414
<body>
1515
<div id="app"></div>

fastapi_forge/template/{{cookiecutter.project_name}}/pyproject.toml

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
[project]
2+
name = "{{cookiecutter.project_name}}"
3+
version = "0.1.0"
4+
description = "Add your description here"
5+
readme = "README.md"
6+
requires-python = ">=3.12"
27
dependencies = [
38
"fastapi>=0.115.8",
49
"uvicorn>=0.34.0",
510
"pydantic-settings>=2.7.1",
611
"pydantic>=2.10.6",
712
"email-validator>=2.2.0",
8-
"schemahound>=0.1.14",
913
"loguru>=0.7.3",
1014
"yarl>=1.18.3",
1115
"ruff>=0.9.4",
@@ -46,11 +50,7 @@ dependencies = [
4650
{%- if cookiecutter.use_logfire -%}
4751
"logfire[aio-pika,asyncpg,fastapi,httpx,sqlalchemy,system-metrics]>=3.16.0",
4852
{%- endif %}
49-
]name = "{{cookiecutter.project_name}}"
50-
version = "0.1.0"
51-
description = "Add your description here"
52-
readme = "README.md"
53-
requires-python = ">=3.12"
53+
]
5454

5555
[tool.pytest.ini_options]
5656
env = [
@@ -59,7 +59,7 @@ env = [
5959
]
6060

6161
[tool.ruff]
62-
exclude = ["migrations", ".venv/", "Lib"]
62+
exclude = ["migrations",".venv/", "Lib"]
6363
target-version = "py312"
6464
line-length = 88
6565
indent-width = 4
@@ -133,12 +133,13 @@ line-ending = "auto"
133133
"__init__.py" = ["F401"]
134134

135135
[tool.mypy]
136-
plugins = ["pydantic.mypy", {% if cookiecutter.use_postgres -%}"sqlalchemy.ext.mypy.plugin"{% endif %}]warn_return_any = false
136+
warn_return_any = false
137137
namespace_packages = true
138138
strict = true
139139
ignore_missing_imports = true
140140
pretty = true
141141
show_error_codes = true
142142
implicit_reexport = true
143143
disable_error_code = ["prop-decorator", "override", "import-untyped"]
144-
exclude = ["migrations"]
144+
plugins = ["pydantic.mypy", {% if cookiecutter.use_postgres -%}"sqlalchemy.ext.mypy.plugin"{% endif %}]
145+
exclude = ["migrations"]

fastapi_forge/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/dependencies/auth_dependencies.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ class HTTPBearer(_HTTPBearer):
1616
Returns access token as str.
1717
"""
1818

19-
async def __call__(self, request: Request) -> str | None: # type: ignore
19+
async def __call__(self, request: Request) -> str | None:
2020
"""Return access token."""
2121
try:
2222
obj = await super().__call__(request)
23-
return obj.credentials if obj else None
24-
except HTTPException:
23+
except HTTPException as err:
2524
msg = "Missing token."
26-
raise exceptions.Http401(msg)
25+
raise exceptions.Http401(msg) from err
26+
else:
27+
return obj.credentials if obj else None
2728

2829

2930
auth_scheme = HTTPBearer()

fastapi_forge/type_info_registry.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from pydantic.dataclasses import dataclass
66

77
from fastapi_forge.enums import FieldDataTypeEnum
8+
from fastapi_forge.logger import logger
89

910
EnumName = Annotated[str, Field(...)]
1011

@@ -48,9 +49,10 @@ def __init__(self) -> None:
4849

4950
def register(self, key: T, data_type: TypeInfo) -> None:
5051
if key in self:
51-
raise KeyError(
52+
logger.error(
5253
f"{self.__class__.__name__}: Key '{key}' is already registered."
5354
)
55+
return
5456
self._registry[key] = data_type
5557

5658
def get(self, key: T) -> TypeInfo:

frontend/src/components/modal/AddFieldModal.vue

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
<div class="input-group">
1010
<label class="field-label">Type</label>
11-
<select class="field-select" v-model="type">
11+
<select class="field-select" v-model="type" :disabled="isPrimaryKey">
1212
<option disabled value="">-- Select type --</option>
1313
<option value="String">String</option>
1414
<option value="Int">Int</option>
@@ -32,13 +32,24 @@
3232

3333
<div class="input-group">
3434
<label class="field-label">Default value</label>
35-
<select class="field-select" v-model="defaultValue" v-if="type === 'Enum'">
35+
<select
36+
class="field-select"
37+
v-model="defaultValue"
38+
v-if="type === 'Enum'"
39+
:disabled="isPrimaryKey"
40+
>
3641
<option value="" />
3742
<option v-for="v in selectedEnum?.values" :key="v.name" :value="v.name">
3843
{{ v.name }}
3944
</option>
4045
</select>
41-
<input class="field-input" v-model="defaultValue" type="text" v-else />
46+
<input
47+
class="field-input"
48+
v-model="defaultValue"
49+
type="text"
50+
v-else
51+
:disabled="isPrimaryKey || createdAtTimestamp || updatedAtTimestamp"
52+
/>
4253
</div>
4354
</div>
4455

@@ -49,14 +60,20 @@
4960
<label><input type="checkbox" v-model="isIndex" /> Index</label>
5061
</div>
5162

63+
<div class="checkbox-group" v-if="type === 'DateTime'">
64+
<label class="field-label">Timestamp</label>
65+
<label><input type="checkbox" v-model="createdAtTimestamp" /> Created</label>
66+
<label><input type="checkbox" v-model="updatedAtTimestamp" /> Updated</label>
67+
</div>
68+
5269
<div class="action-group">
5370
<button class="save-field-btn" @click="saveField">Save</button>
5471
</div>
5572
</main>
5673
</template>
5774

5875
<script setup lang="ts">
59-
import { ref } from "vue"
76+
import { ref, watch } from "vue"
6077
import { useProjectStore } from "@/stores/useProjectStore"
6178
import { useModalStore } from "@/stores/useModalStore"
6279
import type { EnumT, FieldType } from "@/types/types"
@@ -69,14 +86,60 @@ const projectStore = useProjectStore()
6986
const modalStore = useModalStore()
7087
7188
const selectedEnum = ref<EnumT | undefined>()
72-
7389
const fieldName = ref("")
7490
const type = ref("")
7591
const defaultValue = ref("")
7692
const isPrimaryKey = ref(false)
7793
const isNullable = ref(false)
7894
const isUnique = ref(false)
7995
const isIndex = ref(false)
96+
const metadata = ref(null)
97+
const extraKwargs = ref({})
98+
const createdAtTimestamp = ref(false)
99+
const updatedAtTimestamp = ref(false)
100+
101+
watch(isPrimaryKey, (newVal) => {
102+
if (newVal) {
103+
type.value = "UUID"
104+
defaultValue.value = "uuid.uuid4"
105+
}
106+
})
107+
108+
const resetTimestamp = () => {
109+
if (defaultValue.value === "datetime.now(timezone.utc)") {
110+
defaultValue.value = ""
111+
}
112+
extraKwargs.value = {}
113+
}
114+
115+
watch(createdAtTimestamp, (newVal) => {
116+
if (newVal) {
117+
updatedAtTimestamp.value = false
118+
defaultValue.value = "datetime.now(timezone.utc)"
119+
extraKwargs.value = {}
120+
} else if (!updatedAtTimestamp.value) {
121+
resetTimestamp()
122+
}
123+
})
124+
125+
watch(updatedAtTimestamp, (newVal) => {
126+
if (newVal) {
127+
createdAtTimestamp.value = false
128+
defaultValue.value = "datetime.now(timezone.utc)"
129+
extraKwargs.value = { onupdate: "datetime.now(timezone.utc)" }
130+
} else if (!createdAtTimestamp.value) {
131+
resetTimestamp()
132+
}
133+
})
134+
135+
watch(type, (newVal) => {
136+
if (newVal === "DateTime") return
137+
if (createdAtTimestamp.value || updatedAtTimestamp.value) {
138+
resetTimestamp()
139+
createdAtTimestamp.value = false
140+
updatedAtTimestamp.value = false
141+
}
142+
})
80143
81144
const saveField = () => {
82145
if (!fieldName.value || !type.value) return
@@ -89,6 +152,7 @@ const saveField = () => {
89152
isUnique: isUnique.value,
90153
isIndex: isIndex.value,
91154
defaultValue: defaultValue.value || undefined,
155+
metadata: metadata.value || undefined,
92156
})
93157
modalStore.close()
94158
}
@@ -101,46 +165,39 @@ const saveField = () => {
101165
gap: 1rem;
102166
padding: 1rem;
103167
}
104-
105168
.input-container {
106169
display: flex;
107170
flex-direction: column;
108171
gap: 1rem;
109172
}
110-
111173
.input-group {
112174
display: flex;
113175
flex-direction: column;
114176
gap: 0.15rem;
115177
}
116-
117178
.field-label {
118179
font-weight: bold;
119180
margin-bottom: 5px;
120181
}
121-
122182
.field-input,
123183
.field-select {
124184
border: 2px solid black;
125185
border-radius: 6px;
126186
padding: 0.6rem;
127187
background-color: white;
128188
}
129-
130189
.checkbox-group {
131190
display: flex;
132191
flex-direction: column;
133192
gap: 0.5rem;
134193
font-weight: bold;
135194
}
136-
137195
.action-group {
138196
gap: 0.5rem;
139197
margin-top: 2rem;
140198
display: flex;
141199
flex-direction: column;
142200
}
143-
144201
.save-field-btn {
145202
width: 100%;
146203
padding: 0.5rem 1rem;
@@ -152,7 +209,6 @@ const saveField = () => {
152209
transform 0.1s ease-in-out,
153210
box-shadow 0.1s ease-in-out;
154211
}
155-
156212
.save-field-btn:hover {
157213
cursor: pointer;
158214
transform: translate(2px, 2px);

0 commit comments

Comments
 (0)