@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 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 `db`. If no project config is present, it starts against an in-memory SQLite database so you can still experiment immediately.
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/generated/models
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
- 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:
828
850
  typeDeclarationImportPrefix: "../models",
829
- // Optional overrides for non-conventional model names or paths:
830
851
  typeDeclarations: {
831
- admin_users: { path: "../models/AdminAccount", className: "AdminAccount" },
852
+ admin_users: { path: "../AdminAccount", className: "AdminAccount" },
832
853
  },
833
854
  };
834
855
  ```
835
856
 
836
- 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:
837
858
 
838
859
  | Table | Generated augmentation |
839
860
  |-------|------------------------|
840
- | `users` | `../models/User` / `User` |
841
- | `blog_posts` | `../models/BlogPost` / `BlogPost` |
842
- | `categories` | `../models/Category` / `Category` |
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
- // generated/model-types/users.d.ts
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 "../models/User" {
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, writeFile, rm } from "fs/promises";
7
+ import { mkdir, rm, writeFile } from "fs/promises";
8
8
  import { join } from "path";
9
- import { tmpdir } from "os";
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 dir = join(tmpdir(), "bunny-repl");
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('Bunny REPL ready. Use Model, Schema, db, and your own imports.');
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 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);
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 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");
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: config.typeDeclarationModelsDir,
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
- 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}`);
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: config.typeDeclarationModelsDir,
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> Create a new migration");
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 = 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.1",
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",