@stacksjs/database 0.70.163 → 0.70.164

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
@@ -50,7 +50,7 @@ For help, discussion about best practices, or any other conversation that would
50
50
 
51
51
  For casual chit-chat with others using this package:
52
52
 
53
- [Join the Stacks Discord Server](https://discord.gg/stacksjs)
53
+ [Join the Stacks Discord Server](https://stacksjs.com/discord)
54
54
 
55
55
  ## 📄 License
56
56
 
package/dist/index.d.ts CHANGED
@@ -10,8 +10,6 @@ export type {
10
10
  PostgresConfig,
11
11
  SqliteConfig,
12
12
  } from './driver-config';
13
- export type { GenerateOptions } from './factory';
14
- export type { ScaffoldOptions, ScaffoldResult } from './seed-scaffold';
15
13
  export type { DeclaredFK, FkAuditResult, FkOrphan, FkOrphanReport, LiveFK } from './fk-audit';
16
14
  export type { DeclaredUnique, LiveUniqueIndex, UniqueAuditResult } from './unique-audit';
17
15
  export type {
@@ -83,22 +81,10 @@ export * from './types';
83
81
  export * from './migrations';
84
82
  // Query logger DI hook (router calls setQueryTracker on init)
85
83
  export { setQueryTracker, logQuery } from './query-logger';
86
- // Class-based seeders (supplements the model-attribute auto-seeder)
87
- export { Seeder, runClassSeeders } from './class-seeder';
88
84
  // Zero-downtime migration helpers
89
85
  export { addColumnSafely, backfillInBatches, renameColumnSafely } from './safe-migrations';
90
86
  // Seeding
91
87
  export * from './seeder';
92
- // stacksjs/stacks#1919 — public factory API. The canonical replacement
93
- // for the legacy `useSeeder` trait + auto-walker. Class seeders call
94
- // `factory.generate(Model, opts)` explicitly so there's one
95
- // orchestration layer per table, no double-fire on tables that have
96
- // both a `useSeeder` trait and a class seeder file.
97
- export { factory, generate as factoryGenerate } from './factory';
98
- // `buddy seed:scaffold` codemod — generates class-seeder files for
99
- // every model with a `useSeeder` trait, easing the migration off the
100
- // auto-walker.
101
- export { scaffoldClassSeedersFromModels, renderSeederFile } from './seed-scaffold';
102
88
  // Driver utilities
103
89
  export * from './drivers/index';
104
90
  // Custom migrations (jobs, errors, etc.)
package/dist/index.js CHANGED
@@ -17,11 +17,8 @@ export * from "./utils";
17
17
  export * from "./types";
18
18
  export * from "./migrations";
19
19
  export { setQueryTracker, logQuery } from "./query-logger";
20
- export { Seeder, runClassSeeders } from "./class-seeder";
21
20
  export { addColumnSafely, backfillInBatches, renameColumnSafely } from "./safe-migrations";
22
21
  export * from "./seeder";
23
- export { factory, generate as factoryGenerate } from "./factory";
24
- export { scaffoldClassSeedersFromModels, renderSeederFile } from "./seed-scaffold";
25
22
  export * from "./drivers";
26
23
  export * from "./custom";
27
24
  export * from "./auth-tables";
@@ -297,7 +297,7 @@ async function countAppliedMigrations() {
297
297
  }
298
298
  async function writeMigrateMarker(appliedCount) {
299
299
  try {
300
- const fs = await import("node:fs/promises"), dir = path.projectPath(".stacks");
300
+ const fs = await import("node:fs/promises"), dir = path.frameworkRuntimePath();
301
301
  await fs.mkdir(dir, { recursive: !0 });
302
302
  const file = `${dir}/last-migrate-result.json`, body = JSON.stringify({
303
303
  appliedCount,
package/dist/seeder.d.ts CHANGED
@@ -6,24 +6,14 @@ import type { Attribute, Model } from '@stacksjs/types';
6
6
  */
7
7
  export declare function isProtectedModel(name: string): boolean;
8
8
  /**
9
- * Direct entry point for `factory.generate(Model, opts)` — exported
10
- * under a distinct name so the new public API in `factory.ts` can call
11
- * into the same insert path the legacy walker uses without leaking the
12
- * `SeederModel` type. See stacksjs/stacks#1919.
13
- */
14
- export declare function seedModelDirect(model: SeederModel, options: SeederConfig): Promise<SeedResult>;
15
- /**
16
- * Main seeding function
17
- * Seeds the database using model factory functions
18
- * Loads models from both framework defaults and user-defined models,
19
- * with user models taking precedence.
9
+ * Seeds the database from your models.
10
+ *
11
+ * Walks every model that declares a `useSeeder` trait and fills its table
12
+ * using the per-attribute `factory: faker => …` declarations. Models are
13
+ * loaded from `app/Models/` and, with `includeDefaults`, the framework's
14
+ * built-in models too - user models win on a name collision.
20
15
  *
21
- * @deprecated stacksjs/stacks#1919 the model auto-walker is no
22
- * longer invoked by `./buddy seed`. Migrate each `useSeeder` trait to
23
- * a class seeder via `./buddy seed:scaffold`, then call
24
- * `factory.generate(Model, opts)` from inside each seeder. This
25
- * function remains exported for programmatic back-compat but is
26
- * scheduled for removal.
16
+ * This is what `./buddy seed` runs.
27
17
  */
28
18
  export declare function seed(config?: SeederConfig): Promise<SeedSummary>;
29
19
  /**
package/dist/seeder.js CHANGED
@@ -148,9 +148,6 @@ async function generateRecords(model, verbose = !1) {
148
148
  }
149
149
  return records;
150
150
  }
151
- export function seedModelDirect(model, options) {
152
- return seedModel(model, options);
153
- }
154
151
  async function seedModel(model, options) {
155
152
  const startTime = Date.now();
156
153
  try {
@@ -259,7 +256,6 @@ export async function seed(config = {}) {
259
256
  duration: Date.now() - startTime
260
257
  };
261
258
  }
262
- log.warn(`[seed] The \`useSeeder\` trait + auto-walker is deprecated (stacksjs/stacks#1919, #1929). Run \`./buddy seed:scaffold\` to generate a class seeder per \`useSeeder\` model AND strip the trait from the model in one pass. The walker + trait are scheduled for removal in the next major. Affected: ${models.map((m) => m.name).join(", ")}`);
263
259
  if (config.only && config.only.length > 0)
264
260
  models = models.filter((m) => config.only.includes(m.name));
265
261
  if (config.except && config.except.length > 0)
package/dist/types.js CHANGED
@@ -54,7 +54,7 @@ const SAFE_FILTER_OPERATORS = new Set([
54
54
  "is",
55
55
  "is not"
56
56
  ]);
57
- function inlineSqlLiteral(_value) {
57
+ function inlineSqlLiteral(value) {
58
58
  if (value === null || value === void 0)
59
59
  return "NULL";
60
60
  if (typeof value === "number") {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/database",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.163",
5
+ "version": "0.70.164",
6
6
  "description": "The Stacks database integration.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -60,15 +60,15 @@
60
60
  "dynamodb-tooling": "^0.3.2"
61
61
  },
62
62
  "devDependencies": {
63
- "@stacksjs/cli": "0.70.163",
64
- "@stacksjs/config": "0.70.163",
65
- "@stacksjs/logging": "0.70.163",
66
- "@stacksjs/router": "0.70.163",
63
+ "@stacksjs/cli": "0.70.164",
64
+ "@stacksjs/config": "0.70.164",
65
+ "@stacksjs/logging": "0.70.164",
66
+ "@stacksjs/router": "0.70.164",
67
67
  "better-dx": "^0.2.17",
68
- "@stacksjs/path": "0.70.163",
69
- "@stacksjs/query-builder": "0.70.163",
70
- "@stacksjs/storage": "0.70.163",
71
- "@stacksjs/strings": "0.70.163",
72
- "@stacksjs/utils": "0.70.163"
68
+ "@stacksjs/path": "0.70.164",
69
+ "@stacksjs/query-builder": "0.70.164",
70
+ "@stacksjs/storage": "0.70.164",
71
+ "@stacksjs/strings": "0.70.164",
72
+ "@stacksjs/utils": "0.70.164"
73
73
  }
74
74
  }
@@ -1,65 +0,0 @@
1
- /**
2
- * Topologically sort seeders by their declared `dependencies`. Ties
3
- * (and dependency-free seeders) come out in alphabetical order so the
4
- * result is deterministic across filesystems and runs.
5
- *
6
- * Unknown dependency names are dropped from the graph with a warning —
7
- * they may refer to model-factory seeders that ran earlier in the
8
- * `buddy seed` pipeline, or be stale references from a rename. The run
9
- * doesn't fail because of them.
10
- *
11
- * Cycles throw with the offending class names in the error message.
12
- *
13
- * Exported for testing.
14
- */
15
- export declare function topoSortSeeders(seeders: Array<{ name: string, dependencies?: string[] }>): string[];
16
- /*.ts`; an explicit `--class` filters to one.
17
- *
18
- * Ordering:
19
- * 1. Files matching `*.ts` (excluding `_*.ts`) are imported.
20
- * 2. If any seeder declares `dependencies`, the runnable set is
21
- * topologically sorted (alphabetical tie-break).
22
- * 3. Otherwise, the alphabetical order from `Array.sort()` wins —
23
- * cheaper than the topo path and predictable across filesystems.
24
- *
25
- * Class-name filtering via `options.class` short-circuits both paths
26
- * and runs only that one seeder. Cross-seeder dependencies are NOT
27
- * resolved transitively in that mode — the caller takes responsibility
28
- * for whatever prereqs are needed.
29
- *
30
- * See stacksjs/stacks#1855 for the original report of unsorted FS
31
- * iteration producing zero-row seed runs.
32
- */
33
- export declare function runClassSeeders(options?: RunOptions): Promise<{ ran: string[], skipped: string[] }>;
34
- declare interface RunOptions {
35
- class?: string
36
- dir?: string
37
- }
38
- /**
39
- * Base class for class-based seeders. Subclass this and implement
40
- * `async run()`. Seeders may call `this.call()` to invoke other
41
- * seeders, mirroring Laravel's nested-seeder pattern.
42
- *
43
- * Cross-seeder ordering can be declared explicitly via `dependencies`:
44
- *
45
- * ```ts
46
- * export default class JudgeSeeder extends Seeder {
47
- * dependencies = ['CourtHouseSeeder']
48
- * async run() { ... }
49
- * }
50
- * ```
51
- *
52
- * The class name of each dependency is matched against the class names
53
- * `runClassSeeders` discovered in the seeders directory. Unknown
54
- * dependency names are warned about but don't fail the run (they may
55
- * refer to a model-factory seeder run earlier in `buddy seed`).
56
- *
57
- * When no `dependencies` are declared, seeders run in alphabetical
58
- * order — predictable across filesystems and good enough for projects
59
- * that name seeders by data flow (CourtHouse → Judge → Review).
60
- */
61
- export declare abstract class Seeder {
62
- dependencies?: string[];
63
- abstract run(): Promise<void> | void;
64
- protected call(other: new () => Seeder): Promise<void>;
65
- }
@@ -1,116 +0,0 @@
1
- import { log } from "@stacksjs/logging";
2
- import { path } from "@stacksjs/path";
3
- import { fs } from "@stacksjs/storage";
4
-
5
- export class Seeder {
6
- dependencies;
7
- async call(other) {
8
- await new other().run();
9
- }
10
- }
11
- export function topoSortSeeders(seeders) {
12
- const known = new Set(seeders.map((s) => s.name)), effectiveDeps = new Map;
13
- for (const s of seeders) {
14
- const deps = new Set;
15
- for (const dep of s.dependencies ?? []) {
16
- if (dep === s.name)
17
- continue;
18
- if (!known.has(dep)) {
19
- log.warn(`[seeder] ${s.name} depends on '${dep}' but no seeder by that name was discovered \u2014 ignoring`);
20
- continue;
21
- }
22
- deps.add(dep);
23
- }
24
- effectiveDeps.set(s.name, deps);
25
- }
26
- const indegree = new Map, successors = new Map;
27
- for (const s of seeders) {
28
- indegree.set(s.name, effectiveDeps.get(s.name).size);
29
- successors.set(s.name, new Set);
30
- }
31
- for (const s of seeders)
32
- for (const dep of effectiveDeps.get(s.name))
33
- successors.get(dep).add(s.name);
34
- const ready = seeders.filter((s) => indegree.get(s.name) === 0).map((s) => s.name).sort(), result = [];
35
- while (ready.length > 0) {
36
- const next = ready.shift();
37
- result.push(next);
38
- const newlyReady = [];
39
- for (const succ of successors.get(next)) {
40
- const left = (indegree.get(succ) ?? 0) - 1;
41
- indegree.set(succ, left);
42
- if (left === 0)
43
- newlyReady.push(succ);
44
- }
45
- if (newlyReady.length > 0) {
46
- ready.push(...newlyReady);
47
- ready.sort();
48
- }
49
- }
50
- if (result.length !== seeders.length) {
51
- const unresolved = seeders.map((s) => s.name).filter((n) => !result.includes(n));
52
- throw Error(`[seeder] Cycle in seeder \`dependencies\` among: ${unresolved.join(", ")}`);
53
- }
54
- return result;
55
- }
56
- export async function runClassSeeders(options = {}) {
57
- try {
58
- const { injectGlobalAutoImports } = await import("@stacksjs/server");
59
- await injectGlobalAutoImports();
60
- } catch {}
61
- const dir = options.dir ?? path.projectPath("database/seeders"), ran = [], skipped = [];
62
- if (!fs.existsSync(dir)) {
63
- log.info(`[seeder] No class seeders directory at ${dir}`);
64
- return { ran, skipped };
65
- }
66
- const files = fs.readdirSync(dir).filter((f) => f.endsWith(".ts") && !f.startsWith("_")).sort(), loaded = [];
67
- for (const file of files) {
68
- const className = file.replace(/\.ts$/, "");
69
- try {
70
- const mod = await import(`${dir}/${file}`), Klass = mod.default ?? mod[className];
71
- if (!Klass) {
72
- log.warn(`[seeder] ${file} has no default export`);
73
- skipped.push(className);
74
- continue;
75
- }
76
- const inst = new Klass;
77
- if (typeof inst.run !== "function") {
78
- log.warn(`[seeder] ${className} does not implement run()`);
79
- skipped.push(className);
80
- continue;
81
- }
82
- loaded.push({ className, inst });
83
- } catch (err) {
84
- log.error(`[seeder] ${className} failed to load:`, err);
85
- skipped.push(className);
86
- }
87
- }
88
- const declaresDeps = loaded.some((l) => (l.inst.dependencies?.length ?? 0) > 0);
89
- let order;
90
- if (declaresDeps)
91
- try {
92
- order = topoSortSeeders(loaded.map((l) => ({ name: l.className, dependencies: l.inst.dependencies })));
93
- } catch (err) {
94
- log.error(err instanceof Error ? err.message : String(err));
95
- return { ran, skipped: [...skipped, ...loaded.map((l) => l.className)] };
96
- }
97
- else
98
- order = loaded.map((l) => l.className);
99
- const byName = new Map(loaded.map((l) => [l.className, l.inst]));
100
- for (const className of order) {
101
- if (options.class && className !== options.class) {
102
- skipped.push(className);
103
- continue;
104
- }
105
- const inst = byName.get(className);
106
- try {
107
- log.info(`[seeder] Running ${className}\u2026`);
108
- await inst.run();
109
- ran.push(className);
110
- } catch (err) {
111
- log.error(`[seeder] ${className} failed:`, err);
112
- skipped.push(className);
113
- }
114
- }
115
- return { ran, skipped };
116
- }
package/dist/factory.d.ts DELETED
@@ -1,41 +0,0 @@
1
- import type { Attribute, Model } from '@stacksjs/types';
2
- import type { SeedResult } from './seeder';
3
- /**
4
- * Build the internal `SeederModel`-shaped payload that `seedModelDirect`
5
- * expects from a public-API call. Pure function — exported separately
6
- * so tests can assert the override-precedence rules without touching
7
- * the database.
8
- *
9
- * Precedence (lowest → highest): per-attribute `factory` output →
10
- * global `options.with` → per-row `options.rows[i]`. All keys are
11
- * snake-cased before insert so callers can use the model's camelCase
12
- * attribute names without thinking about column naming.
13
- */
14
- export declare function buildSeederPayload(modelInput: unknown, options?: GenerateOptions): {
15
- name: string
16
- table: string
17
- count: number
18
- fixtures: Array<Record<string, unknown>>
19
- attributes: Record<string, Attribute>
20
- model: Model
21
- };
22
- /**
23
- * Generate factory rows for a model and insert them. Designed to be
24
- * called from a class seeder.
25
- *
26
- * Honours the model's per-attribute `factory: faker => …` declarations
27
- * — exactly the same code path as the legacy auto-walker — but without
28
- * the implicit "every model with `useSeeder` fires on every run"
29
- * coupling. See stacksjs/stacks#1919 for the rationale.
30
- */
31
- export declare function generate(modelInput: unknown, options?: GenerateOptions): Promise<SeedResult>;
32
- export declare const factory: {
33
- generate: typeof generate
34
- };
35
- export declare interface GenerateOptions {
36
- count?: number
37
- fresh?: boolean
38
- verbose?: boolean
39
- with?: Record<string, unknown>
40
- rows?: Array<Record<string, unknown>>
41
- }
package/dist/factory.js DELETED
@@ -1,51 +0,0 @@
1
- import { log } from "@stacksjs/logging";
2
- import { seedModelDirect } from "./seeder";
3
- function resolveDefinition(input) {
4
- if (input && typeof input === "object") {
5
- const obj = input;
6
- if (obj._definition && typeof obj._definition === "object" && "name" in obj._definition)
7
- return obj._definition;
8
- if ("name" in obj && (("attributes" in obj) || ("table" in obj) || ("traits" in obj)))
9
- return obj;
10
- }
11
- throw Error("factory.generate: expected a Stacks model (the default export of app/Models/*.ts, or a defineModel() return value). Got something without a `.name` field.");
12
- }
13
- function snakeCase(str) {
14
- return str.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z\d])([A-Z])/g, "$1_$2").replace(/(\d)([A-Za-z])/g, "$1_$2").toLowerCase();
15
- }
16
- function snakeCaseKeys(input) {
17
- const out = {};
18
- for (const [key, value] of Object.entries(input))
19
- out[snakeCase(key)] = value;
20
- return out;
21
- }
22
- export function buildSeederPayload(modelInput, options = {}) {
23
- const def = resolveDefinition(modelInput), name = def.name, attributes = def.attributes ?? {}, useSeederConfig = def.traits?.useSeeder, seederDefault = useSeederConfig && typeof useSeederConfig === "object" ? useSeederConfig : void 0, count = options.count ?? seederDefault?.count ?? 10, globalOverrides = options.with ? snakeCaseKeys(options.with) : void 0, perRow = options.rows ?? seederDefault?.fixtures ?? [], fixtureCount = Math.max(count, perRow.length), fixtures = [];
24
- for (let i = 0;i < fixtureCount; i++) {
25
- const row = perRow[i];
26
- if (!globalOverrides && !row)
27
- continue;
28
- fixtures[i] = { ...globalOverrides ?? {}, ...row ? snakeCaseKeys(row) : {} };
29
- }
30
- const table = def.table ?? `${snakeCase(name)}s`;
31
- return { name, table, count: fixtureCount, fixtures, attributes, model: def };
32
- }
33
- export async function generate(modelInput, options = {}) {
34
- const payload = buildSeederPayload(modelInput, options);
35
- try {
36
- return await seedModelDirect({
37
- ...payload,
38
- filePath: ""
39
- }, {
40
- fresh: options.fresh,
41
- verbose: options.verbose ?? !1
42
- });
43
- } catch (err) {
44
- const message = err instanceof Error ? err.message : String(err);
45
- log.error(`[factory] generate(${payload.name}) failed: ${message}`);
46
- throw err;
47
- }
48
- }
49
- export const factory = {
50
- generate
51
- };
@@ -1,34 +0,0 @@
1
- /**
2
- * Remove a single `useSeeder` / `seedable` object-property from model
3
- * source text (stacksjs/stacks#1929). Brace-aware (balances nested
4
- * `{}` and skips string literals) and conservative: only strips the
5
- * documented value shapes (`true`, `false`, or a `{ … }` object). For
6
- * anything else (an identifier, a function call, a spread) it returns
7
- * `changed: false` so the caller can flag it for manual cleanup
8
- * instead of risking a mangled file.
9
- *
10
- * Exported for unit tests.
11
- */
12
- export declare function stripUseSeederTrait(source: string): { source: string, changed: boolean, skipped: boolean };
13
- /**
14
- * Walk the configured models directory, find every model whose
15
- * `traits.useSeeder` is truthy, and write a class-seeder file for it.
16
- * Returns a structured report so the CLI command can render a summary
17
- * without re-parsing log lines.
18
- */
19
- export declare function scaffoldClassSeedersFromModels(options?: ScaffoldOptions): Promise<ScaffoldResult>;
20
- /** Pure renderer — exported for unit tests. */
21
- export declare function renderSeederFile(modelName: string, modelImportPath: string, count: number): string;
22
- export declare interface ScaffoldOptions {
23
- modelsDir?: string
24
- seedersDir?: string
25
- force?: boolean
26
- dryRun?: boolean
27
- }
28
- export declare interface ScaffoldResult {
29
- generated: Array<{ model: string, file: string }>
30
- skipped: Array<{ model: string, file: string, reason: 'already-exists' | 'no-useseeder' }>
31
- errors: Array<{ model: string, error: string }>
32
- strippedTrait: Array<{ model: string, file: string }>
33
- traitStripSkipped: Array<{ model: string, file: string }>
34
- }
@@ -1,144 +0,0 @@
1
- import { log } from "@stacksjs/logging";
2
- import { path } from "@stacksjs/path";
3
- import { fs } from "@stacksjs/storage";
4
- export function stripUseSeederTrait(source) {
5
- let out = source, changed = !1, skipped = !1;
6
- for (const name of ["useSeeder", "seedable"]) {
7
- const m = new RegExp(`\\b${name}\\s*:`).exec(out);
8
- if (!m)
9
- continue;
10
- const keyStart = m.index;
11
- let i = keyStart + m[0].length;
12
- while (i < out.length && /\s/.test(out[i]))
13
- i++;
14
- if (out[i] === "{") {
15
- let depth = 0, inStr = null;
16
- for (;i < out.length; i++) {
17
- const ch = out[i];
18
- if (inStr) {
19
- if (ch === "\\") {
20
- i++;
21
- continue;
22
- }
23
- if (ch === inStr)
24
- inStr = null;
25
- continue;
26
- }
27
- if (ch === '"' || ch === "'" || ch === "`") {
28
- inStr = ch;
29
- continue;
30
- }
31
- if (ch === "{")
32
- depth++;
33
- else if (ch === "}") {
34
- depth--;
35
- if (depth === 0) {
36
- i++;
37
- break;
38
- }
39
- }
40
- }
41
- } else if (out.startsWith("true", i) || out.startsWith("false", i))
42
- i += out.startsWith("true", i) ? 4 : 5;
43
- else {
44
- skipped = !0;
45
- continue;
46
- }
47
- let end = i;
48
- while (end < out.length && (out[end] === " " || out[end] === "\t"))
49
- end++;
50
- if (out[end] === ",")
51
- end++;
52
- while (end < out.length && (out[end] === " " || out[end] === "\t"))
53
- end++;
54
- if (out[end] === "/" && out[end + 1] === "/")
55
- while (end < out.length && out[end] !== `
56
- `)
57
- end++;
58
- let start = keyStart;
59
- while (start > 0 && (out[start - 1] === " " || out[start - 1] === "\t"))
60
- start--;
61
- if (start > 0 && out[start - 1] === `
62
- ` && out[end] === `
63
- `)
64
- end++;
65
- out = out.slice(0, start) + out.slice(end);
66
- changed = !0;
67
- }
68
- return { source: out, changed, skipped: skipped && !changed };
69
- }
70
- const SEEDER_TEMPLATE = (modelName, modelImportPath, count) => `import { factory, Seeder } from '@stacksjs/database'
71
- import ${modelName} from '${modelImportPath}'
72
-
73
- export default class ${modelName}Seeder extends Seeder {
74
- async run(): Promise<void> {
75
- await factory.generate(${modelName}, { count: ${count} })
76
- }
77
- }
78
- `;
79
- function relativeModelImport(seedersDir, modelFilePath) {
80
- const noExt = path.relative(seedersDir, modelFilePath).replace(/\\/g, "/").replace(/\.ts$/, "");
81
- return noExt.startsWith(".") ? noExt : `./${noExt}`;
82
- }
83
- export async function scaffoldClassSeedersFromModels(options = {}) {
84
- const modelsDir = options.modelsDir ?? path.userModelsPath(), seedersDir = options.seedersDir ?? path.projectPath("database/seeders"), result = { generated: [], skipped: [], errors: [], strippedTrait: [], traitStripSkipped: [] };
85
- if (!fs.existsSync(modelsDir)) {
86
- log.warn(`[seed:scaffold] No models directory at ${modelsDir}`);
87
- return result;
88
- }
89
- if (!options.dryRun && !fs.existsSync(seedersDir))
90
- fs.mkdirSync(seedersDir, { recursive: !0 });
91
- const entries = fs.readdirSync(modelsDir, { withFileTypes: !0 });
92
- for (const entry of entries) {
93
- if (!entry.isFile() || !entry.name.endsWith(".ts"))
94
- continue;
95
- if (entry.name.startsWith("_") || entry.name.startsWith("index"))
96
- continue;
97
- const modelFilePath = path.join(modelsDir, entry.name);
98
- let modelDef;
99
- try {
100
- const module = await import(modelFilePath);
101
- modelDef = module.default || module;
102
- } catch (err) {
103
- result.errors.push({ model: entry.name, error: err.message });
104
- continue;
105
- }
106
- if (!modelDef || !modelDef.name) {
107
- result.errors.push({ model: entry.name, error: "missing default export with `name` field" });
108
- continue;
109
- }
110
- const useSeeder = modelDef.traits?.useSeeder ?? modelDef.traits?.seedable;
111
- if (!useSeeder) {
112
- result.skipped.push({ model: modelDef.name, file: "", reason: "no-useseeder" });
113
- continue;
114
- }
115
- const count = typeof useSeeder === "object" && "count" in useSeeder ? useSeeder.count : 10, seederFileName = `${modelDef.name}Seeder.ts`, seederFilePath = path.join(seedersDir, seederFileName);
116
- if (fs.existsSync(seederFilePath) && !options.force)
117
- result.skipped.push({ model: modelDef.name, file: seederFilePath, reason: "already-exists" });
118
- else {
119
- const importPath = relativeModelImport(seedersDir, modelFilePath), content = SEEDER_TEMPLATE(modelDef.name, importPath, count);
120
- if (options.dryRun)
121
- log.info(`[seed:scaffold] would write ${seederFilePath}`);
122
- else
123
- fs.writeFileSync(seederFilePath, content, "utf-8");
124
- result.generated.push({ model: modelDef.name, file: seederFilePath });
125
- }
126
- try {
127
- const modelSource = fs.readFileSync(modelFilePath, "utf-8"), { source: stripped, changed, skipped } = stripUseSeederTrait(modelSource);
128
- if (changed) {
129
- if (options.dryRun)
130
- log.info(`[seed:scaffold] would strip useSeeder trait from ${modelFilePath}`);
131
- else
132
- fs.writeFileSync(modelFilePath, stripped, "utf-8");
133
- result.strippedTrait.push({ model: modelDef.name, file: modelFilePath });
134
- } else if (skipped)
135
- result.traitStripSkipped.push({ model: modelDef.name, file: modelFilePath });
136
- } catch (err) {
137
- result.errors.push({ model: modelDef.name, error: `trait strip failed: ${err.message}` });
138
- }
139
- }
140
- return result;
141
- }
142
- export function renderSeederFile(modelName, modelImportPath, count) {
143
- return SEEDER_TEMPLATE(modelName, modelImportPath, count);
144
- }