db-model-router 1.0.7 → 1.0.9

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 (47) hide show
  1. package/README.md +25 -4
  2. package/db-manager/.dbmanager.sqlite-shm +0 -0
  3. package/db-manager/.dbmanager.sqlite-wal +0 -0
  4. package/demo/.env.example +1 -0
  5. package/demo/app.js +2 -2
  6. package/demo/commons/db.js +0 -11
  7. package/demo/middleware/tenantIsolation.js +2 -0
  8. package/demo/package-lock.json +1224 -62
  9. package/demo/package.json +6 -3
  10. package/demo/routes/addresses/index.js +5 -1
  11. package/demo/routes/auth/index.js +1 -1
  12. package/demo/routes/carts/cart_items/index.js +5 -1
  13. package/demo/routes/carts/index.js +9 -1
  14. package/demo/routes/categories/index.js +5 -1
  15. package/demo/routes/coupons/index.js +5 -1
  16. package/demo/routes/index.js +1 -15
  17. package/demo/routes/orders/index.js +13 -1
  18. package/demo/routes/orders/order_items/index.js +5 -1
  19. package/demo/routes/orders/payments/index.js +5 -1
  20. package/demo/routes/orders/shipments/index.js +5 -1
  21. package/demo/routes/products/index.js +13 -1
  22. package/demo/routes/products/product_images/index.js +5 -1
  23. package/demo/routes/products/product_reviews/index.js +5 -1
  24. package/demo/routes/products/product_variants/index.js +5 -1
  25. package/demo/routes/roles/index.js +1 -1
  26. package/demo/routes/tenants/index.js +1 -1
  27. package/demo/routes/users/index.js +1 -1
  28. package/demo/routes/wishlists/index.js +5 -1
  29. package/demo/seeds/saas-seed.js +1 -1
  30. package/docs/dbmr-schema-spec.md +393 -0
  31. package/package.json +4 -2
  32. package/skill/SKILL.md +47 -4
  33. package/src/cli/commands/generate.js +45 -15
  34. package/src/cli/diff-engine.js +17 -5
  35. package/src/cli/generate-migration.js +207 -19
  36. package/src/cli/generate-route.js +156 -58
  37. package/src/cli/generate-saas-structure.js +8 -1
  38. package/src/cli/init/dependencies.js +5 -1
  39. package/src/cli/init/generators.js +4 -81
  40. package/src/cli/init.js +1 -2
  41. package/src/cli/saas/generate-saas-middleware.js +2 -0
  42. package/src/cli/saas/generate-saas-routes.js +3 -13
  43. package/src/cli/saas/generate-saas-tests.js +473 -0
  44. package/src/commons/route.js +6 -6
  45. /package/demo/migrations/{20260509170349_create_migrations_table.sql → 20260510193736_create_migrations_table.sql} +0 -0
  46. /package/demo/migrations/{20260509170349_create_saas_tables.sql → 20260510193737_create_saas_tables.sql} +0 -0
  47. /package/demo/migrations/{20260509170349_create_tables.sql → 20260510193737_create_tables.sql} +0 -0
