@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.
- package/dist/auth-tables.d.ts +60 -0
- package/dist/auth-tables.js +220 -0
- package/dist/class-seeder.d.ts +65 -0
- package/dist/class-seeder.js +116 -0
- package/dist/column.d.ts +17 -0
- package/dist/column.js +26 -0
- package/dist/custom/audits.d.ts +16 -0
- package/dist/custom/audits.js +57 -0
- package/dist/custom/errors.d.ts +1 -0
- package/dist/custom/errors.js +48 -0
- package/dist/custom/index.d.ts +3 -0
- package/dist/custom/index.js +3 -0
- package/dist/custom/jobs.d.ts +3 -0
- package/dist/custom/jobs.js +449 -0
- package/dist/database.d.ts +89 -0
- package/dist/database.js +178 -0
- package/dist/defaults.d.ts +48 -0
- package/dist/defaults.js +48 -0
- package/dist/driver-config.d.ts +149 -0
- package/dist/driver-config.js +144 -0
- package/dist/drivers/defaults/index.d.ts +2 -0
- package/dist/drivers/defaults/index.js +2 -0
- package/dist/drivers/defaults/passwords.d.ts +4 -0
- package/dist/drivers/defaults/passwords.js +106 -0
- package/dist/drivers/defaults/traits.d.ts +33 -0
- package/dist/drivers/defaults/traits.js +1125 -0
- package/dist/drivers/dynamodb.d.ts +200 -0
- package/dist/drivers/dynamodb.js +607 -0
- package/dist/drivers/helpers.d.ts +35 -0
- package/dist/drivers/helpers.js +206 -0
- package/dist/drivers/index.d.ts +16 -0
- package/dist/drivers/index.js +9 -0
- package/dist/drivers/mysql.d.ts +7 -0
- package/dist/drivers/mysql.js +322 -0
- package/dist/drivers/postgres.d.ts +7 -0
- package/dist/drivers/postgres.js +411 -0
- package/dist/drivers/sqlite.d.ts +20 -0
- package/dist/drivers/sqlite.js +397 -0
- package/dist/factory.d.ts +41 -0
- package/dist/factory.js +51 -0
- package/dist/fk-audit.d.ts +101 -0
- package/dist/fk-audit.js +181 -0
- package/dist/index.d.ts +149 -0
- package/dist/index.js +55 -0
- package/dist/migration-lock.d.ts +23 -0
- package/dist/migration-lock.js +143 -0
- package/dist/migrations.d.ts +76 -0
- package/dist/migrations.js +549 -0
- package/dist/notification-tables.d.ts +20 -0
- package/dist/notification-tables.js +54 -0
- package/dist/query-logger.d.ts +26 -0
- package/dist/query-logger.js +213 -0
- package/dist/query-parser.d.ts +4 -0
- package/dist/query-parser.js +93 -0
- package/dist/rbac-tables.d.ts +17 -0
- package/dist/rbac-tables.js +84 -0
- package/dist/safe-migrations.d.ts +72 -0
- package/dist/safe-migrations.js +59 -0
- package/dist/schema.d.ts +4 -0
- package/dist/schema.js +10 -0
- package/dist/seed-scaffold.d.ts +34 -0
- package/dist/seed-scaffold.js +144 -0
- package/dist/seeder.d.ts +116 -0
- package/dist/seeder.js +363 -0
- package/dist/sql-helpers.d.ts +33 -0
- package/dist/sql-helpers.js +24 -0
- package/dist/table.d.ts +7 -0
- package/dist/table.js +26 -0
- package/dist/tools/setup.d.ts +1 -0
- package/dist/tools/setup.js +6 -0
- package/dist/transaction-context.d.ts +52 -0
- package/dist/transaction-context.js +62 -0
- package/dist/types.d.ts +151 -0
- package/dist/types.js +23 -0
- package/dist/unique-audit.d.ts +60 -0
- package/dist/unique-audit.js +174 -0
- package/dist/utils.d.ts +189 -0
- package/dist/utils.js +163 -0
- package/dist/uuid-columns.d.ts +22 -0
- package/dist/uuid-columns.js +68 -0
- package/dist/validators.d.ts +26 -0
- package/dist/validators.js +122 -0
- package/package.json +11 -11
|
@@ -0,0 +1,144 @@
|
|
|
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
|
+
}
|
package/dist/seeder.d.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Attribute, Model } from '@stacksjs/types';
|
|
2
|
+
/**
|
|
3
|
+
* Test whether a model name is on the protected list.
|
|
4
|
+
* Exported for downstream tooling (CI lint rules, custom seeders) so the
|
|
5
|
+
* list stays a single source of truth.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isProtectedModel(name: string): boolean;
|
|
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.
|
|
20
|
+
*
|
|
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.
|
|
27
|
+
*/
|
|
28
|
+
export declare function seed(config?: SeederConfig): Promise<SeedSummary>;
|
|
29
|
+
/**
|
|
30
|
+
* Seed a specific model by name
|
|
31
|
+
* Searches both default and user models
|
|
32
|
+
*/
|
|
33
|
+
export declare function seedModel$(modelName: string, options?: { count?: number, fresh?: boolean, verbose?: boolean }): Promise<SeedResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Fresh seed - truncate all tables and reseed
|
|
36
|
+
*/
|
|
37
|
+
export declare function freshSeed(config?: SeederConfig): Promise<SeedSummary>;
|
|
38
|
+
/**
|
|
39
|
+
* Get list of seedable models without seeding
|
|
40
|
+
* Returns models from both default and user directories
|
|
41
|
+
*/
|
|
42
|
+
export declare function listSeedableModels(): Promise<Array<{ name: string, table: string, count: number, source: 'default' | 'user' }>>;
|
|
43
|
+
/**
|
|
44
|
+
* Models that touch live auth state and are unsafe to auto-seed on an
|
|
45
|
+
* already-populated database (stacksjs/stacks#1852).
|
|
46
|
+
*
|
|
47
|
+
* The motivating incident: a userland `app/Models/OauthClient.ts` shipped
|
|
48
|
+
* with the default `useSeeder: { count: 10 }` trait. Every `./buddy seed`
|
|
49
|
+
* re-rolled the `oauth_clients` table — including the row at id=1, the
|
|
50
|
+
* Personal Access Client whose `secret` is part of the encryption key
|
|
51
|
+
* used to derive each issued access token's `encryptedId`. With the
|
|
52
|
+
* secret rotated, every previously-issued token failed validation at
|
|
53
|
+
* `decrypt(encryptedId, clientSecret)`, surfacing as a generic
|
|
54
|
+
* "Unauthorized. Invalid token." 401 with no log line indicating what
|
|
55
|
+
* actually happened.
|
|
56
|
+
*
|
|
57
|
+
* Models on this list are skipped by default. They are seeded when:
|
|
58
|
+
*
|
|
59
|
+
* - `fresh: true` is passed (the seeder truncates first; live tokens
|
|
60
|
+
* are gone anyway, so re-rolling the PAC secret is harmless), OR
|
|
61
|
+
* - `allowProtected: true` is passed (explicit opt-in escape hatch
|
|
62
|
+
* surfaced as `./buddy seed --allow-protected`).
|
|
63
|
+
*
|
|
64
|
+
* The list is conservative: any model whose rows participate in token
|
|
65
|
+
* issuance / validation / refresh belongs here.
|
|
66
|
+
*/
|
|
67
|
+
export declare const PROTECTED_MODELS: readonly string[];
|
|
68
|
+
/**
|
|
69
|
+
* Seeder configuration options
|
|
70
|
+
*/
|
|
71
|
+
export declare interface SeederConfig {
|
|
72
|
+
modelsDir?: string
|
|
73
|
+
defaultCount?: number
|
|
74
|
+
verbose?: boolean
|
|
75
|
+
fresh?: boolean
|
|
76
|
+
only?: string[]
|
|
77
|
+
except?: string[]
|
|
78
|
+
includeDefaults?: boolean
|
|
79
|
+
allowProtected?: boolean
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Result of a single model seeding operation
|
|
83
|
+
*/
|
|
84
|
+
export declare interface SeedResult {
|
|
85
|
+
model: string
|
|
86
|
+
table: string
|
|
87
|
+
count: number
|
|
88
|
+
success: boolean
|
|
89
|
+
error?: string
|
|
90
|
+
duration: number
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Result of the entire seeding operation
|
|
94
|
+
*/
|
|
95
|
+
export declare interface SeedSummary {
|
|
96
|
+
total: number
|
|
97
|
+
successful: number
|
|
98
|
+
failed: number
|
|
99
|
+
results: SeedResult[]
|
|
100
|
+
duration: number
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Parsed model with seeding information
|
|
104
|
+
*/
|
|
105
|
+
export declare interface SeederModel {
|
|
106
|
+
name: string
|
|
107
|
+
table: string
|
|
108
|
+
count: number
|
|
109
|
+
fixtures: Array<Record<string, unknown>>
|
|
110
|
+
attributes: Record<string, Attribute>
|
|
111
|
+
model: Model
|
|
112
|
+
filePath: string
|
|
113
|
+
}
|
|
114
|
+
// Legacy exports for backwards compatibility
|
|
115
|
+
export { seed as runSeeders };
|
|
116
|
+
export { freshSeed as freshWithSeed };
|
package/dist/seeder.js
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { log } from "@stacksjs/logging";
|
|
2
|
+
import { db, ensureDatabaseConfigLoaded } from "./utils";
|
|
3
|
+
import { faker } from "@stacksjs/faker";
|
|
4
|
+
import { path } from "@stacksjs/path";
|
|
5
|
+
import { hashMake } from "@stacksjs/security";
|
|
6
|
+
import { fs } from "@stacksjs/storage";
|
|
7
|
+
function defaultModelsPath(subpath) {
|
|
8
|
+
return path.frameworkPath(`defaults/app/Models/${subpath || ""}`);
|
|
9
|
+
}
|
|
10
|
+
export const PROTECTED_MODELS = Object.freeze([
|
|
11
|
+
"OauthClient",
|
|
12
|
+
"OauthAccessToken",
|
|
13
|
+
"OauthRefreshToken",
|
|
14
|
+
"PersonalAccessToken"
|
|
15
|
+
]);
|
|
16
|
+
export function isProtectedModel(name) {
|
|
17
|
+
return PROTECTED_MODELS.includes(name);
|
|
18
|
+
}
|
|
19
|
+
function snakeCase(str) {
|
|
20
|
+
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();
|
|
21
|
+
}
|
|
22
|
+
async function loadModelsFromDir(modelsDir, recursive = !1) {
|
|
23
|
+
const models = [];
|
|
24
|
+
if (!fs.existsSync(modelsDir))
|
|
25
|
+
return models;
|
|
26
|
+
const entries = fs.readdirSync(modelsDir, { withFileTypes: !0 });
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
const fullPath = path.join(modelsDir, entry.name);
|
|
29
|
+
if (entry.isDirectory() && recursive) {
|
|
30
|
+
const subModels = await loadModelsFromDir(fullPath, !0);
|
|
31
|
+
models.push(...subModels);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (!entry.name.endsWith(".ts") || entry.name.startsWith("index") || entry.name.startsWith("_"))
|
|
35
|
+
continue;
|
|
36
|
+
try {
|
|
37
|
+
const module = await import(fullPath), modelDef = module.default || module;
|
|
38
|
+
if (!modelDef)
|
|
39
|
+
continue;
|
|
40
|
+
const useSeeder = modelDef.traits?.useSeeder ?? modelDef.traits?.seedable;
|
|
41
|
+
if (!useSeeder)
|
|
42
|
+
continue;
|
|
43
|
+
let count = 10, fixtures = [];
|
|
44
|
+
if (typeof useSeeder === "object" && "count" in useSeeder) {
|
|
45
|
+
const opts = useSeeder;
|
|
46
|
+
count = opts.count;
|
|
47
|
+
fixtures = opts.fixtures ?? [];
|
|
48
|
+
}
|
|
49
|
+
const modelName = modelDef.name || entry.name.replace(".ts", ""), tableName = modelDef.table || snakeCase(modelName) + "s";
|
|
50
|
+
models.push({
|
|
51
|
+
name: modelName,
|
|
52
|
+
table: tableName,
|
|
53
|
+
count: Math.max(count, fixtures.length),
|
|
54
|
+
fixtures,
|
|
55
|
+
attributes: modelDef.attributes || {},
|
|
56
|
+
model: modelDef,
|
|
57
|
+
filePath: fullPath
|
|
58
|
+
});
|
|
59
|
+
} catch (err) {
|
|
60
|
+
log.error(`Failed to load model ${entry.name}:`, err);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return models;
|
|
64
|
+
}
|
|
65
|
+
async function loadAllModels(userModelsDir, verbose = !1, includeDefaults = !1) {
|
|
66
|
+
const defaultDir = defaultModelsPath(), userModels = await loadModelsFromDir(userModelsDir, !1);
|
|
67
|
+
if (userModels.length > 0 && !includeDefaults)
|
|
68
|
+
return userModels;
|
|
69
|
+
const defaultModels = await loadModelsFromDir(defaultDir, !0), modelMap = new Map;
|
|
70
|
+
for (const model of defaultModels)
|
|
71
|
+
modelMap.set(model.name, model);
|
|
72
|
+
for (const model of userModels) {
|
|
73
|
+
if (modelMap.has(model.name) && verbose)
|
|
74
|
+
log.info(` User model "${model.name}" overrides default`);
|
|
75
|
+
modelMap.set(model.name, model);
|
|
76
|
+
}
|
|
77
|
+
return Array.from(modelMap.values());
|
|
78
|
+
}
|
|
79
|
+
async function loadModels(modelsDir) {
|
|
80
|
+
return loadModelsFromDir(modelsDir, !1);
|
|
81
|
+
}
|
|
82
|
+
function isPasswordField(fieldName, attr) {
|
|
83
|
+
const lowerName = fieldName.toLowerCase();
|
|
84
|
+
if (lowerName === "password" || lowerName.endsWith("_password") || lowerName.endsWith("password"))
|
|
85
|
+
return !0;
|
|
86
|
+
if (attr.hidden === !0 && lowerName.includes("pass"))
|
|
87
|
+
return !0;
|
|
88
|
+
return !1;
|
|
89
|
+
}
|
|
90
|
+
async function generateRecord(attributes, modelName, verbose = !1) {
|
|
91
|
+
const record = {};
|
|
92
|
+
for (const [fieldName, attr] of Object.entries(attributes)) {
|
|
93
|
+
const columnName = snakeCase(fieldName);
|
|
94
|
+
let value;
|
|
95
|
+
if (attr.factory && typeof attr.factory === "function")
|
|
96
|
+
try {
|
|
97
|
+
value = attr.factory(faker);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
100
|
+
if (verbose)
|
|
101
|
+
log.warn(` Factory failed for ${modelName}.${fieldName}: ${errorMsg}`);
|
|
102
|
+
if (attr.default !== void 0)
|
|
103
|
+
value = attr.default;
|
|
104
|
+
else
|
|
105
|
+
value = inferDefaultValue(fieldName);
|
|
106
|
+
}
|
|
107
|
+
else if (attr.default !== void 0)
|
|
108
|
+
value = attr.default;
|
|
109
|
+
else
|
|
110
|
+
continue;
|
|
111
|
+
if (isPasswordField(fieldName, attr) && typeof value === "string")
|
|
112
|
+
try {
|
|
113
|
+
value = await hashMake(value, { algorithm: "bcrypt" });
|
|
114
|
+
} catch (err) {
|
|
115
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
116
|
+
if (verbose)
|
|
117
|
+
log.warn(` Failed to hash password for ${modelName}.${fieldName}: ${errorMsg}`);
|
|
118
|
+
}
|
|
119
|
+
record[columnName] = value;
|
|
120
|
+
}
|
|
121
|
+
return record;
|
|
122
|
+
}
|
|
123
|
+
function inferDefaultValue(fieldName) {
|
|
124
|
+
const lowerName = fieldName.toLowerCase();
|
|
125
|
+
if (lowerName.startsWith("is") || lowerName.startsWith("has") || lowerName.endsWith("able"))
|
|
126
|
+
return !1;
|
|
127
|
+
if (lowerName.includes("count") || lowerName.includes("amount") || lowerName.includes("quantity"))
|
|
128
|
+
return 0;
|
|
129
|
+
if (lowerName.includes("url") || lowerName.includes("link"))
|
|
130
|
+
return "https://example.com";
|
|
131
|
+
if (lowerName.includes("email"))
|
|
132
|
+
return faker.internet.email();
|
|
133
|
+
if (lowerName.includes("name"))
|
|
134
|
+
return faker.person.fullName();
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
function fixtureToColumns(fixture) {
|
|
138
|
+
const out = {};
|
|
139
|
+
for (const [key, value] of Object.entries(fixture))
|
|
140
|
+
out[snakeCase(key)] = value;
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
async function generateRecords(model, verbose = !1) {
|
|
144
|
+
const records = [];
|
|
145
|
+
for (let i = 0;i < model.count; i++) {
|
|
146
|
+
const record = await generateRecord(model.attributes, model.name, verbose && i === 0), fixture = model.fixtures[i];
|
|
147
|
+
records.push(fixture ? { ...record, ...fixtureToColumns(fixture) } : record);
|
|
148
|
+
}
|
|
149
|
+
return records;
|
|
150
|
+
}
|
|
151
|
+
export function seedModelDirect(model, options) {
|
|
152
|
+
return seedModel(model, options);
|
|
153
|
+
}
|
|
154
|
+
async function seedModel(model, options) {
|
|
155
|
+
const startTime = Date.now();
|
|
156
|
+
try {
|
|
157
|
+
try {
|
|
158
|
+
await db.selectFrom(model.table).limit(0).execute();
|
|
159
|
+
} catch (tableErr) {
|
|
160
|
+
const msg = tableErr?.message || "";
|
|
161
|
+
if (msg.includes("does not exist") || msg.includes("no such table") || msg.includes("doesn't exist")) {
|
|
162
|
+
log.info(` Skipping ${model.name}: table "${model.table}" does not exist`);
|
|
163
|
+
return {
|
|
164
|
+
model: model.name,
|
|
165
|
+
table: model.table,
|
|
166
|
+
count: 0,
|
|
167
|
+
success: !0,
|
|
168
|
+
duration: Date.now() - startTime
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
throw tableErr;
|
|
172
|
+
}
|
|
173
|
+
if (!options.fresh) {
|
|
174
|
+
if (await db.selectFrom(model.table).selectAll().limit(1).executeTakeFirst()) {
|
|
175
|
+
if (options.verbose)
|
|
176
|
+
log.info(` ${model.name}: table already has rows \u2014 skipping (use --fresh to replace)`);
|
|
177
|
+
return {
|
|
178
|
+
model: model.name,
|
|
179
|
+
table: model.table,
|
|
180
|
+
count: 0,
|
|
181
|
+
success: !0,
|
|
182
|
+
duration: Date.now() - startTime
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const records = await generateRecords(model, options.verbose);
|
|
187
|
+
if (records.length === 0)
|
|
188
|
+
return {
|
|
189
|
+
model: model.name,
|
|
190
|
+
table: model.table,
|
|
191
|
+
count: 0,
|
|
192
|
+
success: !0,
|
|
193
|
+
duration: Date.now() - startTime
|
|
194
|
+
};
|
|
195
|
+
if (options.fresh)
|
|
196
|
+
try {
|
|
197
|
+
await db.deleteFrom(model.table).execute();
|
|
198
|
+
if (options.verbose)
|
|
199
|
+
log.info(` Truncated table: ${model.table}`);
|
|
200
|
+
} catch {}
|
|
201
|
+
const batchSize = 100;
|
|
202
|
+
let inserted = 0;
|
|
203
|
+
for (let i = 0;i < records.length; i += batchSize) {
|
|
204
|
+
const batch = records.slice(i, i + batchSize);
|
|
205
|
+
await db.insertInto(model.table).values(batch).execute();
|
|
206
|
+
inserted += batch.length;
|
|
207
|
+
}
|
|
208
|
+
if (options.verbose)
|
|
209
|
+
log.success(` Seeded ${model.name}: ${inserted} records`);
|
|
210
|
+
return {
|
|
211
|
+
model: model.name,
|
|
212
|
+
table: model.table,
|
|
213
|
+
count: inserted,
|
|
214
|
+
success: !0,
|
|
215
|
+
duration: Date.now() - startTime
|
|
216
|
+
};
|
|
217
|
+
} catch (err) {
|
|
218
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
219
|
+
if (options.verbose)
|
|
220
|
+
log.error(` Failed to seed ${model.name}: ${errorMessage}`);
|
|
221
|
+
return {
|
|
222
|
+
model: model.name,
|
|
223
|
+
table: model.table,
|
|
224
|
+
count: 0,
|
|
225
|
+
success: !1,
|
|
226
|
+
error: errorMessage,
|
|
227
|
+
duration: Date.now() - startTime
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function sortModelsByDependencies(models) {
|
|
232
|
+
const priority = {
|
|
233
|
+
User: 0,
|
|
234
|
+
Team: 1,
|
|
235
|
+
Project: 2
|
|
236
|
+
};
|
|
237
|
+
return models.sort((a, b) => {
|
|
238
|
+
const priorityA = priority[a.name] ?? 10, priorityB = priority[b.name] ?? 10;
|
|
239
|
+
return priorityA - priorityB;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
export async function seed(config = {}) {
|
|
243
|
+
const startTime = Date.now();
|
|
244
|
+
await ensureDatabaseConfigLoaded();
|
|
245
|
+
const modelsDir = config.modelsDir || path.userModelsPath(), verbose = config.verbose ?? !0;
|
|
246
|
+
if (verbose) {
|
|
247
|
+
log.info("Seeding database using model factories...");
|
|
248
|
+
log.info(`User models directory: ${modelsDir}`);
|
|
249
|
+
log.info(`Default models directory: ${defaultModelsPath()}`);
|
|
250
|
+
}
|
|
251
|
+
let models = await loadAllModels(modelsDir, verbose, config.includeDefaults ?? !1);
|
|
252
|
+
if (models.length === 0) {
|
|
253
|
+
log.warn("No seedable models found in defaults or user directories");
|
|
254
|
+
return {
|
|
255
|
+
total: 0,
|
|
256
|
+
successful: 0,
|
|
257
|
+
failed: 0,
|
|
258
|
+
results: [],
|
|
259
|
+
duration: Date.now() - startTime
|
|
260
|
+
};
|
|
261
|
+
}
|
|
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
|
+
if (config.only && config.only.length > 0)
|
|
264
|
+
models = models.filter((m) => config.only.includes(m.name));
|
|
265
|
+
if (config.except && config.except.length > 0)
|
|
266
|
+
models = models.filter((m) => !config.except.includes(m.name));
|
|
267
|
+
if (!config.fresh && !config.allowProtected) {
|
|
268
|
+
const skipped = [];
|
|
269
|
+
models = models.filter((m) => {
|
|
270
|
+
if (isProtectedModel(m.name)) {
|
|
271
|
+
skipped.push(m);
|
|
272
|
+
return !1;
|
|
273
|
+
}
|
|
274
|
+
return !0;
|
|
275
|
+
});
|
|
276
|
+
if (skipped.length > 0) {
|
|
277
|
+
log.info(`Skipped ${skipped.length} protected auth model(s) to avoid invalidating live sessions: ${skipped.map((m) => m.name).join(", ")}`);
|
|
278
|
+
log.info(" Re-run with --fresh (truncates tables first) or --allow-protected to include them.");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
models = sortModelsByDependencies(models);
|
|
282
|
+
if (verbose)
|
|
283
|
+
log.info(`Found ${models.length} seedable model(s)`);
|
|
284
|
+
const results = [];
|
|
285
|
+
for (const model of models) {
|
|
286
|
+
if (verbose)
|
|
287
|
+
log.info(`Seeding ${model.name} (${model.count} records)...`);
|
|
288
|
+
try {
|
|
289
|
+
const result = await seedModel(model, config);
|
|
290
|
+
results.push(result);
|
|
291
|
+
} catch (err) {
|
|
292
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
293
|
+
if (verbose)
|
|
294
|
+
log.error(` Unexpected error seeding ${model.name}: ${errorMessage}`);
|
|
295
|
+
results.push({
|
|
296
|
+
model: model.name,
|
|
297
|
+
table: model.table,
|
|
298
|
+
count: 0,
|
|
299
|
+
success: !1,
|
|
300
|
+
error: errorMessage,
|
|
301
|
+
duration: 0
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const successful = results.filter((r) => r.success).length, failed = results.filter((r) => !r.success).length, totalRecords = results.reduce((sum, r) => sum + r.count, 0);
|
|
306
|
+
if (verbose) {
|
|
307
|
+
log.info("");
|
|
308
|
+
if (failed === 0) {
|
|
309
|
+
log.success("Database seeded successfully!");
|
|
310
|
+
log.info(` Total records: ${totalRecords}`);
|
|
311
|
+
log.info(` Models seeded: ${successful}`);
|
|
312
|
+
} else {
|
|
313
|
+
log.warn(`Seeding completed with ${failed} failure(s)`);
|
|
314
|
+
log.info(` Successful: ${successful}`);
|
|
315
|
+
log.info(` Failed: ${failed}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
total: results.length,
|
|
320
|
+
successful,
|
|
321
|
+
failed,
|
|
322
|
+
results,
|
|
323
|
+
duration: Date.now() - startTime
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
export async function seedModel$(modelName, options = {}) {
|
|
327
|
+
const modelsDir = path.userModelsPath(), model = (await loadAllModels(modelsDir, options.verbose)).find((m) => m.name === modelName);
|
|
328
|
+
if (!model)
|
|
329
|
+
throw Error(`Model not found: ${modelName}`);
|
|
330
|
+
if (options.count)
|
|
331
|
+
model.count = options.count;
|
|
332
|
+
return seedModel(model, {
|
|
333
|
+
fresh: options.fresh,
|
|
334
|
+
verbose: options.verbose ?? !0
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
export async function freshSeed(config = {}) {
|
|
338
|
+
return seed({ ...config, fresh: !0 });
|
|
339
|
+
}
|
|
340
|
+
export async function listSeedableModels() {
|
|
341
|
+
const modelsDir = path.userModelsPath(), defaultDir = defaultModelsPath(), defaultModels = await loadModelsFromDir(defaultDir, !0), userModels = await loadModelsFromDir(modelsDir, !1), result = [], seen = new Set;
|
|
342
|
+
for (const m of userModels) {
|
|
343
|
+
result.push({
|
|
344
|
+
name: m.name,
|
|
345
|
+
table: m.table,
|
|
346
|
+
count: m.count,
|
|
347
|
+
source: "user"
|
|
348
|
+
});
|
|
349
|
+
seen.add(m.name);
|
|
350
|
+
}
|
|
351
|
+
for (const m of defaultModels)
|
|
352
|
+
if (!seen.has(m.name))
|
|
353
|
+
result.push({
|
|
354
|
+
name: m.name,
|
|
355
|
+
table: m.table,
|
|
356
|
+
count: m.count,
|
|
357
|
+
source: "default"
|
|
358
|
+
});
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export { seed as runSeeders };
|
|
363
|
+
export { freshSeed as freshWithSeed };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create SQL dialect helpers for a given driver.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { sqlHelpers } from '@stacksjs/database'
|
|
7
|
+
* const sql = sqlHelpers('postgres')
|
|
8
|
+
* await db.unsafe(`SELECT * FROM users WHERE id = ${sql.param(1)}`, [userId])
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export declare function sqlHelpers(driver: string): SqlDialectHelpers;
|
|
12
|
+
/**
|
|
13
|
+
* SQL Dialect Helpers
|
|
14
|
+
*
|
|
15
|
+
* Cross-database compatibility utilities for PostgreSQL, MySQL, and SQLite.
|
|
16
|
+
* Centralizes the isPostgres/isMysql/now/boolTrue/boolFalse/param helpers
|
|
17
|
+
* that were previously duplicated across tokens.ts, auth-tables.ts, and setup.ts.
|
|
18
|
+
*/
|
|
19
|
+
export declare interface SqlDialectHelpers {
|
|
20
|
+
driver: string
|
|
21
|
+
isPostgres: boolean
|
|
22
|
+
isMysql: boolean
|
|
23
|
+
isSqlite: boolean
|
|
24
|
+
now: string
|
|
25
|
+
boolTrue: string
|
|
26
|
+
boolFalse: string
|
|
27
|
+
autoIncrement: string
|
|
28
|
+
primaryKey: string
|
|
29
|
+
pkColumn: string
|
|
30
|
+
nullableTimestamp: string
|
|
31
|
+
param: (index: number) => string
|
|
32
|
+
params: (...values: unknown[]) => { sql: string, values: unknown[] }
|
|
33
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function sqlHelpers(driver) {
|
|
2
|
+
const isPostgres = driver === "postgres", isMysql = driver === "mysql" || driver === "singlestore";
|
|
3
|
+
return {
|
|
4
|
+
driver,
|
|
5
|
+
isPostgres,
|
|
6
|
+
isMysql,
|
|
7
|
+
isSqlite: !isPostgres && !isMysql,
|
|
8
|
+
now: isPostgres || isMysql ? "NOW()" : "datetime('now')",
|
|
9
|
+
boolTrue: isPostgres ? "true" : "1",
|
|
10
|
+
boolFalse: isPostgres ? "false" : "0",
|
|
11
|
+
autoIncrement: isPostgres ? "SERIAL" : "INTEGER",
|
|
12
|
+
primaryKey: isPostgres ? "PRIMARY KEY" : isMysql ? "PRIMARY KEY AUTO_INCREMENT" : "PRIMARY KEY AUTOINCREMENT",
|
|
13
|
+
pkColumn: isPostgres ? "id SERIAL PRIMARY KEY" : isMysql ? "id INTEGER PRIMARY KEY AUTO_INCREMENT" : "id INTEGER PRIMARY KEY AUTOINCREMENT",
|
|
14
|
+
nullableTimestamp: isMysql ? "TIMESTAMP NULL" : "TIMESTAMP",
|
|
15
|
+
param(index) {
|
|
16
|
+
return isPostgres ? `$${index}` : "?";
|
|
17
|
+
},
|
|
18
|
+
params(...values) {
|
|
19
|
+
if (isPostgres)
|
|
20
|
+
return { sql: values.map((_, i) => `$${i + 1}`).join(", "), values };
|
|
21
|
+
return { sql: values.map(() => "?").join(", "), values };
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|