@compilr-dev/factory 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/factory/registry.js +4 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +4 -0
  4. package/dist/toolkits/react-fastapi/api.d.ts +9 -0
  5. package/dist/toolkits/react-fastapi/api.js +378 -0
  6. package/dist/toolkits/react-fastapi/config.d.ts +9 -0
  7. package/dist/toolkits/react-fastapi/config.js +128 -0
  8. package/dist/toolkits/react-fastapi/helpers.d.ts +13 -0
  9. package/dist/toolkits/react-fastapi/helpers.js +54 -0
  10. package/dist/toolkits/react-fastapi/index.d.ts +11 -0
  11. package/dist/toolkits/react-fastapi/index.js +55 -0
  12. package/dist/toolkits/react-fastapi/seed.d.ts +12 -0
  13. package/dist/toolkits/react-fastapi/seed.js +54 -0
  14. package/dist/toolkits/react-fastapi/shared.d.ts +6 -0
  15. package/dist/toolkits/react-fastapi/shared.js +6 -0
  16. package/dist/toolkits/react-fastapi/static.d.ts +8 -0
  17. package/dist/toolkits/react-fastapi/static.js +123 -0
  18. package/dist/toolkits/react-go/api.d.ts +12 -0
  19. package/dist/toolkits/react-go/api.js +523 -0
  20. package/dist/toolkits/react-go/config.d.ts +9 -0
  21. package/dist/toolkits/react-go/config.js +129 -0
  22. package/dist/toolkits/react-go/helpers.d.ts +13 -0
  23. package/dist/toolkits/react-go/helpers.js +49 -0
  24. package/dist/toolkits/react-go/index.d.ts +11 -0
  25. package/dist/toolkits/react-go/index.js +55 -0
  26. package/dist/toolkits/react-go/seed.d.ts +12 -0
  27. package/dist/toolkits/react-go/seed.js +55 -0
  28. package/dist/toolkits/react-go/shared.d.ts +6 -0
  29. package/dist/toolkits/react-go/shared.js +6 -0
  30. package/dist/toolkits/react-go/static.d.ts +8 -0
  31. package/dist/toolkits/react-go/static.js +122 -0
  32. package/package.json +1 -1
@@ -27,6 +27,10 @@ export const defaultRegistry = new ToolkitRegistry();
27
27
  import { reactNodeToolkit } from '../toolkits/react-node/index.js';
28
28
  import { nextPrismaToolkit } from '../toolkits/next-prisma/index.js';
29
29
  import { staticLandingToolkit } from '../toolkits/static-landing/index.js';
30
+ import { reactFastapiToolkit } from '../toolkits/react-fastapi/index.js';
31
+ import { reactGoToolkit } from '../toolkits/react-go/index.js';
30
32
  defaultRegistry.register(reactNodeToolkit);
31
33
  defaultRegistry.register(nextPrismaToolkit);
32
34
  defaultRegistry.register(staticLandingToolkit);
35
+ defaultRegistry.register(reactFastapiToolkit);
36
+ defaultRegistry.register(reactGoToolkit);
package/dist/index.d.ts CHANGED
@@ -21,4 +21,6 @@ export type { WriteResult } from './factory/file-writer.js';
21
21
  export { reactNodeToolkit } from './toolkits/react-node/index.js';
22
22
  export { nextPrismaToolkit } from './toolkits/next-prisma/index.js';
23
23
  export { staticLandingToolkit } from './toolkits/static-landing/index.js';
24
+ export { reactFastapiToolkit } from './toolkits/react-fastapi/index.js';
25
+ export { reactGoToolkit } from './toolkits/react-go/index.js';
24
26
  export { factoryScaffoldSkill, factorySkills } from './factory/skill.js';
package/dist/index.js CHANGED
@@ -25,5 +25,9 @@ export { reactNodeToolkit } from './toolkits/react-node/index.js';
25
25
  export { nextPrismaToolkit } from './toolkits/next-prisma/index.js';
26
26
  // Static Landing Page Toolkit