@@ -0,0 +1,393 @@
1
+ # dbmr.schema.json — Column Rule Specification
2
+
3
+ This document defines the full column rule syntax for `dbmr.schema.json`. Column rules are pipe-delimited strings that describe the **data type**, **sub-type**, and **validation constraints** for each column.
4
+
5
+ ---
6
+
7
+ ## Syntax
8
+
9
+ ```
10
+ [required|]<type>[:<subtype>][|<validator>[|<validator>...]]
11
+ ```
12
+
13
+ **Parts:**
14
+
15
+ | Part | Required | Description |
16
+ | ----------- | -------- | -------------------------------------------------------------- |
17
+ | `required` | No | Prefix — marks column as NOT NULL |
18
+ | `type` | Yes | Base data type (see table below) |
19
+ | `:subtype` | No | Colon-suffixed sub-type for finer SQL type control |
20
+ | `validator` | No | One or more validation rules (node-input-validator compatible) |
21
+
22
+ **Examples:**
23
+
24
+ ```
25
+ "name": "required|string" # VARCHAR(255) NOT NULL
26
+ "description": "string:text" # TEXT, nullable
27
+ "body": "required|string:longtext" # LONGTEXT NOT NULL
28
+ "email": "required|string|email|maxLength:255" # VARCHAR(255) NOT NULL + email validation
29
+ "phone": "string|phoneNumber" # VARCHAR(255) + phone validation
30
+ "stock": "required|integer:unsigned" # INT UNSIGNED NOT NULL
31
+ "view_count": "integer:bigint" # BIGINT, nullable
32
+ "price": "required|numeric:decimal(10,4)" # DECIMAL(10,4) NOT NULL
33
+ "rating": "required|integer|min:1|max:5" # INTEGER NOT NULL + range validation
34
+ "slug": "required|string|regex:^[a-z0-9-]+$" # VARCHAR(255) NOT NULL + pattern validation
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Base Types
40
+
41
+ | Type | Default SQL Type | Description |
42
+ | ---------------- | ---------------- | ------------------------------ |
43
+ | `auto_increment` | SERIAL / INT AI | Auto-incrementing primary key |
44
+ | `string` | VARCHAR(255) | Text/string columns |
45
+ | `integer` | INTEGER | Whole number columns |
46
+ | `numeric` | DECIMAL(12,2) | Decimal/floating-point columns |
47
+ | `boolean` | BOOLEAN | True/false columns |
48
+ | `datetime` | TIMESTAMP | Date and time columns |
49
+ | `object` | JSON / JSONB | JSON data columns |
50
+
51
+ ---
52
+
53
+ ## Sub-Types (colon syntax)
54
+
55
+ Sub-types refine the SQL column type generated for each adapter. If no sub-type is specified, the default mapping is used.
56
+
57
+ ### String Sub-Types
58
+
59
+ | Sub-Type | MySQL / MariaDB | PostgreSQL | SQLite3 | MSSQL | Oracle |
60
+ | ------------ | --------------- | ------------ | ------- | ---------------- | ------------- |
61
+ | _(default)_ | VARCHAR(255) | VARCHAR(255) | TEXT | NVARCHAR(255) | VARCHAR2(255) |
62
+ | `text` | TEXT | TEXT | TEXT | NVARCHAR(MAX) | CLOB |
63
+ | `mediumtext` | MEDIUMTEXT | TEXT | TEXT | NVARCHAR(MAX) | CLOB |
64
+ | `longtext` | LONGTEXT | TEXT | TEXT | NVARCHAR(MAX) | CLOB |
65
+ | `char` | CHAR(255) | CHAR(255) | TEXT | NCHAR(255) | CHAR(255) |
66
+ | `char(N)` | CHAR(N) | CHAR(N) | TEXT | NCHAR(N) | CHAR(N) |
67
+ | `varchar(N)` | VARCHAR(N) | VARCHAR(N) | TEXT | NVARCHAR(N) | VARCHAR2(N) |
68
+ | `uuid` | CHAR(36) | UUID | TEXT | UNIQUEIDENTIFIER | CHAR(36) |
69
+
70
+ **Usage:**
71
+
72
+ ```json
73
+ "description": "string:text"
74
+ "content": "required|string:longtext"
75
+ "code": "string:char(10)"
76
+ "external_id": "string:uuid"
77
+ "title": "required|string:varchar(500)"
78
+ ```
79
+
80
+ ### Integer Sub-Types
81
+
82
+ | Sub-Type | MySQL / MariaDB | PostgreSQL | SQLite3 | MSSQL | Oracle |
83
+ | ----------------- | --------------- | ---------- | ------- | -------- | ---------- |
84
+ | _(default)_ | INT | INTEGER | INTEGER | INT | NUMBER(10) |
85
+ | `tinyint` | TINYINT | SMALLINT | INTEGER | TINYINT | NUMBER(3) |
86
+ | `smallint` | SMALLINT | SMALLINT | INTEGER | SMALLINT | NUMBER(5) |
87
+ | `bigint` | BIGINT | BIGINT | INTEGER | BIGINT | NUMBER(19) |
88
+ | `unsigned` | INT UNSIGNED | INTEGER | INTEGER | INT | NUMBER(10) |
89
+ | `bigint_unsigned` | BIGINT UNSIGNED | BIGINT | INTEGER | BIGINT | NUMBER(19) |
90
+
91
+ **Usage:**
92
+
93
+ ```json
94
+ "stock_quantity": "required|integer:unsigned"
95
+ "view_count": "integer:bigint"
96
+ "flags": "integer:tinyint"
97
+ "population": "integer:bigint_unsigned"
98
+ ```
99
+
100
+ ### Numeric Sub-Types
101
+
102
+ | Sub-Type | MySQL / MariaDB | PostgreSQL | SQLite3 | MSSQL | Oracle |
103
+ | -------------- | --------------- | ---------------- | ------- | ------------- | ------------- |
104
+ | _(default)_ | DECIMAL(12,2) | DECIMAL(12,2) | REAL | DECIMAL(12,2) | NUMBER(12,2) |
105
+ | `float` | FLOAT | REAL | REAL | FLOAT | FLOAT |
106
+ | `double` | DOUBLE | DOUBLE PRECISION | REAL | FLOAT | BINARY_DOUBLE |
107
+ | `decimal(P,S)` | DECIMAL(P,S) | DECIMAL(P,S) | REAL | DECIMAL(P,S) | NUMBER(P,S) |
108
+ | `money` | DECIMAL(19,4) | MONEY | REAL | MONEY | NUMBER(19,4) |
109
+
110
+ **Usage:**
111
+
112
+ ```json
113
+ "price": "required|numeric:decimal(10,4)"
114
+ "weight": "numeric:float"
115
+ "latitude": "numeric:double"
116
+ "balance": "required|numeric:money"
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Validation Rules
122
+
123
+ Validation rules are appended after the type (and optional sub-type) using the pipe `|` separator. These map directly to [node-input-validator](https://www.npmjs.com/package/node-input-validator) rules and are enforced at the API layer during insert/update operations.
124
+
125
+ ### Available Validators
126
+
127
+ | Validator | Description | Example |
128
+ | --------------------- | ---------------------------------------- | -------------------------------- |
129
+ | `email` | Must be a valid email address | `string\|email` |
130
+ | `phoneNumber` | Must be a valid phone number | `string\|phoneNumber` |
131
+ | `url` | Must be a valid URL | `string\|url` |
132
+ | `ip` | Must be a valid IP address | `string\|ip` |
133
+ | `minLength:N` | Minimum string length | `string\|minLength:3` |
134
+ | `maxLength:N` | Maximum string length | `string\|maxLength:100` |
135
+ | `lengthBetween:N1,N2` | String length must be between N1 and N2 | `string\|lengthBetween:3,50` |
136
+ | `min:N` | Minimum numeric value | `integer\|min:0` |
137
+ | `max:N` | Maximum numeric value | `integer\|max:100` |
138
+ | `between:N1,N2` | Numeric value must be between N1 and N2 | `integer\|between:1,5` |
139
+ | `regex:PATTERN` | Must match the regex pattern | `string\|regex:^[a-z0-9-]+$` |
140
+ | `alpha` | Only alphabetic characters | `string\|alpha` |
141
+ | `alphaNumeric` | Only alphanumeric characters | `string\|alphaNumeric` |
142
+ | `alphaDash` | Alphanumeric, dashes, and underscores | `string\|alphaDash` |
143
+ | `in:val1,val2,...` | Must be one of the listed values (enum) | `string\|in:active,inactive` |
144
+ | `notIn:val1,val2,...` | Must NOT be one of the listed values | `string\|notIn:banned,suspended` |
145
+ | `digits:N` | Must be exactly N digits | `string\|digits:6` |
146
+ | `digitsBetween:N1,N2` | Digit count must be between N1 and N2 | `string\|digitsBetween:4,8` |
147
+ | `dateFormat:FORMAT` | Must match date format (e.g. YYYY-MM-DD) | `string\|dateFormat:YYYY-MM-DD` |
148
+ | `json` | Must be valid JSON string | `string\|json` |
149
+ | `same:field` | Must match another field's value | `string\|same:password` |
150
+ | `different:field` | Must differ from another field's value | `string\|different:old_email` |
151
+
152
+ ### Validation Rule Parsing
153
+
154
+ The parser distinguishes between **type/sub-type tokens** and **validation tokens**:
155
+
156
+ 1. `required` — always a modifier (NOT NULL)
157
+ 2. First non-`required` token — the **base type** (string, integer, numeric, boolean, object, datetime, auto_increment)
158
+ 3. If the base type contains a colon `:` — the part after `:` is the **sub-type**
159
+ 4. All remaining pipe-separated tokens — **validation rules**
160
+
161
+ **Parsing example:**
162
+
163
+ ```
164
+ "required|string:text|minLength:10|maxLength:5000"
165
+ ```
166
+
167
+ | Token | Role |
168
+ | ---------------- | ---------- |
169
+ | `required` | Modifier |
170
+ | `string:text` | Type + Sub |
171
+ | `minLength:10` | Validator |
172
+ | `maxLength:5000` | Validator |
173
+
174
+ Result:
175
+
176
+ - SQL: `TEXT NOT NULL`
177
+ - Validation: `{ required: true, minLength: 10, maxLength: 5000 }`
178
+
179
+ ---
180
+
181
+ ## Full Example Schema
182
+
183
+ ```json
184
+ {
185
+ "adapter": "postgres",
186
+ "framework": "express",
187
+ "options": {
188
+ "session": "redis",
189
+ "rateLimiting": true,
190
+ "helmet": true,
191
+ "logger": true,
192
+ "loki": false
193
+ },
194
+ "tables": {
195
+ "products": {
196
+ "columns": {
197
+ "product_id": "auto_increment",
198
+ "category_id": "required|integer:unsigned",
199
+ "name": "required|string:varchar(300)|minLength:3|maxLength:300",
200
+ "slug": "required|string|regex:^[a-z0-9-]+$|maxLength:300",
201
+ "description": "string:text|maxLength:5000",
202
+ "short_description": "string:varchar(500)|maxLength:500",
203
+ "sku": "required|string|alphaNumeric|minLength:3|maxLength:50",
204
+ "price": "required|numeric:decimal(10,2)|min:0",
205
+ "compare_at_price": "numeric:decimal(10,2)|min:0",
206
+ "cost_price": "numeric:decimal(10,2)|min:0",
207
+ "currency": "required|string|minLength:3|maxLength:3",
208
+ "stock_quantity": "required|integer:unsigned|min:0",
209
+ "low_stock_threshold": "integer:unsigned|min:0",
210
+ "weight": "numeric:float|min:0",
211
+ "weight_unit": "string|in:kg,lb,oz,g",
212
+ "is_active": "boolean",
213
+ "is_featured": "boolean",
214
+ "is_deleted": "boolean",
215
+ "meta": "object",
216
+ "created_at": "datetime",
217
+ "modified_at": "datetime"
218
+ },
219
+ "pk": "product_id",
220
+ "unique": ["sku", "slug"],
221
+ "softDelete": "is_deleted",
222
+ "timestamps": {
223
+ "created_at": "created_at",
224
+ "modified_at": "modified_at"
225
+ },
226
+ "parent": null
227
+ },
228
+ "users": {
229
+ "columns": {
230
+ "user_id": "auto_increment",
231
+ "name": "required|string|minLength:2|maxLength:100",
232
+ "email": "required|string|email|maxLength:255",
233
+ "phone": "string|phoneNumber",
234
+ "password_hash": "required|string:varchar(500)",
235
+ "avatar_url": "string|url",
236
+ "bio": "string:text|maxLength:2000",
237
+ "age": "integer|min:13|max:150",
238
+ "role": "required|string|in:admin,user,moderator",
239
+ "login_count": "integer:unsigned|min:0",
240
+ "is_verified": "boolean",
241
+ "is_deleted": "boolean",
242
+ "last_login_ip": "string|ip",
243
+ "metadata": "object|json",
244
+ "created_at": "datetime",
245
+ "modified_at": "datetime"
246
+ },
247
+ "pk": "user_id",
248
+ "unique": ["email"],
249
+ "softDelete": "is_deleted",
250
+ "timestamps": {
251
+ "created_at": "created_at",
252
+ "modified_at": "modified_at"
253
+ },
254
+ "parent": null
255
+ },
256
+ "addresses": {
257
+ "columns": {
258
+ "address_id": "auto_increment",
259
+ "user_id": "required|integer:unsigned",
260
+ "label": "string|in:home,work,billing,shipping|maxLength:50",
261
+ "line1": "required|string|minLength:5|maxLength:255",
262
+ "line2": "string|maxLength:255",
263
+ "city": "required|string|minLength:2|maxLength:100",
264
+ "state": "required|string|minLength:2|maxLength:100",
265
+ "postal_code": "required|string|minLength:3|maxLength:20",
266
+ "country": "required|string|minLength:2|maxLength:2",
267
+ "is_default": "boolean",
268
+ "created_at": "datetime",
269
+ "modified_at": "datetime"
270
+ },
271
+ "pk": "address_id",
272
+ "unique": ["address_id"],
273
+ "timestamps": {
274
+ "created_at": "created_at",
275
+ "modified_at": "modified_at"
276
+ },
277
+ "parent": null
278
+ },
279
+ "orders": {
280
+ "columns": {
281
+ "order_id": "auto_increment",
282
+ "user_id": "required|integer:unsigned",
283
+ "order_number": "required|string|alphaDash|maxLength:50",
284
+ "status": "required|string|in:pending,processing,shipped,delivered,cancelled",
285
+ "subtotal": "required|numeric:money|min:0",
286
+ "tax_amount": "required|numeric:money|min:0",
287
+ "shipping_amount": "required|numeric:money|min:0",
288
+ "discount_amount": "numeric:money|min:0",
289
+ "total": "required|numeric:money|min:0",
290
+ "currency": "required|string|minLength:3|maxLength:3",
291
+ "notes": "string:text|maxLength:2000",
292
+ "created_at": "datetime",
293
+ "modified_at": "datetime"
294
+ },
295
+ "pk": "order_id",
296
+ "unique": ["order_number"],
297
+ "timestamps": {
298
+ "created_at": "created_at",
299
+ "modified_at": "modified_at"
300
+ },
301
+ "parent": null
302
+ }
303
+ }
304
+ }
305
+ ```
306
+
307
+ ---
308
+
309
+ ## How It Affects Code Generation
310
+
311
+ ### Migration Generation
312
+
313
+ The sub-type controls the SQL column type in generated migrations:
314
+
315
+ ```sql
316
+ -- "name": "required|string:varchar(300)|minLength:3|maxLength:300"
317
+ CREATE TABLE products (
318
+ name VARCHAR(300) NOT NULL,
319
+ ...
320
+ );
321
+
322
+ -- "description": "string:text|maxLength:5000"
323
+ CREATE TABLE products (
324
+ description TEXT,
325
+ ...
326
+ );
327
+
328
+ -- "stock_quantity": "required|integer:unsigned|min:0"
329
+ CREATE TABLE products (
330
+ stock_quantity INT UNSIGNED NOT NULL, -- MySQL
331
+ stock_quantity INTEGER NOT NULL, -- PostgreSQL (no unsigned)
332
+ ...
333
+ );
334
+ ```
335
+
336
+ ### Model Generation
337
+
338
+ Validation rules are extracted and passed to the model structure for runtime enforcement:
339
+
340
+ ```js
341
+ // Generated model structure
342
+ const products = model(
343
+ db,
344
+ "products",
345
+ {
346
+ name: "required|string|minLength:3|maxLength:300",
347
+ slug: "required|string|regex:^[a-z0-9-]+$|maxLength:300",
348
+ price: "required|numeric|min:0",
349
+ weight_unit: "string|in:kg,lb,oz,g",
350
+ // ...
351
+ },
352
+ "product_id",
353
+ ["sku", "slug"],
354
+ { safeDelete: "is_deleted" },
355
+ );
356
+ ```
357
+
358
+ The model passes these rules to `node-input-validator` on every insert/update/patch operation. Sub-types are stripped — only the base type and validators are kept in the runtime structure.
359
+
360
+ ### OpenAPI Generation
361
+
362
+ Validators map to OpenAPI schema properties:
363
+
364
+ | Validator | OpenAPI Property |
365
+ | --------------- | ------------------------------------- |
366
+ | `email` | `format: "email"` |
367
+ | `url` | `format: "uri"` |
368
+ | `ip` | `format: "ipv4"` |
369
+ | `phoneNumber` | `pattern: "^\\+?[0-9\\s\\-\\(\\)]+$"` |
370
+ | `minLength:N` | `minLength: N` |
371
+ | `maxLength:N` | `maxLength: N` |
372
+ | `min:N` | `minimum: N` |
373
+ | `max:N` | `maximum: N` |
374
+ | `between:N1,N2` | `minimum: N1, maximum: N2` |
375
+ | `in:a,b,c` | `enum: ["a", "b", "c"]` |
376
+ | `regex:PATTERN` | `pattern: "PATTERN"` |
377
+ | `alpha` | `pattern: "^[a-zA-Z]+$"` |
378
+ | `alphaNumeric` | `pattern: "^[a-zA-Z0-9]+$"` |
379
+ | `alphaDash` | `pattern: "^[a-zA-Z0-9_-]+$"` |
380
+
381
+ ---
382
+
383
+ ## Backward Compatibility
384
+
385
+ The new syntax is fully backward compatible:
386
+
387
+ - `"required|string"` — still works (VARCHAR(255) NOT NULL, no extra validation)
388
+ - `"integer"` — still works (INTEGER, nullable, no extra validation)
389
+ - `"string:text"` — new sub-type, no validation
390
+ - `"required|string|email"` — new validation, default sub-type
391
+ - `"required|string:text|minLength:10|maxLength:5000"` — full syntax
392
+
393
+ Existing schemas without sub-types or validators continue to work unchanged.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "db-model-router",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Generative API Creation using mysql2 and express libraries in node js",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -20,7 +20,8 @@
20
20
  "test:mssql": "dotenv -e env/.env.mssql -- mocha test/adapters/mssql.*.test.js --timeout 30000 --exit",
21
21
  "test:kafka": "dotenv -e env/.env.kafka -- mocha test/kafka.test.js test/kafka.integration.test.js --timeout 30000 --exit",
22
22
  "test:properties": "mocha test/properties/*.property.test.js --timeout 30000 --exit",
23
- "test:all": "mocha test/adapters/*.test.js test/properties/*.property.test.js test/function.test.js --timeout 30000 --exit",
23
+ "test:all": "mocha test/adapters/*.test.js test/properties/*.property.test.js test/function.test.js test/commands/*.test.js --timeout 300000 --exit",
24
+ "test:command": "mocha test/commands/*.test.js --timeout 30000 --exit",
24
25
  "demo:clear": "node -e \"var fs=require('fs'),p=require('path'),d=p.join(__dirname,'demo');fs.existsSync(d)&&fs.rmSync(d,{recursive:true,force:true});fs.mkdirSync(d,{recursive:true})\"",
25
26
  "demo:create": "node scripts/demo-create.js"
26
27
  },
