db-model-router 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,255 @@
1
+ "use strict";
2
+
3
+ const VALID_ADAPTERS = new Set([
4
+ "mysql",
5
+ "mariadb",
6
+ "postgres",
7
+ "sqlite3",
8
+ "mongodb",
9
+ "mssql",
10
+ "cockroachdb",
11
+ "oracle",
12
+ "redis",
13
+ "dynamodb",
14
+ ]);
15
+
16
+ const VALID_FRAMEWORKS = new Set(["express", "ultimate-express"]);
17
+
18
+ const COLUMN_RULE_RE =
19
+ /^(required\|)?(string|integer|numeric|boolean|object|datetime|auto_increment)$/;
20
+
21
+ class SchemaValidationError extends Error {
22
+ constructor(errors) {
23
+ super(`Schema validation failed: ${errors.length} error(s)`);
24
+ this.errors = errors;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Validate a raw schema object and collect all errors.
30
+ * @param {object} raw — parsed JSON object
31
+ * @returns {{ valid: boolean, errors: Array<{ path: string, message: string }> }}
32
+ */
33
+ function validateSchema(raw) {
34
+ const errors = [];
35
+
36
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw)) {
37
+ errors.push({ path: "", message: "Schema must be a non-null object" });
38
+ return { valid: false, errors };
39
+ }
40
+
41
+ // adapter
42
+ if (!raw.adapter || typeof raw.adapter !== "string") {
43
+ errors.push({
44
+ path: "adapter",
45
+ message: "adapter is required and must be a string",
46
+ });
47
+ } else if (!VALID_ADAPTERS.has(raw.adapter)) {
48
+ errors.push({
49
+ path: "adapter",
50
+ message: `Invalid adapter "${raw.adapter}". Must be one of: ${[...VALID_ADAPTERS].join(", ")}`,
51
+ });
52
+ }
53
+
54
+ // framework
55
+ if (!raw.framework || typeof raw.framework !== "string") {
56
+ errors.push({
57
+ path: "framework",
58
+ message: "framework is required and must be a string",
59
+ });
60
+ } else if (!VALID_FRAMEWORKS.has(raw.framework)) {
61
+ errors.push({
62
+ path: "framework",
63
+ message: `Invalid framework "${raw.framework}". Must be one of: ${[...VALID_FRAMEWORKS].join(", ")}`,
64
+ });
65
+ }
66
+
67
+ // tables
68
+ if (
69
+ raw.tables == null ||
70
+ typeof raw.tables !== "object" ||
71
+ Array.isArray(raw.tables)
72
+ ) {
73
+ errors.push({
74
+ path: "tables",
75
+ message: "tables is required and must be an object",
76
+ });
77
+ } else {
78
+ validateTables(raw.tables, errors);
79
+ }
80
+
81
+ // relationships
82
+ if (raw.relationships !== undefined) {
83
+ if (!Array.isArray(raw.relationships)) {
84
+ errors.push({
85
+ path: "relationships",
86
+ message: "relationships must be an array",
87
+ });
88
+ } else {
89
+ const tableNames =
90
+ raw.tables &&
91
+ typeof raw.tables === "object" &&
92
+ !Array.isArray(raw.tables)
93
+ ? new Set(Object.keys(raw.tables))
94
+ : new Set();
95
+ validateRelationships(raw.relationships, tableNames, errors);
96
+ }
97
+ }
98
+
99
+ // options
100
+ if (raw.options !== undefined) {
101
+ if (
102
+ raw.options == null ||
103
+ typeof raw.options !== "object" ||
104
+ Array.isArray(raw.options)
105
+ ) {
106
+ errors.push({ path: "options", message: "options must be an object" });
107
+ }
108
+ }
109
+
110
+ return { valid: errors.length === 0, errors };
111
+ }
112
+
113
+ /**
114
+ * Validate all table entries.
115
+ */
116
+ function validateTables(tables, errors) {
117
+ for (const [tableName, tableDef] of Object.entries(tables)) {
118
+ const basePath = `tables.${tableName}`;
119
+
120
+ if (
121
+ tableDef == null ||
122
+ typeof tableDef !== "object" ||
123
+ Array.isArray(tableDef)
124
+ ) {
125
+ errors.push({
126
+ path: basePath,
127
+ message: `Table "${tableName}" must be an object`,
128
+ });
129
+ continue;
130
+ }
131
+
132
+ // columns
133
+ if (
134
+ tableDef.columns == null ||
135
+ typeof tableDef.columns !== "object" ||
136
+ Array.isArray(tableDef.columns)
137
+ ) {
138
+ errors.push({
139
+ path: `${basePath}.columns`,
140
+ message: `Table "${tableName}" must have a columns object`,
141
+ });
142
+ continue;
143
+ }
144
+
145
+ const columnNames = new Set(Object.keys(tableDef.columns));
146
+ const pk = tableDef.pk || "id";
147
+
148
+ // Validate each column rule
149
+ for (const [colName, rule] of Object.entries(tableDef.columns)) {
150
+ if (typeof rule !== "string" || !COLUMN_RULE_RE.test(rule)) {
151
+ errors.push({
152
+ path: `${basePath}.columns.${colName}`,
153
+ message: `Invalid column rule "${rule}" for column "${colName}". Must match pattern: (required|)?(string|integer|numeric|boolean|object)`,
154
+ });
155
+ }
156
+ }
157
+
158
+ // Validate unique entries
159
+ if (tableDef.unique !== undefined) {
160
+ if (!Array.isArray(tableDef.unique)) {
161
+ errors.push({
162
+ path: `${basePath}.unique`,
163
+ message: `unique must be an array in table "${tableName}"`,
164
+ });
165
+ } else {
166
+ for (let i = 0; i < tableDef.unique.length; i++) {
167
+ const entry = tableDef.unique[i];
168
+ if (typeof entry !== "string") {
169
+ errors.push({
170
+ path: `${basePath}.unique[${i}]`,
171
+ message: `unique entry must be a string in table "${tableName}"`,
172
+ });
173
+ } else if (entry !== pk && !columnNames.has(entry)) {
174
+ errors.push({
175
+ path: `${basePath}.unique[${i}]`,
176
+ message: `unique entry "${entry}" does not match any column or the primary key "${pk}" in table "${tableName}"`,
177
+ });
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ // Validate softDelete
184
+ if (tableDef.softDelete !== undefined && tableDef.softDelete !== null) {
185
+ if (typeof tableDef.softDelete !== "string") {
186
+ errors.push({
187
+ path: `${basePath}.softDelete`,
188
+ message: `softDelete must be a string in table "${tableName}"`,
189
+ });
190
+ } else if (!columnNames.has(tableDef.softDelete)) {
191
+ errors.push({
192
+ path: `${basePath}.softDelete`,
193
+ message: `softDelete column "${tableDef.softDelete}" does not exist in table "${tableName}"`,
194
+ });
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Validate all relationship entries.
202
+ */
203
+ function validateRelationships(relationships, tableNames, errors) {
204
+ for (let i = 0; i < relationships.length; i++) {
205
+ const rel = relationships[i];
206
+ const basePath = `relationships[${i}]`;
207
+
208
+ if (rel == null || typeof rel !== "object" || Array.isArray(rel)) {
209
+ errors.push({
210
+ path: basePath,
211
+ message: "Each relationship must be an object",
212
+ });
213
+ continue;
214
+ }
215
+
216
+ if (!rel.parent || typeof rel.parent !== "string") {
217
+ errors.push({
218
+ path: `${basePath}.parent`,
219
+ message: "Relationship must have a parent string",
220
+ });
221
+ } else if (!tableNames.has(rel.parent)) {
222
+ errors.push({
223
+ path: `${basePath}.parent`,
224
+ message: `Relationship parent "${rel.parent}" does not reference an existing table`,
225
+ });
226
+ }
227
+
228
+ if (!rel.child || typeof rel.child !== "string") {
229
+ errors.push({
230
+ path: `${basePath}.child`,
231
+ message: "Relationship must have a child string",
232
+ });
233
+ } else if (!tableNames.has(rel.child)) {
234
+ errors.push({
235
+ path: `${basePath}.child`,
236
+ message: `Relationship child "${rel.child}" does not reference an existing table`,
237
+ });
238
+ }
239
+
240
+ if (!rel.foreignKey || typeof rel.foreignKey !== "string") {
241
+ errors.push({
242
+ path: `${basePath}.foreignKey`,
243
+ message: "Relationship must have a foreignKey string",
244
+ });
245
+ }
246
+ }
247
+ }
248
+
249
+ module.exports = {
250
+ SchemaValidationError,
251
+ validateSchema,
252
+ VALID_ADAPTERS,
253
+ VALID_FRAMEWORKS,
254
+ COLUMN_RULE_RE,
255
+ };
package/src/serve.js CHANGED
@@ -21,8 +21,10 @@ if (process.env.NODE_ENV === "TEST") {
21
21
  port = process.env.port;
22
22
  }
23
23
 
24
- const { db, model, route } = require("./index");
25
- db.connect({
24
+ const { init, model, route } = require("./index");
25
+ const dbModule = require("./index");
26
+ init("mysql");
27
+ dbModule.db.connect({
26
28
  connectionLimit: 100,
27
29
  host: process.env.DB_HOST,
28
30
  user: process.env.DB_USER,
@@ -32,7 +34,7 @@ db.connect({
32
34
  charset: "utf8mb4",
33
35
  });
34
36
  const test = model(
35
- db,
37
+ dbModule.db,
36
38
  "test",
37
39
  {
38
40
  test_id: "required|integer",
package/docs/README.md DELETED
@@ -1,208 +0,0 @@
1
- # db-model-router
2
-
3
- A database-agnostic REST API generator for Node.js. Works with Express or ultimate-express (a high-performance drop-in replacement). Define a model, get a full CRUD API with filtering, pagination, and bulk operations — backed by any of 9 supported databases.
4
-
5
- ## Supported Adapters
6
-
7
- | Adapter | Module Key | Driver | Install |
8
- | ---------------------------------------- | ------------- | ------------------------ | ---------------------------------------------------------------------- |
9
- | [MySQL](#mysql-example) | `mysql` | mysql2 | `npm i db-model-router mysql2` |
10
- | [PostgreSQL](./adapters/postgres.md) | `postgres` | pg | `npm i db-model-router pg` |
11
- | [SQLite3](./adapters/sqlite3.md) | `sqlite3` | better-sqlite3 | `npm i db-model-router better-sqlite3` |
12
- | [MongoDB](./adapters/mongodb.md) | `mongodb` | mongodb | `npm i db-model-router mongodb` |
13
- | [MSSQL](./adapters/mssql.md) | `mssql` | mssql | `npm i db-model-router mssql` |
14
- | [CockroachDB](./adapters/cockroachdb.md) | `cockroachdb` | pg | `npm i db-model-router pg` |
15
- | [Oracle](./adapters/oracle.md) | `oracle` | oracledb | `npm i db-model-router oracledb` |
16
- | [Redis](./adapters/redis.md) | `redis` | ioredis | `npm i db-model-router ioredis` |
17
- | [DynamoDB](./adapters/dynamodb.md) | `dynamodb` | @aws-sdk/client-dynamodb | `npm i db-model-router @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb` |
18
-
19
- ## Installation
20
-
21
- Install the core package, your preferred Express framework, and the driver for your database:
22
-
23
- ```bash
24
- # Pick your Express framework (one of the two)
25
- npm install express
26
- # OR for ~6x faster performance:
27
- npm install ultimate-express
28
-
29
- # Then install db-model-router + your database driver
30
- npm install db-model-router <driver>
31
- ```
32
-
33
- Both `express` and `ultimate-express` are optional peer dependencies — the library auto-detects which one is installed (preferring `ultimate-express` when both are present). All database drivers are also optional peer dependencies.
34
-
35
- ## MySQL Example
36
-
37
- ### 1. Connect
38
-
39
- ```js
40
- const { init, db, model, route } = require("db-model-router");
41
-
42
- // Default adapter is mysql, so init() is optional
43
- db.connect({
44
- host: "localhost",
45
- port: 3306,
46
- user: "root",
47
- password: "password",
48
- database: "my_app",
49
- connectionLimit: 100,
50
- });
51
- ```
52
-
53
- ### 2. Define a Model
54
-
55
- ```js
56
- const users = model(
57
- db,
58
- "users",
59
- {
60
- name: "required|string",
61
- email: "required|string",
62
- age: "required|integer",
63
- meta: "object",
64
- },
65
- "id",
66
- ["email"],
67
- { safeDelete: "is_deleted" },
68
- );
69
- ```
70
-
71
- Schema types: `string`, `integer`, `numeric`, `object`. Prefix with `required|` to enforce on insert/update.
72
-
73
- ### 3. Mount REST Routes
74
-
75
- ```js
76
- // Works with either express or ultimate-express
77
- const express = require("express"); // or require("ultimate-express")
78
- const app = express();
79
- app.use(express.json());
80
- app.use("/users", route(users));
81
- app.listen(3000);
82
- ```
83
-
84
- This creates 9 endpoints:
85
-
86
- | Method | Path | Description |
87
- | ------ | ------------ | ------------------------------- |
88
- | GET | `/users/:id` | Get one record by PK |
89
- | POST | `/users/:id` | Insert a single record |
90
- | PUT | `/users/:id` | Update a single record |
91
- | PATCH | `/users/:id` | Partial update (changed fields) |
92
- | DELETE | `/users/:id` | Delete a single record |
93
- | GET | `/users/` | List with pagination |
94
- | POST | `/users/` | Bulk insert (`{ data: [...] }`) |
95
- | PUT | `/users/` | Bulk update (`{ data: [...] }`) |
96
- | DELETE | `/users/` | Bulk delete |
97
-
98
- ### 4. Payload Override
99
-
100
- Inject values from the request into every payload (useful for multi-tenant apps):
101
-
102
- ```js
103
- app.use("/users", route(users, { user_id: "user.user_id" }));
104
- ```
105
-
106
- ## Model API
107
-
108
- All model methods are async.
109
-
110
- ### insert / update / patch / upsert
111
-
112
- ```js
113
- const user = await users.insert({ name: "Alice", email: "a@b.com", age: 30 });
114
- const bulk = await users.insert({ data: [{ ... }, { ... }] });
115
- const updated = await users.update({ id: 1, name: "Alice V2", email: "a@b.com", age: 31 });
116
- const patched = await users.patch({ id: 1, age: 35 }); // partial — only updates age
117
- ```
118
-
119
- ### byId / find / findOne / list
120
-
121
- ```js
122
- await users.byId(1); // record or null
123
- await users.find({ name: "Alice" }); // { data: [...], count }
124
- await users.findOne({ email: "a@b.com" }); // record or false
125
- await users.list({ page: 0, size: 10, sort: ["-age"] }); // { data: [...], count }
126
- ```
127
-
128
- ### remove
129
-
130
- ```js
131
- await users.remove(1);
132
- await users.remove({ name: "Bob" });
133
- ```
134
-
135
- ## Filter System
136
-
137
- Structure: `[OR_groups[AND_conditions[column, operator, value]]]`
138
-
139
- Operators: `=`, `like`, `not like`, `in`, `not in`, `<`, `>`, `<=`, `>=`, `!=`
140
-
141
- ```js
142
- // Alice AND age 30
143
- await db.get("users", [
144
- [
145
- ["name", "=", "Alice"],
146
- ["age", "=", 30],
147
- ],
148
- ]);
149
-
150
- // Alice OR age > 30
151
- await db.get("users", [[["name", "=", "Alice"]], [["age", ">", 30]]]);
152
- ```
153
-
154
- ## Switching Adapters
155
-
156
- ```js
157
- const { init, db, model, route } = require("db-model-router");
158
- init("postgres"); // or "mongodb", "sqlite3", "mssql", etc.
159
- db.connect({
160
- host: "localhost",
161
- port: 5432,
162
- user: "postgres",
163
- password: "password",
164
- database: "my_app",
165
- });
166
- ```
167
-
168
- The model and route APIs remain identical across all adapters.
169
-
170
- ## CLI Tools
171
-
172
- ### generate-app
173
-
174
- Scaffolds a complete Express REST API from an existing database.
175
-
176
- ```bash
177
- db-model-router-generate-app --type mysql --env .env
178
- db-model-router-generate-app --type sqlite3 --database ./myapp.db --output ./my-api
179
- db-model-router-generate-app --type postgres --env .env --tables users,posts,posts.comments
180
- ```
181
-
182
- Creates: `app.js`, `models/`, `routes/`, `middleware/logger.js`, `.env.example`, `openapi.json`
183
-
184
- ### generate-model
185
-
186
- Introspects DB → generates model files with auto-detected PK, unique indexes, timestamps, soft-delete.
187
-
188
- ```bash
189
- db-model-router-generate-model --type mysql --env .env --output ./models [--tables users,posts]
190
- ```
191
-
192
- ### generate-route
193
-
194
- Generates route files + OpenAPI spec from models. Supports parent-child via dot notation.
195
-
196
- ```bash
197
- db-model-router-generate-route --models ./models --output ./routes [--tables posts,posts.comments]
198
- ```
199
-
200
- `posts.comments` → nested route `posts/:post_id/comments` with FK scoping.
201
-
202
- ## License
203
-
204
- Apache-2.0
205
-
206
- ## LLM Skill Reference
207
-
208
- For AI/LLM integration, see the [Skill Reference](./SKILL.md).