@bunnykit/orm 0.1.0 → 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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Bunny
2
2
 
3
+ > **Bun-only package.** Install with:
4
+ >
5
+ > ```bash
6
+ > bun add @bunnykit/orm
7
+ > ```
8
+ >
9
+ > npm, yarn, pnpm, and Node.js runtime usage are not supported.
10
+
3
11
  An **Eloquent-inspired ORM** built specifically for [Bun](https://bun.sh)'s native `bun:sql` client. Supports **SQLite**, **MySQL**, and **PostgreSQL** with full TypeScript typing, a chainable query builder, schema migrations, model observers, and polymorphic relations.
4
12
 
5
13
  ---
@@ -24,8 +32,6 @@ An **Eloquent-inspired ORM** built specifically for [Bun](https://bun.sh)'s nati
24
32
  bun add @bunnykit/orm
25
33
  ```
26
34
 
27
- > **Note:** This package is Bun-only. Install and run it with Bun >= 1.1; npm, yarn, pnpm, and Node.js runtime usage are not supported.
28
-
29
35
  ---
30
36
 
31
37
  ## Configuration
@@ -46,7 +52,15 @@ export default {
46
52
  // username: "root",
47
53
  // password: "secret",
48
54
  },
49
- 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
+ },
50
64
  };
51
65
  ```
52
66
 
@@ -54,7 +68,9 @@ Or use environment variables:
54
68
 
55
69
  ```bash
56
70
  export DATABASE_URL="sqlite://app.db"
57
- 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"
58
74
  ```
59
75
 
60
76
  ---
@@ -137,6 +153,16 @@ await user.save();
137
153
  await user.delete();
138
154
  ```
139
155
 
156
+ ### REPL
157
+
158
+ Start an interactive Bunny session with the ORM already loaded:
159
+
160
+ ```bash
161
+ bunny repl
162
+ ```
163
+
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.
165
+
140
166
  ---
141
167
 
142
168
  ## Schema Builder
@@ -683,6 +709,9 @@ ObserverRegistry.register(User, {
683
709
  # Create a new migration file
684
710
  bun run bunny migrate:make CreateUsersTable
685
711
 
712
+ # Create in a specific folder
713
+ bun run bunny migrate:make CreateUsersTable ./database/tenant-migrations
714
+
686
715
  # Run all pending migrations
687
716
  bun run bunny migrate
688
717
 
@@ -718,12 +747,14 @@ Migrations are tracked in a `migrations` table (auto-created on first run).
718
747
 
719
748
  ### Auto Type Generation
720
749
 
721
- 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:
722
753
 
723
754
  ```bash
724
755
  bun run bunny migrate
725
756
  # → Migrated: 2026xxxx_create_users_table.ts
726
- # → Regenerated types in ./src/generated/models
757
+ # → Regenerated types in ./src/models/types, ./src/admin/models/types
727
758
  ```
728
759
 
729
760
  No extra step needed — your models stay in sync with the schema automatically.
@@ -798,6 +829,9 @@ user.email = "a@example.com"; // ✅ typed setter
798
829
  # Generate into default directory (./generated/models)
799
830
  bun run bunny types:generate
800
831
 
832
+ # Generate into model-local ./types folders when modelsPath is configured
833
+ bun run bunny types:generate
834
+
801
835
  # Generate into a custom directory
802
836
  bun run bunny types:generate ./src/generated
803
837
  ```
@@ -807,23 +841,26 @@ Or configure in `bunny.config.ts`:
807
841
  ```ts
808
842
  export default {
809
843
  connection: { url: "sqlite://app.db" },
810
- migrationsPath: "./database/migrations",
811
- typesOutDir: "./src/generated/model-types", // auto-regenerate .d.ts files on every migration
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:
812
850
  typeDeclarationImportPrefix: "../models",
813
- // Optional overrides for non-conventional model names or paths:
814
851
  typeDeclarations: {
815
- admin_users: { path: "../models/AdminAccount", className: "AdminAccount" },
852
+ admin_users: { path: "../AdminAccount", className: "AdminAccount" },
816
853
  },
817
854
  };
818
855
  ```
819
856
 
820
- With `typeDeclarationImportPrefix`, Bunny conventionally maps tables to singular PascalCase model modules:
857
+ With `modelsPath`, Bunny conventionally maps tables to singular PascalCase model modules and writes the generated declarations into `types/` beside each model root:
821
858
 
822
859
  | Table | Generated augmentation |
823
860
  |-------|------------------------|
824
- | `users` | `../models/User` / `User` |
825
- | `blog_posts` | `../models/BlogPost` / `BlogPost` |
826
- | `categories` | `../models/Category` / `Category` |
861
+ | `users` | `../User` / `User` |
862
+ | `blog_posts` | `../BlogPost` / `BlogPost` |
863
+ | `categories` | `../Category` / `Category` |
827
864
 
828
865
  Set `typeDeclarationSingularModels: false` if your model classes use plural names.
829
866
 
@@ -832,7 +869,7 @@ Set `typeDeclarationSingularModels: false` if your model classes use plural name
832
869
  For each table, Bunny generates an `Attributes` interface. If you configure `typeDeclarations`, it also augments your real model class:
833
870
 
834
871
  ```ts
835
- // generated/model-types/users.d.ts
872
+ // src/models/types/users.d.ts
836
873
  export interface UsersAttributes {
837
874
  id: number;
838
875
  name: string;
@@ -840,7 +877,7 @@ export interface UsersAttributes {
840
877
  created_at: string;
841
878
  }
842
879
 
843
- declare module "../models/User" {
880
+ declare module "../User" {
844
881
  interface User extends UsersAttributes {}
845
882
  }
846
883
  ```
package/dist/bin/bunny.js CHANGED
@@ -4,8 +4,201 @@ 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, rm, writeFile } from "fs/promises";
7
8
  import { join } from "path";
8
- async function loadConfig() {
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
+ }
21
+ async function createReplBootstrap(config) {
22
+ const tmpRoot = process.env.BUNNY_REPL_TMPDIR || "/private/tmp";
23
+ const dir = join(tmpRoot, "bunny-repl");
24
+ await mkdir(dir, { recursive: true });
25
+ const bootstrapPath = join(dir, `bootstrap-${Date.now()}-${Math.random().toString(36).slice(2)}.ts`);
26
+ const modelRoots = normalizePathList(config.modelsPath || config.typeDeclarationModelsDir);
27
+ const source = `
28
+ import {
29
+ BelongsTo,
30
+ BelongsToMany,
31
+ Blueprint,
32
+ Builder,
33
+ Connection,
34
+ Grammar,
35
+ HasMany,
36
+ HasManyThrough,
37
+ HasOne,
38
+ HasOneThrough,
39
+ Migration,
40
+ MigrationCreator,
41
+ Migrator,
42
+ MorphMany,
43
+ MorphMap,
44
+ MorphOne,
45
+ MorphTo,
46
+ MorphToMany,
47
+ MySqlGrammar,
48
+ ObserverRegistry,
49
+ PostgresGrammar,
50
+ Schema,
51
+ SQLiteGrammar,
52
+ TypeGenerator,
53
+ TypeMapper,
54
+ Model
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";
60
+
61
+ const connection = new Connection(${JSON.stringify(config.connection)});
62
+ Model.setConnection(connection);
63
+ Schema.setConnection(connection);
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
+
115
+ Object.assign(globalThis, {
116
+ Connection,
117
+ Builder,
118
+ Blueprint,
119
+ Grammar,
120
+ SQLiteGrammar,
121
+ MySqlGrammar,
122
+ PostgresGrammar,
123
+ Model,
124
+ HasMany,
125
+ BelongsTo,
126
+ HasOne,
127
+ HasManyThrough,
128
+ HasOneThrough,
129
+ BelongsToMany,
130
+ MorphMap,
131
+ MorphTo,
132
+ MorphOne,
133
+ MorphMany,
134
+ MorphToMany,
135
+ ObserverRegistry,
136
+ Migration,
137
+ Migrator,
138
+ MigrationCreator,
139
+ TypeGenerator,
140
+ TypeMapper,
141
+ Schema,
142
+ db: connection,
143
+ connection,
144
+ Models: loadedModels,
145
+ });
146
+
147
+ console.log(\`Bunny REPL ready. Loaded \${Object.keys(loadedModels).length} model classes from modelsPath.\`);
148
+ `;
149
+ await writeFile(bootstrapPath, source, "utf-8");
150
+ return bootstrapPath;
151
+ }
152
+ async function runRepl(config, replArgs) {
153
+ const bootstrapPath = await createReplBootstrap(config);
154
+ await mkdir("/private/tmp/bunny-repl-cache", { recursive: true });
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
+ },
163
+ terminal: {
164
+ cols: process.stdout.columns || 80,
165
+ rows: process.stdout.rows || 24,
166
+ data(_terminal, data) {
167
+ process.stdout.write(data);
168
+ },
169
+ },
170
+ });
171
+ const stdin = process.stdin;
172
+ const terminal = proc.terminal;
173
+ const restoreRawMode = stdin.isTTY && typeof stdin.setRawMode === "function";
174
+ if (restoreRawMode) {
175
+ stdin.setRawMode(true);
176
+ }
177
+ stdin.resume();
178
+ const onData = (chunk) => {
179
+ terminal.write(chunk);
180
+ };
181
+ stdin.on("data", onData);
182
+ const cleanup = async () => {
183
+ stdin.off("data", onData);
184
+ if (restoreRawMode) {
185
+ stdin.setRawMode(false);
186
+ }
187
+ terminal.close();
188
+ await rm(bootstrapPath, { force: true });
189
+ };
190
+ process.once("SIGINT", () => {
191
+ terminal.close();
192
+ });
193
+ process.once("SIGTERM", () => {
194
+ terminal.close();
195
+ });
196
+ terminal.write(`.load ${bootstrapPath}\n`);
197
+ const exitCode = await proc.exited;
198
+ await cleanup();
199
+ return exitCode;
200
+ }
201
+ async function loadConfig(allowFallback = false) {
9
202
  const configPath = join(process.cwd(), "bunny.config.ts");
10
203
  if (existsSync(configPath)) {
11
204
  const mod = await import(configPath);
@@ -21,7 +214,8 @@ async function loadConfig() {
21
214
  if (url) {
22
215
  return {
23
216
  connection: { url },
24
- migrationsPath: process.env.MIGRATIONS_PATH || "./database/migrations",
217
+ migrationsPath: parseEnvPathSetting(process.env.MIGRATIONS_PATH) || "./database/migrations",
218
+ modelsPath: parseEnvPathSetting(process.env.MODELS_PATH),
25
219
  };
26
220
  }
27
221
  const driver = process.env.DB_CONNECTION;
@@ -36,7 +230,15 @@ async function loadConfig() {
36
230
  password: process.env.DB_PASSWORD,
37
231
  filename: process.env.DB_DATABASE,
38
232
  },
39
- migrationsPath: process.env.MIGRATIONS_PATH || "./database/migrations",
233
+ migrationsPath: parseEnvPathSetting(process.env.MIGRATIONS_PATH) || "./database/migrations",
234
+ modelsPath: parseEnvPathSetting(process.env.MODELS_PATH),
235
+ };
236
+ }
237
+ if (allowFallback) {
238
+ return {
239
+ connection: { url: "sqlite://:memory:" },
240
+ migrationsPath: parseEnvPathSetting(process.env.MIGRATIONS_PATH) || "./database/migrations",
241
+ modelsPath: parseEnvPathSetting(process.env.MODELS_PATH),
40
242
  };
41
243
  }
42
244
  throw new Error("No database configuration found. Create bunny.config.ts or set DATABASE_URL / DB_CONNECTION environment variables.");
@@ -47,41 +249,58 @@ async function main() {
47
249
  if (command === "migrate:make") {
48
250
  const name = args[1];
49
251
  if (!name) {
50
- console.error("Usage: bun run bunny migrate:make <name>");
252
+ console.error("Usage: bun run bunny migrate:make <name> [directory]");
51
253
  process.exit(1);
52
254
  }
53
255
  const config = await loadConfig();
54
256
  const creator = new MigrationCreator();
55
- const path = await creator.create(name, config.migrationsPath);
257
+ const migrationRoots = normalizePathList(config.migrationsPath);
258
+ const targetPath = args[2] || migrationRoots[0] || "./database/migrations";
259
+ const path = await creator.create(name, targetPath);
56
260
  console.log(`Created migration: ${path}`);
57
261
  return;
58
262
  }
59
263
  if (command === "types:generate") {
60
264
  const config = await loadConfig();
61
265
  const connection = new Connection(config.connection);
62
- const outDir = args[1] || config.typesOutDir || "./generated/models";
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");
63
270
  const generator = new TypeGenerator(connection, {
64
271
  outDir,
65
272
  stubs: config.typeStubs,
66
273
  declarations: !config.typeStubs,
67
274
  modelDeclarations: config.typeDeclarations,
68
- modelDirectory: config.typeDeclarationModelsDir,
275
+ modelDirectory: !useModelTypesFolder ? modelRoots[0] : undefined,
276
+ modelDirectories: useModelTypesFolder ? modelRoots : undefined,
69
277
  modelImportPrefix: config.typeDeclarationImportPrefix,
70
278
  singularModels: config.typeDeclarationSingularModels,
279
+ declarationDirName: "types",
71
280
  });
72
281
  await generator.generate();
73
- console.log(`Generated model type declarations in ${outDir}`);
282
+ const outputLabel = useModelTypesFolder ? modelRoots.map((root) => join(root, "types")).join(", ") : outDir;
283
+ console.log(`Generated model type declarations in ${outputLabel}`);
74
284
  return;
75
285
  }
286
+ if (command === "repl") {
287
+ const config = await loadConfig(true);
288
+ const replArgs = args.slice(1);
289
+ const exitCode = await runRepl(config, replArgs);
290
+ process.exit(exitCode);
291
+ }
76
292
  const config = await loadConfig();
77
293
  const connection = new Connection(config.connection);
294
+ const modelRoots = normalizePathList(config.modelsPath || config.typeDeclarationModelsDir);
78
295
  const migrator = new Migrator(connection, config.migrationsPath, config.typesOutDir, {
79
296
  declarations: !config.typeStubs,
80
297
  stubs: config.typeStubs,
81
298
  modelDeclarations: config.typeDeclarations,
82
- modelDirectory: config.typeDeclarationModelsDir,
299
+ modelDirectory: modelRoots[0],
300
+ modelDirectories: modelRoots.length > 1 ? modelRoots : undefined,
83
301
  modelImportPrefix: config.typeDeclarationImportPrefix,
84
302
  singularModels: config.typeDeclarationSingularModels,
303
+ declarationDirName: "types",
85
304
  });
86
305
  if (command === "migrate") {
87
306
  await migrator.run();
@@ -96,10 +315,12 @@ async function main() {
96
315
  else {
97
316
  console.log("Usage:");
98
317
  console.log(" bun run bunny migrate Run pending migrations");
99
- console.log(" bun run bunny migrate:make <name> Create a new migration");
318
+ console.log(" bun run bunny migrate:make <name> [dir] Create a new migration");
100
319
  console.log(" bun run bunny migrate:rollback Rollback the last batch");
101
320
  console.log(" bun run bunny migrate:status Show migration status");
102
321
  console.log(" bun run bunny types:generate [dir] Generate model type declarations from DB schema");
322
+ console.log(" bun run bunny repl Start a Bunny REPL with Model, Schema, and db loaded");
323
+ console.log(" Falls back to in-memory SQLite when no config is present");
103
324
  }
104
325
  }
105
326
  main().catch((err) => {
@@ -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 = await readdir(this.path);
43
- return files
44
- .filter((f) => f.endsWith(".ts") || f.endsWith(".js"))
45
- .sort((a, b) => a.localeCompare(b));
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.includes(f));
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
- if (!this.typesOutDir)
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: this.typesOutDir,
128
+ outDir,
116
129
  });
117
130
  await generator.generate();
118
- console.log(`Regenerated types in ${this.typesOutDir}`);
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.includes(file) ? "Ran" : "Pending",
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 fullPath = resolve(this.path, file);
130
- const module = await import(fullPath);
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
- for (const table of tables) {
16
- const columns = await this.getColumns(table);
17
- const className = this.toClassName(table);
18
- const interfaceName = `${className}Attributes`;
19
- const lines = [];
20
- const declarationOnly = this.options.declarations ?? !this.options.stubs;
21
- if (!declarationOnly) {
22
- lines.push(`import { Model } from "@bunnykit/orm";`);
23
- lines.push("");
24
- }
25
- lines.push(`export interface ${interfaceName} {`);
26
- for (const col of columns) {
27
- const tsType = TypeMapper.sqlToTsType(col.type, col.nullable);
28
- lines.push(` ${col.name}${col.nullable ? "?" : ""}: ${tsType};`);
29
- }
30
- lines.push("}");
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(` get ${col.name}(): ${tsType} {`);
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 fileName = `${snakeCase(className)}.${declarationOnly ? "d.ts" : "ts"}`;
56
- const filePath = join(this.options.outDir, fileName);
57
- await writeFile(filePath, lines.join("\n") + "\n", "utf-8");
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 || "";
@@ -1 +1,3 @@
1
1
  export declare function snakeCase(str: string): string;
2
+ export declare function normalizePathList(value?: string | string[]): string[];
3
+ export declare function toPosixPath(value: string): string;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunnykit/orm",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "An Eloquent-inspired ORM for Bun's native SQL client supporting SQLite, MySQL, and PostgreSQL.",
5
5
  "license": "MIT",
6
6
  "packageManager": "bun@1.3.12",