@@ -109,6 +110,7 @@
109
110
  }
110
111
  },
111
112
  "devDependencies": {
113
+ "@faker-js/faker": "^10.4.0",
112
114
  "dotenv-cli": "^11.0.0",
113
115
  "express": "^4.21.0",
114
116
  "faker": "^5.5.3",
package/skill/SKILL.md CHANGED
@@ -97,7 +97,7 @@ const users = model(
97
97
  {
98
98
  safeDelete: "is_deleted", // soft-delete column
99
99
  created_at: "created_at", // auto-managed timestamp
100
- modified_at: "updated_at", // auto-managed timestamp
100
+ modified_at: "modified_at", // auto-managed timestamp
101
101
  },
102
102
  );
103
103
 
@@ -343,12 +343,15 @@ Requires a `.env` file with `DB_TYPE` and connection variables.
343
343
  "email": "required|string",
344
344
  "is_deleted": "boolean",
345
345
  "created_at": "datetime",
346
- "updated_at": "datetime"
346
+ "modified_at": "datetime"
347
347
  },
348
348
  "pk": "user_id",
349
349
  "unique": ["email"],
350
350
  "softDelete": "is_deleted",
351
- "timestamps": { "created_at": "created_at", "modified_at": "updated_at" },
351
+ "timestamps": {
352
+ "created_at": "created_at",
353
+ "modified_at": "modified_at"
354
+ },
352
355
  "parent": null