27
27
  export { staticLandingToolkit } from './toolkits/static-landing/index.js';
28
+ // React + FastAPI Toolkit
29
+ export { reactFastapiToolkit } from './toolkits/react-fastapi/index.js';
30
+ // React + Go Toolkit
31
+ export { reactGoToolkit } from './toolkits/react-go/index.js';
28
32
  // Factory skill (Phase 5)
29
33
  export { factoryScaffoldSkill, factorySkills } from './factory/skill.js';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * React+FastAPI Toolkit — API Generator
3
+ *
4
+ * Generates: server/main.py, server/models.py,
5
+ * server/database/{entity}.py, server/routers/{entity}.py
6
+ */
7
+ import type { ApplicationModel } from '../../model/types.js';
8
+ import type { FactoryFile } from '../types.js';
9
+ export declare function generateApiFiles(model: ApplicationModel): FactoryFile[];
@@ -0,0 +1,378 @@
1
+ /**
2
+ * React+FastAPI Toolkit — API Generator
3
+ *
4
+ * Generates: server/main.py, server/models.py,
5
+ * server/database/{entity}.py, server/routers/{entity}.py
6
+ */
7
+ import { toCamelCase, toKebabCase } from '../../model/naming.js';
8
+ import { pyType, fkFieldName, belongsToRels, hasManyRels } from './helpers.js';
9
+ import { toSnakeCase } from './helpers.js';
10
+ import { generateSeedData } from './seed.js';
11
+ export function generateApiFiles(model) {
12
+ return [
13
+ generateServerMain(model),
14
+ generateModels(model),
15
+ ...model.entities.flatMap((entity) => [
16
+ generateDatabase(model, entity),
17
+ generateRouter(model, entity),
18
+ ]),
19
+ ];
20
+ }
21
+ // =============================================================================
22
+ // Server Entry Point
23
+ // =============================================================================
24
+ function generateServerMain(model) {
25
+ const routerImports = model.entities
26
+ .map((e) => {
27
+ const snakeName = toSnakeCase(e.pluralName);
28
+ return `from server.routers.${snakeName} import router as ${snakeName}_router`;
29
+ })
30
+ .join('\n');
31
+ const routerIncludes = model.entities
32
+ .map((e) => {
33
+ const snakeName = toSnakeCase(e.pluralName);
34
+ const apiRoute = '/api/' + toKebabCase(e.pluralName).toLowerCase();
35
+ return `app.include_router(${snakeName}_router, prefix="${apiRoute}")`;
36
+ })
37
+ .join('\n');
38
+ return {
39
+ path: 'server/main.py',
40
+ content: `from fastapi import FastAPI
41
+ from fastapi.middleware.cors import CORSMiddleware
42
+ ${routerImports}
43
+
44
+ app = FastAPI(title="${model.identity.name}")
45
+
46
+ app.add_middleware(
47
+ CORSMiddleware,
48
+ allow_origins=["*"],
49
+ allow_credentials=True,
50
+ allow_methods=["*"],
51
+ allow_headers=["*"],
52
+ )
53
+
54
+ ${routerIncludes}
55
+
56
+ if __name__ == "__main__":
57
+ import uvicorn
58
+ uvicorn.run("server.main:app", host="0.0.0.0", port=8000, reload=True)
59
+ `,
60
+ };
61
+ }
62
+ // =============================================================================
63
+ // Pydantic Models
64
+ // =============================================================================
65
+ function generateModels(model) {
66
+ const modelBlocks = [];
67
+ for (const entity of model.entities) {
68
+ const btoRels = belongsToRels(entity);
69
+ // Create model (no id/timestamps)
70
+ const createFields = [];
71
+ for (const field of entity.fields) {
72
+ const pyT = pyType(field);
73
+ if (field.required) {
74
+ createFields.push(` ${field.name}: ${pyT}`);
75
+ }
76
+ else {
77
+ createFields.push(` ${field.name}: Optional[${pyT}] = None`);
78
+ }
79
+ }
80
+ for (const rel of btoRels) {
81
+ const fk = fkFieldName(rel);
82
+ createFields.push(` ${fk}: int`);
83
+ }
84
+ // Update model (all Optional)
85
+ const updateFields = [];
86
+ for (const field of entity.fields) {
87
+ const pyT = pyType(field);
88
+ updateFields.push(` ${field.name}: Optional[${pyT}] = None`);
89
+ }
90
+ for (const rel of btoRels) {
91
+ const fk = fkFieldName(rel);
92
+ updateFields.push(` ${fk}: Optional[int] = None`);
93
+ }
94
+ // Response model (id + all fields + timestamps)
95
+ const responseFields = [' id: int'];
96
+ for (const field of entity.fields) {
97
+ const pyT = pyType(field);
98
+ if (field.required) {
99
+ responseFields.push(` ${field.name}: ${pyT}`);
100
+ }
101
+ else {
102
+ responseFields.push(` ${field.name}: Optional[${pyT}] = None`);
103
+ }
104
+ }
105
+ for (const rel of btoRels) {
106
+ const fk = fkFieldName(rel);
107
+ responseFields.push(` ${fk}: int`);
108
+ }
109
+ responseFields.push(' createdAt: str');
110
+ responseFields.push(' updatedAt: str');
111
+ modelBlocks.push(`
112
+ class ${entity.name}Create(BaseModel):
113
+ ${createFields.join('\n')}
114
+
115
+ class ${entity.name}Update(BaseModel):
116
+ ${updateFields.join('\n')}
117
+
118
+ class ${entity.name}Response(BaseModel):
119
+ ${responseFields.join('\n')}
120
+ model_config = ConfigDict(populate_by_name=True)`);
121
+ }
122
+ return {
123
+ path: 'server/models.py',
124
+ content: `from typing import Optional
125
+ from pydantic import BaseModel, ConfigDict
126
+ ${modelBlocks.join('\n')}
127
+ `,
128
+ };
129
+ }
130
+ // =============================================================================
131
+ // Database (Data Store)
132
+ // =============================================================================
133
+ function generateDatabase(model, entity) {
134
+ const snakeName = toSnakeCase(entity.pluralName);
135
+ // Collect searchable fields (string + enum types)
136
+ const searchableFields = entity.fields
137
+ .filter((f) => f.type === 'string' || f.type === 'enum')
138
+ .map((f) => `item.get("${f.name}", "")`);
139
+ const searchLogic = searchableFields.length > 0
140
+ ? ` q = search.lower()
141
+ result = [
142
+ item for item in result
143
+ if any(
144
+ q in str(v).lower()
145
+ for v in [${searchableFields.join(', ')}]
146
+ if v is not None
147
+ )
148
+ ]`
149
+ : ` q = search.lower()
150
+ result = [
151
+ item for item in result
152
+ if any(
153
+ q in str(v).lower()
154
+ for v in item.values()
155
+ if v is not None
156
+ )
157
+ ]`;
158
+ const seedData = generateSeedData(model, entity);
159
+ return {
160
+ path: `server/database/${snakeName}.py`,
161
+ content: `"""In-memory data store for ${entity.pluralName}."""
162
+
163
+ from datetime import datetime, timezone
164
+ from typing import Optional
165
+
166
+
167
+ ${seedData}
168
+
169
+
170
+ class DataStore:
171
+ def __init__(self):
172
+ self.items: list[dict] = [dict(item) for item in SEED_DATA]
173
+ self.next_id: int = len(self.items) + 1
174
+
175
+ def get_all(self) -> list[dict]:
176
+ return self.items
177
+
178
+ def get_by_id(self, item_id: int) -> Optional[dict]:
179
+ for item in self.items:
180
+ if item["id"] == item_id:
181
+ return item
182
+ return None
183
+
184
+ def query(
185
+ self,
186
+ search: Optional[str] = None,
187
+ filters: Optional[dict[str, str]] = None,
188
+ ) -> dict:
189
+ result = list(self.items)
190
+ if search:
191
+ ${searchLogic}
192
+ if filters:
193
+ for field, value in filters.items():
194
+ result = [
195
+ item for item in result
196
+ if str(item.get(field, "")) == value
197
+ ]
198
+ return {"data": result, "total": len(result)}
199
+
200
+ def create(self, data: dict) -> dict:
201
+ now = datetime.now(timezone.utc).isoformat()
202
+ item = {
203
+ **data,
204
+ "id": self.next_id,
205
+ "createdAt": now,
206
+ "updatedAt": now,
207
+ }
208
+ self.next_id += 1
209
+ self.items.append(item)
210
+ return item
211
+
212
+ def update(self, item_id: int, data: dict) -> Optional[dict]:
213
+ for i, item in enumerate(self.items):
214
+ if item["id"] == item_id:
215
+ now = datetime.now(timezone.utc).isoformat()
216
+ self.items[i] = {**item, **data, "id": item_id, "updatedAt": now}
217
+ return self.items[i]
218
+ return None
219
+
220
+ def remove(self, item_id: int) -> bool:
221
+ for i, item in enumerate(self.items):
222
+ if item["id"] == item_id:
223
+ self.items.pop(i)
224
+ return True
225
+ return False
226
+
227
+
228
+ store = DataStore()
229
+ `,
230
+ };
231
+ }
232
+ // =============================================================================
233
+ // Router
234
+ // =============================================================================
235
+ function generateRouter(model, entity) {
236
+ const snakeName = toSnakeCase(entity.pluralName);
237
+ const btoRels = belongsToRels(entity);
238
+ const hmRels = hasManyRels(entity);
239
+ // Imports for belongsTo populate
240
+ const populateImports = btoRels
241
+ .map((rel) => {
242
+ const targetEntity = model.entities.find((e) => e.name === rel.target);
243
+ if (!targetEntity)
244
+ return '';
245
+ const targetSnake = toSnakeCase(targetEntity.pluralName);
246
+ return `from server.database.${targetSnake} import store as ${targetSnake}_store`;
247
+ })
248
+ .filter(Boolean)
249
+ .join('\n');
250
+ // Imports for hasMany includes
251
+ const hasManyImports = hmRels
252
+ .map((rel) => {
253
+ const targetEntity = model.entities.find((e) => e.name === rel.target);
254
+ if (!targetEntity)
255
+ return '';
256
+ const targetSnake = toSnakeCase(targetEntity.pluralName);
257
+ return `from server.database.${targetSnake} import store as ${targetSnake}_store`;
258
+ })
259
+ .filter(Boolean)
260
+ .join('\n');
261
+ // Deduplicate imports (if same entity appears in both belongsTo and hasMany targets)
262
+ const allImports = [
263
+ ...new Set([...populateImports.split('\n'), ...hasManyImports.split('\n')].filter(Boolean)),
264
+ ].join('\n');
265
+ // Populate function for belongsTo
266
+ const populateLogic = btoRels.length > 0
267
+ ? `
268
+
269
+ def populate(item: dict) -> dict:
270
+ result = dict(item)
271
+ ${btoRels
272
+ .map((rel) => {
273
+ const fk = fkFieldName(rel);
274
+ const targetEntity = model.entities.find((e) => e.name === rel.target);
275
+ if (!targetEntity)
276
+ return '';
277
+ const targetSnake = toSnakeCase(targetEntity.pluralName);
278
+ const targetVar = rel.target.charAt(0).toLowerCase() + rel.target.slice(1);
279
+ return ` if result.get("${fk}"):
280
+ result["${targetVar}"] = ${targetSnake}_store.get_by_id(result["${fk}"])`;
281
+ })
282
+ .filter(Boolean)
283
+ .join('\n')}
284
+ return result
285
+ `
286
+ : '';
287
+ // GET list with optional populate
288
+ const getListReturn = btoRels.length > 0
289
+ ? ' return {"data": [populate(item) for item in result["data"]], "total": result["total"]}'
290
+ : ' return {"data": result["data"], "total": result["total"]}';
291
+ // GET by ID with ?include= for hasMany
292
+ let getByIdBody;
293
+ if (hmRels.length > 0) {
294
+ const includeChecks = hmRels
295
+ .map((rel) => {
296
+ const targetEntity = model.entities.find((e) => e.name === rel.target);
297
+ if (!targetEntity)
298
+ return '';
299
+ const targetVar = toCamelCase(targetEntity.pluralName);
300
+ const targetSnake = toSnakeCase(targetEntity.pluralName);
301
+ const fkRel = targetEntity.relationships.find((r) => r.type === 'belongsTo' && r.target === entity.name) ?? { type: 'belongsTo', target: entity.name };
302
+ const fk = fkFieldName(fkRel);
303
+ return ` if "${targetVar}" in include:
304
+ result["${targetVar}"] = [
305
+ r for r in ${targetSnake}_store.get_all()
306
+ if r.get("${fk}") == item["id"]
307
+ ]`;
308
+ })
309
+ .filter(Boolean)
310
+ .join('\n');
311
+ const populateExpr = btoRels.length > 0 ? 'populate(item)' : 'dict(item)';
312
+ getByIdBody = ` item = store.get_by_id(item_id)
313
+ if not item:
314
+ raise HTTPException(status_code=404, detail="Not found")
315
+ include_param = request.query_params.get("include", "")
316
+ include = [s.strip() for s in include_param.split(",") if s.strip()]
317
+ result = ${populateExpr}
318
+ ${includeChecks}
319
+ return result`;
320
+ }
321
+ else {
322
+ const getByIdReturn = btoRels.length > 0 ? ' return populate(item)' : ' return item';
323
+ getByIdBody = ` item = store.get_by_id(item_id)
324
+ if not item:
325
+ raise HTTPException(status_code=404, detail="Not found")
326
+ ${getByIdReturn}`;
327
+ }
328
+ // Determine if we need Request import
329
+ const needsRequest = hmRels.length > 0;
330
+ return {
331
+ path: `server/routers/${snakeName}.py`,
332
+ content: `import re
333
+ from fastapi import APIRouter, HTTPException${needsRequest ? ', Request' : ''}
334
+ from server.models import ${entity.name}Create, ${entity.name}Update
335
+ from server.database.${snakeName} import store
336
+ ${allImports ? allImports + '\n' : ''}
337
+ router = APIRouter()
338
+ ${populateLogic}
339
+
340
+ @router.get("/")
341
+ def list_items(request: Request):
342
+ search = request.query_params.get("search")
343
+ filters: dict[str, str] = {}
344
+ for key, value in request.query_params.items():
345
+ match = re.match(r"^filter\\[(.+)\\]$", key)
346
+ if match:
347
+ filters[match.group(1)] = value
348
+ result = store.query(search=search, filters=filters if filters else None)
349
+ ${getListReturn}
350
+
351
+
352
+ @router.get("/{item_id}")
353
+ def get_item(item_id: int${needsRequest ? ', request: Request' : ''}):
354
+ ${getByIdBody}
355
+
356
+
357
+ @router.post("/", status_code=201)
358
+ def create_item(body: ${entity.name}Create):
359
+ return store.create(body.model_dump())
360
+
361
+
362
+ @router.put("/{item_id}")
363
+ def update_item(item_id: int, body: ${entity.name}Update):
364
+ item = store.update(item_id, {k: v for k, v in body.model_dump().items() if v is not None})
365
+ if not item:
366
+ raise HTTPException(status_code=404, detail="Not found")
367
+ return item
368
+
369
+
370
+ @router.delete("/{item_id}", status_code=204)
371
+ def delete_item(item_id: int):
372
+ success = store.remove(item_id)
373
+ if not success:
374
+ raise HTTPException(status_code=404, detail="Not found")
375
+ return None
376
+ `,
377
+ };
378
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * React+FastAPI Toolkit — Configuration Files Generator
3
+ *
4
+ * Generates: package.json, vite.config.ts, tailwind.config.js,
5
+ * tsconfig.json, postcss.config.js, requirements.txt
6
+ */
7
+ import type { ApplicationModel } from '../../model/types.js';
8
+ import type { FactoryFile } from '../types.js';
9
+ export declare function generateConfigFiles(model: ApplicationModel): FactoryFile[];
@@ -0,0 +1,128 @@
1
+ /**
2
+ * React+FastAPI Toolkit — Configuration Files Generator
3
+ *
4
+ * Generates: package.json, vite.config.ts, tailwind.config.js,
5
+ * tsconfig.json, postcss.config.js, requirements.txt
6
+ */
7
+ import { toKebabCase } from '../../model/naming.js';
8
+ import { generateColorShades } from '../shared/color-utils.js';
9
+ export function generateConfigFiles(model) {
10
+ const appSlug = toKebabCase(model.identity.name);
11
+ return [
12
+ generatePackageJson(appSlug),
13
+ generateViteConfig(),
14
+ generateTailwindConfig(model),
15
+ generateTsConfig(),
16
+ generatePostCssConfig(),
17
+ generateRequirementsTxt(),
18
+ ];
19
+ }
20
+ function generatePackageJson(appSlug) {
21
+ const pkg = {
22
+ name: appSlug,
23
+ version: '0.1.0',
24
+ private: true,
25
+ type: 'module',
26
+ scripts: {
27
+ dev: 'concurrently "vite" "uvicorn server.main:app --reload --port 8000"',
28
+ build: 'vite build',
29
+ preview: 'vite preview',
30
+ server: 'uvicorn server.main:app --reload --port 8000',
31
+ },
32
+ dependencies: {
33
+ react: '^18.3.1',
34
+ 'react-dom': '^18.3.1',
35
+ 'react-router-dom': '^6.28.0',
36
+ },
37
+ devDependencies: {
38
+ '@types/react': '^18.3.12',
39
+ '@types/react-dom': '^18.3.1',
40
+ '@vitejs/plugin-react': '^4.3.4',
41
+ autoprefixer: '^10.4.20',
42
+ concurrently: '^9.1.0',
43
+ postcss: '^8.4.49',
44
+ '@tailwindcss/forms': '^0.5.9',
45
+ tailwindcss: '^3.4.15',
46
+ typescript: '^5.6.3',
47
+ vite: '^6.0.0',
48
+ },
49
+ };
50
+ return { path: 'package.json', content: JSON.stringify(pkg, null, 2) + '\n' };
51
+ }
52
+ function generateViteConfig() {
53
+ return {
54
+ path: 'vite.config.ts',
55
+ content: `import { defineConfig } from 'vite';
56
+ import react from '@vitejs/plugin-react';
57
+
58
+ export default defineConfig({
59
+ plugins: [react()],
60
+ server: {
61
+ proxy: {
62
+ '/api': 'http://localhost:8000',
63
+ },
64
+ },
65
+ });
66
+ `,
67
+ };
68
+ }
69
+ function generateTailwindConfig(model) {
70
+ return {
71
+ path: 'tailwind.config.js',
72
+ content: `import forms from '@tailwindcss/forms';
73
+
74
+ /** @type {import('tailwindcss').Config} */
75
+ export default {
76
+ content: ['./index.html', './src/**/*.{ts,tsx}'],
77
+ darkMode: 'class',
78
+ theme: {
79
+ extend: {
80
+ colors: {
81
+ primary: ${generateColorShades(model.theme.primaryColor)},
82
+ },
83
+ },
84
+ },
85
+ plugins: [forms],
86
+ };
87
+ `,
88
+ };
89
+ }
90
+ function generateTsConfig() {
91
+ const config = {
92
+ compilerOptions: {
93
+ target: 'ES2022',
94
+ module: 'ESNext',
95
+ moduleResolution: 'bundler',
96
+ jsx: 'react-jsx',
97
+ strict: true,
98
+ esModuleInterop: true,
99
+ skipLibCheck: true,
100
+ forceConsistentCasingInFileNames: true,
101
+ resolveJsonModule: true,
102
+ isolatedModules: true,
103
+ noEmit: true,
104
+ },
105
+ include: ['src'],
106
+ };
107
+ return { path: 'tsconfig.json', content: JSON.stringify(config, null, 2) + '\n' };
108
+ }
109
+ function generatePostCssConfig() {
110
+ return {
111
+ path: 'postcss.config.js',
112
+ content: `export default {
113
+ plugins: {
114
+ tailwindcss: {},
115
+ autoprefixer: {},
116
+ },
117
+ };
118
+ `,
119
+ };
120
+ }
121
+ function generateRequirementsTxt() {
122
+ return {
123
+ path: 'requirements.txt',
124
+ content: `fastapi>=0.115.0
125
+ uvicorn[standard]>=0.32.0
126
+ `,
127
+ };
128
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * React+FastAPI Toolkit — Helpers
3
+ *
4
+ * Python-specific helpers plus re-exports from shared helpers.
5
+ */
6
+ import type { Field } from '../../model/types.js';
7
+ /** Map a model FieldType to a Python type string. */
8
+ export declare function pyType(field: Field): string;
9
+ /** Generate a Python default value string for a field. */
10
+ export declare function pyDefault(field: Field): string;
11
+ /** Convert PascalCase or camelCase to snake_case. */
12
+ export declare function toSnakeCase(name: string): string;
13
+ export { indent, tsType, fkFieldName, belongsToRels, hasManyRels, routePath, apiPath, inputType, generateImports, } from '../shared/helpers.js';
@@ -0,0 +1,54 @@
1
+ /**
2
+ * React+FastAPI Toolkit — Helpers
3
+ *
4
+ * Python-specific helpers plus re-exports from shared helpers.
5
+ */
6
+ /** Map a model FieldType to a Python type string. */
7
+ export function pyType(field) {
8
+ const lowerName = field.name.toLowerCase();
9
+ switch (field.type) {
10
+ case 'string':
11
+ case 'enum':
12
+ case 'date':
13
+ return 'str';
14
+ case 'number':
15
+ if (lowerName.includes('price') ||
16
+ lowerName.includes('cost') ||
17
+ lowerName.includes('amount')) {
18
+ return 'float';
19
+ }
20
+ return 'int';
21
+ case 'boolean':
22
+ return 'bool';
23
+ default:
24
+ return 'str';
25
+ }
26
+ }
27
+ /** Generate a Python default value string for a field. */
28
+ export function pyDefault(field) {
29
+ if (!field.required)
30
+ return 'None';
31
+ switch (field.type) {
32
+ case 'string':
33
+ case 'date':
34
+ return "''";
35
+ case 'enum': {
36
+ const values = field.enumValues ?? [];
37
+ return values.length > 0 ? `'${values[0]}'` : "''";
38
+ }
39
+ case 'number':
40
+ return '0';
41
+ case 'boolean':
42
+ return 'False';
43
+ default:
44
+ return "''";
45
+ }
46
+ }
47
+ /** Convert PascalCase or camelCase to snake_case. */
48
+ export function toSnakeCase(name) {
49
+ return name
50
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
51
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1_$2')
52
+ .toLowerCase();
53
+ }
54
+ export { indent, tsType, fkFieldName, belongsToRels, hasManyRels, routePath, apiPath, inputType, generateImports, } from '../shared/helpers.js';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * React+FastAPI Toolkit
3
+ *
4
+ * Generates a full-stack MVP: React 18 + Vite + Tailwind + FastAPI (Python).
5
+ * Deterministic: same ApplicationModel -> same output files.
6
+ *
7
+ * Frontend is identical to react-node (same React components, Vite config, Tailwind).
8
+ * Backend is Python FastAPI with in-memory data stores.
9
+ */
10
+ import type { FactoryToolkit } from '../types.js';
11
+ export declare const reactFastapiToolkit: FactoryToolkit;