db-model-router 1.0.6 → 1.0.7

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 (137) hide show
  1. package/README.md +150 -11
  2. package/TODO.md +0 -15
  3. package/db-manager/.dbmanager.sqlite +0 -0
  4. package/db-manager/README.md +223 -0
  5. package/db-manager/adapter-proxy.js +361 -0
  6. package/db-manager/demo/cockroachdb.env +6 -0
  7. package/db-manager/demo/demo.sqlite +0 -0
  8. package/db-manager/demo/dynamodb.env +7 -0
  9. package/db-manager/demo/mongodb.env +4 -0
  10. package/db-manager/demo/mssql.env +6 -0
  11. package/db-manager/demo/mysql.env +6 -0
  12. package/db-manager/demo/oracle.env +6 -0
  13. package/db-manager/demo/postgres.env +6 -0
  14. package/db-manager/demo/redis.env +4 -0
  15. package/db-manager/demo/seeds/cockroachdb.sql +32 -0
  16. package/db-manager/demo/seeds/mssql.sql +32 -0
  17. package/db-manager/demo/seeds/mysql.sql +32 -0
  18. package/db-manager/demo/seeds/oracle.sql +43 -0
  19. package/db-manager/demo/seeds/postgres.sql +32 -0
  20. package/db-manager/demo/seeds/sqlite3.sql +32 -0
  21. package/db-manager/demo/sqlite3.env +2 -0
  22. package/db-manager/metadata-db.js +170 -0
  23. package/db-manager/public/.gitkeep +1 -0
  24. package/db-manager/public/css/style.css +1413 -0
  25. package/db-manager/public/js/app.js +1370 -0
  26. package/db-manager/routes/api.js +388 -0
  27. package/db-manager/routes/views.js +61 -0
  28. package/db-manager/server.js +39 -0
  29. package/db-manager/utils/build-filter-config.js +18 -0
  30. package/db-manager/utils/csv-export.js +59 -0
  31. package/db-manager/utils/export-filename.js +39 -0
  32. package/db-manager/utils/filter-tables.js +20 -0
  33. package/db-manager/utils/parse-filters.js +93 -0
  34. package/db-manager/utils/sort-state.js +35 -0
  35. package/db-manager/views/.gitkeep +1 -0
  36. package/db-manager/views/dashboard.ejs +53 -0
  37. package/db-manager/views/history.ejs +52 -0
  38. package/db-manager/views/index.ejs +35 -0
  39. package/db-manager/views/layout.ejs +31 -0
  40. package/db-manager/views/partials/data-panel.ejs +74 -0
  41. package/db-manager/views/partials/header.ejs +36 -0
  42. package/db-manager/views/partials/sidebar.ejs +30 -0
  43. package/db-manager/views/query.ejs +58 -0
  44. package/dbmr.schema.json +22 -44
  45. package/demo/.dockerignore +7 -0
  46. package/demo/.env.example +14 -0
  47. package/demo/Dockerfile +20 -0
  48. package/demo/app.js +39 -0
  49. package/demo/commons/add_migration.js +43 -0
  50. package/demo/commons/db.js +28 -0
  51. package/demo/commons/migrate.js +68 -0
  52. package/demo/commons/modules.js +18 -0
  53. package/demo/commons/password.js +36 -0
  54. package/demo/commons/security.js +30 -0
  55. package/demo/commons/session.js +13 -0
  56. package/demo/commons/webhook.js +81 -0
  57. package/demo/dbmr.schema.json +338 -0
  58. package/demo/middleware/authenticate.js +14 -0
  59. package/demo/middleware/hasPermission.js +30 -0
  60. package/demo/middleware/logger.js +67 -0
  61. package/demo/middleware/tenantIsolation.js +17 -0
  62. package/demo/migrations/20260509170349_create_migrations_table.sql +6 -0
  63. package/demo/migrations/20260509170349_create_saas_tables.sql +69 -0
  64. package/demo/migrations/20260509170349_create_tables.sql +193 -0
  65. package/demo/models/addresses.js +24 -0
  66. package/demo/models/cart_items.js +20 -0
  67. package/demo/models/carts.js +18 -0
  68. package/demo/models/categories.js +22 -0
  69. package/demo/models/coupons.js +25 -0
  70. package/demo/models/index.js +43 -0
  71. package/demo/models/order_items.js +23 -0
  72. package/demo/models/orders.js +27 -0
  73. package/demo/models/payments.js +23 -0
  74. package/demo/models/product_images.js +20 -0
  75. package/demo/models/product_reviews.js +22 -0
  76. package/demo/models/product_variants.js +22 -0
  77. package/demo/models/products.js +32 -0
  78. package/demo/models/role_permissions.js +17 -0
  79. package/demo/models/roles.js +17 -0
  80. package/demo/models/shipments.js +21 -0
  81. package/demo/models/tenants.js +18 -0
  82. package/demo/models/users.js +23 -0
  83. package/demo/models/webhook_logs.js +22 -0
  84. package/demo/models/webhooks.js +19 -0
  85. package/demo/models/wishlists.js +17 -0
  86. package/demo/openapi.json +7000 -0
  87. package/demo/package-lock.json +2810 -0
  88. package/demo/package.json +43 -0
  89. package/demo/routes/addresses/index.js +6 -0
  90. package/demo/routes/auth/index.js +55 -0
  91. package/demo/routes/carts/cart_items/index.js +7 -0
  92. package/demo/routes/carts/index.js +6 -0
  93. package/demo/routes/categories/index.js +6 -0
  94. package/demo/routes/coupons/index.js +6 -0
  95. package/demo/routes/docs.js +18 -0
  96. package/demo/routes/health.js +35 -0
  97. package/demo/routes/index.js +54 -0
  98. package/demo/routes/orders/index.js +6 -0
  99. package/demo/routes/orders/order_items/index.js +7 -0
  100. package/demo/routes/orders/payments/index.js +7 -0
  101. package/demo/routes/orders/shipments/index.js +7 -0
  102. package/demo/routes/products/index.js +6 -0
  103. package/demo/routes/products/product_images/index.js +7 -0
  104. package/demo/routes/products/product_reviews/index.js +7 -0
  105. package/demo/routes/products/product_variants/index.js +7 -0
  106. package/demo/routes/roles/index.js +75 -0
  107. package/demo/routes/roles/permissions/index.js +47 -0
  108. package/demo/routes/tenants/index.js +45 -0
  109. package/demo/routes/users/index.js +45 -0
  110. package/demo/routes/wishlists/index.js +6 -0
  111. package/demo/seeds/saas-seed.js +329 -0
  112. package/docker-compose.yml +61 -0
  113. package/package.json +120 -113
  114. package/scripts/demo-create.js +1 -1
  115. package/skill/SKILL.md +119 -3
  116. package/src/cli/commands/db-manager.js +134 -0
  117. package/src/cli/commands/generate.js +106 -60
  118. package/src/cli/commands/help.js +0 -1
  119. package/src/cli/generate-route.js +60 -21
  120. package/src/cli/generate-saas-structure.js +122 -0
  121. package/src/cli/init/generators.js +6 -0
  122. package/src/cli/init.js +8 -0
  123. package/src/cli/main.js +8 -1
  124. package/src/cli/saas/generate-saas-middleware.js +108 -0
  125. package/src/cli/saas/generate-saas-migrations.js +480 -0
  126. package/src/cli/saas/generate-saas-models.js +211 -0
  127. package/src/cli/saas/generate-saas-openapi.js +419 -0
  128. package/src/cli/saas/generate-saas-routes.js +435 -0
  129. package/src/cli/saas/generate-saas-seeds.js +243 -0
  130. package/src/cli/saas/generate-saas-utils.js +176 -0
  131. package/src/commons/kafka.js +139 -0
  132. package/src/commons/model.js +29 -9
  133. package/src/index.js +2 -0
  134. package/src/mssql/db.js +41 -3
  135. package/src/mysql/db.js +3 -0
  136. package/src/postgres/db.js +6 -0
  137. package/src/cli/generate-db-manager.js +0 -1573
