@stacksjs/database 0.70.88 → 0.70.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/auth-tables.d.ts +60 -0
  2. package/dist/auth-tables.js +220 -0
  3. package/dist/class-seeder.d.ts +65 -0
  4. package/dist/class-seeder.js +116 -0
  5. package/dist/column.d.ts +17 -0
  6. package/dist/column.js +26 -0
  7. package/dist/custom/audits.d.ts +16 -0
  8. package/dist/custom/audits.js +57 -0
  9. package/dist/custom/errors.d.ts +1 -0
  10. package/dist/custom/errors.js +48 -0
  11. package/dist/custom/index.d.ts +3 -0
  12. package/dist/custom/index.js +3 -0
  13. package/dist/custom/jobs.d.ts +3 -0
  14. package/dist/custom/jobs.js +449 -0
  15. package/dist/database.d.ts +89 -0
  16. package/dist/database.js +178 -0
  17. package/dist/defaults.d.ts +48 -0
  18. package/dist/defaults.js +48 -0
  19. package/dist/driver-config.d.ts +149 -0
  20. package/dist/driver-config.js +144 -0
  21. package/dist/drivers/defaults/index.d.ts +2 -0
  22. package/dist/drivers/defaults/index.js +2 -0
  23. package/dist/drivers/defaults/passwords.d.ts +4 -0
  24. package/dist/drivers/defaults/passwords.js +106 -0
  25. package/dist/drivers/defaults/traits.d.ts +33 -0
  26. package/dist/drivers/defaults/traits.js +1125 -0
  27. package/dist/drivers/dynamodb.d.ts +200 -0
  28. package/dist/drivers/dynamodb.js +607 -0
  29. package/dist/drivers/helpers.d.ts +35 -0
  30. package/dist/drivers/helpers.js +206 -0
  31. package/dist/drivers/index.d.ts +16 -0
  32. package/dist/drivers/index.js +9 -0
  33. package/dist/drivers/mysql.d.ts +7 -0
  34. package/dist/drivers/mysql.js +322 -0
  35. package/dist/drivers/postgres.d.ts +7 -0
  36. package/dist/drivers/postgres.js +411 -0
  37. package/dist/drivers/sqlite.d.ts +20 -0
  38. package/dist/drivers/sqlite.js +397 -0
  39. package/dist/factory.d.ts +41 -0
  40. package/dist/factory.js +51 -0
  41. package/dist/fk-audit.d.ts +101 -0
  42. package/dist/fk-audit.js +181 -0
  43. package/dist/index.d.ts +149 -0
  44. package/dist/index.js +55 -0
  45. package/dist/migration-lock.d.ts +23 -0
  46. package/dist/migration-lock.js +143 -0
  47. package/dist/migrations.d.ts +76 -0
  48. package/dist/migrations.js +549 -0
  49. package/dist/notification-tables.d.ts +20 -0
  50. package/dist/notification-tables.js +54 -0
  51. package/dist/query-logger.d.ts +26 -0
  52. package/dist/query-logger.js +213 -0
  53. package/dist/query-parser.d.ts +4 -0
  54. package/dist/query-parser.js +93 -0
  55. package/dist/rbac-tables.d.ts +17 -0
  56. package/dist/rbac-tables.js +84 -0
  57. package/dist/safe-migrations.d.ts +72 -0
  58. package/dist/safe-migrations.js +59 -0
  59. package/dist/schema.d.ts +4 -0
  60. package/dist/schema.js +10 -0
  61. package/dist/seed-scaffold.d.ts +34 -0
  62. package/dist/seed-scaffold.js +144 -0
  63. package/dist/seeder.d.ts +116 -0
  64. package/dist/seeder.js +363 -0
  65. package/dist/sql-helpers.d.ts +33 -0
  66. package/dist/sql-helpers.js +24 -0
  67. package/dist/table.d.ts +7 -0
  68. package/dist/table.js +26 -0
  69. package/dist/tools/setup.d.ts +1 -0
  70. package/dist/tools/setup.js +6 -0
  71. package/dist/transaction-context.d.ts +52 -0
  72. package/dist/transaction-context.js +62 -0
  73. package/dist/types.d.ts +151 -0
  74. package/dist/types.js +23 -0
  75. package/dist/unique-audit.d.ts +60 -0
  76. package/dist/unique-audit.js +174 -0
  77. package/dist/utils.d.ts +189 -0
  78. package/dist/utils.js +163 -0
  79. package/dist/uuid-columns.d.ts +22 -0
  80. package/dist/uuid-columns.js +68 -0
  81. package/dist/validators.d.ts +26 -0
  82. package/dist/validators.js +122 -0
  83. package/package.json +11 -11
