@bunnykit/orm 0.1.1 → 0.1.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.
- package/README.md +36 -15
- package/dist/bin/bunny.js +104 -14
- package/dist/src/migration/Migrator.d.ts +3 -2
- package/dist/src/migration/Migrator.js +67 -24
- package/dist/src/typegen/TypeGenerator.d.ts +3 -0
- package/dist/src/typegen/TypeGenerator.js +71 -51
- package/dist/src/utils.d.ts +2 -0
- package/dist/src/utils.js +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,7 +52,15 @@ export default {
|
|
|
52
52
|
// username: "root",
|
|
53
53
|
// password: "secret",
|
|
54
54
|
},
|
|
55
|
-
migrationsPath: "./database/migrations",
|
|
55
|
+
migrationsPath: ["./database/migrations", "./database/tenant-migrations"],
|
|
56
|
+
modelsPath: ["./src/models", "./src/admin/models"],
|
|
57
|
+
// Optional legacy type output directory
|
|
58
|
+
typesOutDir: "./src/generated/model-types",
|
|
59
|
+
// Optional typegen overrides
|
|
60
|
+
typeDeclarationImportPrefix: "../models",
|
|
61
|
+
typeDeclarations: {
|
|
62
|
+
admin_users: { path: "../AdminAccount", className: "AdminAccount" },
|
|
63
|
+
},
|
|
56
64
|
};
|
|
57
65
|
```
|
|
58
66
|
|
|
@@ -60,7 +68,9 @@ Or use environment variables:
|
|
|
60
68
|
|
|
61
69
|
```bash
|
|
62
70
|
export DATABASE_URL="sqlite://app.db"
|
|
63
|
-
export MIGRATIONS_PATH="./database/migrations"
|
|
71
|
+
export MIGRATIONS_PATH="./database/migrations,./database/tenant-migrations"
|
|
72
|
+
export MODELS_PATH="./src/models,./src/admin/models"
|
|
73
|
+
export TYPES_OUT_DIR="./src/generated/model-types"
|
|
64
74
|
```
|
|
65
75
|
|
|
66
76
|
---
|
|
@@ -151,7 +161,7 @@ Start an interactive Bunny session with the ORM already loaded:
|
|
|
151
161
|
bunny repl
|
|
152
162
|
```
|
|
153
163
|
|
|
154
|
-
The REPL exposes `Model`, `Schema`, `Connection`, and `
|
|
164
|
+
The REPL exposes `Model`, `Schema`, `Connection`, `db`, and a `Models` map. Any model files under `modelsPath` are loaded automatically and also registered by class name on the global scope. If no project config is present, it starts against an in-memory SQLite database so you can still experiment immediately.
|
|
155
165
|
|
|
156
166
|
---
|
|
157
167
|
|
|
@@ -699,6 +709,9 @@ ObserverRegistry.register(User, {
|
|
|
699
709
|
# Create a new migration file
|
|
700
710
|
bun run bunny migrate:make CreateUsersTable
|
|
701
711
|
|
|
712
|
+
# Create in a specific folder
|
|
713
|
+
bun run bunny migrate:make CreateUsersTable ./database/tenant-migrations
|
|
714
|
+
|
|
702
715
|
# Run all pending migrations
|
|
703
716
|
bun run bunny migrate
|
|
704
717
|
|
|
@@ -734,12 +747,14 @@ Migrations are tracked in a `migrations` table (auto-created on first run).
|
|
|
734
747
|
|
|
735
748
|
### Auto Type Generation
|
|
736
749
|
|
|
737
|
-
If you set `typesOutDir` in your config, types are **automatically regenerated** after every `migrate` and `migrate:rollback
|
|
750
|
+
If you set `typesOutDir` in your config, types are **automatically regenerated** after every `migrate` and `migrate:rollback`.
|
|
751
|
+
|
|
752
|
+
If you set `modelsPath`, Bunny writes declaration files into a `types/` folder next to each model root and regenerates those files after migrations too:
|
|
738
753
|
|
|
739
754
|
```bash
|
|
740
755
|
bun run bunny migrate
|
|
741
756
|
# → Migrated: 2026xxxx_create_users_table.ts
|
|
742
|
-
# → Regenerated types in ./src/
|
|
757
|
+
# → Regenerated types in ./src/models/types, ./src/admin/models/types
|
|
743
758
|
```
|
|
744
759
|
|
|
745
760
|
No extra step needed — your models stay in sync with the schema automatically.
|
|
@@ -814,6 +829,9 @@ user.email = "a@example.com"; // ✅ typed setter
|
|
|
814
829
|
# Generate into default directory (./generated/models)
|
|
815
830
|
bun run bunny types:generate
|
|
816
831
|
|
|
832
|
+
# Generate into model-local ./types folders when modelsPath is configured
|
|
833
|
+
bun run bunny types:generate
|
|
834
|
+
|
|
817
835
|
# Generate into a custom directory
|
|
818
836
|
bun run bunny types:generate ./src/generated
|
|
819
837
|
```
|
|
@@ -823,23 +841,26 @@ Or configure in `bunny.config.ts`:
|
|
|
823
841
|
```ts
|
|
824
842
|
export default {
|
|
825
843
|
connection: { url: "sqlite://app.db" },
|
|
826
|
-
migrationsPath: "./database/migrations",
|
|
827
|
-
|
|
844
|
+
migrationsPath: ["./database/migrations", "./database/tenant-migrations"],
|
|
845
|
+
modelsPath: ["./src/models", "./src/admin/models"],
|
|
846
|
+
// Optional legacy output directory, still supported when you do not want
|
|
847
|
+
// the generated files beside each model root:
|
|
848
|
+
typesOutDir: "./src/generated/model-types",
|
|
849
|
+
// Optional override for custom module resolution when using typesOutDir:
|
|
828
850
|
typeDeclarationImportPrefix: "../models",
|
|
829
|
-
// Optional overrides for non-conventional model names or paths:
|
|
830
851
|
typeDeclarations: {
|
|
831
|
-
admin_users: { path: "../
|
|
852
|
+
admin_users: { path: "../AdminAccount", className: "AdminAccount" },
|
|
832
853
|
},
|
|
833
854
|
};
|
|
834
855
|
```
|
|
835
856
|
|
|
836
|
-
With `
|
|
857
|
+
With `modelsPath`, Bunny conventionally maps tables to singular PascalCase model modules and writes the generated declarations into `types/` beside each model root:
|
|
837
858
|
|
|
838
859
|
| Table | Generated augmentation |
|
|
839
860
|
|-------|------------------------|
|
|
840
|
-
| `users` | `../
|
|
841
|
-
| `blog_posts` | `../
|
|
842
|
-
| `categories` | `../
|
|
861
|
+
| `users` | `../User` / `User` |
|
|
862
|
+
| `blog_posts` | `../BlogPost` / `BlogPost` |
|
|
863
|
+
| `categories` | `../Category` / `Category` |
|
|
843
864
|
|
|
844
865
|
Set `typeDeclarationSingularModels: false` if your model classes use plural names.
|
|
845
866
|
|
|
@@ -848,7 +869,7 @@ Set `typeDeclarationSingularModels: false` if your model classes use plural name
|
|
|
848
869
|
For each table, Bunny generates an `Attributes` interface. If you configure `typeDeclarations`, it also augments your real model class:
|
|
849
870
|
|
|
850
871
|
```ts
|
|
851
|
-
//
|
|
872
|
+
// src/models/types/users.d.ts
|
|
852
873
|
export interface UsersAttributes {
|
|
853
874
|
id: number;
|
|
854
875
|
name: string;
|
|
@@ -856,7 +877,7 @@ export interface UsersAttributes {
|
|
|
856
877
|
created_at: string;
|
|
857
878
|
}
|
|
858
879
|
|
|
859
|
-
declare module "../
|
|
880
|
+
declare module "../User" {
|
|
860
881
|
interface User extends UsersAttributes {}
|
|
861
882
|
}
|
|
862
883
|
```
|
package/dist/bin/bunny.js
CHANGED
|
@@ -4,13 +4,26 @@ import { Migrator } from "../src/migration/Migrator.js";
|
|
|
4
4
|
import { MigrationCreator } from "../src/migration/MigrationCreator.js";
|
|
5
5
|
import { TypeGenerator } from "../src/typegen/TypeGenerator.js";
|
|
6
6
|
import { existsSync } from "fs";
|
|
7
|
-
import { mkdir,
|
|
7
|
+
import { mkdir, rm, writeFile } from "fs/promises";
|
|
8
8
|
import { join } from "path";
|
|
9
|
-
import {
|
|
9
|
+
import { normalizePathList } from "../src/utils.js";
|
|
10
|
+
function parseEnvPathSetting(value) {
|
|
11
|
+
if (!value)
|
|
12
|
+
return undefined;
|
|
13
|
+
const paths = value
|
|
14
|
+
.split(",")
|
|
15
|
+
.map((item) => item.trim())
|
|
16
|
+
.filter((item) => item.length > 0);
|
|
17
|
+
if (paths.length === 0)
|
|
18
|
+
return undefined;
|
|
19
|
+
return paths.length === 1 ? paths[0] : paths;
|
|
20
|
+
}
|
|
10
21
|
async function createReplBootstrap(config) {
|
|
11
|
-
const
|
|
22
|
+
const tmpRoot = process.env.BUNNY_REPL_TMPDIR || "/private/tmp";
|
|
23
|
+
const dir = join(tmpRoot, "bunny-repl");
|
|
12
24
|
await mkdir(dir, { recursive: true });
|
|
13
25
|
const bootstrapPath = join(dir, `bootstrap-${Date.now()}-${Math.random().toString(36).slice(2)}.ts`);
|
|
26
|
+
const modelRoots = normalizePathList(config.modelsPath || config.typeDeclarationModelsDir);
|
|
14
27
|
const source = `
|
|
15
28
|
import {
|
|
16
29
|
BelongsTo,
|
|
@@ -40,11 +53,65 @@ async function createReplBootstrap(config) {
|
|
|
40
53
|
TypeMapper,
|
|
41
54
|
Model
|
|
42
55
|
} from "@bunnykit/orm";
|
|
56
|
+
import { existsSync } from "fs";
|
|
57
|
+
import { readdir } from "fs/promises";
|
|
58
|
+
import { basename, extname, join, resolve } from "path";
|
|
59
|
+
import { pathToFileURL } from "url";
|
|
43
60
|
|
|
44
61
|
const connection = new Connection(${JSON.stringify(config.connection)});
|
|
45
62
|
Model.setConnection(connection);
|
|
46
63
|
Schema.setConnection(connection);
|
|
47
64
|
|
|
65
|
+
const modelRoots = ${JSON.stringify(modelRoots)};
|
|
66
|
+
|
|
67
|
+
async function walkFiles(dir) {
|
|
68
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
69
|
+
const files = [];
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
if (entry.name === "types") continue;
|
|
72
|
+
const fullPath = join(dir, entry.name);
|
|
73
|
+
if (entry.isDirectory()) {
|
|
74
|
+
files.push(...await walkFiles(fullPath));
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (!entry.isFile()) continue;
|
|
78
|
+
const name = entry.name;
|
|
79
|
+
if (name.endsWith(".d.ts") || name.endsWith(".test.ts") || name.endsWith(".spec.ts")) continue;
|
|
80
|
+
if (![".ts", ".js", ".mts", ".mjs", ".cts", ".cjs"].includes(extname(name))) continue;
|
|
81
|
+
files.push(fullPath);
|
|
82
|
+
}
|
|
83
|
+
return files;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function loadModels(roots) {
|
|
87
|
+
const loaded = {};
|
|
88
|
+
for (const root of roots) {
|
|
89
|
+
const resolvedRoot = resolve(process.cwd(), root);
|
|
90
|
+
if (!existsSync(resolvedRoot)) continue;
|
|
91
|
+
const files = await walkFiles(resolvedRoot);
|
|
92
|
+
for (const file of files.sort()) {
|
|
93
|
+
const mod = await import(pathToFileURL(file).href);
|
|
94
|
+
for (const [exportName, exported] of Object.entries(mod)) {
|
|
95
|
+
if (exportName === "default") continue;
|
|
96
|
+
if (typeof exported === "function" && exported.prototype instanceof Model) {
|
|
97
|
+
const modelName = exportName;
|
|
98
|
+
loaded[modelName] = exported;
|
|
99
|
+
globalThis[modelName] = exported;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (typeof mod.default === "function" && mod.default.prototype instanceof Model) {
|
|
103
|
+
const modelName = mod.default.name || basename(file, extname(file));
|
|
104
|
+
loaded[modelName] = mod.default;
|
|
105
|
+
globalThis[modelName] = mod.default;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
globalThis.Models = loaded;
|
|
110
|
+
return loaded;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const loadedModels = await loadModels(modelRoots);
|
|
114
|
+
|
|
48
115
|
Object.assign(globalThis, {
|
|
49
116
|
Connection,
|
|
50
117
|
Builder,
|
|
@@ -74,16 +141,25 @@ async function createReplBootstrap(config) {
|
|
|
74
141
|
Schema,
|
|
75
142
|
db: connection,
|
|
76
143
|
connection,
|
|
144
|
+
Models: loadedModels,
|
|
77
145
|
});
|
|
78
146
|
|
|
79
|
-
console.log(
|
|
147
|
+
console.log(\`Bunny REPL ready. Loaded \${Object.keys(loadedModels).length} model classes from modelsPath.\`);
|
|
80
148
|
`;
|
|
81
149
|
await writeFile(bootstrapPath, source, "utf-8");
|
|
82
150
|
return bootstrapPath;
|
|
83
151
|
}
|
|
84
152
|
async function runRepl(config, replArgs) {
|
|
85
153
|
const bootstrapPath = await createReplBootstrap(config);
|
|
154
|
+
await mkdir("/private/tmp/bunny-repl-cache", { recursive: true });
|
|
86
155
|
const proc = Bun.spawn(["bun", "repl", ...replArgs], {
|
|
156
|
+
env: {
|
|
157
|
+
...process.env,
|
|
158
|
+
TMPDIR: "/private/tmp",
|
|
159
|
+
TEMP: "/private/tmp",
|
|
160
|
+
TMP: "/private/tmp",
|
|
161
|
+
BUN_RUNTIME_TRANSPILER_CACHE_PATH: "/private/tmp/bunny-repl-cache",
|
|
162
|
+
},
|
|
87
163
|
terminal: {
|
|
88
164
|
cols: process.stdout.columns || 80,
|
|
89
165
|
rows: process.stdout.rows || 24,
|
|
@@ -138,7 +214,8 @@ async function loadConfig(allowFallback = false) {
|
|
|
138
214
|
if (url) {
|
|
139
215
|
return {
|
|
140
216
|
connection: { url },
|
|
141
|
-
migrationsPath: process.env.MIGRATIONS_PATH || "./database/migrations",
|
|
217
|
+
migrationsPath: parseEnvPathSetting(process.env.MIGRATIONS_PATH) || "./database/migrations",
|
|
218
|
+
modelsPath: parseEnvPathSetting(process.env.MODELS_PATH),
|
|
142
219
|
};
|
|
143
220
|
}
|
|
144
221
|
const driver = process.env.DB_CONNECTION;
|
|
@@ -153,13 +230,15 @@ async function loadConfig(allowFallback = false) {
|
|
|
153
230
|
password: process.env.DB_PASSWORD,
|
|
154
231
|
filename: process.env.DB_DATABASE,
|
|
155
232
|
},
|
|
156
|
-
migrationsPath: process.env.MIGRATIONS_PATH || "./database/migrations",
|
|
233
|
+
migrationsPath: parseEnvPathSetting(process.env.MIGRATIONS_PATH) || "./database/migrations",
|
|
234
|
+
modelsPath: parseEnvPathSetting(process.env.MODELS_PATH),
|
|
157
235
|
};
|
|
158
236
|
}
|
|
159
237
|
if (allowFallback) {
|
|
160
238
|
return {
|
|
161
239
|
connection: { url: "sqlite://:memory:" },
|
|
162
|
-
migrationsPath: process.env.MIGRATIONS_PATH || "./database/migrations",
|
|
240
|
+
migrationsPath: parseEnvPathSetting(process.env.MIGRATIONS_PATH) || "./database/migrations",
|
|
241
|
+
modelsPath: parseEnvPathSetting(process.env.MODELS_PATH),
|
|
163
242
|
};
|
|
164
243
|
}
|
|
165
244
|
throw new Error("No database configuration found. Create bunny.config.ts or set DATABASE_URL / DB_CONNECTION environment variables.");
|
|
@@ -170,30 +249,38 @@ async function main() {
|
|
|
170
249
|
if (command === "migrate:make") {
|
|
171
250
|
const name = args[1];
|
|
172
251
|
if (!name) {
|
|
173
|
-
console.error("Usage: bun run bunny migrate:make <name>");
|
|
252
|
+
console.error("Usage: bun run bunny migrate:make <name> [directory]");
|
|
174
253
|
process.exit(1);
|
|
175
254
|
}
|
|
176
255
|
const config = await loadConfig();
|
|
177
256
|
const creator = new MigrationCreator();
|
|
178
|
-
const
|
|
257
|
+
const migrationRoots = normalizePathList(config.migrationsPath);
|
|
258
|
+
const targetPath = args[2] || migrationRoots[0] || "./database/migrations";
|
|
259
|
+
const path = await creator.create(name, targetPath);
|
|
179
260
|
console.log(`Created migration: ${path}`);
|
|
180
261
|
return;
|
|
181
262
|
}
|
|
182
263
|
if (command === "types:generate") {
|
|
183
264
|
const config = await loadConfig();
|
|
184
265
|
const connection = new Connection(config.connection);
|
|
185
|
-
const
|
|
266
|
+
const modelRoots = normalizePathList(config.modelsPath || config.typeDeclarationModelsDir);
|
|
267
|
+
const explicitOutDir = args[1];
|
|
268
|
+
const useModelTypesFolder = !explicitOutDir && !config.typesOutDir && modelRoots.length > 0;
|
|
269
|
+
const outDir = explicitOutDir || config.typesOutDir || (useModelTypesFolder ? join(modelRoots[0], "types") : "./generated/models");
|
|
186
270
|
const generator = new TypeGenerator(connection, {
|
|
187
271
|
outDir,
|
|
188
272
|
stubs: config.typeStubs,
|
|
189
273
|
declarations: !config.typeStubs,
|
|
190
274
|
modelDeclarations: config.typeDeclarations,
|
|
191
|
-
modelDirectory:
|
|
275
|
+
modelDirectory: !useModelTypesFolder ? modelRoots[0] : undefined,
|
|
276
|
+
modelDirectories: useModelTypesFolder ? modelRoots : undefined,
|
|
192
277
|
modelImportPrefix: config.typeDeclarationImportPrefix,
|
|
193
278
|
singularModels: config.typeDeclarationSingularModels,
|
|
279
|
+
declarationDirName: "types",
|
|
194
280
|
});
|
|
195
281
|
await generator.generate();
|
|
196
|
-
|
|
282
|
+
const outputLabel = useModelTypesFolder ? modelRoots.map((root) => join(root, "types")).join(", ") : outDir;
|
|
283
|
+
console.log(`Generated model type declarations in ${outputLabel}`);
|
|
197
284
|
return;
|
|
198
285
|
}
|
|
199
286
|
if (command === "repl") {
|
|
@@ -204,13 +291,16 @@ async function main() {
|
|
|
204
291
|
}
|
|
205
292
|
const config = await loadConfig();
|
|
206
293
|
const connection = new Connection(config.connection);
|
|
294
|
+
const modelRoots = normalizePathList(config.modelsPath || config.typeDeclarationModelsDir);
|
|
207
295
|
const migrator = new Migrator(connection, config.migrationsPath, config.typesOutDir, {
|
|
208
296
|
declarations: !config.typeStubs,
|
|
209
297
|
stubs: config.typeStubs,
|
|
210
298
|
modelDeclarations: config.typeDeclarations,
|
|
211
|
-
modelDirectory:
|
|
299
|
+
modelDirectory: modelRoots[0],
|
|
300
|
+
modelDirectories: modelRoots.length > 1 ? modelRoots : undefined,
|
|
212
301
|
modelImportPrefix: config.typeDeclarationImportPrefix,
|
|
213
302
|
singularModels: config.typeDeclarationSingularModels,
|
|
303
|
+
declarationDirName: "types",
|
|
214
304
|
});
|
|
215
305
|
if (command === "migrate") {
|
|
216
306
|
await migrator.run();
|
|
@@ -225,7 +315,7 @@ async function main() {
|
|
|
225
315
|
else {
|
|
226
316
|
console.log("Usage:");
|
|
227
317
|
console.log(" bun run bunny migrate Run pending migrations");
|
|
228
|
-
console.log(" bun run bunny migrate:make <name>
|
|
318
|
+
console.log(" bun run bunny migrate:make <name> [dir] Create a new migration");
|
|
229
319
|
console.log(" bun run bunny migrate:rollback Rollback the last batch");
|
|
230
320
|
console.log(" bun run bunny migrate:status Show migration status");
|
|
231
321
|
console.log(" bun run bunny types:generate [dir] Generate model type declarations from DB schema");
|
|
@@ -5,9 +5,9 @@ export declare class Migrator {
|
|
|
5
5
|
private path;
|
|
6
6
|
private typesOutDir?;
|
|
7
7
|
private typeGeneratorOptions;
|
|
8
|
-
constructor(connection: Connection, path: string, typesOutDir?: string | undefined, typeGeneratorOptions?: Omit<TypeGeneratorOptions, "outDir">);
|
|
8
|
+
constructor(connection: Connection, path: string | string[], typesOutDir?: string | undefined, typeGeneratorOptions?: Omit<TypeGeneratorOptions, "outDir">);
|
|
9
|
+
private getPaths;
|
|
9
10
|
private ensureMigrationsTable;
|
|
10
|
-
private getRan;
|
|
11
11
|
private getLastBatchNumber;
|
|
12
12
|
private getMigrationFiles;
|
|
13
13
|
run(): Promise<void>;
|
|
@@ -18,4 +18,5 @@ export declare class Migrator {
|
|
|
18
18
|
status: string;
|
|
19
19
|
}[]>;
|
|
20
20
|
private resolve;
|
|
21
|
+
private getRan;
|
|
21
22
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
1
2
|
import { readdir } from "fs/promises";
|
|
2
|
-
import { resolve } from "path";
|
|
3
|
+
import { basename, join, relative, resolve } from "path";
|
|
3
4
|
import { Schema } from "../schema/Schema.js";
|
|
4
5
|
import { Builder } from "../query/Builder.js";
|
|
5
6
|
import { TypeGenerator } from "../typegen/TypeGenerator.js";
|
|
7
|
+
import { normalizePathList, toPosixPath } from "../utils.js";
|
|
6
8
|
export class Migrator {
|
|
7
9
|
connection;
|
|
8
10
|
path;
|
|
@@ -15,6 +17,9 @@ export class Migrator {
|
|
|
15
17
|
this.typeGeneratorOptions = typeGeneratorOptions;
|
|
16
18
|
Schema.setConnection(connection);
|
|
17
19
|
}
|
|
20
|
+
getPaths() {
|
|
21
|
+
return normalizePathList(this.path);
|
|
22
|
+
}
|
|
18
23
|
async ensureMigrationsTable() {
|
|
19
24
|
const exists = await Schema.hasTable("migrations");
|
|
20
25
|
if (!exists) {
|
|
@@ -25,13 +30,6 @@ export class Migrator {
|
|
|
25
30
|
});
|
|
26
31
|
}
|
|
27
32
|
}
|
|
28
|
-
async getRan() {
|
|
29
|
-
await this.ensureMigrationsTable();
|
|
30
|
-
const results = await new Builder(this.connection, "migrations")
|
|
31
|
-
.orderBy("id", "asc")
|
|
32
|
-
.get();
|
|
33
|
-
return results.map((row) => row.migration);
|
|
34
|
-
}
|
|
35
33
|
async getLastBatchNumber() {
|
|
36
34
|
const result = await new Builder(this.connection, "migrations")
|
|
37
35
|
.select("MAX(batch) as batch")
|
|
@@ -39,15 +37,28 @@ export class Migrator {
|
|
|
39
37
|
return result?.batch || 0;
|
|
40
38
|
}
|
|
41
39
|
async getMigrationFiles() {
|
|
42
|
-
const files =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
const files = [];
|
|
41
|
+
for (const path of this.getPaths()) {
|
|
42
|
+
if (!existsSync(path))
|
|
43
|
+
continue;
|
|
44
|
+
const entries = await readdir(path);
|
|
45
|
+
for (const fileName of entries) {
|
|
46
|
+
if (!fileName.endsWith(".ts") && !fileName.endsWith(".js"))
|
|
47
|
+
continue;
|
|
48
|
+
const fullPath = resolve(path, fileName);
|
|
49
|
+
files.push({
|
|
50
|
+
id: toPosixPath(relative(process.cwd(), fullPath)),
|
|
51
|
+
fileName,
|
|
52
|
+
fullPath,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return files.sort((a, b) => a.fileName.localeCompare(b.fileName) || a.id.localeCompare(b.id));
|
|
46
57
|
}
|
|
47
58
|
async run() {
|
|
48
59
|
const ran = await this.getRan();
|
|
49
60
|
const files = await this.getMigrationFiles();
|
|
50
|
-
const pending = files.filter((f) => !ran.
|
|
61
|
+
const pending = files.filter((f) => !ran.has(f.id) && !ran.has(f.fileName));
|
|
51
62
|
if (pending.length === 0) {
|
|
52
63
|
console.log("Nothing to migrate.");
|
|
53
64
|
return;
|
|
@@ -56,14 +67,14 @@ export class Migrator {
|
|
|
56
67
|
await this.connection.beginTransaction();
|
|
57
68
|
try {
|
|
58
69
|
for (const file of pending) {
|
|
59
|
-
const migration = await this.resolve(file);
|
|
60
|
-
console.log(`Migrating: ${file}`);
|
|
70
|
+
const migration = await this.resolve(file.id);
|
|
71
|
+
console.log(`Migrating: ${file.id}`);
|
|
61
72
|
await migration.up();
|
|
62
73
|
await new Builder(this.connection, "migrations").insert({
|
|
63
|
-
migration: file,
|
|
74
|
+
migration: file.id,
|
|
64
75
|
batch,
|
|
65
76
|
});
|
|
66
|
-
console.log(`Migrated: ${file}`);
|
|
77
|
+
console.log(`Migrated: ${file.id}`);
|
|
67
78
|
}
|
|
68
79
|
await this.connection.commit();
|
|
69
80
|
await this.generateTypesIfNeeded();
|
|
@@ -107,31 +118,63 @@ export class Migrator {
|
|
|
107
118
|
}
|
|
108
119
|
}
|
|
109
120
|
async generateTypesIfNeeded() {
|
|
110
|
-
|
|
121
|
+
const modelDirectories = normalizePathList(this.typeGeneratorOptions.modelDirectories || this.typeGeneratorOptions.modelDirectory);
|
|
122
|
+
if (!this.typesOutDir && modelDirectories.length === 0)
|
|
111
123
|
return;
|
|
124
|
+
const outDir = this.typesOutDir || join(modelDirectories[0], this.typeGeneratorOptions.declarationDirName || "types");
|
|
112
125
|
const generator = new TypeGenerator(this.connection, {
|
|
113
126
|
declarations: true,
|
|
114
127
|
...this.typeGeneratorOptions,
|
|
115
|
-
outDir
|
|
128
|
+
outDir,
|
|
116
129
|
});
|
|
117
130
|
await generator.generate();
|
|
118
|
-
|
|
131
|
+
const label = this.typesOutDir || modelDirectories.map((dir) => join(dir, this.typeGeneratorOptions.declarationDirName || "types")).join(", ");
|
|
132
|
+
console.log(`Regenerated types in ${label}`);
|
|
119
133
|
}
|
|
120
134
|
async status() {
|
|
121
135
|
const ran = await this.getRan();
|
|
122
136
|
const files = await this.getMigrationFiles();
|
|
123
137
|
return files.map((file) => ({
|
|
124
|
-
migration: file,
|
|
125
|
-
status: ran.
|
|
138
|
+
migration: file.id,
|
|
139
|
+
status: ran.has(file.id) || ran.has(file.fileName) ? "Ran" : "Pending",
|
|
126
140
|
}));
|
|
127
141
|
}
|
|
128
142
|
async resolve(file) {
|
|
129
|
-
const
|
|
130
|
-
const
|
|
143
|
+
const normalized = toPosixPath(file);
|
|
144
|
+
const candidates = new Set();
|
|
145
|
+
if (normalized.includes("/")) {
|
|
146
|
+
candidates.add(resolve(process.cwd(), normalized));
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
for (const path of this.getPaths()) {
|
|
150
|
+
candidates.add(resolve(path, normalized));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const matches = [...candidates].filter((candidate) => existsSync(candidate));
|
|
154
|
+
if (matches.length === 0) {
|
|
155
|
+
throw new Error(`Migration ${file} could not be found in the configured migration paths.`);
|
|
156
|
+
}
|
|
157
|
+
if (matches.length > 1) {
|
|
158
|
+
throw new Error(`Migration ${file} is ambiguous across multiple migration paths.`);
|
|
159
|
+
}
|
|
160
|
+
const module = await import(matches[0]);
|
|
131
161
|
const MigrationClass = module.default || Object.values(module)[0];
|
|
132
162
|
if (!MigrationClass) {
|
|
133
163
|
throw new Error(`Migration ${file} does not export a class.`);
|
|
134
164
|
}
|
|
135
165
|
return new MigrationClass();
|
|
136
166
|
}
|
|
167
|
+
async getRan() {
|
|
168
|
+
await this.ensureMigrationsTable();
|
|
169
|
+
const results = await new Builder(this.connection, "migrations")
|
|
170
|
+
.orderBy("id", "asc")
|
|
171
|
+
.get();
|
|
172
|
+
const ran = new Set();
|
|
173
|
+
for (const row of results) {
|
|
174
|
+
const migration = toPosixPath(String(row.migration));
|
|
175
|
+
ran.add(migration);
|
|
176
|
+
ran.add(basename(migration));
|
|
177
|
+
}
|
|
178
|
+
return ran;
|
|
179
|
+
}
|
|
137
180
|
}
|
|
@@ -9,8 +9,10 @@ export interface TypeGeneratorOptions {
|
|
|
9
9
|
declarations?: boolean;
|
|
10
10
|
modelDeclarations?: Record<string, string | ModelDeclaration>;
|
|
11
11
|
modelDirectory?: string;
|
|
12
|
+
modelDirectories?: string[];
|
|
12
13
|
modelImportPrefix?: string;
|
|
13
14
|
singularModels?: boolean;
|
|
15
|
+
declarationDirName?: string;
|
|
14
16
|
}
|
|
15
17
|
export declare class TypeGenerator {
|
|
16
18
|
private connection;
|
|
@@ -23,6 +25,7 @@ export declare class TypeGenerator {
|
|
|
23
25
|
private singularizeTable;
|
|
24
26
|
private singularizeWord;
|
|
25
27
|
private getTables;
|
|
28
|
+
private getDeclarationTargets;
|
|
26
29
|
private getCurrentDatabase;
|
|
27
30
|
private getColumns;
|
|
28
31
|
private toClassName;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "fs/promises";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { TypeMapper } from "./TypeMapper.js";
|
|
4
|
-
import { snakeCase } from "../utils.js";
|
|
4
|
+
import { normalizePathList, snakeCase } from "../utils.js";
|
|
5
5
|
export class TypeGenerator {
|
|
6
6
|
connection;
|
|
7
7
|
options;
|
|
@@ -10,71 +10,75 @@ export class TypeGenerator {
|
|
|
10
10
|
this.options = options;
|
|
11
11
|
}
|
|
12
12
|
async generate() {
|
|
13
|
-
await mkdir(this.options.outDir, { recursive: true });
|
|
14
13
|
const tables = await this.getTables();
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
lines.push("");
|
|
32
|
-
const modelDeclaration = this.getModelDeclaration(table, className);
|
|
33
|
-
if (declarationOnly && modelDeclaration) {
|
|
34
|
-
lines.push(`declare module "${modelDeclaration.path}" {`);
|
|
35
|
-
lines.push(` interface ${modelDeclaration.className} extends ${interfaceName} {}`);
|
|
36
|
-
lines.push("}");
|
|
37
|
-
lines.push("");
|
|
38
|
-
}
|
|
39
|
-
if (!declarationOnly && this.options.stubs) {
|
|
40
|
-
lines.push(`export class ${className}Base extends Model<${interfaceName}> {`);
|
|
41
|
-
lines.push(` static table = "${table}";`);
|
|
42
|
-
lines.push("");
|
|
14
|
+
const declarationOnly = this.options.declarations ?? !this.options.stubs;
|
|
15
|
+
const targets = declarationOnly
|
|
16
|
+
? this.getDeclarationTargets()
|
|
17
|
+
: [{ outDir: this.options.outDir }];
|
|
18
|
+
for (const target of targets) {
|
|
19
|
+
await mkdir(target.outDir, { recursive: true });
|
|
20
|
+
for (const table of tables) {
|
|
21
|
+
const columns = await this.getColumns(table);
|
|
22
|
+
const className = this.toClassName(table);
|
|
23
|
+
const interfaceName = `${className}Attributes`;
|
|
24
|
+
const lines = [];
|
|
25
|
+
if (!declarationOnly) {
|
|
26
|
+
lines.push(`import { Model } from "@bunnykit/orm";`);
|
|
27
|
+
lines.push("");
|
|
28
|
+
}
|
|
29
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
43
30
|
for (const col of columns) {
|
|
44
31
|
const tsType = TypeMapper.sqlToTsType(col.type, col.nullable);
|
|
45
|
-
lines.push(`
|
|
46
|
-
lines.push(` return this.getAttribute("${col.name}");`);
|
|
47
|
-
lines.push(` }`);
|
|
48
|
-
lines.push(` set ${col.name}(value: ${tsType}) {`);
|
|
49
|
-
lines.push(` this.setAttribute("${col.name}", value);`);
|
|
50
|
-
lines.push(` }`);
|
|
51
|
-
lines.push("");
|
|
32
|
+
lines.push(` ${col.name}${col.nullable ? "?" : ""}: ${tsType};`);
|
|
52
33
|
}
|
|
53
34
|
lines.push("}");
|
|
35
|
+
lines.push("");
|
|
36
|
+
const modelDeclaration = this.getModelDeclaration(table, className, target.modelImportPrefix);
|
|
37
|
+
if (declarationOnly && modelDeclaration) {
|
|
38
|
+
lines.push(`declare module "${modelDeclaration.path}" {`);
|
|
39
|
+
lines.push(` interface ${modelDeclaration.className} extends ${interfaceName} {}`);
|
|
40
|
+
lines.push("}");
|
|
41
|
+
lines.push("");
|
|
42
|
+
}
|
|
43
|
+
if (!declarationOnly && this.options.stubs) {
|
|
44
|
+
lines.push(`export class ${className}Base extends Model<${interfaceName}> {`);
|
|
45
|
+
lines.push(` static table = "${table}";`);
|
|
46
|
+
lines.push("");
|
|
47
|
+
for (const col of columns) {
|
|
48
|
+
const tsType = TypeMapper.sqlToTsType(col.type, col.nullable);
|
|
49
|
+
lines.push(` get ${col.name}(): ${tsType} {`);
|
|
50
|
+
lines.push(` return this.getAttribute("${col.name}");`);
|
|
51
|
+
lines.push(` }`);
|
|
52
|
+
lines.push(` set ${col.name}(value: ${tsType}) {`);
|
|
53
|
+
lines.push(` this.setAttribute("${col.name}", value);`);
|
|
54
|
+
lines.push(` }`);
|
|
55
|
+
lines.push("");
|
|
56
|
+
}
|
|
57
|
+
lines.push("}");
|
|
58
|
+
}
|
|
59
|
+
const fileName = `${snakeCase(className)}.${declarationOnly ? "d.ts" : "ts"}`;
|
|
60
|
+
const filePath = join(target.outDir, fileName);
|
|
61
|
+
await writeFile(filePath, lines.join("\n") + "\n", "utf-8");
|
|
54
62
|
}
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
const indexLines = tables.map((table) => {
|
|
64
|
+
const className = this.toClassName(table);
|
|
65
|
+
const fileName = snakeCase(className);
|
|
66
|
+
return `export * from "./${fileName}";`;
|
|
67
|
+
});
|
|
68
|
+
await writeFile(join(target.outDir, `index.${declarationOnly ? "d.ts" : "ts"}`), indexLines.join("\n") + "\n", "utf-8");
|
|
58
69
|
}
|
|
59
|
-
const declarationOnly = this.options.declarations ?? !this.options.stubs;
|
|
60
|
-
const indexLines = tables.map((table) => {
|
|
61
|
-
const className = this.toClassName(table);
|
|
62
|
-
const fileName = snakeCase(className);
|
|
63
|
-
return `export * from "./${fileName}";`;
|
|
64
|
-
});
|
|
65
|
-
await writeFile(join(this.options.outDir, `index.${declarationOnly ? "d.ts" : "ts"}`), indexLines.join("\n") + "\n", "utf-8");
|
|
66
70
|
}
|
|
67
|
-
getModelDeclaration(table, fallbackClassName) {
|
|
71
|
+
getModelDeclaration(table, fallbackClassName, modelImportPrefix) {
|
|
68
72
|
const declaration = this.options.modelDeclarations?.[table];
|
|
69
73
|
if (!declaration)
|
|
70
|
-
return this.getConventionModelDeclaration(table);
|
|
74
|
+
return this.getConventionModelDeclaration(table, modelImportPrefix);
|
|
71
75
|
if (typeof declaration === "string") {
|
|
72
76
|
return { path: declaration, className: this.toModelClassName(table, fallbackClassName) };
|
|
73
77
|
}
|
|
74
78
|
return { path: declaration.path, className: declaration.className || this.toModelClassName(table, fallbackClassName) };
|
|
75
79
|
}
|
|
76
|
-
getConventionModelDeclaration(table) {
|
|
77
|
-
const prefix = this.options.modelImportPrefix || this.options.modelDirectory;
|
|
80
|
+
getConventionModelDeclaration(table, modelImportPrefix) {
|
|
81
|
+
const prefix = modelImportPrefix || this.options.modelImportPrefix || this.options.modelDirectory;
|
|
78
82
|
if (!prefix)
|
|
79
83
|
return null;
|
|
80
84
|
const className = this.toModelClassName(table);
|
|
@@ -129,6 +133,22 @@ export class TypeGenerator {
|
|
|
129
133
|
return rows.map((r) => r.table_name);
|
|
130
134
|
}
|
|
131
135
|
}
|
|
136
|
+
getDeclarationTargets() {
|
|
137
|
+
const modelDirectories = normalizePathList(this.options.modelDirectories || this.options.modelDirectory);
|
|
138
|
+
if (modelDirectories.length === 0) {
|
|
139
|
+
return [
|
|
140
|
+
{
|
|
141
|
+
outDir: this.options.outDir,
|
|
142
|
+
modelImportPrefix: this.options.modelImportPrefix || this.options.modelDirectory || "",
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
const declarationDirName = this.options.declarationDirName || "types";
|
|
147
|
+
return modelDirectories.map((dir) => ({
|
|
148
|
+
outDir: join(dir, declarationDirName),
|
|
149
|
+
modelImportPrefix: this.options.modelImportPrefix || "..",
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
132
152
|
async getCurrentDatabase() {
|
|
133
153
|
const rows = await this.connection.query("SELECT DATABASE() as db");
|
|
134
154
|
return rows[0]?.db || "";
|
package/dist/src/utils.d.ts
CHANGED
package/dist/src/utils.js
CHANGED
|
@@ -4,3 +4,12 @@ export function snakeCase(str) {
|
|
|
4
4
|
.toLowerCase()
|
|
5
5
|
.replace(/^_/, "");
|
|
6
6
|
}
|
|
7
|
+
export function normalizePathList(value) {
|
|
8
|
+
if (!value)
|
|
9
|
+
return [];
|
|
10
|
+
const values = Array.isArray(value) ? value : [value];
|
|
11
|
+
return values.map((item) => item.trim()).filter((item) => item.length > 0);
|
|
12
|
+
}
|
|
13
|
+
export function toPosixPath(value) {
|
|
14
|
+
return value.replace(/\\/g, "/");
|
|
15
|
+
}
|
package/package.json
CHANGED