db-model-router 1.0.8 → 1.0.10
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 +25 -4
- package/db-manager/.dbmanager.sqlite-shm +0 -0
- package/db-manager/.dbmanager.sqlite-wal +0 -0
- package/demo/package-lock.json +1255 -93
- package/demo/package.json +5 -1
- package/demo/routes/index.js +0 -14
- package/demo/seeds/saas-seed.js +1 -1
- package/docs/dbmr-schema-spec.md +393 -0
- package/package.json +4 -2
- package/skill/SKILL.md +47 -4
- package/src/cli/commands/generate.js +39 -9
- package/src/cli/diff-engine.js +19 -7
- package/src/cli/generate-migration.js +207 -19
- package/src/cli/generate-route.js +152 -54
- package/src/cli/init/dependencies.js +4 -0
- package/src/cli/saas/generate-saas-routes.js +3 -13
- /package/demo/migrations/{20260510092158_create_migrations_table.sql → 20260518204325_create_migrations_table.sql} +0 -0
- /package/demo/migrations/{20260510092159_create_saas_tables.sql → 20260518204325_create_saas_tables.sql} +0 -0
- /package/demo/migrations/{20260510092159_create_tables.sql → 20260518204325_create_tables.sql} +0 -0
package/demo/package.json
CHANGED
package/demo/routes/index.js
CHANGED
|
@@ -17,13 +17,6 @@ import couponsRoute from "#routes/coupons/index.js";
|
|
|
17
17
|
import ordersRoute from "#routes/orders/index.js";
|
|
18
18
|
import productsRoute from "#routes/products/index.js";
|
|
19
19
|
import wishlistsRoute from "#routes/wishlists/index.js";
|
|
20
|
-
import product_imagesChildRoute from "#routes/products/product_images/index.js";
|
|
21
|
-
import product_variantsChildRoute from "#routes/products/product_variants/index.js";
|
|
22
|
-
import product_reviewsChildRoute from "#routes/products/product_reviews/index.js";
|
|
23
|
-
import cart_itemsChildRoute from "#routes/carts/cart_items/index.js";
|
|
24
|
-
import order_itemsChildRoute from "#routes/orders/order_items/index.js";
|
|
25
|
-
import paymentsChildRoute from "#routes/orders/payments/index.js";
|
|
26
|
-
import shipmentsChildRoute from "#routes/orders/shipments/index.js";
|
|
27
20
|
import docsRoute from "#routes/docs.js";
|
|
28
21
|
|
|
29
22
|
// SaaS routes
|
|
@@ -34,13 +27,6 @@ router.use("/roles", saasRolesRoute);
|
|
|
34
27
|
router.use("/roles/:role_id/permissions", saasPermissionsRoute);
|
|
35
28
|
|
|
36
29
|
router.use("/docs", docsRoute);
|
|
37
|
-
router.use("/products/:product_id/product_images", product_imagesChildRoute);
|
|
38
|
-
router.use("/products/:product_id/product_variants", product_variantsChildRoute);
|
|
39
|
-
router.use("/products/:product_id/product_reviews", product_reviewsChildRoute);
|
|
40
|
-
router.use("/carts/:cart_id/cart_items", cart_itemsChildRoute);
|
|
41
|
-
router.use("/orders/:order_id/order_items", order_itemsChildRoute);
|
|
42
|
-
router.use("/orders/:order_id/payments", paymentsChildRoute);
|
|
43
|
-
router.use("/orders/:order_id/shipments", shipmentsChildRoute);
|
|
44
30
|
|
|
45
31
|
// Schema-generated routes
|
|
46
32
|
router.use("/addresses", addressesRoute);
|
package/demo/seeds/saas-seed.js
CHANGED
|
@@ -13,7 +13,7 @@ const SUPER_ADMIN_EMAIL = "admin@system.local";
|
|
|
13
13
|
* Generated password for the Super Admin.
|
|
14
14
|
* This is cryptographically random and unique per generation.
|
|
15
15
|
*/
|
|
16
|
-
const SUPER_ADMIN_PASSWORD = "
|
|
16
|
+
const SUPER_ADMIN_PASSWORD = "d8a53d980af3a1516a211b4875dcec77";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Super Admin permissions: all actions for all modules with global scope.
|
|
@@ -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.
|
|
3
|
+
"version": "1.0.10",
|
|
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
|
|
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: "
|
|
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
|
-
"
|
|
346
|
+
"modified_at": "datetime"
|
|
347
347
|
},
|
|
348
348
|
"pk": "user_id",
|
|
349
349
|
"unique": ["email"],
|
|
350
350
|
"softDelete": "is_deleted",
|
|
351
|
-
"timestamps": {
|
|
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: `
|
|
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
|
-
//
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
|
@@ -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
|
}
|