db-model-router 1.0.5 → 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 (136) hide show
  1. package/README.md +150 -11
  2. package/TODO.md +0 -14
  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 +23 -45
  45. package/demo/.env.example +1 -0
  46. package/demo/app.js +3 -1
  47. package/demo/commons/db.js +11 -0
  48. package/demo/commons/migrate.js +3 -0
  49. package/demo/commons/modules.js +18 -0
  50. package/demo/commons/password.js +36 -0
  51. package/demo/commons/webhook.js +81 -0
  52. package/demo/dbmr.schema.json +22 -46
  53. package/demo/middleware/authenticate.js +14 -0
  54. package/demo/middleware/hasPermission.js +30 -0
  55. package/demo/middleware/tenantIsolation.js +17 -0
  56. package/demo/migrations/20260509170349_create_saas_tables.sql +69 -0
  57. package/demo/migrations/{20260430155809_create_tables.sql → 20260509170349_create_tables.sql} +11 -25
  58. package/demo/models/addresses.js +5 -3
  59. package/demo/models/cart_items.js +5 -3
  60. package/demo/models/carts.js +5 -3
  61. package/demo/models/categories.js +5 -3
  62. package/demo/models/coupons.js +5 -3
  63. package/demo/models/index.js +43 -0
  64. package/demo/models/order_items.js +4 -2
  65. package/demo/models/orders.js +5 -3
  66. package/demo/models/payments.js +5 -3
  67. package/demo/models/product_images.js +4 -2
  68. package/demo/models/product_reviews.js +5 -3
  69. package/demo/models/product_variants.js +5 -3
  70. package/demo/models/products.js +5 -3
  71. package/demo/models/role_permissions.js +17 -0
  72. package/demo/models/roles.js +17 -0
  73. package/demo/models/shipments.js +5 -3
  74. package/demo/models/tenants.js +18 -0
  75. package/demo/models/users.js +12 -8
  76. package/demo/models/webhook_logs.js +22 -0
  77. package/demo/models/webhooks.js +19 -0
  78. package/demo/models/wishlists.js +4 -2
  79. package/demo/openapi.json +1744 -616
  80. package/demo/package-lock.json +24 -24
  81. package/demo/package.json +9 -0
  82. package/demo/routes/{addresses.js → addresses/index.js} +1 -1
  83. package/demo/routes/auth/index.js +55 -0
  84. package/demo/routes/carts/{cart_items.js → cart_items/index.js} +1 -1
  85. package/demo/routes/{carts.js → carts/index.js} +1 -1
  86. package/demo/routes/{categories.js → categories/index.js} +1 -1
  87. package/demo/routes/{coupons.js → coupons/index.js} +1 -1
  88. package/demo/routes/index.js +39 -24
  89. package/demo/routes/{orders.js → orders/index.js} +1 -1
  90. package/demo/routes/orders/{order_items.js → order_items/index.js} +1 -1
  91. package/demo/routes/orders/{payments.js → payments/index.js} +1 -1
  92. package/demo/routes/orders/{shipments.js → shipments/index.js} +1 -1
  93. package/demo/routes/{products.js → products/index.js} +1 -1
  94. package/demo/routes/products/{product_images.js → product_images/index.js} +1 -1
  95. package/demo/routes/products/{product_reviews.js → product_reviews/index.js} +1 -1
  96. package/demo/routes/products/{product_variants.js → product_variants/index.js} +1 -1
  97. package/demo/routes/roles/index.js +75 -0
  98. package/demo/routes/roles/permissions/index.js +47 -0
  99. package/demo/routes/tenants/index.js +45 -0
  100. package/demo/routes/users/index.js +45 -0
  101. package/demo/routes/{wishlists.js → wishlists/index.js} +1 -1
  102. package/demo/seeds/saas-seed.js +329 -0
  103. package/docker-compose.yml +61 -0
  104. package/package.json +120 -113
  105. package/scripts/demo-create.js +1 -1
  106. package/skill/SKILL.md +119 -3
  107. package/src/cli/commands/db-manager.js +134 -0
  108. package/src/cli/commands/generate.js +112 -43
  109. package/src/cli/commands/help.js +0 -1
  110. package/src/cli/diff-engine.js +2 -1
  111. package/src/cli/generate-model.js +9 -4
  112. package/src/cli/generate-openapi.js +40 -13
  113. package/src/cli/generate-route.js +61 -22
  114. package/src/cli/generate-saas-structure.js +122 -0
  115. package/src/cli/init/generators.js +42 -30
  116. package/src/cli/init.js +8 -0
  117. package/src/cli/main.js +8 -1
  118. package/src/cli/saas/generate-saas-middleware.js +108 -0
  119. package/src/cli/saas/generate-saas-migrations.js +480 -0
  120. package/src/cli/saas/generate-saas-models.js +211 -0
  121. package/src/cli/saas/generate-saas-openapi.js +419 -0
  122. package/src/cli/saas/generate-saas-routes.js +435 -0
  123. package/src/cli/saas/generate-saas-seeds.js +243 -0
  124. package/src/cli/saas/generate-saas-utils.js +176 -0
  125. package/src/commons/kafka.js +139 -0
  126. package/src/commons/model.js +29 -9
  127. package/src/index.js +2 -0
  128. package/src/mssql/db.js +41 -3
  129. package/src/mysql/db.js +3 -0
  130. package/src/postgres/db.js +6 -0
  131. package/src/sqlite3/db.js +11 -0
  132. package/demo/docs/llm.md +0 -197
  133. package/demo/llms.txt +0 -70
  134. package/demo/routes/users.js +0 -6
  135. package/src/cli/commands/generate-llm-docs.js +0 -418
  136. /package/demo/migrations/{20260430155808_create_migrations_table.sql → 20260509170349_create_migrations_table.sql} +0 -0
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * SaaS model generator.
5
+ *
6
+ * Generates model files for all SaaS tables following the existing
7
+ * `generateModelFile` pattern with `model(db, table, structure, pk, unique, option)`.
8
+ * Models are adapter-agnostic (validation rules, not SQL types).
9
+ */
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Model Definitions
13
+ // ---------------------------------------------------------------------------
14
+
15
+ /**
16
+ * SaaS model definitions in the format expected by the model system.
17
+ * Each entry defines: table name, structure (validation rules), primary key,
18
+ * unique constraints, and option (timestamp columns).
19
+ */
20
+ const MODEL_DEFINITIONS = [
21
+ {
22
+ table: "tenants",
23
+ structure: {
24
+ name: "required|string",
25
+ slug: "required|string",
26
+ attributes: "object",
27
+ },
28
+ primary_key: "tenant_id",
29
+ unique: ["slug"],
30
+ option: { created_at: "created_at", modified_at: "modified_at" },
31
+ },
32
+ {
33
+ table: "roles",
34
+ structure: {
35
+ tenant_id: "integer",
36
+ name: "required|string",
37
+ },
38
+ primary_key: "role_id",
39
+ unique: ["tenant_id", "name"],
40
+ option: { created_at: "created_at", modified_at: "modified_at" },
41
+ },
42
+ {
43
+ table: "users",
44
+ structure: {
45
+ email: "required|string",
46
+ phone: "string",
47
+ password_hash: "required|string",
48
+ name: "required|string",
49
+ unique_attribute: "required|string",
50
+ tenant_id: "integer",
51
+ role_id: "required|integer",
52
+ attributes: "object",
53
+ },
54
+ primary_key: "user_id",
55
+ unique: ["tenant_id", "unique_attribute"],
56
+ option: { created_at: "created_at", modified_at: "modified_at" },
57
+ },
58
+ {
59
+ table: "role_permissions",
60
+ structure: {
61
+ role_id: "required|integer",
62
+ permission: "required|object",
63
+ },
64
+ primary_key: "role_permission_id",
65
+ unique: ["role_permission_id"],
66
+ option: { created_at: "created_at", modified_at: "modified_at" },
67
+ },
68
+ {
69
+ table: "webhooks",
70
+ structure: {
71
+ tenant_id: "required|integer",
72
+ url: "required|string",
73
+ key: "required|string",
74
+ secret: "required|string",
75
+ },
76
+ primary_key: "webhook_id",
77
+ unique: ["webhook_id"],
78
+ option: { created_at: "created_at", modified_at: "modified_at" },
79
+ },
80
+ {
81
+ table: "webhook_logs",
82
+ structure: {
83
+ webhook_id: "required|integer",
84
+ tenant_id: "required|integer",
85
+ event_type: "required|string",
86
+ payload: "required|object",
87
+ status: "required|string",
88
+ response_body: "string",
89
+ response_status_code: "integer",
90
+ },
91
+ primary_key: "webhook_log_id",
92
+ unique: ["webhook_log_id"],
93
+ option: { created_at: "created_at" },
94
+ },
95
+ ];
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Code Generation
99
+ // ---------------------------------------------------------------------------
100
+
101
+ /**
102
+ * Generate the content of a single model file following the existing pattern.
103
+ *
104
+ * @param {object} def - Model definition object
105
+ * @returns {string} Generated model file content
106
+ */
107
+ function generateModelContent(def) {
108
+ const varName = def.table;
109
+ const structStr = JSON.stringify(def.structure, null, 4);
110
+ const uniqueStr = JSON.stringify(def.unique);
111
+
112
+ const optionParts = [];
113
+ if (def.option.created_at)
114
+ optionParts.push(`created_at: "${def.option.created_at}"`);
115
+ if (def.option.modified_at)
116
+ optionParts.push(`modified_at: "${def.option.modified_at}"`);
117
+ const optionStr = `{ ${optionParts.join(", ")} }`;
118
+
119
+ return `import dbModelRouter from "db-model-router";
120
+
121
+ const { db, model } = dbModelRouter;
122
+
123
+ const ${varName} = model(
124
+ db,
125
+ "${def.table}",
126
+ ${structStr},
127
+ "${def.primary_key}",
128
+ ${uniqueStr},
129
+ ${optionStr},
130
+ );
131
+
132
+ export default ${varName};
133
+ `;
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Public API
138
+ // ---------------------------------------------------------------------------
139
+
140
+ /**
141
+ * Generate model files for all SaaS tables.
142
+ *
143
+ * @param {string} adapter - Database adapter name (accepted for API consistency, not used for model content)
144
+ * @param {string[]} [additionalTables] - Additional table names from dbmr schema to include in models/index.js
145
+ * @returns {Array<{ relPath: string, content: string }>}
146
+ */
147
+ function generateSaasModels(adapter, additionalTables) {
148
+ const results = [];
149
+
150
+ for (const def of MODEL_DEFINITIONS) {
151
+ results.push({
152
+ relPath: `models/${def.table}.js`,
153
+ content: generateModelContent(def),
154
+ });
155
+ }
156
+
157
+ // Generate models/index.js that re-exports all models (SaaS + dbmr) as named exports
158
+ results.push({
159
+ relPath: "models/index.js",
160
+ content: generateModelsIndex(additionalTables || []),
161
+ });
162
+
163
+ return results;
164
+ }
165
+
166
+ /**
167
+ * Generate the models/index.js barrel file.
168
+ * Imports all SaaS models and any additional dbmr schema models,
169
+ * then re-exports them all as named exports.
170
+ *
171
+ * @param {string[]} additionalTables - Additional table names from dbmr schema
172
+ * @returns {string}
173
+ */
174
+ function generateModelsIndex(additionalTables) {
175
+ const saasTableNames = MODEL_DEFINITIONS.map((def) => def.table);
176
+ // Filter out dbmr tables that overlap with SaaS tables
177
+ const dbmrTables = (additionalTables || []).filter(
178
+ (t) => !saasTableNames.includes(t),
179
+ );
180
+
181
+ let code = "";
182
+ // SaaS model imports
183
+ for (const def of MODEL_DEFINITIONS) {
184
+ code += `import ${def.table} from "./${def.table}.js";\n`;
185
+ }
186
+ // dbmr schema model imports
187
+ for (const table of dbmrTables) {
188
+ code += `import ${safeVarName(table)} from "./${table}.js";\n`;
189
+ }
190
+
191
+ code += "\nexport {\n";
192
+ for (const def of MODEL_DEFINITIONS) {
193
+ code += ` ${def.table},\n`;
194
+ }
195
+ for (const table of dbmrTables) {
196
+ code += ` ${safeVarName(table)},\n`;
197
+ }
198
+ code += "};\n";
199
+ return code;
200
+ }
201
+
202
+ function safeVarName(name) {
203
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) return name;
204
+ return name.replace(/[^a-zA-Z0-9_$]/g, "_");
205
+ }
206
+
207
+ module.exports = {
208
+ generateSaasModels,
209
+ generateModelContent,
210
+ MODEL_DEFINITIONS,
211
+ };
@@ -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 };