@skalfa/skalfa-api-core 1.0.2

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 (249) hide show
  1. package/.github/workflows/publish.yml +40 -0
  2. package/dist/auth/auth.d.ts +19 -0
  3. package/dist/auth/auth.js +227 -0
  4. package/dist/auth/auth.js.map +1 -0
  5. package/dist/auth/index.d.ts +1 -0
  6. package/dist/auth/index.js +2 -0
  7. package/dist/auth/index.js.map +1 -0
  8. package/dist/auth.util.d.ts +19 -0
  9. package/dist/auth.util.js +183 -0
  10. package/dist/auth.util.js.map +1 -0
  11. package/dist/commands/cli.d.ts +1 -0
  12. package/dist/commands/cli.js +78 -0
  13. package/dist/commands/cli.js.map +1 -0
  14. package/dist/commands/make/basic-controller.d.ts +2 -0
  15. package/dist/commands/make/basic-controller.js +40 -0
  16. package/dist/commands/make/basic-controller.js.map +1 -0
  17. package/dist/commands/make/basic-migration.d.ts +5 -0
  18. package/dist/commands/make/basic-migration.js +60 -0
  19. package/dist/commands/make/basic-migration.js.map +1 -0
  20. package/dist/commands/make/basic-model.d.ts +2 -0
  21. package/dist/commands/make/basic-model.js +25 -0
  22. package/dist/commands/make/basic-model.js.map +1 -0
  23. package/dist/commands/make/basic-seeder.d.ts +3 -0
  24. package/dist/commands/make/basic-seeder.js +32 -0
  25. package/dist/commands/make/basic-seeder.js.map +1 -0
  26. package/dist/commands/make/blueprint.d.ts +2 -0
  27. package/dist/commands/make/blueprint.js +29 -0
  28. package/dist/commands/make/blueprint.js.map +1 -0
  29. package/dist/commands/make/da-migration.d.ts +5 -0
  30. package/dist/commands/make/da-migration.js +60 -0
  31. package/dist/commands/make/da-migration.js.map +1 -0
  32. package/dist/commands/make/light-controller.d.ts +3 -0
  33. package/dist/commands/make/light-controller.js +54 -0
  34. package/dist/commands/make/light-controller.js.map +1 -0
  35. package/dist/commands/make/light-model.d.ts +3 -0
  36. package/dist/commands/make/light-model.js +50 -0
  37. package/dist/commands/make/light-model.js.map +1 -0
  38. package/dist/commands/make/mail.d.ts +2 -0
  39. package/dist/commands/make/mail.js +41 -0
  40. package/dist/commands/make/mail.js.map +1 -0
  41. package/dist/commands/make/notification.d.ts +2 -0
  42. package/dist/commands/make/notification.js +33 -0
  43. package/dist/commands/make/notification.js.map +1 -0
  44. package/dist/commands/make/queue.d.ts +2 -0
  45. package/dist/commands/make/queue.js +35 -0
  46. package/dist/commands/make/queue.js.map +1 -0
  47. package/dist/commands/runner/barrels.d.ts +3 -0
  48. package/dist/commands/runner/barrels.js +78 -0
  49. package/dist/commands/runner/barrels.js.map +1 -0
  50. package/dist/commands/runner/blueprint/controller-generation.d.ts +1 -0
  51. package/dist/commands/runner/blueprint/controller-generation.js +147 -0
  52. package/dist/commands/runner/blueprint/controller-generation.js.map +1 -0
  53. package/dist/commands/runner/blueprint/documentation-generation.d.ts +6 -0
  54. package/dist/commands/runner/blueprint/documentation-generation.js +337 -0
  55. package/dist/commands/runner/blueprint/documentation-generation.js.map +1 -0
  56. package/dist/commands/runner/blueprint/migration-generation.d.ts +1 -0
  57. package/dist/commands/runner/blueprint/migration-generation.js +120 -0
  58. package/dist/commands/runner/blueprint/migration-generation.js.map +1 -0
  59. package/dist/commands/runner/blueprint/model-generation.d.ts +1 -0
  60. package/dist/commands/runner/blueprint/model-generation.js +122 -0
  61. package/dist/commands/runner/blueprint/model-generation.js.map +1 -0
  62. package/dist/commands/runner/blueprint/runner.d.ts +23 -0
  63. package/dist/commands/runner/blueprint/runner.js +139 -0
  64. package/dist/commands/runner/blueprint/runner.js.map +1 -0
  65. package/dist/commands/runner/blueprint/seeder-generation.d.ts +1 -0
  66. package/dist/commands/runner/blueprint/seeder-generation.js +40 -0
  67. package/dist/commands/runner/blueprint/seeder-generation.js.map +1 -0
  68. package/dist/commands/runner/da-migration.d.ts +39 -0
  69. package/dist/commands/runner/da-migration.js +262 -0
  70. package/dist/commands/runner/da-migration.js.map +1 -0
  71. package/dist/commands/runner/migration.d.ts +11 -0
  72. package/dist/commands/runner/migration.js +188 -0
  73. package/dist/commands/runner/migration.js.map +1 -0
  74. package/dist/commands/runner/seeder.d.ts +3 -0
  75. package/dist/commands/runner/seeder.js +40 -0
  76. package/dist/commands/runner/seeder.js.map +1 -0
  77. package/dist/commands/stubs/index.d.ts +14 -0
  78. package/dist/commands/stubs/index.js +277 -0
  79. package/dist/commands/stubs/index.js.map +1 -0
  80. package/dist/context/context.d.ts +7 -0
  81. package/dist/context/context.js +11 -0
  82. package/dist/context/context.js.map +1 -0
  83. package/dist/context/index.d.ts +1 -0
  84. package/dist/context/index.js +2 -0
  85. package/dist/context/index.js.map +1 -0
  86. package/dist/context.util.d.ts +7 -0
  87. package/dist/context.util.js +11 -0
  88. package/dist/context.util.js.map +1 -0
  89. package/dist/controller/controller.d.ts +118 -0
  90. package/dist/controller/controller.js +147 -0
  91. package/dist/controller/controller.js.map +1 -0
  92. package/dist/controller/index.d.ts +1 -0
  93. package/dist/controller/index.js +2 -0
  94. package/dist/controller/index.js.map +1 -0
  95. package/dist/controller.util.d.ts +118 -0
  96. package/dist/controller.util.js +144 -0
  97. package/dist/controller.util.js.map +1 -0
  98. package/dist/conversion/conversion.d.ts +8 -0
  99. package/dist/conversion/conversion.js +52 -0
  100. package/dist/conversion/conversion.js.map +1 -0
  101. package/dist/conversion/index.d.ts +1 -0
  102. package/dist/conversion/index.js +2 -0
  103. package/dist/conversion/index.js.map +1 -0
  104. package/dist/conversion.util.d.ts +8 -0
  105. package/dist/conversion.util.js +52 -0
  106. package/dist/conversion.util.js.map +1 -0
  107. package/dist/db/db.d.ts +84 -0
  108. package/dist/db/db.js +177 -0
  109. package/dist/db/db.js.map +1 -0
  110. package/dist/db/index.d.ts +1 -0
  111. package/dist/db/index.js +2 -0
  112. package/dist/db/index.js.map +1 -0
  113. package/dist/db.util.d.ts +84 -0
  114. package/dist/db.util.js +177 -0
  115. package/dist/db.util.js.map +1 -0
  116. package/dist/index.d.ts +21 -0
  117. package/dist/index.js +14 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/logger/index.d.ts +1 -0
  120. package/dist/logger/index.js +2 -0
  121. package/dist/logger/index.js.map +1 -0
  122. package/dist/logger/logger.d.ts +30 -0
  123. package/dist/logger/logger.js +126 -0
  124. package/dist/logger/logger.js.map +1 -0
  125. package/dist/logger.util.d.ts +30 -0
  126. package/dist/logger.util.js +126 -0
  127. package/dist/logger.util.js.map +1 -0
  128. package/dist/mail/index.d.ts +1 -0
  129. package/dist/mail/index.js +2 -0
  130. package/dist/mail/index.js.map +1 -0
  131. package/dist/mail/mail.d.ts +21 -0
  132. package/dist/mail/mail.js +53 -0
  133. package/dist/mail/mail.js.map +1 -0
  134. package/dist/mail.util.d.ts +21 -0
  135. package/dist/mail.util.js +53 -0
  136. package/dist/mail.util.js.map +1 -0
  137. package/dist/middleware/index.d.ts +1 -0
  138. package/dist/middleware/index.js +2 -0
  139. package/dist/middleware/index.js.map +1 -0
  140. package/dist/middleware/middleware.d.ts +263 -0
  141. package/dist/middleware/middleware.js +233 -0
  142. package/dist/middleware/middleware.js.map +1 -0
  143. package/dist/middleware.util.d.ts +263 -0
  144. package/dist/middleware.util.js +233 -0
  145. package/dist/middleware.util.js.map +1 -0
  146. package/dist/model/index.d.ts +3 -0
  147. package/dist/model/index.js +4 -0
  148. package/dist/model/index.js.map +1 -0
  149. package/dist/model/model.d.ts +204 -0
  150. package/dist/model/model.js +1495 -0
  151. package/dist/model/model.js.map +1 -0
  152. package/dist/model.util.d.ts +204 -0
  153. package/dist/model.util.js +1495 -0
  154. package/dist/model.util.js.map +1 -0
  155. package/dist/permission/index.d.ts +1 -0
  156. package/dist/permission/index.js +2 -0
  157. package/dist/permission/index.js.map +1 -0
  158. package/dist/permission/permission.d.ts +38 -0
  159. package/dist/permission/permission.js +91 -0
  160. package/dist/permission/permission.js.map +1 -0
  161. package/dist/permission.util.d.ts +38 -0
  162. package/dist/permission.util.js +91 -0
  163. package/dist/permission.util.js.map +1 -0
  164. package/dist/registry/index.d.ts +1 -0
  165. package/dist/registry/index.js +2 -0
  166. package/dist/registry/index.js.map +1 -0
  167. package/dist/registry/registry.d.ts +28 -0
  168. package/dist/registry/registry.js +19 -0
  169. package/dist/registry/registry.js.map +1 -0
  170. package/dist/registry.util.d.ts +28 -0
  171. package/dist/registry.util.js +19 -0
  172. package/dist/registry.util.js.map +1 -0
  173. package/dist/route/index.d.ts +1 -0
  174. package/dist/route/index.js +2 -0
  175. package/dist/route/index.js.map +1 -0
  176. package/dist/route/route.d.ts +1 -0
  177. package/dist/route/route.js +12 -0
  178. package/dist/route/route.js.map +1 -0
  179. package/dist/route.util.d.ts +1 -0
  180. package/dist/route.util.js +12 -0
  181. package/dist/route.util.js.map +1 -0
  182. package/dist/storage/index.d.ts +1 -0
  183. package/dist/storage/index.js +2 -0
  184. package/dist/storage/index.js.map +1 -0
  185. package/dist/storage/storage.d.ts +56 -0
  186. package/dist/storage/storage.js +86 -0
  187. package/dist/storage/storage.js.map +1 -0
  188. package/dist/storage.util.d.ts +56 -0
  189. package/dist/storage.util.js +82 -0
  190. package/dist/storage.util.js.map +1 -0
  191. package/dist/validation/index.d.ts +1 -0
  192. package/dist/validation/index.js +2 -0
  193. package/dist/validation/index.js.map +1 -0
  194. package/dist/validation/validation.d.ts +7 -0
  195. package/dist/validation/validation.js +245 -0
  196. package/dist/validation/validation.js.map +1 -0
  197. package/dist/validation.util.d.ts +7 -0
  198. package/dist/validation.util.js +237 -0
  199. package/dist/validation.util.js.map +1 -0
  200. package/package.json +34 -0
  201. package/src/auth/auth.ts +282 -0
  202. package/src/auth/index.ts +1 -0
  203. package/src/commands/cli.ts +89 -0
  204. package/src/commands/make/basic-controller.ts +49 -0
  205. package/src/commands/make/basic-migration.ts +89 -0
  206. package/src/commands/make/basic-model.ts +32 -0
  207. package/src/commands/make/basic-seeder.ts +38 -0
  208. package/src/commands/make/blueprint.ts +36 -0
  209. package/src/commands/make/da-migration.ts +90 -0
  210. package/src/commands/make/light-controller.ts +67 -0
  211. package/src/commands/make/light-model.ts +61 -0
  212. package/src/commands/make/mail.ts +51 -0
  213. package/src/commands/make/notification.ts +43 -0
  214. package/src/commands/make/queue.ts +45 -0
  215. package/src/commands/runner/barrels.ts +85 -0
  216. package/src/commands/runner/blueprint/controller-generation.ts +194 -0
  217. package/src/commands/runner/blueprint/documentation-generation.ts +463 -0
  218. package/src/commands/runner/blueprint/migration-generation.ts +153 -0
  219. package/src/commands/runner/blueprint/model-generation.ts +149 -0
  220. package/src/commands/runner/blueprint/runner.ts +181 -0
  221. package/src/commands/runner/blueprint/seeder-generation.ts +55 -0
  222. package/src/commands/runner/da-migration.ts +333 -0
  223. package/src/commands/runner/migration.ts +245 -0
  224. package/src/commands/runner/seeder.ts +44 -0
  225. package/src/commands/stubs/index.ts +289 -0
  226. package/src/context/context.ts +17 -0
  227. package/src/context/index.ts +1 -0
  228. package/src/controller/controller.ts +240 -0
  229. package/src/controller/index.ts +1 -0
  230. package/src/conversion/conversion.ts +65 -0
  231. package/src/conversion/index.ts +1 -0
  232. package/src/index.ts +22 -0
  233. package/src/logger/index.ts +1 -0
  234. package/src/logger/logger.ts +177 -0
  235. package/src/mail/index.ts +1 -0
  236. package/src/mail/mail.ts +86 -0
  237. package/src/middleware/index.ts +1 -0
  238. package/src/middleware/middleware.ts +289 -0
  239. package/src/permission/index.ts +1 -0
  240. package/src/permission/permission.ts +136 -0
  241. package/src/registry/index.ts +1 -0
  242. package/src/registry/registry.ts +37 -0
  243. package/src/route/index.ts +1 -0
  244. package/src/route/route.ts +12 -0
  245. package/src/storage/index.ts +1 -0
  246. package/src/storage/storage.ts +107 -0
  247. package/src/validation/index.ts +1 -0
  248. package/src/validation/validation.ts +346 -0
  249. package/tsconfig.json +23 -0
