create-projx 1.5.2 → 1.5.4

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 (3) hide show
  1. package/README.md +22 -19
  2. package/dist/index.js +136 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -66,6 +66,7 @@ If this saves you even one hour, it's already paid for itself. (It's free.)
66
66
  ## Why teams pick Projx and stay
67
67
 
68
68
  - **It actually runs.** Every template is tested in CI before release. No "looks right" surprises.
69
+ - **Tests ship with the code.** `gen entity` writes integration tests alongside every model — 11 tests per entity, against a real database. You start green, not scrambling.
69
70
  - **Auto-entity pattern.** Define a data model, get CRUD routes, validation, OpenAPI docs, and a typed UI for free. Backend, frontend, and mobile all stay in sync.
70
71
  - **Updates don't nuke your code.** `projx update` does a 3-tier merge — your custom controllers, pages, and config survive template upgrades. No rewrites.
71
72
  - **No lock-in.** Projx generates files and walks away. Delete the `.projx` config and it's just a normal repo.
@@ -96,7 +97,7 @@ Projx is a shared operating system for teams that ship with both:
96
97
  - **AI agents** get a deterministic CLI to call instead of guessing folder structures.
97
98
  - **Teams** get consistency across repos, less drift, and fewer surprise refactors.
98
99
 
99
- If you're an AI agent reading this, see [SKILL.md](SKILL.md) for trigger conditions, decision rules, and the canonical invocation. Short version:
100
+ If you're an AI agent reading this, see [SKILL.md](SKILL.md) for trigger conditions, decision rules, and the canonical invocation.
100
101
 
101
102
  ## Quick Start
102
103
 
@@ -188,7 +189,7 @@ To skip root-level files (docker-compose, README), add `skip` to `.projx`:
188
189
 
189
190
  ```json
190
191
  {
191
- "version": "1.4.2",
192
+ "version": "1.5.2",
192
193
  "components": ["fastapi", "frontend"],
193
194
  "skip": ["docker-compose.yml", "README.md"]
194
195
  }
@@ -273,12 +274,14 @@ When both `fastapi` and `fastify` exist, the entity generates in the **primary b
273
274
 
274
275
  Override with `--ai` (fastapi) or `--backend` (fastify).
275
276
 
276
- | Component | Generated |
277
- | ------------------------- | --------------------------------------------------------------------------- |
278
- | Primary backend (fastapi) | `src/entities/<name>/_model.py` — auto-discovered by registry |
279
- | Primary backend (fastify) | `src/modules/<name>/schemas.ts` + `index.ts` + Prisma model + app.ts import |
280
- | `frontend` | `src/types/<name>.ts` — TypeScript interface + Create/Update variants |
281
- | `mobile` | `lib/entities/<name>/model.dart` — Dart class with fromJson/toJson/copyWith |
277
+ | Component | Generated |
278
+ | ------------------------- | --------------------------------------------------------------------------------------------- |
279
+ | Primary backend (fastapi) | `src/entities/<name>/_model.py` + `tests/test_<name>_entity.py` model + 11 CRUD/auth tests |
280
+ | Primary backend (fastify) | `src/modules/<name>/schemas.ts` + `index.ts` + Prisma model + `tests/modules/<name>.test.ts` |
281
+ | `frontend` | `src/types/<name>.ts` — TypeScript interface + Create/Update variants |
282
+ | `mobile` | `lib/entities/<name>/model.dart` — Dart class with fromJson/toJson/copyWith |
283
+
284
+ **Tests included**: every `gen entity` writes a working integration test file alongside the model — 11 tests for FastAPI (extending `BaseEntityApiTest`), 11 tests for Fastify (via `describeCrudEntity`). Both run against a real database (Postgres for Fastify, SQLite-in-memory for FastAPI today). New entities ship green from day one — no scrambling to bolt on tests at go-live.
282
285
 
283
286
  No migrations — run `alembic revision --autogenerate` or `prisma migrate dev` (via your package manager) when ready.
284
287
 
@@ -362,16 +365,6 @@ npm test # run tests
362
365
  npm run build # build CLI
363
366
  ```
364
367
 
365
- ## Badge
366
-
367
- Add this to your project's README:
368
-
369
- ```md
370
- [![Built with Projx](https://img.shields.io/badge/Built%20with-Projx-blue)](https://github.com/ukanhaupa/projx)
371
- ```
372
-
373
- ---
374
-
375
368
  ## Try it now
376
369
 
377
370
  You're still reading. Stop reading. Run this:
@@ -382,7 +375,17 @@ npx create-projx my-app
382
375
 
383
376
  Pick whatever you need from the menu — backend-only, AI app, mobile, full-stack, just infra. 30 seconds. Free. No signup. If you don't like it, `rm -rf my-app` and we never speak of this again.
384
377
 
