@stacksjs/database 0.70.258 → 0.70.260

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 (50) hide show
  1. package/dist/auth-tables.js +18 -137
  2. package/dist/column.js +1 -26
  3. package/dist/custom/audits.js +20 -54
  4. package/dist/custom/errors.js +16 -46
  5. package/dist/custom/index.js +1 -3
  6. package/dist/custom/jobs.js +13 -137
  7. package/dist/database.js +1 -181
  8. package/dist/datetime-columns.js +2 -79
  9. package/dist/ddl-constraints.js +7 -111
  10. package/dist/defaults.js +1 -48
  11. package/dist/dialect.js +1 -79
  12. package/dist/driver-config.js +1 -172
  13. package/dist/drivers/defaults/index.js +1 -1
  14. package/dist/drivers/defaults/traits.js +1 -29
  15. package/dist/drivers/dynamodb.js +1 -607
  16. package/dist/drivers/helpers.js +1 -206
  17. package/dist/drivers/index.js +1 -9
  18. package/dist/drivers/mysql.js +58 -299
  19. package/dist/drivers/postgres.js +78 -368
  20. package/dist/drivers/sqlite.js +61 -379
  21. package/dist/ensure-database.js +1 -145
  22. package/dist/fk-audit.js +3 -187
  23. package/dist/index.js +1 -64
  24. package/dist/managed-columns.js +1 -59
  25. package/dist/migration-dialect.js +4 -107
  26. package/dist/migration-ledger.js +1 -382
  27. package/dist/migration-lock.js +1 -143
  28. package/dist/migrations.js +15 -1118
  29. package/dist/model-sources.js +1 -76
  30. package/dist/notification-tables.js +4 -49
  31. package/dist/query-logger.js +2 -241
  32. package/dist/query-parser.js +1 -93
  33. package/dist/rbac-tables.js +6 -61
  34. package/dist/relation-columns.js +1 -66
  35. package/dist/replicas.js +1 -74
  36. package/dist/safe-migrations.js +2 -52
  37. package/dist/schema.js +1 -10
  38. package/dist/seeder.js +1 -457
  39. package/dist/sql-helpers.js +1 -50
  40. package/dist/table.js +1 -26
  41. package/dist/tools/setup.js +1 -6
  42. package/dist/trait-tables.js +8 -153
  43. package/dist/transaction-context.js +1 -62
  44. package/dist/types.js +1 -98
  45. package/dist/unique-audit.js +3 -155
  46. package/dist/utils.js +1 -285
  47. package/dist/uuid-columns.js +1 -68
  48. package/dist/validators.js +1 -122
  49. package/dist/vschema.js +2 -121
  50. package/package.json +20 -13