@@ -0,0 +1,419 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * SaaS OpenAPI spec generator.
5
+ *
6
+ * Generates OpenAPI 3.0 paths and schemas for the SaaS-specific routes:
7
+ * - Auth routes (login, logout)
8
+ * - CRUD routes for users, tenants, roles with middleware annotations
9
+ * - Nested role_permissions routes
10
+ *
11
+ * This is merged into the main openapi.json alongside schema-generated paths.
12
+ */
13
+
14
+ const { MODEL_DEFINITIONS } = require("./generate-saas-models");
15
+
16
+ /**
17
+ * Generate the SaaS portion of the OpenAPI spec.
18
+ * Returns paths and schemas to be merged into the main spec.
19
+ *
20
+ * @returns {{ paths: object, schemas: object, securitySchemes: object }}
21
+ */
22
+ function generateSaasOpenAPIPaths() {
23
+ const paths = {};
24
+ const schemas = {};
25
+
26
+ // --- Security scheme ---
27
+ const securitySchemes = {
28
+ sessionAuth: {
29
+ type: "apiKey",
30
+ in: "cookie",
31
+ name: "connect.sid",
32
+ description: "Session cookie authentication",
33
+ },
34
+ };
35
+
36
+ const security = [{ sessionAuth: [] }];
37
+
38
+ // --- Auth routes ---
39
+ paths["/api/auth/login"] = {
40
+ post: {
41
+ tags: ["auth"],
42
+ summary: "Login and create session",
43
+ description:
44
+ "Authenticate with email and password. On success, populates session with user, role, and permissions.",
45
+ requestBody: {
46
+ required: true,
47
+ content: {
48
+ "application/json": {
49
+ schema: {
50
+ type: "object",
51
+ required: ["email", "password"],
52
+ properties: {
53
+ email: {
54
+ type: "string",
55
+ format: "email",
56
+ description: "User email address",
57
+ },
58
+ password: { type: "string", description: "User password" },
59
+ },
60
+ },
61
+ },
62
+ },
63
+ },
64
+ responses: {
65
+ 200: {
66
+ description: "Login successful",
67
+ content: {
68
+ "application/json": {
69
+ schema: {
70
+ type: "object",
71
+ properties: {
72
+ message: { type: "string", example: "Login successful" },
73
+ user: {
74
+ type: "object",
75
+ properties: {
76
+ id: { type: "integer" },
77
+ email: { type: "string" },
78
+ name: { type: "string" },
79
+ },
80
+ },
81
+ },
82
+ },
83
+ },
84
+ },
85
+ },
86
+ 401: { description: "Invalid credentials" },
87
+ 500: { description: "Internal server error" },
88
+ },
89
+ },
90
+ };
91
+
92
+ paths["/api/auth/logout"] = {
93
+ post: {
94
+ tags: ["auth"],
95
+ summary: "Logout and destroy session",
96
+ description: "Destroys the current session. Requires authentication.",
97
+ security,
98
+ responses: {
99
+ 200: {
100
+ description: "Logout successful",
101
+ content: {
102
+ "application/json": {
103
+ schema: {
104
+ type: "object",
105
+ properties: {
106
+ message: { type: "string", example: "Logout successful" },
107
+ },
108
+ },
109
+ },
110
+ },
111
+ },
112
+ 401: { description: "Unauthorized - no valid session" },
113
+ 500: { description: "Failed to destroy session" },
114
+ },
115
+ },
116
+ };
117
+
118
+ // --- SaaS CRUD routes ---
119
+ const crudTables = [
120
+ { table: "users", tag: "users", prefix: "/api/users", module: "users" },
121
+ {
122
+ table: "tenants",
123
+ tag: "tenants",
124
+ prefix: "/api/tenants",
125
+ module: "tenants",
126
+ },
127
+ { table: "roles", tag: "roles", prefix: "/api/roles", module: "roles" },
128
+ ];
129
+
130
+ for (const { table, tag, prefix, module } of crudTables) {
131
+ const modelDef = MODEL_DEFINITIONS.find((m) => m.table === table);
132
+ if (!modelDef) continue;
133
+
134
+ const pk = modelDef.primary_key;
135
+ const schemaName = capitalize(table);
136
+
137
+ // Build schema
138
+ const properties = {};
139
+ const required = [];
140
+ properties[pk] = { type: "integer", description: "Primary key" };
141
+ for (const [col, rule] of Object.entries(modelDef.structure)) {
142
+ const parsed = parseRule(rule);
143
+ properties[col] = { type: parsed.type };
144
+ if (parsed.required) required.push(col);
145
+ }
146
+ schemas[schemaName] = {
147
+ type: "object",
148
+ properties,
149
+ ...(required.length > 0 ? { required } : {}),
150
+ };
151
+
152
+ const ref = { $ref: `#/components/schemas/${schemaName}` };
153
+
154
+ // GET / - List
155
+ paths[`${prefix}/`] = {
156
+ get: {
157
+ tags: [tag],
158
+ summary: `List ${table}`,
159
+ description: `List all ${table}. Requires authentication, tenant isolation, and ${module}:read permission.`,
160
+ security,
161
+ parameters: [
162
+ {
163
+ name: "page",
164
+ in: "query",
165
+ schema: { type: "integer", default: 0 },
166
+ },
167
+ {
168
+ name: "size",
169
+ in: "query",
170
+ schema: { type: "integer", default: 30 },
171
+ },
172
+ ],
173
+ responses: {
174
+ 200: {
175
+ description: "Success",
176
+ content: {
177
+ "application/json": {
178
+ schema: {
179
+ type: "object",
180
+ properties: {
181
+ data: { type: "array", items: ref },
182
+ count: { type: "integer" },
183
+ },
184
+ },
185
+ },
186
+ },
187
+ },
188
+ 401: { description: "Unauthorized" },
189
+ 403: { description: "Forbidden - insufficient permissions" },
190
+ },
191
+ },
192
+ post: {
193
+ tags: [tag],
194
+ summary: `Create ${table.replace(/s$/, "")}`,
195
+ description: `Create a new ${table.replace(/s$/, "")}. Requires authentication, tenant isolation, and ${module}:write permission.`,
196
+ security,
197
+ requestBody: {
198
+ required: true,
199
+ content: { "application/json": { schema: ref } },
200
+ },
201
+ responses: {
202
+ 201: {
203
+ description: "Created",
204
+ content: { "application/json": { schema: ref } },
205
+ },
206
+ 401: { description: "Unauthorized" },
207
+ 403: { description: "Forbidden - insufficient permissions" },
208
+ 500: { description: "Internal server error" },
209
+ },
210
+ },
211
+ };
212
+
213
+ // GET /:id, PUT /:id, DELETE /:id
214
+ paths[`${prefix}/{${pk}}`] = {
215
+ get: {
216
+ tags: [tag],
217
+ summary: `Get ${table.replace(/s$/, "")} by ID`,
218
+ description: `Get a single ${table.replace(/s$/, "")} by ${pk}. Requires ${module}:read permission.`,
219
+ security,
220
+ parameters: [
221
+ { name: pk, in: "path", required: true, schema: { type: "integer" } },
222
+ ],
223
+ responses: {
224
+ 200: {
225
+ description: "Success",
226
+ content: { "application/json": { schema: ref } },
227
+ },
228
+ 401: { description: "Unauthorized" },
229
+ 403: { description: "Forbidden" },
230
+ 404: { description: "Not found" },
231
+ },
232
+ },
233
+ put: {
234
+ tags: [tag],
235
+ summary: `Update ${table.replace(/s$/, "")}`,
236
+ description: `Update a ${table.replace(/s$/, "")} by ${pk}. Requires ${module}:update permission.`,
237
+ security,
238
+ parameters: [
239
+ { name: pk, in: "path", required: true, schema: { type: "integer" } },
240
+ ],
241
+ requestBody: {
242
+ required: true,
243
+ content: { "application/json": { schema: ref } },
244
+ },
245
+ responses: {
246
+ 200: {
247
+ description: "Updated",
248
+ content: { "application/json": { schema: ref } },
249
+ },
250
+ 401: { description: "Unauthorized" },
251
+ 403: { description: "Forbidden" },
252
+ 404: { description: "Not found" },
253
+ },
254
+ },
255
+ delete: {
256
+ tags: [tag],
257
+ summary: `Delete ${table.replace(/s$/, "")}`,
258
+ description: `Delete a ${table.replace(/s$/, "")} by ${pk}. Requires ${module}:delete permission.`,
259
+ security,
260
+ parameters: [
261
+ { name: pk, in: "path", required: true, schema: { type: "integer" } },
262
+ ],
263
+ responses: {
264
+ 200: { description: "Deleted" },
265
+ 401: { description: "Unauthorized" },
266
+ 403: { description: "Forbidden" },
267
+ 404: { description: "Not found" },
268
+ },
269
+ },
270
+ };
271
+ }
272
+
273
+ // --- Nested role_permissions routes ---
274
+ const rpDef = MODEL_DEFINITIONS.find((m) => m.table === "role_permissions");
275
+ if (rpDef) {
276
+ const rpPk = rpDef.primary_key;
277
+ const rpSchemaName = "RolePermission";
278
+ const rpProperties = {};
279
+ const rpRequired = [];
280
+ rpProperties[rpPk] = { type: "integer", description: "Primary key" };
281
+ for (const [col, rule] of Object.entries(rpDef.structure)) {
282
+ const parsed = parseRule(rule);
283
+ rpProperties[col] = { type: parsed.type };
284
+ if (parsed.required) rpRequired.push(col);
285
+ }
286
+ schemas[rpSchemaName] = {
287
+ type: "object",
288
+ properties: rpProperties,
289
+ ...(rpRequired.length > 0 ? { required: rpRequired } : {}),
290
+ };
291
+ const rpRef = { $ref: `#/components/schemas/${rpSchemaName}` };
292
+
293
+ const rpPrefix = "/api/roles/{role_id}/permissions";
294
+ const roleIdParam = {
295
+ name: "role_id",
296
+ in: "path",
297
+ required: true,
298
+ schema: { type: "integer" },
299
+ description: "Role ID",
300
+ };
301
+
302
+ paths[`${rpPrefix}/`] = {
303
+ get: {
304
+ tags: ["permissions"],
305
+ summary: "List permissions for a role",
306
+ description:
307
+ "List all permission entries for a specific role. Requires permissions:read.",
308
+ security,
309
+ parameters: [roleIdParam],
310
+ responses: {
311
+ 200: {
312
+ description: "Success",
313
+ content: {
314
+ "application/json": {
315
+ schema: {
316
+ type: "object",
317
+ properties: { data: { type: "array", items: rpRef } },
318
+ },
319
+ },
320
+ },
321
+ },
322
+ 401: { description: "Unauthorized" },
323
+ 403: { description: "Forbidden" },
324
+ },
325
+ },
326
+ post: {
327
+ tags: ["permissions"],
328
+ summary: "Add permission to role",
329
+ description:
330
+ "Create a new permission entry for a role. Requires permissions:write.",
331
+ security,
332
+ parameters: [roleIdParam],
333
+ requestBody: {
334
+ required: true,
335
+ content: { "application/json": { schema: rpRef } },
336
+ },
337
+ responses: {
338
+ 201: {
339
+ description: "Created",
340
+ content: { "application/json": { schema: rpRef } },
341
+ },
342
+ 401: { description: "Unauthorized" },
343
+ 403: { description: "Forbidden" },
344
+ },
345
+ },
346
+ };
347
+
348
+ paths[`${rpPrefix}/{${rpPk}}`] = {
349
+ put: {
350
+ tags: ["permissions"],
351
+ summary: "Update a role permission",
352
+ description: `Update a permission entry by ${rpPk}. Requires permissions:update.`,
353
+ security,
354
+ parameters: [
355
+ roleIdParam,
356
+ {
357
+ name: rpPk,
358
+ in: "path",
359
+ required: true,
360
+ schema: { type: "integer" },
361
+ },
362
+ ],
363
+ requestBody: {
364
+ required: true,
365
+ content: { "application/json": { schema: rpRef } },
366
+ },
367
+ responses: {
368
+ 200: {
369
+ description: "Updated",
370
+ content: { "application/json": { schema: rpRef } },
371
+ },
372
+ 401: { description: "Unauthorized" },
373
+ 403: { description: "Forbidden" },
374
+ },
375
+ },
376
+ delete: {
377
+ tags: ["permissions"],
378
+ summary: "Delete a role permission",
379
+ description: `Delete a permission entry by ${rpPk}. Requires permissions:delete.`,
380
+ security,
381
+ parameters: [
382
+ roleIdParam,
383
+ {
384
+ name: rpPk,
385
+ in: "path",
386
+ required: true,
387
+ schema: { type: "integer" },
388
+ },
389
+ ],
390
+ responses: {
391
+ 200: { description: "Deleted" },
392
+ 401: { description: "Unauthorized" },
393
+ 403: { description: "Forbidden" },
394
+ },
395
+ },
396
+ };
397
+ }
398
+
399
+ return { paths, schemas, securitySchemes };
400
+ }
401
+
402
+ function parseRule(rule) {
403
+ const parts = rule.split("|");
404
+ const isRequired = parts.includes("required");
405
+ let type = "string";
406
+ for (const p of parts) {
407
+ if (p === "integer") type = "integer";
408
+ else if (p === "numeric") type = "number";
409
+ else if (p === "object") type = "object";
410
+ else if (p === "string") type = "string";
411
+ }
412
+ return { type, required: isRequired };
413
+ }
414
+
415
+ function capitalize(str) {
416
+ return str.charAt(0).toUpperCase() + str.slice(1);
417
+ }
418
+
419
+ module.exports = { generateSaasOpenAPIPaths };