@@ -0,0 +1,463 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { conversion } from "@utils";
4
+
5
+ const ERD_PATH = path.join(process.cwd(), "storage", "documentation", "entity");
6
+ const API_PATH = path.join(process.cwd(), "storage", "documentation", "api");
7
+
8
+ // =========================================>
9
+ // ## Command: Blueprint documentation generation
10
+ // =========================================>
11
+ export async function generatePostmanAPIDocumentation(
12
+ documentations: Array<Record<string, any>>
13
+ ): Promise<boolean> {
14
+ const collectionName = process.env.APP_NAME || "API Collection";
15
+ const filePath = path.join(API_PATH, `${collectionName}.postman.json`);
16
+
17
+ if (!fs.existsSync(API_PATH)) {
18
+ fs.mkdirSync(API_PATH, { recursive: true });
19
+ }
20
+
21
+ if (fs.existsSync(filePath)) {
22
+ fs.unlinkSync(filePath);
23
+ }
24
+
25
+ const folders = new Map<string, any>();
26
+
27
+ for (const documentation of documentations) {
28
+ const { controllers, schema } = documentation;
29
+
30
+ for (const route of controllers) {
31
+ const folderName = route.split("-").map((w: string) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
32
+
33
+ const crudOperations = [
34
+ {
35
+ name: `Get All ${folderName}`,
36
+ method: "GET",
37
+ path: `${route}`,
38
+ body: {},
39
+ },
40
+ {
41
+ name: `Create ${folderName}`,
42
+ method: "POST",
43
+ path: `${route}`,
44
+ body: extractSchema(schema),
45
+ },
46
+ {
47
+ name: `Update ${folderName}`,
48
+ method: "PUT",
49
+ path: `${route}/{id}`,
50
+ body: extractSchema(schema),
51
+ },
52
+ {
53
+ name: `Delete ${folderName}`,
54
+ method: "DELETE",
55
+ path: `${route}/{id}`,
56
+ body: {},
57
+ },
58
+ ];
59
+
60
+ const items = crudOperations.map((endpoint) => ({
61
+ name: endpoint.name,
62
+ request: {
63
+ method: endpoint.method,
64
+ header: [],
65
+ body: {
66
+ mode: "raw",
67
+ raw: JSON.stringify(endpoint.body, null, 2),
68
+ },
69
+ url: {
70
+ raw: `{{base_url}}/${endpoint.path}`,
71
+ host: ["{{base_url}}"],
72
+ path: endpoint.path.split("/"),
73
+ },
74
+ },
75
+ }));
76
+
77
+ if (!folders.has(folderName)) {
78
+ folders.set(folderName, {
79
+ name: folderName,
80
+ items: []
81
+ });
82
+ }
83
+
84
+ folders.get(folderName).items.push(...items);
85
+ }
86
+ }
87
+
88
+ const collection = {
89
+ info: {
90
+ name: collectionName,
91
+ schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
92
+ },
93
+ item: Array.from(folders.values()),
94
+ };
95
+
96
+ fs.writeFileSync(filePath, JSON.stringify(collection, null, 2), "utf-8");
97
+
98
+ return true;
99
+ }
100
+
101
+ function extractSchema(schema: Record<string, string>) {
102
+ const obj: Record<string, any> = {};
103
+ for (const key of Object.keys(schema)) {
104
+ if (schema[key].includes("fillable")) {
105
+ obj[key] = "";
106
+ }
107
+ }
108
+ return obj;
109
+ }
110
+
111
+ // =========================================>
112
+ // ## Blueprint: mermaid entity documentation generation
113
+ // =========================================>
114
+ export async function generateMermaidEntityDocumentation(
115
+ blueprintFile: string,
116
+ blueprints: any[]
117
+ ) {
118
+ const filePath = getMermaidERDPath(blueprintFile);
119
+
120
+ if (!fs.existsSync(ERD_PATH)) {
121
+ fs.mkdirSync(ERD_PATH, { recursive: true });
122
+ }
123
+
124
+ const entities: string[] = [];
125
+ const relations: string[] = [];
126
+
127
+ for (const bp of blueprints) {
128
+ entities.push(renderMermaidEntity(bp.model, bp.schema ?? {}));
129
+
130
+ if (bp.relations) {
131
+ relations.push(renderMermaidRelations(bp.model, bp.relations));
132
+ }
133
+ }
134
+
135
+ const content = `
136
+ erDiagram
137
+ ${entities.join("\n")}
138
+ ${relations.join("\n")}
139
+ `;
140
+
141
+ fs.writeFileSync(filePath, content.trim(), "utf-8");
142
+ }
143
+
144
+ function getMermaidERDPath(blueprintFile: string) {
145
+ const name = blueprintFile.replace(".blueprint.json", "");
146
+ return path.join(ERD_PATH, `${name}.mmd`);
147
+ }
148
+
149
+ function mapToMermaidType(def: string): string {
150
+ if (def.includes("type:string")) return "string";
151
+ if (def.includes("type:bigInteger")) return "bigint";
152
+ if (def.includes("type:integer")) return "int";
153
+ if (def.includes("type:json")) return "json";
154
+ if (def.includes("type:timestamp")) return "timestamp";
155
+ return "string";
156
+ }
157
+
158
+ function renderMermaidEntity(model: string, schema: Record<string, string>) {
159
+ const table = conversion.strSnake(conversion.strPlural(model.split("/").pop()!));
160
+
161
+ const fields = Object.entries(schema).map(([name, def]) => ` ${mapToMermaidType(def)} ${name}`).join("\n");
162
+
163
+ return `
164
+ ${table} {
165
+ ${fields}
166
+ }
167
+ `;
168
+ }
169
+
170
+ function renderMermaidRelations(
171
+ model: string,
172
+ relations: Record<string, string>
173
+ ) {
174
+ const source = conversion.strSnake(
175
+ conversion.strPlural(model.split("/").pop()!)
176
+ );
177
+
178
+ return Object.entries(relations)
179
+ .map(([name, def]) => {
180
+ const targetModel = def.replace(/\[\]|\[1\]|:/g, "").split(" ")[0];
181
+ const target = conversion.strSnake(conversion.strPlural(targetModel));
182
+
183
+ if (def.startsWith("[]:")) {
184
+ return ` ${source} }o--o{ ${target} : ${name}`;
185
+ }
186
+ if (def.startsWith("[]")) {
187
+ return ` ${source} ||--o{ ${target} : ${name}`;
188
+ }
189
+ if (def.startsWith("[1]")) {
190
+ return ` ${source} ||--|| ${target} : ${name}`;
191
+ }
192
+ return ` ${source} }o--|| ${target} : ${name}`;
193
+ })
194
+ .join("\n");
195
+ }
196
+
197
+ // =========================================>
198
+ // ## Blueprint: drawio entity documentation generation (FAN ROUTING VERSION)
199
+ // =========================================>
200
+ const MODULE_WIDTH = 1600;
201
+ const MODULE_PADDING_X = 80;
202
+ const MODULE_PADDING_Y = 140;
203
+
204
+ const ENTITY_COL_WIDTH = 320;
205
+ const ENTITY_ROW_HEIGHT = 240;
206
+ const ENTITY_COLS = 3;
207
+
208
+ const TABLE_WIDTH = 260;
209
+
210
+ const RELATION_LIFT = 30;
211
+ const RELATION_LANE_GAP = 10;
212
+ const RELATION_LANE_OFFSET = 140;
213
+ const RELATION_HORIZONTAL_GAP = 8;
214
+ const MODULE_RELATION_TOP_OFFSET = 120;
215
+
216
+ function colorFromString(input: string) {
217
+ let hash = 0;
218
+ for (let i = 0; i < input.length; i++) {
219
+ hash = input.charCodeAt(i) + ((hash << 5) - hash);
220
+ }
221
+
222
+ const hue = Math.abs(hash) % 360;
223
+ const saturation = 70;
224
+ const lightness = 60;
225
+
226
+ return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
227
+ }
228
+
229
+ export async function generateDrawioEntityDocumentation(
230
+ blueprintFiles: Array<{ file: string; blueprints: any[] }>
231
+ ) {
232
+ const appName = conversion.strSlug(process.env.APP_NAME || 'app');
233
+ const filePath = path.join(ERD_PATH, `${appName}.drawio.xml`);
234
+
235
+ if (!fs.existsSync(ERD_PATH)) {
236
+ fs.mkdirSync(ERD_PATH, { recursive: true });
237
+ }
238
+
239
+ let xml = drawioHeader();
240
+ const entityPositions = new Map<string, { x: number; y: number }>();
241
+
242
+ blueprintFiles.forEach((file, moduleIndex) => {
243
+ const baseX = moduleIndex * MODULE_WIDTH;
244
+ const baseY = 0;
245
+ const color = colorFromString(file.file);
246
+
247
+ renderDrawioModule(
248
+ file.blueprints,
249
+ baseX,
250
+ baseY,
251
+ color,
252
+ entityPositions,
253
+ chunk => (xml += '\n' + chunk)
254
+ );
255
+ });
256
+
257
+ xml += '\n' + drawioFooter();
258
+ fs.writeFileSync(filePath, xml, 'utf-8');
259
+ }
260
+
261
+ function renderDrawioModule(
262
+ blueprints: any[],
263
+ baseX: number,
264
+ baseY: number,
265
+ color: string,
266
+ entityPositions: Map<string, { x: number; y: number }>,
267
+ push: (xml: string) => void
268
+ ) {
269
+ blueprints.forEach((bp, i) => {
270
+ const table = conversion.strSnake(
271
+ conversion.strPlural(bp.model.split('/').pop()!)
272
+ );
273
+
274
+ const col = i % ENTITY_COLS;
275
+ const row = Math.floor(i / ENTITY_COLS);
276
+
277
+ const x = baseX + MODULE_PADDING_X + col * ENTITY_COL_WIDTH;
278
+ const y = baseY + MODULE_PADDING_Y + row * ENTITY_ROW_HEIGHT;
279
+
280
+ entityPositions.set(table, { x, y });
281
+ renderERDTableFromSchema(table, bp.schema ?? {}, x, y, push);
282
+ });
283
+
284
+ const leftLaneX = baseX - RELATION_LANE_OFFSET;
285
+ const rightLaneX = baseX + MODULE_WIDTH + RELATION_LANE_OFFSET;
286
+
287
+ const moduleRelationTopY = baseY - MODULE_RELATION_TOP_OFFSET;
288
+
289
+ let relationIndex = 0;
290
+
291
+ blueprints.forEach(bp => {
292
+ if (!bp.relations) return;
293
+
294
+ const source = conversion.strSnake(
295
+ conversion.strPlural(bp.model.split('/').pop()!)
296
+ );
297
+
298
+ const sourcePos = entityPositions.get(source);
299
+ if (!sourcePos) return;
300
+
301
+ Object.entries(bp.relations).forEach(([name, def], idx) => {
302
+ const targetModel = (def as string)
303
+ .replace(/\[\]|\[1\]|:/g, '')
304
+ .split(' ')[0];
305
+
306
+ const target = conversion.strSnake(
307
+ conversion.strPlural(targetModel)
308
+ );
309
+
310
+ const targetPos = entityPositions.get(target);
311
+ if (!targetPos) return;
312
+
313
+ const goLeft = targetPos.x < sourcePos.x;
314
+ const laneX = goLeft
315
+ ? leftLaneX
316
+ : rightLaneX;
317
+
318
+ const horizontalY = moduleRelationTopY - relationIndex * RELATION_HORIZONTAL_GAP;
319
+ relationIndex++;
320
+
321
+ const entryX = goLeft
322
+ ? targetPos.x - 10
323
+ : targetPos.x + TABLE_WIDTH + 10;
324
+
325
+ push(
326
+ renderDrawioRelationFinal(
327
+ `rel_${source}_${target}_${idx}`,
328
+ source,
329
+ target,
330
+ name,
331
+ sourcePos.x,
332
+ sourcePos.y,
333
+ targetPos.y,
334
+ laneX,
335
+ entryX,
336
+ horizontalY,
337
+ RELATION_LIFT,
338
+ color
339
+ )
340
+ );
341
+ });
342
+ });
343
+ }
344
+
345
+ function renderDrawioRelationFinal(
346
+ id: string,
347
+ source: string,
348
+ target: string,
349
+ label: string,
350
+ sourceX: number,
351
+ sourceY: number,
352
+ targetY: number,
353
+ laneX: number,
354
+ entryX: number,
355
+ horizontalY: number,
356
+ lift: number,
357
+ color: string
358
+ ) {
359
+ const sourceExitY = sourceY - lift;
360
+ const targetEntryY = targetY + lift;
361
+
362
+ return `
363
+ <mxCell id="${id}" value="${label}"
364
+ style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;strokeColor=${color};html=1;"
365
+ edge="1" parent="1" source="${source}" target="${target}">
366
+ <mxGeometry relative="1" as="geometry">
367
+ <Array as="points">
368
+ <mxPoint x="${sourceX}" y="${sourceExitY}"/>
369
+ <mxPoint x="${laneX}" y="${sourceExitY}"/>
370
+ <mxPoint x="${laneX}" y="${horizontalY}"/>
371
+ <mxPoint x="${entryX}" y="${horizontalY}"/>
372
+ <mxPoint x="${entryX}" y="${targetEntryY}"/>
373
+ </Array>
374
+ </mxGeometry>
375
+ </mxCell>
376
+ `.trim();
377
+ }
378
+
379
+ function drawioHeader(): string {
380
+ return `
381
+ <mxfile>
382
+ <diagram name="ERD">
383
+ <mxGraphModel>
384
+ <root>
385
+ <mxCell id="0"/>
386
+ <mxCell id="1" parent="0"/>
387
+ `.trim();
388
+ }
389
+
390
+ function drawioFooter(): string {
391
+ return `
392
+ </root>
393
+ </mxGraphModel>
394
+ </diagram>
395
+ </mxfile>
396
+ `.trim();
397
+ }
398
+
399
+ function renderERDTableFromSchema(
400
+ table: string,
401
+ schema: Record<string, string>,
402
+ baseX: number,
403
+ baseY: number,
404
+ push: (xml: string) => void
405
+ ) {
406
+ const fields = Object.entries(schema);
407
+ push(renderDrawioTable(table, table, baseX, baseY, fields.length));
408
+
409
+ fields.forEach(([name, def], index) => {
410
+ const rowId = `${table}_row_${index}`;
411
+ const y = 30 + index * 30;
412
+
413
+ push(renderDrawioTableRow(rowId, table, y));
414
+
415
+ const isPK = name === 'id';
416
+ const isFK = name.endsWith('_id');
417
+
418
+ push(renderDrawioTableCell(`${rowId}_key`, rowId, isPK ? 'PK' : isFK ? 'FK' : '', 0, 40, true));
419
+ push(renderDrawioTableCell(`${rowId}_name`, rowId, `${name} : ${mapDrawioType(def)}`, 40, 220));
420
+ });
421
+ }
422
+
423
+ function mapDrawioType(def: string) {
424
+ if (def.includes('type:bigInteger')) return 'bigint';
425
+ if (def.includes('type:integer')) return 'int';
426
+ if (def.includes('type:json')) return 'json';
427
+ if (def.includes('type:timestamp')) return 'timestamp';
428
+ return 'string';
429
+ }
430
+
431
+ // ==========================================
432
+ // We use simple XML-based structure here
433
+ // ==========================================
434
+ function renderDrawioTable(tableId: string, tableName: string, x: number, y: number, rowCount: number) {
435
+ const height = 30 + rowCount * 30;
436
+ return `
437
+ <mxCell id="${tableId}" value="${tableName}"
438
+ style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;"
439
+ vertex="1" parent="1">
440
+ <mxGeometry x="${x}" y="${y}" width="${TABLE_WIDTH}" height="${height}" as="geometry"/>
441
+ </mxCell>
442
+ `.trim();
443
+ }
444
+
445
+ function renderDrawioTableRow(rowId: string, tableId: string, y: number) {
446
+ return `
447
+ <mxCell id="${rowId}" parent="${tableId}" value=""
448
+ style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;"
449
+ vertex="1">
450
+ <mxGeometry y="${y}" width="${TABLE_WIDTH}" height="30" as="geometry"/>
451
+ </mxCell>
452
+ `.trim();
453
+ }
454
+
455
+ function renderDrawioTableCell(cellId: string, rowId: string, value: string, x: number, width: number, bold = false) {
456
+ return `
457
+ <mxCell id="${cellId}" parent="${rowId}" value="${value}"
458
+ style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;fontStyle=${bold ? 1 : 0};overflow=hidden;whiteSpace=wrap;html=1;"
459
+ vertex="1">
460
+ <mxGeometry x="${x}" width="${width}" height="30" as="geometry"/>
461
+ </mxCell>
462
+ `.trim();
463
+ }
@@ -0,0 +1,153 @@
1
+ import { conversion, logger } from "@utils";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { lightMigrationStub } from "../../stubs";
5
+
6
+ // ==================================>
7
+ // ## Command: Blueprint migration generation
8
+ // ==================================>
9
+ export async function migrationGeneration(
10
+ model: string,
11
+ schema: Record<string, string> = {},
12
+ relations: Record<string, string> = {},
13
+ marker: string
14
+ ): Promise<boolean> {
15
+ const name = conversion.strSnake(conversion.strPlural(model.split("/").pop() || ""));
16
+ const basePath = path.join(process.cwd(), "src", "database", "migrations", "0000_00");
17
+ const filename = `${name}.ts`;
18
+ const filePath = path.join(basePath, filename);
19
+ const className = `create${conversion.strPascal(name)}Table`;
20
+
21
+ if (!fs.existsSync(basePath)) {
22
+ fs.mkdirSync(basePath, { recursive: true });
23
+ }
24
+
25
+ const existingMigrations = fs.readdirSync(basePath).filter((f) => f.includes(`${name}`));
26
+
27
+ for (const file of existingMigrations) {
28
+ if (fs.existsSync(path.join(basePath, file))) {
29
+ const content = fs.readFileSync(path.join(basePath, file), "utf-8");
30
+
31
+ if (!content.includes("AUTO-GENERATED BY BLUEPRINT")) {
32
+ logger.info(`Skip overridden file: ${name}.ts`);
33
+ return false;
34
+ }
35
+ }
36
+ }
37
+
38
+ const migrationFields: string[] = [];
39
+ const pivots: string[] = [];
40
+
41
+ if (relations) {
42
+ for (const target of extractBelongsTo(relations)) {
43
+ const [fk, table, isFk] = getTargetRelation(target);
44
+
45
+ if (schema[fk as string]) continue;
46
+
47
+ migrationFields.push(`table.foreignIdFor("${table}"${isFk ? `, "${fk}"` : ""})`);
48
+ }
49
+
50
+ for (const target of extractBelongsToMany(relations)) {
51
+ const sourceTable = name;
52
+ const targetTable = conversion.strSnake(conversion.strPlural(target));
53
+
54
+ if (sourceTable < targetTable) continue;
55
+
56
+ const pivotTable = getTargetTableRelation(sourceTable, targetTable);
57
+
58
+ pivots.push(` await knex.schema.createTable("${pivotTable}", (table) => {`);
59
+ pivots.push(` table.foreignIdFor("${sourceTable}")`);
60
+ pivots.push(` table.foreignIdFor("${targetTable}")`);
61
+ pivots.push(` })`);
62
+ }
63
+ }
64
+
65
+ for (const [column, definition] of Object.entries(schema)) {
66
+ const typeMatch = /type:(\w+),?(\d+)?/.exec(definition);
67
+ const type = typeMatch?.[1] ?? "string";
68
+ const length = typeMatch?.[2];
69
+
70
+ let columnDef = "";
71
+ switch (type) {
72
+ case "bigInt":
73
+ case "bigint":
74
+ case "bigInteger":
75
+ columnDef = `table.bigInteger("${column}").unsigned()`;
76
+ break;
77
+ case "int":
78
+ case "integer":
79
+ columnDef = `table.integer("${column}")`;
80
+ break;
81
+ case "string":
82
+ columnDef = length
83
+ ? `table.string("${column}", ${length})`
84
+ : `table.string("${column}")`;
85
+ break;
86
+ case "text":
87
+ columnDef = `table.text("${column}")`;
88
+ break;
89
+ case "foreignIdFor":
90
+ columnDef = `table.foreignIdFor("${length}")`;
91
+ break;
92
+ default:
93
+ columnDef = `table.${type}("${column}")`;
94
+ break;
95
+ }
96
+
97
+ if (definition.includes("unique")) {
98
+ columnDef += `.unique()`;
99
+ }
100
+ if (definition.includes("required")) {
101
+ columnDef += `.notNullable()`;
102
+ }
103
+ if (definition.includes("index")) {
104
+ columnDef += `.index()`;
105
+ }
106
+
107
+ migrationFields.push(columnDef);
108
+ }
109
+
110
+ const migrationSchema = migrationFields.map((f) => ` ${f}`).join("\n");
111
+
112
+ let stub = lightMigrationStub;
113
+
114
+ stub = stub
115
+ .replace(/{{\s*marker\s*}}/g, marker)
116
+ .replace(/{{\s*className\s*}}/g, className)
117
+ .replace(/{{\s*tableName\s*}}/g, name)
118
+ .replace(/{{\s*schemas\s*}}/g, migrationSchema)
119
+ .replace(/{{\s*pivot\s*}}/g, pivots.join('\n'));
120
+
121
+ fs.writeFileSync(filePath, stub, "utf-8");
122
+
123
+ return true;
124
+ }
125
+
126
+ function extractBelongsTo(relations: Record<string, string>) {
127
+ return Object.values(relations)
128
+ .filter(r => !r.includes("[]"))
129
+ .map(r => r.replace(":", "").trim());
130
+ }
131
+
132
+ function getTargetRelation(model: string) {
133
+ const base = model?.split(",")?.[0].split("/").pop()!;
134
+ const fk = model?.split(",")?.[1] || `${conversion.strSnake(base)}_id`;
135
+
136
+ return [fk, `${conversion.strPlural(conversion.strSnake(base))}`, !!model?.split(",")?.[1]];
137
+ }
138
+
139
+ function extractBelongsToMany(relations: Record<string, string>) {
140
+ return Object.values(relations)
141
+ .filter(r => r.startsWith("[]:"))
142
+ .map(r => r.replace("[]:", "").trim());
143
+ }
144
+
145
+ function getTargetTableRelation(source: string, target: string) {
146
+ const a = conversion.strSnake(
147
+ conversion.strPlural(source.split("/").pop()!)
148
+ );
149
+ const b = conversion.strSnake(
150
+ conversion.strPlural(target.split("/").pop()!)
151
+ );
152
+ return `${a}_has_${b}`;
153
+ }