package/dist/seeder.js CHANGED
@@ -1,457 +1 @@
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
- export 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
- ]), ACCOUNT_MODELS = Object.freeze([
16
- "User",
17
- "Team",
18
- "Customer"
19
- ]);
20
- export function isAccountModel(name) {
21
- return ACCOUNT_MODELS.includes(name);
22
- }
23
- export function isProtectedModel(name) {
24
- return PROTECTED_MODELS.includes(name);
25
- }
26
- function snakeCase(str) {
27
- 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();
28
- }
29
- async function loadModelsFromDir(modelsDir, recursive = !1) {
30
- const models = [];
31
- if (!fs.existsSync(modelsDir))
32
- return models;
33
- const entries = fs.readdirSync(modelsDir, { withFileTypes: !0 });
34
- for (const entry of entries) {
35
- const fullPath = path.join(modelsDir, entry.name);
36
- if (entry.isDirectory() && recursive) {
37
- const subModels = await loadModelsFromDir(fullPath, !0);
38
- models.push(...subModels);
39
- continue;
40
- }
41
- if (!entry.name.endsWith(".ts") || entry.name.startsWith("index") || entry.name.startsWith("_"))
42
- continue;
43
- try {
44
- const module = await import(fullPath), modelDef = module.default || module;
45
- if (!modelDef)
46
- continue;
47
- const useSeeder = modelDef.traits?.useSeeder ?? modelDef.traits?.seedable;
48
- if (!useSeeder)
49
- continue;
50
- let count = 10, fixtures = [];
51
- if (typeof useSeeder === "object" && "count" in useSeeder) {
52
- const opts = useSeeder;
53
- count = opts.count;
54
- fixtures = opts.fixtures ?? [];
55
- }
56
- const modelName = modelDef.name || entry.name.replace(".ts", ""), tableName = modelDef.table || snakeCase(modelName) + "s";
57
- models.push({
58
- name: modelName,
59
- table: tableName,
60
- count: Math.max(count, fixtures.length),
61
- fixtures,
62
- attributes: modelDef.attributes || {},
63
- model: modelDef,
64
- filePath: fullPath
65
- });
66
- } catch (err) {
67
- log.error(`Failed to load model ${entry.name}:`, err);
68
- }
69
- }
70
- return models;
71
- }
72
- async function loadAllModels(userModelsDir, verbose = !1, includeDefaults = !1) {
73
- const defaultDir = defaultModelsPath(), userModels = await loadModelsFromDir(userModelsDir, !1);
74
- if (userModels.length > 0 && !includeDefaults)
75
- return userModels;
76
- const defaultModels = await loadModelsFromDir(defaultDir, !0), modelMap = new Map;
77
- for (const model of defaultModels)
78
- modelMap.set(model.name, model);
79
- for (const model of userModels) {
80
- if (modelMap.has(model.name) && verbose)
81
- log.info(` User model "${model.name}" overrides default`);
82
- modelMap.set(model.name, model);
83
- }
84
- return Array.from(modelMap.values());
85
- }
86
- async function loadModels(modelsDir) {
87
- return loadModelsFromDir(modelsDir, !1);
88
- }
89
- function isPasswordField(fieldName, attr) {
90
- const lowerName = fieldName.toLowerCase();
91
- if (lowerName === "password" || lowerName.endsWith("_password") || lowerName.endsWith("password"))
92
- return !0;
93
- if (attr.hidden === !0 && lowerName.includes("pass"))
94
- return !0;
95
- return !1;
96
- }
97
- async function generateRecord(attributes, modelName, report = !1) {
98
- const record = {};
99
- for (const [fieldName, attr] of Object.entries(attributes)) {
100
- const columnName = snakeCase(fieldName);
101
- let value;
102
- if (attr.factory && typeof attr.factory === "function")
103
- try {
104
- value = attr.factory(faker);
105
- } catch (err) {
106
- const errorMsg = err instanceof Error ? err.message : String(err);
107
- if (report)
108
- log.warn(` Factory failed for ${modelName}.${fieldName}: ${errorMsg} \u2014 seeding the default instead.`);
109
- if (attr.default !== void 0)
110
- value = attr.default;
111
- else
112
- value = inferDefaultValue(fieldName);
113
- }
114
- else if (attr.default !== void 0)
115
- value = attr.default;
116
- else
117
- continue;
118
- if (isPasswordField(fieldName, attr) && typeof value === "string")
119
- try {
120
- value = await hashMake(value, { algorithm: "bcrypt" });
121
- } catch (err) {
122
- const errorMsg = err instanceof Error ? err.message : String(err);
123
- log.warn(` Failed to hash password for ${modelName}.${fieldName}: ${errorMsg}`);
124
- }
125
- record[columnName] = value;
126
- }
127
- return record;
128
- }
129
- function inferDefaultValue(fieldName) {
130
- const lowerName = fieldName.toLowerCase();
131
- if (lowerName.startsWith("is") || lowerName.startsWith("has") || lowerName.endsWith("able"))
132
- return !1;
133
- if (lowerName.includes("count") || lowerName.includes("amount") || lowerName.includes("quantity"))
134
- return 0;
135
- if (lowerName.includes("url") || lowerName.includes("link"))
136
- return "https://example.com";
137
- if (lowerName.includes("email"))
138
- return faker.internet.email();
139
- if (lowerName.includes("name"))
140
- return faker.person.fullName();
141
- return null;
142
- }
143
- function fixtureToColumns(fixture) {
144
- const out = {};
145
- for (const [key, value] of Object.entries(fixture))
146
- out[snakeCase(key)] = value;
147
- return out;
148
- }
149
- async function existingRows(table) {
150
- try {
151
- return await db.selectFrom(table).selectAll().limit(500).execute();
152
- } catch {
153
- return [];
154
- }
155
- }
156
- const modelTables = new Map;
157
- export function registerModelTables(models) {
158
- modelTables.clear();
159
- for (const model of models)
160
- modelTables.set(model.name, model.table);
161
- }
162
- export function parentTable(parent) {
163
- return modelTables.get(parent) ?? `${snakeCase(parent)}s`;
164
- }
165
- async function relationColumns(model, options = {}) {
166
- const parents = parentRelations(model);
167
- if (parents.length === 0)
168
- return [];
169
- const pools = [];
170
- for (const relation of parents) {
171
- const { model: parent, column } = relation;
172
- if (model.attributes[parent])
173
- continue;
174
- if (isAccountModel(parent) && !options.allowProtected)
175
- continue;
176
- const rows = await existingRows(parentTable(parent));
177
- if (rows.length > 0)
178
- pools.push({ column, rows });
179
- }
180
- return chooseRelations(pools, model.count);
181
- }
182
- export function chooseRelations(pools, count) {
183
- if (pools.length === 0)
184
- return [];
185
- const wanted = new Set(pools.map((pool) => pool.column)), specificity = (pool) => {
186
- const sample = pool.rows[0] ?? {};
187
- return [...wanted].filter((column) => column !== pool.column && (column in sample)).length;
188
- }, ordered = [...pools].sort((a, b) => specificity(b) - specificity(a));
189
- return Array.from({ length: count }, () => {
190
- const row = {};
191
- for (const pool of ordered) {
192
- if (row[pool.column] != null)
193
- continue;
194
- const agrees = (candidate) => [...wanted].every((column) => row[column] == null || candidate[column] == null || candidate[column] === row[column]), candidates = pool.rows.filter(agrees), from = candidates.length > 0 ? candidates : pool.rows, chosen = from[Math.floor(Math.random() * from.length)];
195
- row[pool.column] = chosen.id;
196
- for (const column of wanted)
197
- if (column !== pool.column && row[column] == null && chosen[column] != null)
198
- row[column] = chosen[column];
199
- }
200
- return row;
201
- });
202
- }
203
- async function generateRecords(model, options = {}) {
204
- const records = [], relations = await relationColumns(model, options);
205
- for (let i = 0;i < model.count; i++) {
206
- const record = await generateRecord(model.attributes, model.name, i === 0), fixture = model.fixtures[i], relation = relations[i] ?? {}, withRelations = { ...record };
207
- for (const [column, value] of Object.entries(relation))
208
- if (withRelations[column] == null)
209
- withRelations[column] = value;
210
- records.push(fixture ? { ...withRelations, ...fixtureToColumns(fixture) } : withRelations);
211
- }
212
- return records;
213
- }
214
- async function seedModel(model, options) {
215
- const startTime = Date.now();
216
- try {
217
- try {
218
- await db.selectFrom(model.table).limit(0).execute();
219
- } catch (tableErr) {
220
- const msg = tableErr?.message || "";
221
- if (msg.includes("does not exist") || msg.includes("no such table") || msg.includes("doesn't exist")) {
222
- log.info(` Skipping ${model.name}: table "${model.table}" does not exist`);
223
- return {
224
- model: model.name,
225
- table: model.table,
226
- count: 0,
227
- success: !0,
228
- duration: Date.now() - startTime
229
- };
230
- }
231
- throw tableErr;
232
- }
233
- if (!options.fresh && !options.append) {
234
- if (await db.selectFrom(model.table).selectAll().limit(1).executeTakeFirst()) {
235
- if (options.verbose)
236
- log.info(` ${model.name}: table already has rows \u2014 skipping (--append to add more, --fresh to replace)`);
237
- return {
238
- model: model.name,
239
- table: model.table,
240
- count: 0,
241
- success: !0,
242
- duration: Date.now() - startTime
243
- };
244
- }
245
- }
246
- const records = await generateRecords(model, options);
247
- if (records.length === 0)
248
- return {
249
- model: model.name,
250
- table: model.table,
251
- count: 0,
252
- success: !0,
253
- duration: Date.now() - startTime
254
- };
255
- const batchSize = 100;
256
- let inserted = 0;
257
- for (let i = 0;i < records.length; i += batchSize) {
258
- const batch = records.slice(i, i + batchSize);
259
- await db.insertInto(model.table).values(batch).execute();
260
- inserted += batch.length;
261
- }
262
- if (options.verbose)
263
- log.success(` Seeded ${model.name}: ${inserted} records`);
264
- return {
265
- model: model.name,
266
- table: model.table,
267
- count: inserted,
268
- success: !0,
269
- duration: Date.now() - startTime
270
- };
271
- } catch (err) {
272
- const errorMessage = err instanceof Error ? err.message : String(err);
273
- if (options.verbose)
274
- log.error(` Failed to seed ${model.name}: ${errorMessage}`);
275
- return {
276
- model: model.name,
277
- table: model.table,
278
- count: 0,
279
- success: !1,
280
- error: errorMessage,
281
- duration: Date.now() - startTime
282
- };
283
- }
284
- }
285
- export function parentRelations(model) {
286
- const belongsTo = model.model.belongsTo, read = (entry) => {
287
- if (typeof entry === "string")
288
- return entry ? { model: entry, column: `${snakeCase(entry)}_id` } : null;
289
- if (entry && typeof entry === "object") {
290
- const name = String(entry.model ?? "");
291
- if (!name)
292
- return null;
293
- const key = entry.foreignKey;
294
- return { model: name, column: key || `${snakeCase(name)}_id` };
295
- }
296
- return null;
297
- };
298
- return (Array.isArray(belongsTo) ? belongsTo : belongsTo && typeof belongsTo === "object" ? Object.values(belongsTo) : []).map(read).filter((relation) => relation !== null);
299
- }
300
- function parentModels(model) {
301
- return parentRelations(model).map((relation) => relation.model);
302
- }
303
- async function clearTables(models, verbose) {
304
- for (const model of [...models].reverse())
305
- try {
306
- await db.deleteFrom(model.table).execute();
307
- if (verbose)
308
- log.info(` Truncated table: ${model.table}`);
309
- } catch (error) {
310
- const message = error instanceof Error ? error.message : String(error);
311
- if (/no such table|doesn't exist|does not exist/i.test(message))
312
- continue;
313
- throw Error(`Could not empty ${model.table} before seeding: ${message}`);
314
- }
315
- }
316
- function sortModelsByDependencies(models) {
317
- const byName = new Map(models.map((model) => [model.name, model])), ordered = [], state = new Map, visit = (model) => {
318
- const status = state.get(model.name);
319
- if (status === "done" || status === "visiting")
320
- return;
321
- state.set(model.name, "visiting");
322
- for (const parentName of parentModels(model)) {
323
- const parent = byName.get(parentName);
324
- if (parent && parent !== model)
325
- visit(parent);
326
- }
327
- state.set(model.name, "done");
328
- ordered.push(model);
329
- };
330
- for (const model of models)
331
- visit(model);
332
- return ordered;
333
- }
334
- export async function seed(config = {}) {
335
- const startTime = Date.now();
336
- await ensureDatabaseConfigLoaded();
337
- const modelsDir = config.modelsDir || path.userModelsPath(), verbose = config.verbose ?? !0;
338
- if (verbose) {
339
- log.info("Seeding database using model factories...");
340
- log.info(`User models directory: ${modelsDir}`);
341
- log.info(`Default models directory: ${defaultModelsPath()}`);
342
- }
343
- let models = await loadAllModels(modelsDir, verbose, config.includeDefaults ?? !1);
344
- registerModelTables(models);
345
- if (models.length === 0) {
346
- log.warn("No seedable models found in defaults or user directories");
347
- return {
348
- total: 0,
349
- successful: 0,
350
- failed: 0,
351
- results: [],
352
- duration: Date.now() - startTime
353
- };
354
- }
355
- if (config.only && config.only.length > 0)
356
- models = models.filter((m) => config.only.includes(m.name));
357
- if (config.except && config.except.length > 0)
358
- models = models.filter((m) => !config.except.includes(m.name));
359
- if (!config.fresh && !config.allowProtected) {
360
- const skipped = [];
361
- models = models.filter((m) => {
362
- if (isProtectedModel(m.name)) {
363
- skipped.push(m);
364
- return !1;
365
- }
366
- return !0;
367
- });
368
- if (skipped.length > 0) {
369
- log.info(`Skipped ${skipped.length} protected auth model(s) to avoid invalidating live sessions: ${skipped.map((m) => m.name).join(", ")}`);
370
- log.info(" Re-run with --fresh (truncates tables first) or --allow-protected to include them.");
371
- }
372
- }
373
- models = sortModelsByDependencies(models);
374
- if (verbose)
375
- log.info(`Found ${models.length} seedable model(s)`);
376
- if (config.fresh)
377
- await clearTables(models, verbose);
378
- const results = [];
379
- for (const model of models) {
380
- if (verbose)
381
- log.info(`Seeding ${model.name} (${model.count} records)...`);
382
- try {
383
- const result = await seedModel(model, config);
384
- results.push(result);
385
- } catch (err) {
386
- const errorMessage = err instanceof Error ? err.message : String(err);
387
- if (verbose)
388
- log.error(` Unexpected error seeding ${model.name}: ${errorMessage}`);
389
- results.push({
390
- model: model.name,
391
- table: model.table,
392
- count: 0,
393
- success: !1,
394
- error: errorMessage,
395
- duration: 0
396
- });
397
- }
398
- }
399
- const successful = results.filter((r) => r.success).length, failed = results.filter((r) => !r.success).length, totalRecords = results.reduce((sum, r) => sum + r.count, 0);
400
- if (verbose) {
401
- log.info("");
402
- if (failed === 0) {
403
- log.success("Database seeded successfully!");
404
- log.info(` Total records: ${totalRecords}`);
405
- log.info(` Models seeded: ${successful}`);
406
- } else {
407
- log.warn(`Seeding completed with ${failed} failure(s)`);
408
- log.info(` Successful: ${successful}`);
409
- log.info(` Failed: ${failed}`);
410
- }
411
- }
412
- return {
413
- total: results.length,
414
- successful,
415
- failed,
416
- results,
417
- duration: Date.now() - startTime
418
- };
419
- }
420
- export async function seedModel$(modelName, options = {}) {
421
- const modelsDir = path.userModelsPath(), model = (await loadAllModels(modelsDir, options.verbose)).find((m) => m.name === modelName);
422
- if (!model)
423
- throw Error(`Model not found: ${modelName}`);
424
- if (options.count)
425
- model.count = options.count;
426
- return seedModel(model, {
427
- fresh: options.fresh,
428
- verbose: options.verbose ?? !0
429
- });
430
- }
431
- export async function freshSeed(config = {}) {
432
- return seed({ ...config, fresh: !0 });
433
- }
434
- export async function listSeedableModels() {
435
- const modelsDir = path.userModelsPath(), defaultDir = defaultModelsPath(), defaultModels = await loadModelsFromDir(defaultDir, !0), userModels = await loadModelsFromDir(modelsDir, !1), result = [], seen = new Set;
436
- for (const m of userModels) {
437
- result.push({
438
- name: m.name,
439
- table: m.table,
440
- count: m.count,
441
- source: "user"
442
- });
443
- seen.add(m.name);
444
- }
445
- for (const m of defaultModels)
446
- if (!seen.has(m.name))
447
- result.push({
448
- name: m.name,
449
- table: m.table,
450
- count: m.count,
451
- source: "default"
452
- });
453
- return result;
454
- }
455
-
456
- export { seed as runSeeders };
457
- export { freshSeed as freshWithSeed };
1
+ import{log}from"@stacksjs/logging";import{db,ensureDatabaseConfigLoaded}from"./utils";import{faker}from"@stacksjs/faker";import{path}from"@stacksjs/path";import{hashMake}from"@stacksjs/security";import{fs}from"@stacksjs/storage";export function defaultModelsPath(subpath){return path.frameworkPath(`defaults/app/Models/${subpath||""}`)}export const PROTECTED_MODELS=Object.freeze(["OauthClient","OauthAccessToken","OauthRefreshToken","PersonalAccessToken"]),ACCOUNT_MODELS=Object.freeze(["User","Team","Customer"]);export function isAccountModel(name){return ACCOUNT_MODELS.includes(name)}export function isProtectedModel(name){return PROTECTED_MODELS.includes(name)}function snakeCase(str){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()}async function loadModelsFromDir(modelsDir,recursive=!1){const models=[];if(!fs.existsSync(modelsDir))return models;const entries=fs.readdirSync(modelsDir,{withFileTypes:!0});for(const entry of entries){const fullPath=path.join(modelsDir,entry.name);if(entry.isDirectory()&&recursive){const subModels=await loadModelsFromDir(fullPath,!0);models.push(...subModels);continue}if(!entry.name.endsWith(".ts")||entry.name.startsWith("index")||entry.name.startsWith("_"))continue;try{const module=await import(fullPath),modelDef=module.default||module;if(!modelDef)continue;const useSeeder=modelDef.traits?.useSeeder??modelDef.traits?.seedable;if(!useSeeder)continue;let count=10,fixtures=[];if(typeof useSeeder==="object"&&"count"in useSeeder){const opts=useSeeder;count=opts.count;fixtures=opts.fixtures??[]}const modelName=modelDef.name||entry.name.replace(".ts",""),tableName=modelDef.table||snakeCase(modelName)+"s";models.push({name:modelName,table:tableName,count:Math.max(count,fixtures.length),fixtures,attributes:modelDef.attributes||{},model:modelDef,filePath:fullPath})}catch(err){log.error(`Failed to load model ${entry.name}:`,err)}}return models}async function loadAllModels(userModelsDir,verbose=!1,includeDefaults=!1){const defaultDir=defaultModelsPath(),userModels=await loadModelsFromDir(userModelsDir,!1);if(userModels.length>0&&!includeDefaults)return userModels;const defaultModels=await loadModelsFromDir(defaultDir,!0),modelMap=new Map;for(const model of defaultModels)modelMap.set(model.name,model);for(const model of userModels){if(modelMap.has(model.name)&&verbose)log.info(` User model "${model.name}" overrides default`);modelMap.set(model.name,model)}return Array.from(modelMap.values())}async function loadModels(modelsDir){return loadModelsFromDir(modelsDir,!1)}function isPasswordField(fieldName,attr){const lowerName=fieldName.toLowerCase();if(lowerName==="password"||lowerName.endsWith("_password")||lowerName.endsWith("password"))return!0;if(attr.hidden===!0&&lowerName.includes("pass"))return!0;return!1}async function generateRecord(attributes,modelName,report=!1){const record={};for(const[fieldName,attr]of Object.entries(attributes)){const columnName=snakeCase(fieldName);let value;if(attr.factory&&typeof attr.factory==="function")try{value=attr.factory(faker)}catch(err){const errorMsg=err instanceof Error?err.message:String(err);if(report)log.warn(` Factory failed for ${modelName}.${fieldName}: ${errorMsg} \u2014 seeding the default instead.`);if(attr.default!==void 0)value=attr.default;else value=inferDefaultValue(fieldName)}else if(attr.default!==void 0)value=attr.default;else continue;if(isPasswordField(fieldName,attr)&&typeof value==="string")try{value=await hashMake(value,{algorithm:"bcrypt"})}catch(err){const errorMsg=err instanceof Error?err.message:String(err);log.warn(` Failed to hash password for ${modelName}.${fieldName}: ${errorMsg}`)}record[columnName]=value}return record}function inferDefaultValue(fieldName){const lowerName=fieldName.toLowerCase();if(lowerName.startsWith("is")||lowerName.startsWith("has")||lowerName.endsWith("able"))return!1;if(lowerName.includes("count")||lowerName.includes("amount")||lowerName.includes("quantity"))return 0;if(lowerName.includes("url")||lowerName.includes("link"))return"https://example.com";if(lowerName.includes("email"))return faker.internet.email();if(lowerName.includes("name"))return faker.person.fullName();return null}function fixtureToColumns(fixture){const out={};for(const[key,value]of Object.entries(fixture))out[snakeCase(key)]=value;return out}async function existingRows(table){try{return await db.selectFrom(table).selectAll().limit(500).execute()}catch{return[]}}const modelTables=new Map;export function registerModelTables(models){modelTables.clear();for(const model of models)modelTables.set(model.name,model.table)}export function parentTable(parent){return modelTables.get(parent)??`${snakeCase(parent)}s`}async function relationColumns(model,options={}){const parents=parentRelations(model);if(parents.length===0)return[];const pools=[];for(const relation of parents){const{model:parent,column}=relation;if(model.attributes[parent])continue;if(isAccountModel(parent)&&!options.allowProtected)continue;const rows=await existingRows(parentTable(parent));if(rows.length>0)pools.push({column,rows})}return chooseRelations(pools,model.count)}export function chooseRelations(pools,count){if(pools.length===0)return[];const wanted=new Set(pools.map((pool)=>pool.column)),specificity=(pool)=>{const sample=pool.rows[0]??{};return[...wanted].filter((column)=>column!==pool.column&&(column in sample)).length},ordered=[...pools].sort((a,b)=>specificity(b)-specificity(a));return Array.from({length:count},()=>{const row={};for(const pool of ordered){if(row[pool.column]!=null)continue;const agrees=(candidate)=>[...wanted].every((column)=>row[column]==null||candidate[column]==null||candidate[column]===row[column]),candidates=pool.rows.filter(agrees),from=candidates.length>0?candidates:pool.rows,chosen=from[Math.floor(Math.random()*from.length)];row[pool.column]=chosen.id;for(const column of wanted)if(column!==pool.column&&row[column]==null&&chosen[column]!=null)row[column]=chosen[column]}return row})}async function generateRecords(model,options={}){const records=[],relations=await relationColumns(model,options);for(let i=0;i<model.count;i++){const record=await generateRecord(model.attributes,model.name,i===0),fixture=model.fixtures[i],relation=relations[i]??{},withRelations={...record};for(const[column,value]of Object.entries(relation))if(withRelations[column]==null)withRelations[column]=value;records.push(fixture?{...withRelations,...fixtureToColumns(fixture)}:withRelations)}return records}async function seedModel(model,options){const startTime=Date.now();try{try{await db.selectFrom(model.table).limit(0).execute()}catch(tableErr){const msg=tableErr?.message||"";if(msg.includes("does not exist")||msg.includes("no such table")||msg.includes("doesn't exist")){log.info(` Skipping ${model.name}: table "${model.table}" does not exist`);return{model:model.name,table:model.table,count:0,success:!0,duration:Date.now()-startTime}}throw tableErr}if(!options.fresh&&!options.append){if(await db.selectFrom(model.table).selectAll().limit(1).executeTakeFirst()){if(options.verbose)log.info(` ${model.name}: table already has rows \u2014 skipping (--append to add more, --fresh to replace)`);return{model:model.name,table:model.table,count:0,success:!0,duration:Date.now()-startTime}}}const records=await generateRecords(model,options);if(records.length===0)return{model:model.name,table:model.table,count:0,success:!0,duration:Date.now()-startTime};const batchSize=100;let inserted=0;for(let i=0;i<records.length;i+=batchSize){const batch=records.slice(i,i+batchSize);await db.insertInto(model.table).values(batch).execute();inserted+=batch.length}if(options.verbose)log.success(` Seeded ${model.name}: ${inserted} records`);return{model:model.name,table:model.table,count:inserted,success:!0,duration:Date.now()-startTime}}catch(err){const errorMessage=err instanceof Error?err.message:String(err);if(options.verbose)log.error(` Failed to seed ${model.name}: ${errorMessage}`);return{model:model.name,table:model.table,count:0,success:!1,error:errorMessage,duration:Date.now()-startTime}}}export function parentRelations(model){const belongsTo=model.model.belongsTo,read=(entry)=>{if(typeof entry==="string")return entry?{model:entry,column:`${snakeCase(entry)}_id`}:null;if(entry&&typeof entry==="object"){const name=String(entry.model??"");if(!name)return null;const key=entry.foreignKey;return{model:name,column:key||`${snakeCase(name)}_id`}}return null};return(Array.isArray(belongsTo)?belongsTo:belongsTo&&typeof belongsTo==="object"?Object.values(belongsTo):[]).map(read).filter((relation)=>relation!==null)}function parentModels(model){return parentRelations(model).map((relation)=>relation.model)}async function clearTables(models,verbose){for(const model of[...models].reverse())try{await db.deleteFrom(model.table).execute();if(verbose)log.info(` Truncated table: ${model.table}`)}catch(error){const message=error instanceof Error?error.message:String(error);if(/no such table|doesn't exist|does not exist/i.test(message))continue;throw Error(`Could not empty ${model.table} before seeding: ${message}`)}}function sortModelsByDependencies(models){const byName=new Map(models.map((model)=>[model.name,model])),ordered=[],state=new Map,visit=(model)=>{const status=state.get(model.name);if(status==="done"||status==="visiting")return;state.set(model.name,"visiting");for(const parentName of parentModels(model)){const parent=byName.get(parentName);if(parent&&parent!==model)visit(parent)}state.set(model.name,"done");ordered.push(model)};for(const model of models)visit(model);return ordered}export async function seed(config={}){const startTime=Date.now();await ensureDatabaseConfigLoaded();const modelsDir=config.modelsDir||path.userModelsPath(),verbose=config.verbose??!0;if(verbose){log.info("Seeding database using model factories...");log.info(`User models directory: ${modelsDir}`);log.info(`Default models directory: ${defaultModelsPath()}`)}let models=await loadAllModels(modelsDir,verbose,config.includeDefaults??!1);registerModelTables(models);if(models.length===0){log.warn("No seedable models found in defaults or user directories");return{total:0,successful:0,failed:0,results:[],duration:Date.now()-startTime}}if(config.only&&config.only.length>0)models=models.filter((m)=>config.only.includes(m.name));if(config.except&&config.except.length>0)models=models.filter((m)=>!config.except.includes(m.name));if(!config.fresh&&!config.allowProtected){const skipped=[];models=models.filter((m)=>{if(isProtectedModel(m.name)){skipped.push(m);return!1}return!0});if(skipped.length>0){log.info(`Skipped ${skipped.length} protected auth model(s) to avoid invalidating live sessions: ${skipped.map((m)=>m.name).join(", ")}`);log.info(" Re-run with --fresh (truncates tables first) or --allow-protected to include them.")}}models=sortModelsByDependencies(models);if(verbose)log.info(`Found ${models.length} seedable model(s)`);if(config.fresh)await clearTables(models,verbose);const results=[];for(const model of models){if(verbose)log.info(`Seeding ${model.name} (${model.count} records)...`);try{const result=await seedModel(model,config);results.push(result)}catch(err){const errorMessage=err instanceof Error?err.message:String(err);if(verbose)log.error(` Unexpected error seeding ${model.name}: ${errorMessage}`);results.push({model:model.name,table:model.table,count:0,success:!1,error:errorMessage,duration:0})}}const successful=results.filter((r)=>r.success).length,failed=results.filter((r)=>!r.success).length,totalRecords=results.reduce((sum,r)=>sum+r.count,0);if(verbose){log.info("");if(failed===0){log.success("Database seeded successfully!");log.info(` Total records: ${totalRecords}`);log.info(` Models seeded: ${successful}`)}else{log.warn(`Seeding completed with ${failed} failure(s)`);log.info(` Successful: ${successful}`);log.info(` Failed: ${failed}`)}}return{total:results.length,successful,failed,results,duration:Date.now()-startTime}}export async function seedModel$(modelName,options={}){const modelsDir=path.userModelsPath(),model=(await loadAllModels(modelsDir,options.verbose)).find((m)=>m.name===modelName);if(!model)throw Error(`Model not found: ${modelName}`);if(options.count)model.count=options.count;return seedModel(model,{fresh:options.fresh,verbose:options.verbose??!0})}export async function freshSeed(config={}){return seed({...config,fresh:!0})}export async function listSeedableModels(){const modelsDir=path.userModelsPath(),defaultDir=defaultModelsPath(),defaultModels=await loadModelsFromDir(defaultDir,!0),userModels=await loadModelsFromDir(modelsDir,!1),result=[],seen=new Set;for(const m of userModels){result.push({name:m.name,table:m.table,count:m.count,source:"user"});seen.add(m.name)}for(const m of defaultModels)if(!seen.has(m.name))result.push({name:m.name,table:m.table,count:m.count,source:"default"});return result}export{seed as runSeeders};export{freshSeed as freshWithSeed};
@@ -1,50 +1 @@
1
- import { dialectCapabilities } from "./dialect";
2
- export function sqlDateTime(value = new Date) {
3
- return value.toISOString().slice(0, -1);
4
- }
5
- export function sqlDateTimeLiteral(value = new Date) {
6
- return `'${sqlDateTime(value)}'`;
7
- }
8
- export function parseSqlDateTime(value) {
9
- if (value === null || value === void 0)
10
- return null;
11
- if (value instanceof Date)
12
- return Number.isNaN(value.getTime()) ? null : value;
13
- if (typeof value === "number")
14
- return Number.isNaN(value) ? null : new Date(value);
15
- if (typeof value !== "string")
16
- return null;
17
- const trimmed = value.trim();
18
- if (!trimmed)
19
- return null;
20
- let normalized = trimmed.replace(" ", "T");
21
- if (!/(?:Z|[+-]\d{2}:?\d{2})$/i.test(normalized))
22
- normalized += "Z";
23
- const parsed = new Date(normalized);
24
- return Number.isNaN(parsed.getTime()) ? null : parsed;
25
- }
26
- export function sqlHelpers(driver) {
27
- const caps = dialectCapabilities(driver), isPostgres = caps.wire === "postgres", isMysql = caps.wire === "mysql", isSqlite = caps.wire === "sqlite";
28
- return {
29
- driver,
30
- isPostgres,
31
- isMysql,
32
- isSqlite,
33
- now: isPostgres || isMysql ? "NOW()" : "datetime('now')",
34
- boolTrue: isPostgres ? "true" : "1",
35
- boolFalse: isPostgres ? "false" : "0",
36
- autoIncrement: isPostgres ? "SERIAL" : "INTEGER",
37
- primaryKey: !caps.supportsAutoIncrement ? "PRIMARY KEY" : isPostgres ? "PRIMARY KEY" : isMysql ? "PRIMARY KEY AUTO_INCREMENT" : "PRIMARY KEY AUTOINCREMENT",
38
- pkColumn: !caps.supportsAutoIncrement ? "id BIGINT NOT NULL PRIMARY KEY" : isPostgres ? "id SERIAL PRIMARY KEY" : isMysql ? "id INTEGER PRIMARY KEY AUTO_INCREMENT" : "id INTEGER PRIMARY KEY AUTOINCREMENT",
39
- datetime: isMysql ? "DATETIME" : "TIMESTAMP",
40
- nullableTimestamp: isMysql ? "DATETIME NULL" : "TIMESTAMP",
41
- param(index) {
42
- return isPostgres ? `$${index}` : "?";
43
- },
44
- params(...values) {
45
- if (isPostgres)
46
- return { sql: values.map((_, i) => `$${i + 1}`).join(", "), values };
47
- return { sql: values.map(() => "?").join(", "), values };
48
- }
49
- };
50
- }
1
+ import{dialectCapabilities}from"./dialect";export function sqlDateTime(value=new Date){return value.toISOString().slice(0,-1)}export function sqlDateTimeLiteral(value=new Date){return`'${sqlDateTime(value)}'`}export function parseSqlDateTime(value){if(value===null||value===void 0)return null;if(value instanceof Date)return Number.isNaN(value.getTime())?null:value;if(typeof value==="number")return Number.isNaN(value)?null:new Date(value);if(typeof value!=="string")return null;const trimmed=value.trim();if(!trimmed)return null;let normalized=trimmed.replace(" ","T");if(!/(?:Z|[+-]\d{2}:?\d{2})$/i.test(normalized))normalized+="Z";const parsed=new Date(normalized);return Number.isNaN(parsed.getTime())?null:parsed}export function sqlHelpers(driver){const caps=dialectCapabilities(driver),isPostgres=caps.wire==="postgres",isMysql=caps.wire==="mysql",isSqlite=caps.wire==="sqlite";return{driver,isPostgres,isMysql,isSqlite,now:isPostgres||isMysql?"NOW()":"datetime('now')",boolTrue:isPostgres?"true":"1",boolFalse:isPostgres?"false":"0",autoIncrement:isPostgres?"SERIAL":"INTEGER",primaryKey:!caps.supportsAutoIncrement?"PRIMARY KEY":isPostgres?"PRIMARY KEY":isMysql?"PRIMARY KEY AUTO_INCREMENT":"PRIMARY KEY AUTOINCREMENT",pkColumn:!caps.supportsAutoIncrement?"id BIGINT NOT NULL PRIMARY KEY":isPostgres?"id SERIAL PRIMARY KEY":isMysql?"id INTEGER PRIMARY KEY AUTO_INCREMENT":"id INTEGER PRIMARY KEY AUTOINCREMENT",datetime:isMysql?"DATETIME":"TIMESTAMP",nullableTimestamp:isMysql?"DATETIME NULL":"TIMESTAMP",param(index){return isPostgres?`$${index}`:"?"},params(...values){if(isPostgres)return{sql:values.map((_,i)=>`$${i+1}`).join(", "),values};return{sql:values.map(()=>"?").join(", "),values}}}}
package/dist/table.js CHANGED
@@ -1,26 +1 @@
1
- import { log } from "@stacksjs/logging";
2
- import { Column } from "./column";
3
-
4
- export class Table {
5
- columns = [];
6
- increments(name) {
7
- const column = new Column(name, "integer", {
8
- primaryKey: !0,
9
- autoIncrement: !0
10
- });
11
- this.columns.push(column);
12
- return column;
13
- }
14
- string(name, varchar = 255) {
15
- const column = new Column(name, `varchar(${varchar})`);
16
- this.columns.push(column);
17
- return column;
18
- }
19
- timestamps() {
20
- this.columns.push(new Column("created_at", "timestamp"));
21
- this.columns.push(new Column("updated_at", "timestamp"));
22
- }
23
- execute() {
24
- log.info(`Creating table with columns: ${this.columns.map((col) => col.name).join(", ")}`);
25
- }
26
- }
1
+ import{log}from"@stacksjs/logging";import{Column}from"./column";export class Table{columns=[];increments(name){const column=new Column(name,"integer",{primaryKey:!0,autoIncrement:!0});this.columns.push(column);return column}string(name,varchar=255){const column=new Column(name,`varchar(${varchar})`);this.columns.push(column);return column}timestamps(){this.columns.push(new Column("created_at","timestamp"));this.columns.push(new Column("updated_at","timestamp"))}execute(){log.info(`Creating table with columns: ${this.columns.map((col)=>col.name).join(", ")}`)}}
@@ -1,6 +1 @@
1
- import { dynamoDb } from "dynamodb-tooling";
2
- const port = 8000;
3
- dynamoDb.launch({
4
- port,
5
- additionalArgs: ["-sharedDb"]
6
- });
1
+ import{dynamoDb}from"dynamodb-tooling";const port=8000;dynamoDb.launch({port,additionalArgs:["-sharedDb"]});