353
356
  },
354
357
  "posts": {
@@ -385,10 +388,50 @@ Requires a `.env` file with `DB_TYPE` and connection variables.
385
388
 
386
389
  ### Column Rules
387
390
 
388
- Format: `(required|)?(string|integer|numeric|boolean|object|datetime|auto_increment)`
391
+ Format: `[required|]<type>[:<subtype>][|<validator>...]`
389
392
 
390
393
  Include ALL columns in schema (PK, timestamps, softDelete). The generator auto-excludes them from model `structure`.
391
394
 
395
+ **Sub-types** (colon syntax) refine the SQL column type:
396
+
397
+ | Base Type | Sub-Types | Default |
398
+ | --------- | ------------------------------------------------------------------------- | --------------- |
399
+ | `string` | `text`, `mediumtext`, `longtext`, `char`, `char(N)`, `varchar(N)`, `uuid` | `varchar(255)` |
400
+ | `integer` | `tinyint`, `smallint`, `bigint`, `unsigned`, `bigint_unsigned` | `int` |
401
+ | `numeric` | `float`, `double`, `decimal(P,S)`, `money` | `decimal(12,2)` |
402
+
403
+ **Validators** (pipe-separated, node-input-validator compatible):
404
+
405
+ | Validator | Description | Example |
406
+ | ------------------ | ---------------------------------- | ---------------------------- |
407
+ | `email` | Valid email format | `string\|email` |
408
+ | `phoneNumber` | Valid phone number | `string\|phoneNumber` |
409
+ | `url` | Valid URL | `string\|url` |
410
+ | `ip` | Valid IP address | `string\|ip` |
411
+ | `minLength:N` | Min string length | `string\|minLength:3` |
412
+ | `maxLength:N` | Max string length | `string\|maxLength:100` |
413
+ | `min:N` | Min numeric value | `integer\|min:0` |
414
+ | `max:N` | Max numeric value | `integer\|max:100` |
415
+ | `between:N1,N2` | Numeric range | `integer\|between:1,5` |
416
+ | `regex:PATTERN` | Regex pattern match | `string\|regex:^[a-z0-9-]+$` |
417
+ | `in:val1,val2,...` | Enum (must be one of) | `string\|in:active,inactive` |
418
+ | `alpha` | Only letters | `string\|alpha` |
419
+ | `alphaNumeric` | Only letters + numbers | `string\|alphaNumeric` |
420
+ | `alphaDash` | Letters, numbers, dash, underscore | `string\|alphaDash` |
421
+
422
+ **Full examples:**
423
+
424
+ ```
425
+ "email": "required|string|email|maxLength:255"
426
+ "description": "string:text|maxLength:5000"
427
+ "price": "required|numeric:decimal(10,2)|min:0"
428
+ "stock": "required|integer:unsigned|min:0"
429
+ "role": "required|string|in:admin,user,moderator"
430
+ "slug": "required|string|regex:^[a-z0-9-]+$|maxLength:300"
431
+ ```
432
+
433
+ For the full specification, see `docs/dbmr-schema-spec.md`.
434
+
392
435
  ### Table Fields
393
436
 
394
437
  | Field | Required | Description |
@@ -97,14 +97,35 @@ async function generate(args, flags, ctx) {
97
97
  const relationships = schema.relationships || [];
98
98
  const tableNames = meta.map((m) => m.table).sort();
99
99
 
100
- // Determine which artifact types to generate
101
- // All flags default to true use --flag=false to disable
102
- const genModels = args.models !== false;
103
- const genRoutes = args.routes !== false;
104
- const genOpenapi = args.openapi !== false;
105
- const genTests = args.tests !== false;
106
- const genMigrations = args.migrations !== false;
107
- const genSaas = args["saas-structure"] !== false;
100
+ // Determine which artifact types to generate.
101
+ // If any specific artifact flag is explicitly set to true, only generate those.
102
+ // Otherwise, all artifact types are generated (unless explicitly set to false).
103
+ const hasExplicitTrue =
104
+ args.models === true ||
105
+ args.routes === true ||
106
+ args.openapi === true ||
107
+ args.tests === true ||
108
+ args.migrations === true;
109
+
110
+ let genModels, genRoutes, genOpenapi, genTests, genMigrations, genSaas;
111
+
112
+ if (hasExplicitTrue) {
113
+ // Selective mode: only generate what was explicitly requested
114
+ genModels = args.models === true;
115
+ genRoutes = args.routes === true;
116
+ genOpenapi = args.openapi === true;
117
+ genTests = args.tests === true;
118
+ genMigrations = args.migrations === true;
119
+ genSaas = args["saas-structure"] === true;
120
+ } else {
121
+ // Default mode: generate all unless explicitly disabled
122
+ genModels = args.models !== false;
123
+ genRoutes = args.routes !== false;
124
+ genOpenapi = args.openapi !== false;
125
+ genTests = args.tests !== false;
126
+ genMigrations = args.migrations !== false;
127
+ genSaas = args["saas-structure"] !== false;
128
+ }
108
129
 
109
130
  const baseDir = process.cwd();
110
131
 
@@ -132,30 +153,30 @@ async function generate(args, flags, ctx) {
132
153
  childrenByParent[rel.parent].push(rel);
133
154
  }
134
155
 
135
- // Generate route files for each table
156
+ // Generate route files for each table (top-level only, skip children)
136
157
  for (const m of meta) {
137
158
  if (nestedChildren.has(m.table)) continue;
138
159
 
139
160
  const children = childrenByParent[m.table] || [];
140
161
  if (children.length > 0) {
141
- // Parent with children: generates index.js that mounts child routes
162
+ // Parent with children: generates route that mounts child routes
142
163
  planned.push({
143
- relPath: `routes/${m.table}/index.js`,
164
+ relPath: `routes/${m.table}.js`,
144
165
  content: generateParentRouteFile(m.table, children),
145
166
  });
146
167
  } else {
147
168
  // Simple table: just CRUD
148
169
  planned.push({
149
- relPath: `routes/${m.table}/index.js`,
170
+ relPath: `routes/${m.table}.js`,
150
171
  content: generateRouteFile(m.table),
151
172
  });
152
173
  }
153
174
  }
154
175
 
155
- // Child route files inside parent folders: routes/<parent>/<child>/index.js
176
+ // Child route files inside parent folders: routes/<parent>/<child>.js
156
177
  for (const rel of relationships) {
157
178
  planned.push({
158
- relPath: `routes/${rel.parent}/${rel.child}/index.js`,
179
+ relPath: `routes/${rel.parent}/${rel.child}.js`,
159
180
  content: generateChildRouteFile(rel.child, rel.parent, rel.foreignKey),
160
181
  });
161
182
  }
@@ -228,7 +249,7 @@ async function generate(args, flags, ctx) {
228
249
  if (nestedChildrenForTests.has(m.table)) continue;
229
250
  planned.push({
230
251
  relPath: `test/${m.table}.test.js`,
231
- content: generateTestFile(m.table, m.primary_key),
252
+ content: generateTestFile(m.table, m.primary_key, m.structure),
232
253
  });
233
254
  }
234
255
 
@@ -286,6 +307,15 @@ async function generate(args, flags, ctx) {
286
307
  }
287
308
 
288
309
  for (const entry of saasFiles) {
310
+ // Skip seed/credential files if they already exist on disk —
311
+ // they contain generated passwords that should not be overwritten.
312
+ if (
313
+ (entry.relPath === "seeds/saas-seed.js" ||
314
+ entry.relPath === "credentials.md") &&
315
+ fs.existsSync(path.join(baseDir, entry.relPath))
316
+ ) {
317
+ continue;
318
+ }
289
319
  planned.push(entry);
290
320
  }
291
321
  }
@@ -5,6 +5,7 @@ const path = require("path");
5
5
  const { generateModelFile } = require("./generate-model.js");
6
6
  const {
7
7
  generateRouteFile,
8
+ generateParentRouteFile,
8
9
  generateChildRouteFile,
9
10
  generateRoutesIndexFile,
10
11
  generateTestFile,
@@ -55,8 +56,11 @@ function buildExpectedFiles(meta, relationships) {
55
56
 
56
57
  // Collect child tables
57
58
  const nestedChildren = new Set();
59
+ const childrenByParent = {};
58
60
  for (const rel of relationships) {
59
61
  nestedChildren.add(rel.child);
62
+ if (!childrenByParent[rel.parent]) childrenByParent[rel.parent] = [];
63
+ childrenByParent[rel.parent].push(rel);
60
64
  }
61
65
 
62
66
  // Model files
@@ -67,10 +71,18 @@ function buildExpectedFiles(meta, relationships) {
67
71
  // Route files (top-level only, skip children)
68
72
  for (const m of meta) {
69
73
  if (nestedChildren.has(m.table)) continue;
70
- expected.set(
71
- `routes/${m.table}.js`,
72
- generateRouteFile(m.table, modelsRelPath),
73
- );
74
+ const children = childrenByParent[m.table] || [];
75
+ if (children.length > 0) {
76
+ expected.set(
77
+ `routes/${m.table}.js`,
78
+ generateParentRouteFile(m.table, children),
79
+ );
80
+ } else {
81
+ expected.set(
82
+ `routes/${m.table}.js`,
83
+ generateRouteFile(m.table, modelsRelPath),
84
+ );
85
+ }
74
86
  }
75
87
 
76
88
  // Child route files in subfolders: routes/<parent>/<child>.js
@@ -100,7 +112,7 @@ function buildExpectedFiles(meta, relationships) {
100
112
  if (nestedChildren.has(m.table)) continue;
101
113
  expected.set(
102
114
  `test/${m.table}.test.js`,
103
- generateTestFile(m.table, m.primary_key),
115
+ generateTestFile(m.table, m.primary_key, m.structure),
104
116
  );
105
117
  }
106
118