gazan-init 0.1.0 → 0.1.1
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.
- package/README.md +761 -41
- package/package.json +1 -1
- package/src/cli/commands/init.js +10 -3
- package/src/config/normalize.js +19 -1
- package/src/generators/packageJson.js +1 -1
- package/src/prompts/index.js +15 -5
- package/src/utils/strings.js +18 -0
package/README.md
CHANGED
|
@@ -93,7 +93,7 @@ GAZAN asks its questions in order, then generates the project:
|
|
|
93
93
|
```text
|
|
94
94
|
┌ GAZAN — backend project initializer
|
|
95
95
|
│
|
|
96
|
-
◇ Project name
|
|
96
|
+
◇ Project name/path
|
|
97
97
|
│ my-api
|
|
98
98
|
│
|
|
99
99
|
◇ Which module system do you want?
|
|
@@ -156,7 +156,73 @@ Next steps:
|
|
|
156
156
|
```
|
|
157
157
|
|
|
158
158
|
If the target directory already has content in it (beyond a stray `.git`), GAZAN asks before
|
|
159
|
-
touching anything — see
|
|
159
|
+
touching anything — see **Existing directories** under [Troubleshooting](#troubleshooting).
|
|
160
|
+
|
|
161
|
+
## Project name and output path
|
|
162
|
+
|
|
163
|
+
The first prompt (`Project name/path`) answers two questions at once: **where** the project is
|
|
164
|
+
written, and **what it's called**. Both are derived safely with Node's `path` APIs — never by
|
|
165
|
+
concatenating strings — so every form below behaves predictably:
|
|
166
|
+
|
|
167
|
+
| You type | Where GAZAN generates | `package.json` `name` |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| `my-api` | `<cwd>/my-api/` (created) | `my-api` |
|
|
170
|
+
| `.` | **directly inside the current directory** — nothing nested | basename of the current directory |
|
|
171
|
+
| `./backend` | `<cwd>/backend/` | `backend` |
|
|
172
|
+
| `../backend` | one directory up from `<cwd>`, named `backend` | `backend` |
|
|
173
|
+
| `/abs/path/to/api` | that exact absolute path | `api` |
|
|
174
|
+
|
|
175
|
+
**The project name always comes from the resolved output directory's basename, not the literal
|
|
176
|
+
text you typed.** This is what makes `.` work correctly:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
mkdir my-api
|
|
180
|
+
cd my-api
|
|
181
|
+
gazan init
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
```text
|
|
185
|
+
Project name/path: .
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
generates directly into `my-api/`:
|
|
189
|
+
|
|
190
|
+
```text
|
|
191
|
+
my-api/
|
|
192
|
+
├── src/
|
|
193
|
+
├── package.json # { "name": "my-api", ... }
|
|
194
|
+
├── README.md
|
|
195
|
+
├── .env.example
|
|
196
|
+
└── ...
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
GAZAN never creates `my-api/./`, `my-api/my-api/`, or any other nested duplicate — `.` resolves
|
|
200
|
+
to `process.cwd()` itself (via `path.resolve`), and the *directory* GAZAN is about to write into
|
|
201
|
+
is what names the project, not the string that happened to be typed. This also means a directory
|
|
202
|
+
name that isn't a valid npm package name (uppercase letters, spaces, dots — e.g. a folder called
|
|
203
|
+
`My Api`) is automatically sanitized into a valid `package.json` name (`my-api`) without changing
|
|
204
|
+
the actual folder name on disk or the human-readable title used in the generated `README.md`.
|
|
205
|
+
|
|
206
|
+
There is currently no separate "application name" prompt distinct from the output path — if you
|
|
207
|
+
need `package.json`'s `name` to differ from the directory GAZAN writes into, rename the directory
|
|
208
|
+
(or edit `name` in the generated `package.json`) after generation.
|
|
209
|
+
|
|
210
|
+
### Current-directory safety
|
|
211
|
+
|
|
212
|
+
Selecting `.` does not bypass GAZAN's normal existing-directory safety check (see **Existing
|
|
213
|
+
directories** under [Troubleshooting](#troubleshooting)) — it applies identically whether the
|
|
214
|
+
target is a brand-new folder or the directory you're already standing in:
|
|
215
|
+
|
|
216
|
+
```text
|
|
217
|
+
? Current directory is not empty.
|
|
218
|
+
❯ Cancel
|
|
219
|
+
Continue (files may be overwritten)
|
|
220
|
+
Use another directory
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Nothing is overwritten until you explicitly choose "Continue," and generation itself always
|
|
224
|
+
happens in a temporary directory first, only copied into place once it fully succeeds — so a
|
|
225
|
+
mid-generation failure never leaves your current directory partially written.
|
|
160
226
|
|
|
161
227
|
## Generated project structure
|
|
162
228
|
|
|
@@ -238,6 +304,55 @@ Controllers and services are classes. A controller is constructed with a service
|
|
|
238
304
|
the database. Routes stay thin — they wire a validator and a controller method together and
|
|
239
305
|
nothing else.
|
|
240
306
|
|
|
307
|
+
## How GAZAN works
|
|
308
|
+
|
|
309
|
+
Step by step, in the order the CLI actually executes them:
|
|
310
|
+
|
|
311
|
+
1. You run `gazan init` (or `npx gazan-init init` / `node bin/gazan.js init` from a clone).
|
|
312
|
+
2. GAZAN checks your Node.js version first and exits with a clear message if it's below 18.
|
|
313
|
+
3. You answer the prompts, in order: **project name/path** → **module system** (CJS/MJS) →
|
|
314
|
+
**language** (JS/TS) → **module aliases** (on/off) → **database** → **architecture** (MVC/HMVC)
|
|
315
|
+
→ **use `src/`?** → **Socket.IO?** → **BullMQ?** → **Redis for rate limiting?** (skipped
|
|
316
|
+
automatically if BullMQ is enabled, since BullMQ already requires Redis) → **authentication**
|
|
317
|
+
(enabled, then which methods — multi-select) → **do you have an `entity.json`?**
|
|
318
|
+
4. If you answered yes to `entity.json`, GAZAN asks for its path and validates it immediately,
|
|
319
|
+
right there in the prompt flow — an invalid file gives you a chance to fix the path and retry
|
|
320
|
+
before anything else happens.
|
|
321
|
+
5. GAZAN resolves your project name/path answer into an output directory (see
|
|
322
|
+
[Project name and output path](#project-name-and-output-path)) and checks it for existing
|
|
323
|
+
content, asking before touching a non-empty directory.
|
|
324
|
+
6. Every answer is normalized into one configuration object — the single shape every generator
|
|
325
|
+
reads from (generators never see raw prompt answers).
|
|
326
|
+
7. If an `entity.json` path was given, it's read and validated again here, against the actual
|
|
327
|
+
selected database (some rules, like MongoDB's primary-key restriction, depend on which database
|
|
328
|
+
you picked).
|
|
329
|
+
8. GAZAN generates the whole project into a temporary directory — not your real target directory
|
|
330
|
+
yet.
|
|
331
|
+
9. The base folder skeleton, `package.json`, `.gitignore`, and (TypeScript only) `tsconfig.json`
|
|
332
|
+
are written.
|
|
333
|
+
10. Environment validation, error classes, and graceful-shutdown scaffolding are generated.
|
|
334
|
+
11. Security middleware is generated — rate limiting, centralized error/not-found handlers, and
|
|
335
|
+
the Zod request-validation middleware (Helmet/CORS are wired directly into `app.js`).
|
|
336
|
+
12. Database artifacts are generated for the selected backend — a Prisma schema, a Mongoose
|
|
337
|
+
connection module, or a native MongoDB client — or nothing at all for "No database" (see
|
|
338
|
+
[Database support](#database-support)).
|
|
339
|
+
13. Redis, BullMQ, and Socket.IO scaffolding are generated, each strictly only if you selected it.
|
|
340
|
+
14. Authentication helpers and middleware are generated for each method you selected.
|
|
341
|
+
15. If you provided an `entity.json`, its models are turned into real code: Mongoose model files
|
|
342
|
+
(Prisma's models already live in the one `schema.prisma` written in step 12), Zod validators,
|
|
343
|
+
services, controllers, and routes — then the aggregate routes file and the `app`/`server`
|
|
344
|
+
entry files are (re)generated to wire everything together.
|
|
345
|
+
16. Module aliases are generated — `tsconfig.json` paths, `jsconfig.json`, `module-alias` config,
|
|
346
|
+
or the ESM loader, depending on your language/module-system combination — only if aliases are
|
|
347
|
+
enabled.
|
|
348
|
+
17. The generated project's own `README.md` is written last, reflecting exactly what was produced
|
|
349
|
+
(see [Generated project README](#generated-project-structure)).
|
|
350
|
+
18. Only once every step above succeeds is the temporary directory copied into your real target
|
|
351
|
+
directory. A failure at any point leaves that directory exactly as it was before you ran
|
|
352
|
+
`gazan init`.
|
|
353
|
+
19. GAZAN prints a checklist of what it did, any warnings worth reading (e.g. the OAuth stub, or
|
|
354
|
+
the MJS+JS experimental loader notice), and the exact next commands to run.
|
|
355
|
+
|
|
241
356
|
## `entity.json`
|
|
242
357
|
|
|
243
358
|
An optional JSON file you can point GAZAN at during `init`. When provided, it becomes the source
|
|
@@ -256,6 +371,72 @@ models[1].relations.author.model:
|
|
|
256
371
|
unknown model 'Userr'. Did you mean 'User'?
|
|
257
372
|
```
|
|
258
373
|
|
|
374
|
+
### Model definition
|
|
375
|
+
|
|
376
|
+
Every entry in `models` needs a `name` and a non-empty `fields` object:
|
|
377
|
+
|
|
378
|
+
```json
|
|
379
|
+
{ "name": "User", "fields": {} }
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
`name`:
|
|
383
|
+
|
|
384
|
+
- **Required**, a string.
|
|
385
|
+
- **Must be a valid identifier** — letters and digits only, starting with a letter
|
|
386
|
+
(`/^[A-Za-z][A-Za-z0-9]*$/`). This is what actually blocks path traversal and unsafe filesystem
|
|
387
|
+
paths — a name like `../User` or `User/../../etc` fails this regex before it can ever reach a
|
|
388
|
+
filename (see [Invalid examples](#invalid-examples)).
|
|
389
|
+
- **Cannot be a JS reserved word** (`class`, `constructor`, `function`, `this`, `export`, …) or —
|
|
390
|
+
for PostgreSQL/Prisma specifically — a Prisma schema keyword (`model`, `datasource`, `generator`,
|
|
391
|
+
`enum`, `type`, `view`).
|
|
392
|
+
- **Must be unique** across the file.
|
|
393
|
+
|
|
394
|
+
**Naming conventions** — GAZAN derives every generated identifier from `name` using the same
|
|
395
|
+
casing rules everywhere, so `User`, `BlogPost`, and `UserProfile` normalize consistently:
|
|
396
|
+
|
|
397
|
+
| `name` | Prisma model / class | camelCase (var/service instance) | kebab-case (file names) | snake_case (table) | Table name (pluralized) | Route path (pluralized) |
|
|
398
|
+
|---|---|---|---|---|---|---|
|
|
399
|
+
| `User` | `User` | `user` | `user` | `user` | `users` | `/api/users` |
|
|
400
|
+
| `BlogPost` | `BlogPost` | `blogPost` | `blog-post` | `blog_post` | `blog_posts` | `/api/blog-posts` |
|
|
401
|
+
| `UserProfile` | `UserProfile` | `userProfile` | `user-profile` | `user_profile` | `user_profiles` | `/api/user-profiles` |
|
|
402
|
+
|
|
403
|
+
Generated file names always follow the kebab-case form: `blog-post.service.ts`,
|
|
404
|
+
`blog-post.controller.ts`, etc. Override the table/collection name with `tableName` if you don't
|
|
405
|
+
want the auto-pluralized default.
|
|
406
|
+
|
|
407
|
+
### Providing entity.json
|
|
408
|
+
|
|
409
|
+
GAZAN asks for this during `init`, after the authentication prompt:
|
|
410
|
+
|
|
411
|
+
```text
|
|
412
|
+
◇ Do you have an entity.json file?
|
|
413
|
+
│ Yes
|
|
414
|
+
│
|
|
415
|
+
◇ Enter entity.json path
|
|
416
|
+
│ ./entity.json
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
- **Both relative and absolute paths work.** A relative path (`./entity.json`, `entity.json`,
|
|
420
|
+
`../shared/entity.json`) is resolved against the directory you ran `gazan init` from
|
|
421
|
+
(`path.resolve(process.cwd(), yourInput)`), same as the project name/path prompt.
|
|
422
|
+
- **It's parsed and validated immediately, in the prompt itself** — not deferred to generation
|
|
423
|
+
time. GAZAN reads the file, `JSON.parse`s it, runs it through the schema (structural) and then
|
|
424
|
+
semantic (cross-field, cross-model) validators, and reports success or a detailed error right
|
|
425
|
+
there.
|
|
426
|
+
- **If the file doesn't exist**, you get `No file exists at: <resolved absolute path>` and are
|
|
427
|
+
asked whether to try a different path.
|
|
428
|
+
- **If it exists but isn't valid JSON**, you get `entity.json is not valid JSON` plus the
|
|
429
|
+
underlying `JSON.parse` error message.
|
|
430
|
+
- **If it's valid JSON but fails schema or semantic validation**, you get every failing field
|
|
431
|
+
listed with its exact path and reason (see [Invalid examples](#invalid-examples)) — never a
|
|
432
|
+
single generic "invalid entity.json".
|
|
433
|
+
- **Retrying** — on any validation failure you're asked "Try a different path?"; declining
|
|
434
|
+
continues `init` with no `entity.json` (generic CRUD scaffolding, no entities) rather than
|
|
435
|
+
aborting the whole run.
|
|
436
|
+
- A validated `entity.json` is re-parsed once more right before generation, against the database
|
|
437
|
+
you actually selected — some rules (MongoDB's primary-key restriction, for example) depend on
|
|
438
|
+
which database is in play, not just the file's own contents.
|
|
439
|
+
|
|
259
440
|
### Basic example
|
|
260
441
|
|
|
261
442
|
```json
|
|
@@ -360,26 +541,60 @@ against both Prisma and Mongoose.
|
|
|
360
541
|
|
|
361
542
|
### Field types
|
|
362
543
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
|
368
|
-
|
|
369
|
-
| `
|
|
370
|
-
| `
|
|
371
|
-
| `
|
|
372
|
-
| `
|
|
373
|
-
| `
|
|
374
|
-
| `
|
|
375
|
-
| `
|
|
376
|
-
| `
|
|
377
|
-
| `
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
544
|
+
`entity.json` supports exactly 13 field types — this list comes straight from the parser's schema
|
|
545
|
+
(`FIELD_TYPES` in `src/parser/entity/schema.js`); any other string is rejected before generation
|
|
546
|
+
even starts (see [Invalid examples](#invalid-examples)).
|
|
547
|
+
|
|
548
|
+
| Type | Description | PostgreSQL (Prisma) | MongoDB (Mongoose) | MongoDB (native driver) |
|
|
549
|
+
|---|---|---|---|---|
|
|
550
|
+
| `string` | Short text | `String` | `String` | — |
|
|
551
|
+
| `text` | Long text | `String` | `String` | — |
|
|
552
|
+
| `number` | Generic number | `Float` | `Number` | — |
|
|
553
|
+
| `integer` | Whole number | `Int` | `Number` | — |
|
|
554
|
+
| `float` | Floating point | `Float` | `Number` | — |
|
|
555
|
+
| `boolean` | True/false | `Boolean` | `Boolean` | — |
|
|
556
|
+
| `date` | Calendar date | `DateTime` | `Date` | — |
|
|
557
|
+
| `datetime` | Date + time | `DateTime` | `Date` | — |
|
|
558
|
+
| `uuid` | UUID string | `String` (`@default(uuid())` when `default: "uuid"`) | `String` | — |
|
|
559
|
+
| `json` | Arbitrary JSON | `Json` | `Mixed` | — |
|
|
560
|
+
| `enum` | Fixed set of values (needs `values`) | Generated Prisma `enum` | `String` with a schema-level enum validator | — |
|
|
561
|
+
| `decimal` | Fixed-precision number | `Decimal` | `Decimal128` | — |
|
|
562
|
+
| `bigint` | Large integer | `BigInt` | `Mixed` (Mongoose has no dedicated bigint type) | — |
|
|
563
|
+
|
|
564
|
+
**The native MongoDB driver has no schema layer at all.** `entity.json` is still validated
|
|
565
|
+
identically regardless of which MongoDB mode you pick, but the native-driver generator doesn't
|
|
566
|
+
emit a per-type mapping, model file, or field definitions — the driver returns/accepts plain JS
|
|
567
|
+
objects, and the generated services (see [Entity-generated application code](#entity-generated-application-code))
|
|
568
|
+
just read/write whatever shape you give them. Field-level constraints (`required`, `unique`,
|
|
569
|
+
`min`/`max`, `length`, `enum` values, …) are enforced by the generated **Zod validator** at the
|
|
570
|
+
route boundary either way — that part doesn't depend on the database backend.
|
|
571
|
+
|
|
572
|
+
### Primary keys
|
|
573
|
+
|
|
574
|
+
Every model has exactly one primary key — either declared explicitly with `primaryKey: true`, or
|
|
575
|
+
synthesized automatically if you don't declare one:
|
|
576
|
+
|
|
577
|
+
```json
|
|
578
|
+
"id": { "type": "uuid", "primaryKey": true, "default": "uuid" }
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
- **At most one field per model may set `primaryKey: true`** — a second one is rejected
|
|
582
|
+
(`model 'X' declares more than one primaryKey field`).
|
|
583
|
+
- **A primary key field cannot be `nullable: true`** — rejected with
|
|
584
|
+
`primary key field 'id' cannot be nullable`.
|
|
585
|
+
- **If no field declares `primaryKey: true`**, GAZAN synthesizes one for you: a field named `id`,
|
|
586
|
+
`type: "uuid"`, `default: "uuid"`, `required: true` — the same shape as the explicit example
|
|
587
|
+
above. You never need to declare a primary key by hand unless you want a different type
|
|
588
|
+
(PostgreSQL only) or a different name.
|
|
589
|
+
- **PostgreSQL/Prisma** — any field type is allowed as a primary key; `autoIncrement: true` is
|
|
590
|
+
valid on `integer`/`bigint` primary keys (`@default(autoincrement())`), and `type: "uuid"` with
|
|
591
|
+
`default: "uuid"` generates `@default(uuid())`.
|
|
592
|
+
- **MongoDB (Mongoose or native)** — the primary key must be `type: "uuid"` **or omitted
|
|
593
|
+
entirely** (falling back to the synthesized UUID `id` above). Every Mongo primary key is routed
|
|
594
|
+
through Mongo's native `_id` field itself, rather than a redundant parallel `id` column, with a
|
|
595
|
+
generated UUID-string default. Any other declared type — and `autoIncrement`, which Mongo has no
|
|
596
|
+
native equivalent for — is rejected at validation time rather than silently approximated (see
|
|
597
|
+
[Invalid examples](#invalid-examples)).
|
|
383
598
|
|
|
384
599
|
### Field attributes
|
|
385
600
|
|
|
@@ -396,9 +611,37 @@ for) is rejected at validation time rather than silently approximated.
|
|
|
396
611
|
| `min` / `max` | Numeric bounds, enforced in the generated Zod validator (and Mongoose `min`/`max`). |
|
|
397
612
|
| `values` | Required for `type: "enum"` — the list of allowed values. |
|
|
398
613
|
|
|
614
|
+
### Required vs nullable
|
|
615
|
+
|
|
616
|
+
These are two different questions, and `entity.json` keeps them separate:
|
|
617
|
+
|
|
618
|
+
- **`required`** — must the caller supply this field when creating a record? It's enforced by the
|
|
619
|
+
generated **Zod validator** at the route boundary (`.optional()` is appended to the field's Zod
|
|
620
|
+
expression when `required` is `false`) — this is a request-validation concern.
|
|
621
|
+
- **`nullable`** — may the *stored value* be `null` in the database? In Prisma, a nullable,
|
|
622
|
+
non-required field becomes an optional scalar (`String?`); a `required` field is always
|
|
623
|
+
non-optional in the schema regardless of `nullable`. In Mongoose, `required: true` sets
|
|
624
|
+
`required: true` on the schema path; `nullable` has no separate Mongoose keyword (Mongoose paths
|
|
625
|
+
are nullable by default unless `required`), so it exists in `entity.json` mainly for
|
|
626
|
+
documentation and for the Zod validator's `.nullable()`.
|
|
627
|
+
- **`required: true` and `nullable: true` are mutually exclusive** — declaring both is rejected at
|
|
628
|
+
validation time: `field cannot be both 'required: true' and 'nullable: true' — required implies
|
|
629
|
+
non-null`.
|
|
630
|
+
- **A `primaryKey` field can never be `nullable: true`** (see [Primary keys](#primary-keys)) —
|
|
631
|
+
rejected regardless of its `required` value.
|
|
632
|
+
- **`default` does not change `required`/`nullable` semantics** — a field with a `default` can
|
|
633
|
+
still be declared `required: true` (the default only applies when the field is omitted from the
|
|
634
|
+
input the *application* passes to the database layer, not at the `entity.json`/Zod level, which
|
|
635
|
+
still requires the caller to supply it unless you also set `required: false`).
|
|
636
|
+
|
|
399
637
|
### Relations
|
|
400
638
|
|
|
401
|
-
Four relation types, declared under a model's `relations` object
|
|
639
|
+
Four relation types, declared under a model's `relations` object (`RELATION_TYPES` in
|
|
640
|
+
`src/parser/entity/schema.js`): `belongsTo`, `hasMany`, `hasOne`, `belongsToMany`.
|
|
641
|
+
|
|
642
|
+
#### `belongsTo`
|
|
643
|
+
|
|
644
|
+
This model holds the foreign key column/field.
|
|
402
645
|
|
|
403
646
|
```json
|
|
404
647
|
{
|
|
@@ -416,32 +659,500 @@ Four relation types, declared under a model's `relations` object:
|
|
|
416
659
|
}
|
|
417
660
|
```
|
|
418
661
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
662
|
+
1. **What it means** — `Post.authorId` points at a `User`.
|
|
663
|
+
2. **Required JSON structure** — `type: "belongsTo"`, `model` (target model name), `foreignKey`
|
|
664
|
+
(required — the name of a real field already declared on *this* model).
|
|
665
|
+
3. **Foreign key behavior** — the `foreignKey` field's declared type must match the target model's
|
|
666
|
+
primary key type, or generation is rejected (`foreignKey 'authorId' has type 'X' but
|
|
667
|
+
'User.id' (its primary key) has type 'Y'`).
|
|
668
|
+
4. **Nullable behavior** — the generated relation field's optionality mirrors the FK scalar's:
|
|
669
|
+
if `authorId` is `required: true` and not `nullable`, Prisma emits `author User @relation(...)`
|
|
670
|
+
(non-optional); if the FK is optional/nullable, it emits `author User? @relation(...)`. Prisma
|
|
671
|
+
rejects a required relation object backed by a nullable FK, so GAZAN always keeps them in sync.
|
|
672
|
+
5. **Reverse relation behavior** — see `hasMany`/`hasOne` below; you don't have to declare the
|
|
673
|
+
reverse side yourself.
|
|
674
|
+
6. **Database output** — Prisma: a scalar FK column plus a `@relation(...)` object field. Mongoose:
|
|
675
|
+
a `String` field with `ref: "<Target>"` (see [Field types](#field-types) — every Mongo FK is a
|
|
676
|
+
`String` because every Mongo primary key is a `String` `_id`).
|
|
677
|
+
7. **Invalid configurations** — missing `foreignKey` (`relations of type 'belongsTo' require a
|
|
678
|
+
'foreignKey'`); `foreignKey` naming a field that doesn't exist on this model; `model` naming a
|
|
679
|
+
model that doesn't exist (with a "did you mean" suggestion); FK/target-PK type mismatch.
|
|
680
|
+
|
|
681
|
+
#### `hasMany` / `hasOne`
|
|
682
|
+
|
|
683
|
+
The *other* model holds the foreign key. You only need to declare either explicitly when you want
|
|
684
|
+
to **name or customize the reverse side yourself** — GAZAN auto-derives a reverse field for any
|
|
685
|
+
`belongsTo` elsewhere in the schema that doesn't already have one, including disambiguating
|
|
686
|
+
multiple relations to the same model (see `Post.author`/`Post.editor` in the
|
|
687
|
+
[realistic example](#realistic-example)) and self-relations.
|
|
688
|
+
|
|
689
|
+
```json
|
|
690
|
+
{
|
|
691
|
+
"models": [
|
|
692
|
+
{
|
|
693
|
+
"name": "User",
|
|
694
|
+
"fields": { "id": { "type": "uuid", "primaryKey": true, "default": "uuid" } },
|
|
695
|
+
"relations": {
|
|
696
|
+
"profile": { "type": "hasOne", "model": "Profile", "foreignKey": "userId" }
|
|
697
|
+
}
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
"name": "Profile",
|
|
701
|
+
"fields": {
|
|
702
|
+
"id": { "type": "uuid", "primaryKey": true, "default": "uuid" },
|
|
703
|
+
"userId": { "type": "uuid", "required": true, "unique": true }
|
|
704
|
+
},
|
|
705
|
+
"relations": {
|
|
706
|
+
"user": { "type": "belongsTo", "model": "User", "foreignKey": "userId" }
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
]
|
|
710
|
+
}
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
1. **What it means** — `hasMany` is the one-to-many reverse side (a `User` `hasMany` `Post`);
|
|
714
|
+
`hasOne` is the one-to-one reverse side (a `User` `hasOne` `Profile`), typically paired with a
|
|
715
|
+
`unique` FK on the other side (as above).
|
|
716
|
+
2. **Required JSON structure** — `type`, `model` (target model name), `foreignKey` (required — the
|
|
717
|
+
name of a field that must exist on the *target* model, not this one).
|
|
718
|
+
3. **Foreign key behavior** — `foreignKey` must exist on the related model
|
|
719
|
+
(`foreignKey 'userId' does not exist on related model 'Profile'` otherwise).
|
|
720
|
+
4. **Nullable behavior** — `hasOne` always generates an optional relation field (`profile
|
|
721
|
+
Profile? @relation(...)` in Prisma) since the related row may not exist yet; `hasMany` generates
|
|
722
|
+
a (possibly empty) array field, which has no nullability concept.
|
|
723
|
+
5. **Reverse relation behavior** — this *is* the reverse side of a `belongsTo`. If you don't
|
|
724
|
+
declare it, GAZAN still generates one automatically for you (a `[]` field in Prisma named after
|
|
725
|
+
the pluralized owning model, disambiguated with `As<RelationName>` when more than one
|
|
726
|
+
`belongsTo` points at the same target — e.g. `postsAsAuthor` / `postsAsEditor`). Declare it
|
|
727
|
+
explicitly only to pick your own field name.
|
|
728
|
+
6. **Database output** — Prisma: `hasMany` → `<name> <Target>[] @relation(...)`; `hasOne` →
|
|
729
|
+
`<name> <Target>? @relation(...)`. Mongoose: a **virtual** (`schema.virtual(name, { ref, localField: "_id", foreignField, justOne })`) — `justOne: true` for `hasOne`, `false` for `hasMany` —
|
|
730
|
+
not a stored field, so it must be `.populate()`d to read.
|
|
731
|
+
7. **Invalid configurations** — `foreignKey` missing, or naming a field absent from the target
|
|
732
|
+
model.
|
|
733
|
+
|
|
734
|
+
#### `belongsToMany`
|
|
735
|
+
|
|
736
|
+
Many-to-many, naming a join via `through`.
|
|
737
|
+
|
|
738
|
+
```json
|
|
739
|
+
{
|
|
740
|
+
"models": [
|
|
741
|
+
{
|
|
742
|
+
"name": "Post",
|
|
743
|
+
"fields": { "id": { "type": "uuid", "primaryKey": true, "default": "uuid" } },
|
|
744
|
+
"relations": {
|
|
745
|
+
"tags": { "type": "belongsToMany", "model": "Tag", "through": "PostTag" }
|
|
746
|
+
}
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
"name": "Tag",
|
|
750
|
+
"fields": { "id": { "type": "uuid", "primaryKey": true, "default": "uuid" } },
|
|
751
|
+
"relations": {
|
|
752
|
+
"posts": { "type": "belongsToMany", "model": "Post", "through": "PostTag" }
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
]
|
|
756
|
+
}
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
```text
|
|
760
|
+
User
|
|
761
|
+
↕
|
|
762
|
+
Role
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
is the same shape: **both sides must declare `belongsToMany`, with the same `through` value.**
|
|
766
|
+
|
|
767
|
+
1. **What it means** — an implicit many-to-many join, with no extra fields on the join itself.
|
|
768
|
+
2. **Required JSON structure** — `type: "belongsToMany"`, `model`, `through` (required — the join
|
|
769
|
+
table/collection name both sides must agree on).
|
|
770
|
+
3. **Foreign key behavior** — none; there's no scalar FK field for a many-to-many, only the array
|
|
771
|
+
relation itself.
|
|
772
|
+
4. **Nullable behavior** — not applicable; both sides are array fields.
|
|
773
|
+
5. **Reverse relation behavior** — **not automatic**, unlike `belongsTo`/`hasMany`/`hasOne`. A
|
|
774
|
+
one-sided `belongsToMany` (declared on `Post` pointing at `Tag`, but not the reverse on `Tag`)
|
|
775
|
+
is rejected: `many-to-many relation 'tags' has no matching belongsToMany declared on 'Tag' back
|
|
776
|
+
to 'Post'`. A mismatched `through` value between the two sides is also rejected: `through
|
|
777
|
+
'PostTag' does not match 'Tag.relations.posts.through' ('PostTags') — both sides of a
|
|
778
|
+
many-to-many must agree`.
|
|
779
|
+
6. **Database output** — Prisma: an implicit many-to-many join table (`<name> <Target>[]
|
|
780
|
+
@relation("<through>")` on both sides — Prisma manages the join table itself). Mongoose: a plain
|
|
781
|
+
array-of-refs field on both sides (`type: [String], ref: "<Target>"`), no separate join
|
|
782
|
+
document.
|
|
783
|
+
7. **Invalid configurations** — one-sided declaration (no matching reverse); mismatched `through`
|
|
784
|
+
values between the two sides; `through` omitted (`relations of type 'belongsToMany' require a
|
|
785
|
+
'through' join model/table name`).
|
|
427
786
|
|
|
428
787
|
**Many-to-many is schema-only.** `belongsToMany` is correctly represented in the generated Prisma
|
|
429
|
-
schema
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
788
|
+
schema and Mongoose model as shown above, but the generated CRUD `create`/`update` endpoints do
|
|
789
|
+
not read or write it — Prisma's nested-write shape (`connect: [...]`) and Mongoose's plain array
|
|
790
|
+
differ enough that GAZAN doesn't attempt a one-size implementation. Manage join-table writes
|
|
791
|
+
through your own service code.
|
|
792
|
+
|
|
793
|
+
### What entity.json supports
|
|
794
|
+
|
|
795
|
+
- Multiple models, each with its own fields and relations
|
|
796
|
+
- All 13 primitive field types (see [Field types](#field-types))
|
|
797
|
+
- UUID and (Postgres-only) auto-incrementing primary keys, or a synthesized default UUID `id`
|
|
798
|
+
- Enums (`type: "enum"` + `values`)
|
|
799
|
+
- Defaults, including the `"uuid"` and `"now"` generation sentinels
|
|
800
|
+
- `required` and `nullable` field flags (independently — see [Required vs nullable](#required-vs-nullable))
|
|
801
|
+
- Unique fields (`unique: true`)
|
|
802
|
+
- Single-column indexes (`index: true`)
|
|
803
|
+
- String length limits (`length`) and numeric/string bounds (`min`/`max`)
|
|
804
|
+
- Custom table/collection names per model (`tableName`)
|
|
805
|
+
- Opting a model out of timestamps (`timestamps: false`) or out of CRUD generation entirely
|
|
806
|
+
(`crud: false` — the model is still validated and, for Mongoose, still gets a model file, but no
|
|
807
|
+
service/controller/route/validator is generated for it)
|
|
808
|
+
- All four relation types: `belongsTo`, `hasMany`, `hasOne`, `belongsToMany`
|
|
809
|
+
- Self-relations (a model relating to itself — see the realistic fixtures in `tests/fixtures/`)
|
|
810
|
+
- Multiple distinct relations between the same two models (auto-disambiguated reverse fields)
|
|
811
|
+
- Automatic sensitive-field detection (`password`/`secret`/`hash`-like field names are stripped
|
|
812
|
+
from generated API responses — see [Authentication](#authentication))
|
|
813
|
+
|
|
814
|
+
### What entity.json does NOT support
|
|
815
|
+
|
|
816
|
+
- **Composite (multi-field) unique constraints or indexes** — every `unique`/`index` is
|
|
817
|
+
single-column; `entity.json` has no syntax for `@@unique([a, b])`-style composite constraints.
|
|
818
|
+
- **Non-UUID MongoDB primary keys, or MongoDB `autoIncrement`** — rejected at validation time (see
|
|
819
|
+
[Primary keys](#primary-keys)).
|
|
820
|
+
- **A one-sided `belongsToMany`** — both sides must declare it (see above).
|
|
821
|
+
- **Automatic many-to-many CRUD writes** — the relation is schema-correct, but generated
|
|
822
|
+
create/update endpoints don't read or write the join (see above).
|
|
823
|
+
- **Arbitrary ORM-specific syntax** — no raw Prisma attributes, no raw Mongoose schema options
|
|
824
|
+
beyond what's listed in [Field attributes](#field-attributes); anything not modeled by the
|
|
825
|
+
schema is rejected by `.strict()` Zod validation (`entity.json failed schema validation`), not
|
|
826
|
+
silently ignored.
|
|
827
|
+
- **Application business logic** — no computed fields, hooks, custom SQL, or arbitrary
|
|
828
|
+
server-side logic. `entity.json` only describes data shape and relations.
|
|
829
|
+
- **Controller or service logic beyond generic CRUD** — you get `create`/`findAll`/`findById`/
|
|
830
|
+
`update`/`delete`; anything more custom is written by hand in the generated service/controller.
|
|
831
|
+
- **Per-model route protection** — `entity.json` has no field for "require auth on this model's
|
|
832
|
+
routes." Wire `middlewares/auth.js` into the generated route file yourself if needed.
|
|
833
|
+
- **Field-level renames between `entity.json` and the database column/field name** — the JSON key
|
|
834
|
+
is the field name everywhere (Prisma column, Mongoose path, Zod key).
|
|
835
|
+
|
|
836
|
+
### Invalid examples
|
|
837
|
+
|
|
838
|
+
Every example below is rejected **before any files are generated** — `entity.json` errors are
|
|
839
|
+
caught at validation time, not partway through a generation run.
|
|
840
|
+
|
|
841
|
+
**Unsafe/invalid model name** (also rejects path-traversal-shaped names):
|
|
842
|
+
|
|
843
|
+
```json
|
|
844
|
+
{ "models": [ { "name": "../User", "fields": { "id": { "type": "uuid", "primaryKey": true } } } ] }
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
```text
|
|
848
|
+
models[0].name:
|
|
849
|
+
model name must be a valid identifier (letters/digits, starting with a letter): '../User'
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
**Unsupported field type:**
|
|
853
|
+
|
|
854
|
+
```json
|
|
855
|
+
{ "models": [ { "name": "User", "fields": { "id": { "type": "unsupported-type" } } } ] }
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
```text
|
|
859
|
+
models[0].fields.id.type:
|
|
860
|
+
type must be one of: string, text, number, integer, float, boolean, date, datetime, uuid, json, enum, decimal, bigint
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
**Unknown relation target** (with a "did you mean" suggestion when one is close enough):
|
|
864
|
+
|
|
865
|
+
```json
|
|
866
|
+
{
|
|
867
|
+
"models": [
|
|
868
|
+
{ "name": "User", "fields": { "id": { "type": "uuid", "primaryKey": true } } },
|
|
869
|
+
{
|
|
870
|
+
"name": "Post",
|
|
871
|
+
"fields": { "authorId": { "type": "uuid", "required": true } },
|
|
872
|
+
"relations": { "author": { "type": "belongsTo", "model": "Userr", "foreignKey": "authorId" } }
|
|
873
|
+
}
|
|
874
|
+
]
|
|
875
|
+
}
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
```text
|
|
879
|
+
models[1].relations.author.model:
|
|
880
|
+
unknown model 'Userr'. Did you mean 'User'?
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
**One-sided many-to-many:**
|
|
884
|
+
|
|
885
|
+
```json
|
|
886
|
+
{
|
|
887
|
+
"models": [
|
|
888
|
+
{ "name": "Post", "fields": { "id": { "type": "uuid", "primaryKey": true } }, "relations": { "tags": { "type": "belongsToMany", "model": "Tag", "through": "PostTag" } } },
|
|
889
|
+
{ "name": "Tag", "fields": { "id": { "type": "uuid", "primaryKey": true } } }
|
|
890
|
+
]
|
|
891
|
+
}
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
```text
|
|
895
|
+
models[0].relations.tags:
|
|
896
|
+
many-to-many relation 'tags' has no matching belongsToMany declared on 'Tag' back to 'Post'
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
**Database-specific: non-UUID MongoDB primary key** (only rejected when the selected database is
|
|
900
|
+
MongoDB — the very same file is valid for PostgreSQL):
|
|
901
|
+
|
|
902
|
+
```json
|
|
903
|
+
{ "models": [ { "name": "User", "fields": { "id": { "type": "integer", "primaryKey": true, "autoIncrement": true } } } ] }
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
```text
|
|
907
|
+
models[0].fields.id.autoIncrement:
|
|
908
|
+
autoIncrement is not supported for MongoDB — it has no native auto-increment primary key
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
**`required` and `nullable` together:**
|
|
912
|
+
|
|
913
|
+
```json
|
|
914
|
+
{ "models": [ { "name": "User", "fields": { "bio": { "type": "text", "required": true, "nullable": true } } } ] }
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
```text
|
|
918
|
+
models[0].fields.bio:
|
|
919
|
+
field cannot be both 'required: true' and 'nullable: true' — required implies non-null
|
|
920
|
+
```
|
|
433
921
|
|
|
434
922
|
## Database support
|
|
435
923
|
|
|
436
|
-
|
|
924
|
+
GAZAN generates **different, mutually exclusive** database artifacts depending on what you pick —
|
|
925
|
+
never a mix of two backends' files in the same project.
|
|
926
|
+
|
|
927
|
+
### entity.json → generated output
|
|
928
|
+
|
|
929
|
+
```text
|
|
930
|
+
entity.json
|
|
931
|
+
│
|
|
932
|
+
├── Entity Parser (schema + semantic validation)
|
|
933
|
+
│
|
|
934
|
+
├── Normalized Entity Model
|
|
935
|
+
│
|
|
936
|
+
├── Database Generator (exactly one of the three runs, per your selection)
|
|
937
|
+
│ ├── Prisma → prisma/schema.prisma
|
|
938
|
+
│ ├── Mongoose → models/<model>.model.{js,ts} (one file per model)
|
|
939
|
+
│ └── Native Mongo → no per-model files (schemaless — see below)
|
|
940
|
+
│
|
|
941
|
+
└── Application Generator (skipped for a model with "crud": false)
|
|
942
|
+
├── Validators → validators/<model>.validator.{js,ts} (Zod)
|
|
943
|
+
├── Services → services/<model>.service.{js,ts} (talks to the DB)
|
|
944
|
+
├── Controllers → controllers/<model>.controller.{js,ts}
|
|
945
|
+
└── Routes → routes/<model>.routes.{js,ts}
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
The same `entity.json` is transformed into whichever database representation you selected — the
|
|
949
|
+
Entity Parser and Normalized Entity Model are identical either way; only the Database Generator
|
|
950
|
+
step (and, for Mongoose, whether a per-model file exists at all) differs.
|
|
951
|
+
|
|
952
|
+
### Database output matrix
|
|
953
|
+
|
|
954
|
+
| Selected database | Generated schema/model output |
|
|
437
955
|
|---|---|
|
|
438
|
-
| PostgreSQL + Prisma | `prisma/schema.prisma`
|
|
439
|
-
| MongoDB + Mongoose |
|
|
440
|
-
| MongoDB (native driver) | No
|
|
441
|
-
|
|
|
956
|
+
| PostgreSQL + Prisma | `prisma/schema.prisma` |
|
|
957
|
+
| MongoDB + Mongoose | `models/*.model.{js,ts}` (one file per model — `src/models/` when using `src/`) |
|
|
958
|
+
| MongoDB (native driver) | No per-model files — a native `MongoClient` config only (`configs/db/index.{js,ts}`) |
|
|
959
|
+
| No database | No database artifacts at all |
|
|
960
|
+
|
|
961
|
+
### PostgreSQL + Prisma
|
|
962
|
+
|
|
963
|
+
Generates exactly one file, from `entity.json`:
|
|
964
|
+
|
|
965
|
+
```text
|
|
966
|
+
prisma/
|
|
967
|
+
└── schema.prisma
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
No Mongoose models and no native-Mongo config are ever generated alongside it. For this
|
|
971
|
+
`entity.json`:
|
|
972
|
+
|
|
973
|
+
```json
|
|
974
|
+
{
|
|
975
|
+
"models": [
|
|
976
|
+
{
|
|
977
|
+
"name": "User",
|
|
978
|
+
"fields": {
|
|
979
|
+
"id": { "type": "uuid", "primaryKey": true, "default": "uuid" },
|
|
980
|
+
"name": { "type": "string", "required": true },
|
|
981
|
+
"email": { "type": "string", "required": true, "unique": true },
|
|
982
|
+
"age": { "type": "integer" }
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
]
|
|
986
|
+
}
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
GAZAN generates:
|
|
990
|
+
|
|
991
|
+
```prisma
|
|
992
|
+
generator client {
|
|
993
|
+
provider = "prisma-client-js"
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
datasource db {
|
|
997
|
+
provider = "postgresql"
|
|
998
|
+
url = env("DATABASE_URL")
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
model User {
|
|
1002
|
+
id String @id @default(uuid())
|
|
1003
|
+
name String
|
|
1004
|
+
email String @unique
|
|
1005
|
+
age Int?
|
|
1006
|
+
createdAt DateTime @default(now())
|
|
1007
|
+
updatedAt DateTime @updatedAt
|
|
1008
|
+
|
|
1009
|
+
@@map("users")
|
|
1010
|
+
}
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
If no `entity.json` is provided, GAZAN still writes a valid starter `prisma/schema.prisma` (one
|
|
1014
|
+
`Example` model) so `prisma generate`/`prisma migrate` work immediately without hand-editing.
|
|
1015
|
+
|
|
1016
|
+
### MongoDB + Mongoose
|
|
1017
|
+
|
|
1018
|
+
Generates one model file per model declared in `entity.json`, under `models/` (`src/models/` when
|
|
1019
|
+
using `src/`):
|
|
1020
|
+
|
|
1021
|
+
```text
|
|
1022
|
+
src/
|
|
1023
|
+
└── models/
|
|
1024
|
+
└── user.model.js # (or .ts — matches your selected language)
|
|
1025
|
+
```
|
|
1026
|
+
|
|
1027
|
+
The **same** `User` model above produces:
|
|
1028
|
+
|
|
1029
|
+
```js
|
|
1030
|
+
const mongoose = require("mongoose");
|
|
1031
|
+
const { randomUUID } = require("node:crypto");
|
|
1032
|
+
|
|
1033
|
+
const userSchema = new mongoose.Schema(
|
|
1034
|
+
{
|
|
1035
|
+
_id: { type: String, default: () => randomUUID() },
|
|
1036
|
+
name: { type: String, required: true },
|
|
1037
|
+
email: { type: String, required: true, unique: true },
|
|
1038
|
+
age: { type: Number },
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
timestamps: true,
|
|
1042
|
+
collection: "users",
|
|
1043
|
+
}
|
|
1044
|
+
);
|
|
1045
|
+
userSchema.set("toJSON", { virtuals: true });
|
|
1046
|
+
const User = mongoose.model("User", userSchema);
|
|
1047
|
+
|
|
1048
|
+
module.exports = User;
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
No `prisma/schema.prisma` is ever generated for this configuration. If no `entity.json` is
|
|
1052
|
+
provided, no model files are generated at all — only the shared `configs/db/index.{js,ts}`
|
|
1053
|
+
Mongoose connection module — since there's nothing to model yet.
|
|
1054
|
+
|
|
1055
|
+
### MongoDB (native driver)
|
|
1056
|
+
|
|
1057
|
+
The lightest-weight database option: **no schema, no per-model files at all.** GAZAN generates a
|
|
1058
|
+
single shared client module —
|
|
1059
|
+
|
|
1060
|
+
```text
|
|
1061
|
+
src/
|
|
1062
|
+
└── configs/
|
|
1063
|
+
└── db/
|
|
1064
|
+
└── index.js # MongoClient wrapper: connect(), getDb(), disconnect()
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
— and, if `entity.json` was provided, generic CRUD **services** per model that call
|
|
1068
|
+
`getDb().collection("<table>")` directly and pass plain JS objects through (see
|
|
1069
|
+
[Entity-generated application code](#entity-generated-application-code)). There is no Mongoose
|
|
1070
|
+
schema layer to generate, so field types/constraints from `entity.json` are enforced only by the
|
|
1071
|
+
generated Zod validator at the route boundary, not by a database-level schema. No Prisma schema
|
|
1072
|
+
and no Mongoose models are ever generated for this configuration.
|
|
1073
|
+
|
|
1074
|
+
### No database
|
|
1075
|
+
|
|
1076
|
+
Selecting "None" generates **zero** database-specific artifacts:
|
|
1077
|
+
|
|
1078
|
+
- No `prisma/` directory, no `models/` directory, no `configs/db/`.
|
|
1079
|
+
- No `@prisma/client`, `prisma`, `mongoose`, or `mongodb` dependency in `package.json`.
|
|
1080
|
+
- No `DATABASE_URL`/`MONGODB_URI` in `.env`/`.env.example`, and no corresponding entry in the
|
|
1081
|
+
generated env validator.
|
|
1082
|
+
- If `entity.json` was still provided, generated services fall back to an in-memory `Map`-backed
|
|
1083
|
+
store per model — enough to exercise the full validator → controller → route → service pipeline
|
|
1084
|
+
end-to-end without any real database, but data does not persist across restarts.
|
|
1085
|
+
|
|
1086
|
+
### Entity-generated application code
|
|
1087
|
+
|
|
1088
|
+
Whether or not a database is selected, providing `entity.json` generates the same **four** kinds
|
|
1089
|
+
of application files per model (skipped for a model with `"crud": false`) — this doesn't change
|
|
1090
|
+
based on database backend, only *where* the files live changes with architecture:
|
|
1091
|
+
|
|
1092
|
+
**MVC** — flat, shared folders, one file per model per folder:
|
|
1093
|
+
|
|
1094
|
+
```text
|
|
1095
|
+
src/
|
|
1096
|
+
├── controllers/
|
|
1097
|
+
│ └── user.controller.js
|
|
1098
|
+
├── services/
|
|
1099
|
+
│ └── user.service.js
|
|
1100
|
+
├── routes/
|
|
1101
|
+
│ └── user.routes.js
|
|
1102
|
+
│ └── index.js # aggregates every model's router under /api/<pluralized-name>
|
|
1103
|
+
└── validators/
|
|
1104
|
+
└── user.validator.js
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
**HMVC** — grouped per model instead of by layer:
|
|
1108
|
+
|
|
1109
|
+
```text
|
|
1110
|
+
src/
|
|
1111
|
+
└── modules/
|
|
1112
|
+
└── user/
|
|
1113
|
+
├── controllers/
|
|
1114
|
+
│ └── user.controller.js
|
|
1115
|
+
├── routes/
|
|
1116
|
+
│ └── user.routes.js
|
|
1117
|
+
├── services/
|
|
1118
|
+
│ └── user.service.js
|
|
1119
|
+
└── validators/
|
|
1120
|
+
└── user.validator.js
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
File naming is always `<model.kebabName>.<kind>.<ext>` (e.g. `blog-post.controller.ts` for a
|
|
1124
|
+
model named `BlogPost`) — see [Model naming](#model-definition) below.
|
|
442
1125
|
|
|
443
1126
|
## Authentication
|
|
444
1127
|
|
|
1128
|
+
The authentication prompt is **multi-select** — you can enable any combination of methods in one
|
|
1129
|
+
run, not just one:
|
|
1130
|
+
|
|
1131
|
+
```text
|
|
1132
|
+
◇ Do you need authentication?
|
|
1133
|
+
│ Yes
|
|
1134
|
+
│
|
|
1135
|
+
◇ Which authentication methods?
|
|
1136
|
+
│ ◻ Email/password
|
|
1137
|
+
│ ◼ JWT
|
|
1138
|
+
│ ◼ Refresh tokens
|
|
1139
|
+
│ ◻ OAuth
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
Internally, your selection normalizes to:
|
|
1143
|
+
|
|
1144
|
+
```json
|
|
1145
|
+
{
|
|
1146
|
+
"authentication": {
|
|
1147
|
+
"enabled": true,
|
|
1148
|
+
"methods": ["jwt", "refresh-token"]
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
(`enabled` is automatically `false` if you decline the prompt, or if every method gets
|
|
1154
|
+
deselected — an empty selection is equivalent to not enabling authentication at all.)
|
|
1155
|
+
|
|
445
1156
|
Selected independently — only what you pick is generated:
|
|
446
1157
|
|
|
447
1158
|
| Method | What's generated |
|
|
@@ -669,7 +1380,16 @@ The matrix covers, in combination: CJS/MJS, JS/TS, MVC/HMVC, `src`/no-`src`, Pos
|
|
|
669
1380
|
MongoDB+Mongoose, MongoDB (native), no database, Redis (with and without BullMQ), BullMQ,
|
|
670
1381
|
Socket.IO, each authentication method in isolation, module aliases (across every module
|
|
671
1382
|
system/language combination, MVC and HMVC), and `entity.json` schemas including self-relations,
|
|
672
|
-
dual foreign keys to the same model, many-to-many, and a six-model realistic schema.
|
|
1383
|
+
dual foreign keys to the same model, `hasOne`, many-to-many, and a six-model realistic schema.
|
|
1384
|
+
|
|
1385
|
+
Before generating a single project, two fast unit-style checks run first (no `npm install`, so
|
|
1386
|
+
they fail fast): the **entity parser checks** — a valid fixture, an inline invalid-schema object,
|
|
1387
|
+
and dedicated fixtures for `hasOne`, an invalid field type, an invalid (path-traversal-shaped)
|
|
1388
|
+
model name, and an invalid relation (unknown target model) each assert the exact error produced —
|
|
1389
|
+
and the **current-directory checks** — generating with `.`, `./backend`, and `../backend` and
|
|
1390
|
+
asserting the output lands exactly where expected, with no nested duplicate directory and a
|
|
1391
|
+
package name derived from the resolved directory. The "no database" case additionally asserts the
|
|
1392
|
+
*absence* of `prisma/`, `models/`, `configs/db/`, and any database dependency or env var.
|
|
673
1393
|
|
|
674
1394
|
## Troubleshooting
|
|
675
1395
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gazan-init",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Interactive CLI that generates production-ready Node.js backend projects — architecture, database, auth, Redis, BullMQ, Socket.IO, and module aliases, all conditional on what you actually select.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
package/src/cli/commands/init.js
CHANGED
|
@@ -32,13 +32,19 @@ function generateAtomically(targetDir, config, entityModel) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
async function resolveTargetDir(projectName) {
|
|
35
|
+
// path.resolve (never raw string concatenation) already gives '.', './backend', '../backend',
|
|
36
|
+
// an absolute path, and a plain name ("my-app") exactly the semantics each one should have:
|
|
37
|
+
// '.' resolves to process.cwd() itself, so GAZAN generates directly INTO the current directory
|
|
38
|
+
// rather than nesting a new folder inside it.
|
|
35
39
|
let targetDir = path.resolve(process.cwd(), projectName);
|
|
36
40
|
|
|
37
41
|
for (;;) {
|
|
38
42
|
if (isDirEmpty(targetDir)) return targetDir;
|
|
39
43
|
|
|
44
|
+
const isCwd = targetDir === process.cwd();
|
|
45
|
+
const label = isCwd ? "Current directory" : `Directory "${path.relative(process.cwd(), targetDir) || targetDir}"`;
|
|
40
46
|
const choice = await p.select({
|
|
41
|
-
message:
|
|
47
|
+
message: `${label} is not empty.`,
|
|
42
48
|
options: [
|
|
43
49
|
{ value: "cancel", label: "Cancel" },
|
|
44
50
|
{ value: "continue", label: "Continue (files may be overwritten)" },
|
|
@@ -108,7 +114,8 @@ async function runInit() {
|
|
|
108
114
|
process.exit(1);
|
|
109
115
|
}
|
|
110
116
|
|
|
111
|
-
const relTarget = path.relative(process.cwd(), targetDir)
|
|
117
|
+
const relTarget = path.relative(process.cwd(), targetDir);
|
|
118
|
+
const cdStep = relTarget ? [` cd ${relTarget}`] : []; // generated directly into the current directory — no cd needed
|
|
112
119
|
|
|
113
120
|
p.outro(
|
|
114
121
|
[
|
|
@@ -116,7 +123,7 @@ async function runInit() {
|
|
|
116
123
|
"",
|
|
117
124
|
"Next steps:",
|
|
118
125
|
"",
|
|
119
|
-
|
|
126
|
+
...cdStep,
|
|
120
127
|
` npm install`,
|
|
121
128
|
` npm run dev`,
|
|
122
129
|
].join("\n")
|
package/src/config/normalize.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { toPackageName } = require("../utils/strings");
|
|
5
|
+
|
|
3
6
|
/**
|
|
4
7
|
* Turns raw prompt answers into the single normalized configuration object
|
|
5
8
|
* that every generator consumes. Generators must never read prompt answers
|
|
@@ -21,8 +24,15 @@ function normalizeConfig(answers) {
|
|
|
21
24
|
const rateLimitStrategy = bullMQ ? "redis" : answers.rateLimitStrategy || "memory";
|
|
22
25
|
const needsRedis = bullMQ || rateLimitStrategy === "redis";
|
|
23
26
|
|
|
27
|
+
// The displayed/used project name always comes from the resolved output directory's basename —
|
|
28
|
+
// not the raw prompt answer — so "." (current directory), "./backend", and "my-app" all name
|
|
29
|
+
// the project after where it actually lands instead of after whatever string was typed.
|
|
30
|
+
const projectName = deriveProjectName(answers.targetDir, answers.projectName);
|
|
31
|
+
const packageName = toPackageName(projectName);
|
|
32
|
+
|
|
24
33
|
return {
|
|
25
|
-
projectName
|
|
34
|
+
projectName,
|
|
35
|
+
packageName,
|
|
26
36
|
targetDir: answers.targetDir,
|
|
27
37
|
moduleSystem,
|
|
28
38
|
language,
|
|
@@ -40,6 +50,14 @@ function normalizeConfig(answers) {
|
|
|
40
50
|
};
|
|
41
51
|
}
|
|
42
52
|
|
|
53
|
+
function deriveProjectName(targetDir, rawInput) {
|
|
54
|
+
if (targetDir) {
|
|
55
|
+
const base = path.basename(path.resolve(targetDir));
|
|
56
|
+
if (base) return base;
|
|
57
|
+
}
|
|
58
|
+
return rawInput;
|
|
59
|
+
}
|
|
60
|
+
|
|
43
61
|
function normalizeDatabase(db) {
|
|
44
62
|
if (!db || db.type === "none") {
|
|
45
63
|
return { type: "none", orm: null };
|
package/src/prompts/index.js
CHANGED
|
@@ -21,14 +21,24 @@ async function runPrompts(defaults = {}) {
|
|
|
21
21
|
p.intro("GAZAN — backend project initializer");
|
|
22
22
|
|
|
23
23
|
const projectName = await ask(p.text, {
|
|
24
|
-
message: "Project name",
|
|
25
|
-
placeholder: "my-app",
|
|
24
|
+
message: "Project name/path",
|
|
25
|
+
placeholder: "my-app (or '.' for the current directory)",
|
|
26
26
|
initialValue: defaults.projectName,
|
|
27
27
|
validate: (value) => {
|
|
28
|
-
if (!value) return "Project name is required";
|
|
29
|
-
|
|
28
|
+
if (!value || !value.trim()) return "Project name/path is required";
|
|
29
|
+
const trimmed = value.trim();
|
|
30
|
+
// '.', './relative', '../relative', and absolute paths are all valid OUTPUT PATHS — they're
|
|
31
|
+
// resolved with Node's `path` APIs (never string concatenation) and are not further
|
|
32
|
+
// restricted here; existing-directory safety is enforced later, once the path is resolved.
|
|
33
|
+
if (trimmed === "." || trimmed.startsWith("./") || trimmed.startsWith("../") || path.isAbsolute(trimmed)) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
if (!/^[a-z0-9][a-z0-9-_]*$/i.test(trimmed)) {
|
|
37
|
+
return "Use letters, digits, - and _ only, or a path like '.', './backend', '../backend'";
|
|
38
|
+
}
|
|
30
39
|
},
|
|
31
40
|
});
|
|
41
|
+
const projectNamePath = projectName.trim();
|
|
32
42
|
|
|
33
43
|
const moduleSystem = await ask(p.select, {
|
|
34
44
|
message: "Which module system do you want?",
|
|
@@ -129,7 +139,7 @@ async function runPrompts(defaults = {}) {
|
|
|
129
139
|
p.outro("Configuration collected.");
|
|
130
140
|
|
|
131
141
|
return {
|
|
132
|
-
projectName,
|
|
142
|
+
projectName: projectNamePath,
|
|
133
143
|
moduleSystem,
|
|
134
144
|
language,
|
|
135
145
|
aliasesEnabled,
|
package/src/utils/strings.js
CHANGED
|
@@ -98,6 +98,23 @@ function didYouMean(target, candidates) {
|
|
|
98
98
|
return bestDistance <= threshold ? best : null;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// npm forbids uppercase, most punctuation, and a leading '.' or '_' in package names — this
|
|
102
|
+
// mirrors those rules so a directory-derived name (e.g. from `.` or `./My Api`) always produces
|
|
103
|
+
// a valid package.json "name" instead of failing `npm install` with an opaque error.
|
|
104
|
+
const NPM_NAME_MAX_LENGTH = 214;
|
|
105
|
+
|
|
106
|
+
function toPackageName(name) {
|
|
107
|
+
let out = String(name || "")
|
|
108
|
+
.trim()
|
|
109
|
+
.toLowerCase()
|
|
110
|
+
.replace(/[^a-z0-9._~-]+/g, "-")
|
|
111
|
+
.replace(/^[._-]+/, "")
|
|
112
|
+
.replace(/-+/g, "-")
|
|
113
|
+
.replace(/-+$/, "");
|
|
114
|
+
if (!out) out = "app";
|
|
115
|
+
return out.slice(0, NPM_NAME_MAX_LENGTH);
|
|
116
|
+
}
|
|
117
|
+
|
|
101
118
|
module.exports = {
|
|
102
119
|
toPascalCase,
|
|
103
120
|
toCamelCase,
|
|
@@ -107,4 +124,5 @@ module.exports = {
|
|
|
107
124
|
pluralize,
|
|
108
125
|
levenshtein,
|
|
109
126
|
didYouMean,
|
|
127
|
+
toPackageName,
|
|
110
128
|
};
|