385
- Star the repo if it saved you time → [github.com/ukanhaupa/projx](https://github.com/ukanhaupa/projx)
378
+ ---
379
+
380
+ ## Badge
381
+
382
+ Add this to your project's README:
383
+
384
+ ```md
385
+ [![Built with Projx](https://img.shields.io/badge/Built%20with-Projx-blue)](https://github.com/ukanhaupa/projx)
386
+ ```
387
+
388
+ ---
386
389
 
387
390
  ## License
388
391
 
package/dist/index.js CHANGED
@@ -2697,6 +2697,128 @@ function generateDartModel(config) {
2697
2697
  lines.push("");
2698
2698
  return lines.join("\n");
2699
2699
  }
2700
+ function pyHttpLiteral(type, variant = "create") {
2701
+ switch (type) {
2702
+ case "string":
2703
+ case "text":
2704
+ return variant === "create" ? '"sample text"' : variant === "update" ? '"updated text"' : '"alt text"';
2705
+ case "number":
2706
+ return variant === "create" ? "42" : variant === "update" ? "100" : "7";
2707
+ case "boolean":
2708
+ return variant === "create" ? "True" : "False";
2709
+ case "date":
2710
+ return variant === "alt" ? '"2026-02-01"' : '"2026-01-01"';
2711
+ case "datetime":
2712
+ return variant === "alt" ? '"2026-02-01T00:00:00"' : '"2026-01-01T00:00:00"';
2713
+ case "json":
2714
+ return "{}";
2715
+ }
2716
+ }
2717
+ function pyOrmLiteral(type, variant = "create") {
2718
+ switch (type) {
2719
+ case "string":
2720
+ case "text":
2721
+ return variant === "create" ? '"sample text"' : variant === "update" ? '"updated text"' : '"alt text"';
2722
+ case "number":
2723
+ return variant === "create" ? "42" : variant === "update" ? "100" : "7";
2724
+ case "boolean":
2725
+ return variant === "create" ? "True" : "False";
2726
+ case "date":
2727
+ return variant === "alt" ? "date(2026, 2, 1)" : "date(2026, 1, 1)";
2728
+ case "datetime":
2729
+ return variant === "alt" ? "datetime(2026, 2, 1, 0, 0, 0)" : "datetime(2026, 1, 1, 0, 0, 0)";
2730
+ case "json":
2731
+ return "{}";
2732
+ }
2733
+ }
2734
+ function tsLiteral(type, variant = "create") {
2735
+ switch (type) {
2736
+ case "string":
2737
+ case "text":
2738
+ return variant === "create" ? "'sample text'" : variant === "update" ? "'updated text'" : "'alt text'";
2739
+ case "number":
2740
+ return variant === "create" ? "42" : variant === "update" ? "100" : "7";
2741
+ case "boolean":
2742
+ return variant === "create" ? "true" : "false";
2743
+ case "date":
2744
+ return variant === "alt" ? "'2026-02-01'" : "'2026-01-01'";
2745
+ case "datetime":
2746
+ return variant === "alt" ? "'2026-02-01T00:00:00.000Z'" : "'2026-01-01T00:00:00.000Z'";
2747
+ case "json":
2748
+ return "{}";
2749
+ }
2750
+ }
2751
+ function pickFilterField(fields) {
2752
+ return fields.find((f) => f.type === "string" || f.type === "text") ?? fields.find((f) => f.type === "number") ?? fields.find((f) => f.type === "boolean") ?? fields[0];
2753
+ }
2754
+ function generateFastapiTest(config) {
2755
+ const className = toPascal(config.name);
2756
+ const snake = toSnake(config.name);
2757
+ const apiUrl = `/api/v1${config.apiPrefix}/`;
2758
+ const filterField = pickFilterField(config.fields);
2759
+ const needsDate = config.fields.some((f) => f.type === "date");
2760
+ const needsDatetime = config.fields.some((f) => f.type === "datetime");
2761
+ const dateImports = [];
2762
+ if (needsDate) dateImports.push("date");
2763
+ if (needsDatetime) dateImports.push("datetime");
2764
+ const lines = [];
2765
+ if (dateImports.length > 0) {
2766
+ lines.push(`from datetime import ${dateImports.join(", ")}`);
2767
+ lines.push("");
2768
+ }
2769
+ lines.push(`from src.entities.${snake} import ${className}`);
2770
+ lines.push(`from tests.base_entity_api_test import BaseEntityApiTest`);
2771
+ lines.push("");
2772
+ lines.push("");
2773
+ lines.push(`class Test${className}Entity(BaseEntityApiTest):`);
2774
+ lines.push(` __test__ = True`);
2775
+ lines.push(` endpoint = "${apiUrl}"`);
2776
+ lines.push(` create_payload = {`);
2777
+ for (const f of config.fields) {
2778
+ lines.push(` "${f.name}": ${pyHttpLiteral(f.type, "create")},`);
2779
+ }
2780
+ lines.push(` }`);
2781
+ const updateField = config.fields[0];
2782
+ lines.push(` update_payload = {"${updateField.name}": ${pyHttpLiteral(updateField.type, "update")}}`);
2783
+ lines.push(` invalid_payload: dict = {}`);
2784
+ lines.push(` filter_field = "${filterField.name}"`);
2785
+ lines.push(` filter_value = ${pyHttpLiteral(filterField.type, "create")}`);
2786
+ lines.push(` other_filter_value = ${pyHttpLiteral(filterField.type, "alt")}`);
2787
+ lines.push("");
2788
+ lines.push(` def make_model(self, index: int, **overrides):`);
2789
+ lines.push(` data = {`);
2790
+ for (const f of config.fields) {
2791
+ lines.push(` "${f.name}": ${pyOrmLiteral(f.type, "create")},`);
2792
+ }
2793
+ lines.push(` }`);
2794
+ lines.push(` data.update(overrides)`);
2795
+ lines.push(` return ${className}(**data)`);
2796
+ lines.push("");
2797
+ return lines.join("\n");
2798
+ }
2799
+ function generateFastifyTest(config) {
2800
+ const className = toPascal(config.name);
2801
+ const basePath = `/api/v1${config.apiPrefix}`;
2802
+ const updateField = config.fields[0];
2803
+ const lines = [];
2804
+ lines.push(`import { describeCrudEntity } from '../helpers/crud-test-base.js';`);
2805
+ lines.push("");
2806
+ lines.push(`describeCrudEntity({`);
2807
+ lines.push(` entityName: '${className}',`);
2808
+ lines.push(` basePath: '${basePath}',`);
2809
+ lines.push(` prismaModel: '${className}',`);
2810
+ lines.push(` createPayload: {`);
2811
+ for (const f of config.fields) {
2812
+ lines.push(` ${f.name}: ${tsLiteral(f.type, "create")},`);
2813
+ }
2814
+ lines.push(` },`);
2815
+ lines.push(` updatePayload: {`);
2816
+ lines.push(` ${updateField.name}: ${tsLiteral(updateField.type, "update")},`);
2817
+ lines.push(` },`);
2818
+ lines.push(`});`);
2819
+ lines.push("");
2820
+ return lines.join("\n");
2821
+ }
2700
2822
  async function resolvePrimaryBackend(cwd, hasFastapi, hasFastify, backendFlag) {
2701
2823
  if (backendFlag) return backendFlag;
2702
2824
  if (hasFastapi && !hasFastify) return "fastapi";
@@ -2780,7 +2902,15 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
2780
2902
  } else {
2781
2903
  await mkdir5(entityDir, { recursive: true });
2782
2904
  await writeFile5(join12(entityDir, "_model.py"), generateFastAPIModel(config));
2905
+ await writeFile5(join12(entityDir, "__init__.py"), "from ._model import *\n");
2783
2906
  generated.push(`${dir}/src/entities/${toSnake(config.name)}/_model.py`);
2907
+ generated.push(`${dir}/src/entities/${toSnake(config.name)}/__init__.py`);
2908
+ const testsDir = join12(cwd, dir, "tests");
2909
+ const testFile = join12(testsDir, `test_${toSnake(config.name)}_entity.py`);
2910
+ if (existsSync11(testsDir) && !existsSync11(testFile)) {
2911
+ await writeFile5(testFile, generateFastapiTest(config));
2912
+ generated.push(`${dir}/tests/test_${toSnake(config.name)}_entity.py`);
2913
+ }
2784
2914
  }
2785
2915
  }
2786
2916
  if (genFastify) {
@@ -2820,6 +2950,12 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
2820
2950
  generated.push(`${dir}/prisma/schema.prisma (model added)`);
2821
2951
  }
2822
2952
  }
2953
+ const testsModulesDir = join12(cwd, dir, "tests/modules");
2954
+ const fastifyTestFile = join12(testsModulesDir, `${toKebab(config.name)}.test.ts`);
2955
+ if (existsSync11(testsModulesDir) && !existsSync11(fastifyTestFile)) {
2956
+ await writeFile5(fastifyTestFile, generateFastifyTest(config));
2957
+ generated.push(`${dir}/tests/modules/${toKebab(config.name)}.test.ts`);
2958
+ }
2823
2959
  }
2824
2960
  }
2825
2961
  if (hasFrontend) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-projx",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
4
4
  "description": "Scaffold production-grade fullstack projects in seconds. FastAPI, Fastify, React, Flutter, Terraform — with auth, database, CI/CD, E2E tests, and Docker. One command, ready to deploy.",
5
5
  "type": "module",
6
6
  "bin": {