@@ -0,0 +1,60 @@
1
+ import { sqlHelpers } from './sql-helpers';
2
+ /**
3
+ * Defensive ALTER guaranteeing `users.email_verified_at` — the column
4
+ * `verifyEmail()` writes and the `verified` middleware reads, but which
5
+ * no generated users migration ever creates (stacksjs/stacks#1948).
6
+ * Pure builder so tests can assert per-dialect DDL without a live DB.
7
+ */
8
+ export declare function usersEmailVerifiedAtSql(sql: SqlHelpers): string;
9
+ /**
10
+ * Defensive ALTER guaranteeing `users.password_changed_at` — the column
11
+ * `resetPassword()` stamps and the token-validation paths read to bind a
12
+ * token's validity to the user's credential state (stacksjs/stacks#1957,
13
+ * a #1947 follow-up). No generated users migration creates it, so the
14
+ * same pure-builder + try/catch-swallow pattern as
15
+ * {@link usersEmailVerifiedAtSql} guarantees it from both schema paths.
16
+ */
17
+ export declare function usersPasswordChangedAtSql(sql: SqlHelpers): string;
18
+ /**
19
+ * Defensive ALTER guaranteeing `users.two_factor_secret` and
20
+ * `users.two_factor_enabled` — storage/framework/core/auth/src/
21
+ * authenticator.ts's TOTP helpers (generateTwoFactorSecret,
22
+ * verifyTwoFactorCode) have existed since early in the framework's
23
+ * history, but nothing ever created the columns a caller would persist
24
+ * them to, or the `passkeys`/`webauthn_challenges` tables
25
+ * storage/framework/core/auth/src/passkey.ts's WebAuthn helpers
26
+ * unconditionally query against — every passkey/2FA call site was
27
+ * dead code pointed at tables that never existed on any install,
28
+ * `buddy new` included. Same defensive ALTER + swallow pattern as
29
+ * {@link usersEmailVerifiedAtSql}.
30
+ */
31
+ export declare function usersTwoFactorColumnsSql(sql: SqlHelpers): string[];
32
+ /**
33
+ * Defensive ALTER guaranteeing `users.stripe_id` — the `billable`
34
+ * model trait's methods (createStripeCustomer/createOrGetStripeUser,
35
+ * used by `user.checkout(...)`) read and write this column
36
+ * unconditionally, but it's runtime-mixin-only (createBillableMethods
37
+ * in orm/define-model.ts, gated behind `traits.billable`): nothing in
38
+ * migration codegen ever creates the column itself, on any model, with
39
+ * or without the trait enabled. Every Stripe checkout call site was
40
+ * dead code pointed at a column that never existed, `buddy new`
41
+ * included — same shape as the passkeys/two_factor gap above (see
42
+ * stacksjs/status#1 Phase 9).
43
+ */
44
+ export declare function usersStripeIdSql(): string;
45
+ /**
46
+ * Runs every `users` guarantee-column ALTER (email_verified_at,
47
+ * password_changed_at, two_factor_secret, two_factor_enabled,
48
+ * stripe_id), each independently try/catch-swallowed so one
49
+ * already-existing column (or a not-yet-existing `users` table) never
50
+ * skips the others. Exported so `buddy migrate`/`migrate:fresh` can
51
+ * call it a second time after the numbered model migrations run — see
52
+ * the call site in {@link migrateAuthTables} for why a single call
53
+ * isn't enough.
54
+ */
55
+ export declare function ensureUsersAuthColumns(sql: SqlHelpers, options?: { verbose?: boolean }): Promise<void>;
56
+ /**
57
+ * Create all authentication tables
58
+ */
59
+ export declare function migrateAuthTables(options?: { verbose?: boolean }): Promise<{ success: boolean, error?: string }>;
60
+ declare type SqlHelpers = ReturnType<typeof sqlHelpers>;
@@ -0,0 +1,220 @@
1
+ import process from "node:process";
2
+ import { randomBytes } from "node:crypto";
3
+ import { log } from "@stacksjs/logging";
4
+ import { env as envVars } from "@stacksjs/env";
5
+ import { db } from "./utils";
6
+ import { sqlHelpers } from "./sql-helpers";
7
+ function getDbDriver() {
8
+ return envVars.DB_CONNECTION || "sqlite";
9
+ }
10
+ export function usersEmailVerifiedAtSql(sql) {
11
+ return `ALTER TABLE users ADD COLUMN email_verified_at ${sql.nullableTimestamp}`;
12
+ }
13
+ export function usersPasswordChangedAtSql(sql) {
14
+ return `ALTER TABLE users ADD COLUMN password_changed_at ${sql.nullableTimestamp}`;
15
+ }
16
+ export function usersTwoFactorColumnsSql(sql) {
17
+ return [
18
+ "ALTER TABLE users ADD COLUMN two_factor_secret VARCHAR(255)",
19
+ `ALTER TABLE users ADD COLUMN two_factor_enabled BOOLEAN NOT NULL DEFAULT ${sql.boolFalse}`,
20
+ "ALTER TABLE users ADD COLUMN two_factor_last_used_step BIGINT"
21
+ ];
22
+ }
23
+ export function usersStripeIdSql() {
24
+ return "ALTER TABLE users ADD COLUMN stripe_id VARCHAR(255)";
25
+ }
26
+ export async function ensureUsersAuthColumns(sql, options = {}) {
27
+ const alters = [
28
+ usersEmailVerifiedAtSql(sql),
29
+ usersPasswordChangedAtSql(sql),
30
+ ...usersTwoFactorColumnsSql(sql),
31
+ usersStripeIdSql()
32
+ ];
33
+ for (const alterSql of alters)
34
+ try {
35
+ await db.unsafe(alterSql).execute();
36
+ } catch {
37
+ if (options.verbose)
38
+ log.debug(`[auth-tables] Skipped (already applied or users missing): ${alterSql}`);
39
+ }
40
+ try {
41
+ await db.unsafe("CREATE UNIQUE INDEX IF NOT EXISTS idx_users_stripe_id ON users(stripe_id)").execute();
42
+ } catch {
43
+ if (options.verbose)
44
+ log.debug("[auth-tables] Skipped users.stripe_id unique index (already applied or users missing)");
45
+ }
46
+ }
47
+ export async function migrateAuthTables(options = {}) {
48
+ const dbDriver = getDbDriver(), sql = sqlHelpers(dbDriver), { isPostgres, boolTrue, now, pkColumn, nullableTimestamp } = sql;
49
+ if (options.verbose)
50
+ log.info(`Creating auth tables for ${dbDriver}...`);
51
+ try {
52
+ if (options.verbose)
53
+ log.info("Creating oauth_clients table...");
54
+ await db.unsafe(`
55
+ CREATE TABLE IF NOT EXISTS oauth_clients (
56
+ ${pkColumn},
57
+ name VARCHAR(255) NOT NULL,
58
+ secret VARCHAR(100),
59
+ provider VARCHAR(255),
60
+ redirect VARCHAR(2000) NOT NULL,
61
+ personal_access_client BOOLEAN NOT NULL DEFAULT ${sql.boolFalse},
62
+ password_client BOOLEAN NOT NULL DEFAULT ${sql.boolFalse},
63
+ revoked BOOLEAN NOT NULL DEFAULT ${sql.boolFalse},
64
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
65
+ updated_at ${nullableTimestamp}
66
+ )
67
+ `).execute();
68
+ if (options.verbose)
69
+ log.info("Creating oauth_access_tokens table...");
70
+ await db.unsafe(`
71
+ CREATE TABLE IF NOT EXISTS oauth_access_tokens (
72
+ ${pkColumn},
73
+ user_id INTEGER NOT NULL,
74
+ oauth_client_id INTEGER NOT NULL,
75
+ token TEXT NOT NULL,
76
+ name VARCHAR(255),
77
+ scopes TEXT,
78
+ revoked BOOLEAN NOT NULL DEFAULT ${sql.boolFalse},
79
+ expires_at ${nullableTimestamp},
80
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
81
+ updated_at ${nullableTimestamp}
82
+ )
83
+ `).execute();
84
+ await createTokenIndex("idx_oauth_access_tokens_token", "oauth_access_tokens", "token");
85
+ if (options.verbose)
86
+ log.info("Creating oauth_refresh_tokens table...");
87
+ await db.unsafe(`
88
+ CREATE TABLE IF NOT EXISTS oauth_refresh_tokens (
89
+ ${pkColumn},
90
+ access_token_id INTEGER NOT NULL,
91
+ token TEXT NOT NULL,
92
+ revoked BOOLEAN NOT NULL DEFAULT ${sql.boolFalse},
93
+ expires_at ${nullableTimestamp},
94
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
95
+ )
96
+ `).execute();
97
+ await createTokenIndex("idx_oauth_refresh_tokens_token", "oauth_refresh_tokens", "token");
98
+ if (options.verbose)
99
+ log.info("Creating password_resets table...");
100
+ await db.unsafe(`
101
+ CREATE TABLE IF NOT EXISTS password_resets (
102
+ ${pkColumn},
103
+ email VARCHAR(255) NOT NULL,
104
+ token VARCHAR(255) NOT NULL,
105
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
106
+ )
107
+ `).execute();
108
+ try {
109
+ await db.unsafe(`
110
+ CREATE INDEX IF NOT EXISTS idx_password_resets_email ON password_resets(email)
111
+ `).execute();
112
+ } catch {}
113
+ if (options.verbose)
114
+ log.info("Creating passkeys table...");
115
+ await db.unsafe(`
116
+ CREATE TABLE IF NOT EXISTS passkeys (
117
+ id VARCHAR(255) PRIMARY KEY,
118
+ cred_public_key TEXT NOT NULL,
119
+ user_id INTEGER NOT NULL,
120
+ webauthn_user_id VARCHAR(255) NOT NULL,
121
+ counter INTEGER NOT NULL DEFAULT 0,
122
+ credential_type VARCHAR(50),
123
+ device_type VARCHAR(50),
124
+ backup_eligible BOOLEAN NOT NULL DEFAULT ${sql.boolFalse},
125
+ backup_status BOOLEAN NOT NULL DEFAULT ${sql.boolFalse},
126
+ transports TEXT,
127
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
128
+ last_used_at ${nullableTimestamp}
129
+ )
130
+ `).execute();
131
+ try {
132
+ await db.unsafe(`
133
+ CREATE INDEX IF NOT EXISTS idx_passkeys_user_id ON passkeys(user_id)
134
+ `).execute();
135
+ } catch {}
136
+ if (options.verbose)
137
+ log.info("Creating webauthn_challenges table...");
138
+ await db.unsafe(`
139
+ CREATE TABLE IF NOT EXISTS webauthn_challenges (
140
+ ${pkColumn},
141
+ user_id INTEGER NOT NULL,
142
+ challenge TEXT NOT NULL,
143
+ purpose VARCHAR(20) NOT NULL,
144
+ expires_at TIMESTAMP NOT NULL,
145
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
146
+ )
147
+ `).execute();
148
+ try {
149
+ await db.unsafe(`
150
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_webauthn_challenges_user_purpose ON webauthn_challenges(user_id, purpose)
151
+ `).execute();
152
+ } catch {}
153
+ if (options.verbose)
154
+ log.info("Creating two_factor_challenges table...");
155
+ await db.unsafe(`
156
+ CREATE TABLE IF NOT EXISTS two_factor_challenges (
157
+ id VARCHAR(255) PRIMARY KEY,
158
+ user_id INTEGER NOT NULL,
159
+ expires_at TIMESTAMP NOT NULL,
160
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
161
+ )
162
+ `).execute();
163
+ try {
164
+ await db.unsafe(`
165
+ CREATE INDEX IF NOT EXISTS idx_two_factor_challenges_user_id ON two_factor_challenges(user_id)
166
+ `).execute();
167
+ } catch {}
168
+ if (options.verbose)
169
+ log.info("Creating two_factor_pending_secrets table...");
170
+ await db.unsafe(`
171
+ CREATE TABLE IF NOT EXISTS two_factor_pending_secrets (
172
+ user_id INTEGER PRIMARY KEY,
173
+ secret VARCHAR(255) NOT NULL,
174
+ expires_at TIMESTAMP NOT NULL,
175
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
176
+ )
177
+ `).execute();
178
+ if (options.verbose)
179
+ log.info("Ensuring users auth columns exist (users may not exist yet on a fresh install \u2014 see ensureUsersAuthColumns)...");
180
+ await ensureUsersAuthColumns(sql, options);
181
+ if (options.verbose)
182
+ log.info("Ensuring personal access client exists...");
183
+ if ((await db.unsafe(`
184
+ SELECT id FROM oauth_clients WHERE personal_access_client = ${boolTrue} LIMIT 1
185
+ `).execute())?.length === 0) {
186
+ const secret = randomBytes(40).toString("hex");
187
+ if (isPostgres)
188
+ await db.unsafe(`
189
+ INSERT INTO oauth_clients (name, secret, provider, redirect, personal_access_client, password_client, revoked, created_at)
190
+ VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
191
+ `, ["Personal Access Client", secret, "local", "http://localhost", !0, !1, !1]).execute();
192
+ else
193
+ await db.unsafe(`
194
+ INSERT INTO oauth_clients (name, secret, provider, redirect, personal_access_client, password_client, revoked, created_at)
195
+ VALUES (?, ?, ?, ?, ?, ?, ?, ${now})
196
+ `, ["Personal Access Client", secret, "local", "http://localhost", 1, 0, 0]).execute();
197
+ if (options.verbose)
198
+ log.success("Personal access client created");
199
+ }
200
+ log.debug("Auth tables migrated successfully");
201
+ return { success: !0 };
202
+ } catch (error) {
203
+ const errorMessage = error instanceof Error ? error.message : String(error);
204
+ log.error("Failed to migrate auth tables:", errorMessage);
205
+ return { success: !1, error: errorMessage };
206
+ }
207
+ }
208
+ async function createTokenIndex(indexName, tableName, column) {
209
+ try {
210
+ await db.unsafe(`
211
+ CREATE INDEX IF NOT EXISTS ${indexName} ON ${tableName}(${column}(255))
212
+ `).execute();
213
+ } catch {
214
+ try {
215
+ await db.unsafe(`
216
+ CREATE INDEX IF NOT EXISTS ${indexName} ON ${tableName}(${column})
217
+ `).execute();
218
+ } catch {}
219
+ }
220
+ }
@@ -0,0 +1,65 @@
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
+ }
@@ -0,0 +1,116 @@
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
+ }
@@ -0,0 +1,17 @@
1
+ declare interface Options {
2
+ notNull?: boolean
3
+ default?: any
4
+ primaryKey?: boolean
5
+ autoIncrement?: boolean
6
+ }
7
+ declare type ColumnType = 'integer' | 'varchar' | 'timestamp' | `varchar(${number})`;
8
+ export declare class Column {
9
+ public name: string;
10
+ public type: ColumnType;
11
+ public options?: Options;
12
+ constructor(name: string, type: ColumnType, options?: Options);
13
+ notNullable(): this;
14
+ defaultTo(value: any): this;
15
+ primary(): this;
16
+ autoIncrement(): this;
17
+ }
package/dist/column.js ADDED
@@ -0,0 +1,26 @@
1
+ export class Column {
2
+ name;
3
+ type;
4
+ options;
5
+ constructor(name, type, options = {}) {
6
+ this.name = name;
7
+ this.type = type;
8
+ this.options = options;
9
+ }
10
+ notNullable() {
11
+ this.options.notNull = !0;
12
+ return this;
13
+ }
14
+ defaultTo(value) {
15
+ this.options.default = value;
16
+ return this;
17
+ }
18
+ primary() {
19
+ this.options.primaryKey = !0;
20
+ return this;
21
+ }
22
+ autoIncrement() {
23
+ this.options.autoIncrement = !0;
24
+ return this;
25
+ }
26
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Create the `model_audits` table used by the `useAudit` ORM trait.
3
+ *
4
+ * The trait writes one row per create/update/delete on any model that
5
+ * declares `traits: { useAudit: true }`. The schema is intentionally
6
+ * polymorphic — `auditable_type` + `auditable_id` rather than per-table
7
+ * audit columns — so adding the trait to a new model never requires a
8
+ * follow-up migration.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { createModelAuditsTable } from '@stacksjs/database'
13
+ * await createModelAuditsTable() // idempotent — checks before writing
14
+ * ```
15
+ */
16
+ export declare function createModelAuditsTable(): Promise<void>;
@@ -0,0 +1,57 @@
1
+ import { log } from "@stacksjs/logging";
2
+ import { database } from "@stacksjs/config";
3
+ import { path } from "@stacksjs/path";
4
+ import { hasTableBeenMigrated } from "../drivers/helpers";
5
+ export async function createModelAuditsTable() {
6
+ if (!["sqlite", "mysql"].includes(getDriver()))
7
+ return;
8
+ if (await hasTableBeenMigrated("model_audits"))
9
+ return;
10
+ let migrationContent = `import type { Database } from '@stacksjs/database'
11
+ import { sql } from '@stacksjs/database'
12
+
13
+ `;
14
+ migrationContent += `export async function up(db: Database<any>) {
15
+ `;
16
+ migrationContent += ` await db.schema
17
+ `;
18
+ migrationContent += ` .createTable('model_audits')
19
+ `;
20
+ migrationContent += ` .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())
21
+ `;
22
+ migrationContent += ` .addColumn('auditable_type', 'varchar(255)', col => col.notNull())
23
+ `;
24
+ migrationContent += ` .addColumn('auditable_id', 'varchar(255)', col => col.notNull())
25
+ `;
26
+ migrationContent += ` .addColumn('event', 'varchar(32)', col => col.notNull())
27
+ `;
28
+ migrationContent += ` .addColumn('old_values', 'text')
29
+ `;
30
+ migrationContent += ` .addColumn('new_values', 'text')
31
+ `;
32
+ migrationContent += ` .addColumn('user_id', 'integer')
33
+ `;
34
+ migrationContent += ` .addColumn('created_at', 'timestamp', col => col.notNull().defaultTo(sql.raw('CURRENT_TIMESTAMP')))
35
+ `;
36
+ migrationContent += ` .execute()
37
+
38
+ `;
39
+ migrationContent += ` await db.schema
40
+ `;
41
+ migrationContent += ` .createIndex('idx_model_audits_lookup')
42
+ `;
43
+ migrationContent += ` .on('model_audits')
44
+ `;
45
+ migrationContent += ` .columns(['auditable_type', 'auditable_id', 'created_at'])
46
+ `;
47
+ migrationContent += ` .execute()
48
+ `;
49
+ migrationContent += `}
50
+ `;
51
+ const migrationFileName = `${new Date().getTime().toString()}-create-model-audits-table.ts`, migrationFilePath = path.userMigrationsPath(migrationFileName);
52
+ await Bun.write(migrationFilePath, migrationContent);
53
+ log.success("Created model_audits table migration");
54
+ }
55
+ function getDriver() {
56
+ return database.default || "";
57
+ }
@@ -0,0 +1 @@
1
+ export declare function createErrorsTable(): Promise<void>;
@@ -0,0 +1,48 @@
1
+ import { log } from "@stacksjs/logging";
2
+ import { database } from "@stacksjs/config";
3
+ import { path } from "@stacksjs/path";
4
+ import { hasTableBeenMigrated } from "../drivers/helpers";
5
+ export async function createErrorsTable() {
6
+ if (["sqlite", "mysql"].includes(getDriver())) {
7
+ if (await hasTableBeenMigrated("errors"))
8
+ return;
9
+ let migrationContent = `import type { Database } from '@stacksjs/database'
10
+ import { sql } from '@stacksjs/database'
11
+
12
+ `;
13
+ migrationContent += `export async function up(db: Database<any>) {
14
+ `;
15
+ migrationContent += ` await db.schema
16
+ `;
17
+ migrationContent += ` .createTable('errors')
18
+ `;
19
+ migrationContent += ` .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())
20
+ `;
21
+ migrationContent += ` .addColumn('type', 'varchar(255)', col => col.notNull())
22
+ `;
23
+ migrationContent += ` .addColumn('message', 'text', col => col.notNull())
24
+ `;
25
+ migrationContent += ` .addColumn('stack', 'text')
26
+ `;
27
+ migrationContent += ` .addColumn('status', 'integer', col => col.notNull().defaultTo(0))
28
+ `;
29
+ migrationContent += ` .addColumn('user_id', 'integer')
30
+ `;
31
+ migrationContent += ` .addColumn('additional_info', 'text')
32
+ `;
33
+ migrationContent += ` .addColumn('created_at', 'timestamp', col => col.notNull().defaultTo(sql.raw('CURRENT_TIMESTAMP')))
34
+ `;
35
+ migrationContent += ` .addColumn('updated_at', 'timestamp')
36
+ `;
37
+ migrationContent += ` .execute()
38
+ `;
39
+ migrationContent += `}
40
+ `;
41
+ const migrationFileName = `${new Date().getTime().toString()}-create-errors-table.ts`, migrationFilePath = path.userMigrationsPath(migrationFileName);
42
+ await Bun.write(migrationFilePath, migrationContent);
43
+ log.success("Created errors table");
44
+ }
45
+ }
46
+ function getDriver() {
47
+ return database.default || "";
48
+ }
@@ -0,0 +1,3 @@
1
+ export * from './audits';
2
+ export * from './errors';
3
+ export * from './jobs';
@@ -0,0 +1,3 @@
1
+ export * from "./audits";
2
+ export * from "./errors";
3
+ export * from "./jobs";
@@ -0,0 +1,3 @@
1
+ import type { MigrationResult } from '../migrations';
2
+ import type { Result } from '@stacksjs/error-handling';
3
+ export declare function createJobsMigration(): Promise<Result<MigrationResult[] | string, Error>>;