@stacksjs/database 0.70.187 → 0.70.188

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/seeder.d.ts CHANGED
@@ -5,6 +5,14 @@ import type { Attribute, Model } from '@stacksjs/types';
5
5
  * list stays a single source of truth.
6
6
  */
7
7
  export declare function isProtectedModel(name: string): boolean;
8
+ /**
9
+ * Choose one consistent set of parent ids per record.
10
+ *
11
+ * Split out from the query above so the rule can be exercised without a
12
+ * database: given the parent rows, the same input always produces rows whose
13
+ * foreign keys agree with one another.
14
+ */
15
+ export declare function chooseRelations(pools: { column: string, rows: Record<string, unknown>[] }[], count: number): Record<string, unknown>[];
8
16
  /**
9
17
  * Seeds the database from your models.
10
18
  *
package/dist/seeder.js CHANGED
@@ -87,7 +87,7 @@ function isPasswordField(fieldName, attr) {
87
87
  return !0;
88
88
  return !1;
89
89
  }
90
- async function generateRecord(attributes, modelName, verbose = !1) {
90
+ async function generateRecord(attributes, modelName, report = !1) {
91
91
  const record = {};
92
92
  for (const [fieldName, attr] of Object.entries(attributes)) {
93
93
  const columnName = snakeCase(fieldName);
@@ -97,8 +97,8 @@ async function generateRecord(attributes, modelName, verbose = !1) {
97
97
  value = attr.factory(faker);
98
98
  } catch (err) {
99
99
  const errorMsg = err instanceof Error ? err.message : String(err);
100
- if (verbose)
101
- log.warn(` Factory failed for ${modelName}.${fieldName}: ${errorMsg}`);
100
+ if (report)
101
+ log.warn(` Factory failed for ${modelName}.${fieldName}: ${errorMsg} \u2014 seeding the default instead.`);
102
102
  if (attr.default !== void 0)
103
103
  value = attr.default;
104
104
  else
@@ -113,8 +113,7 @@ async function generateRecord(attributes, modelName, verbose = !1) {
113
113
  value = await hashMake(value, { algorithm: "bcrypt" });
114
114
  } catch (err) {
115
115
  const errorMsg = err instanceof Error ? err.message : String(err);
116
- if (verbose)
117
- log.warn(` Failed to hash password for ${modelName}.${fieldName}: ${errorMsg}`);
116
+ log.warn(` Failed to hash password for ${modelName}.${fieldName}: ${errorMsg}`);
118
117
  }
119
118
  record[columnName] = value;
120
119
  }
@@ -140,11 +139,54 @@ function fixtureToColumns(fixture) {
140
139
  out[snakeCase(key)] = value;
141
140
  return out;
142
141
  }
143
- async function generateRecords(model, verbose = !1) {
144
- const records = [];
142
+ async function existingRows(table) {
143
+ try {
144
+ return await db.selectFrom(table).selectAll().limit(500).execute();
145
+ } catch {
146
+ return [];
147
+ }
148
+ }
149
+ async function relationColumns(model) {
150
+ const parents = parentModels(model);
151
+ if (parents.length === 0)
152
+ return [];
153
+ const pools = [];
154
+ for (const parent of parents) {
155
+ const column = `${snakeCase(parent)}_id`;
156
+ if (model.attributes[column] || model.attributes[parent])
157
+ continue;
158
+ const rows = await existingRows(`${snakeCase(parent)}s`);
159
+ if (rows.length > 0)
160
+ pools.push({ column, rows });
161
+ }
162
+ return chooseRelations(pools, model.count);
163
+ }
164
+ export function chooseRelations(pools, count) {
165
+ if (pools.length === 0)
166
+ return [];
167
+ const wanted = new Set(pools.map((pool) => pool.column)), specificity = (pool) => {
168
+ const sample = pool.rows[0] ?? {};
169
+ return [...wanted].filter((column) => column !== pool.column && (column in sample)).length;
170
+ }, ordered = [...pools].sort((a, b) => specificity(b) - specificity(a));
171
+ return Array.from({ length: count }, () => {
172
+ const row = {};
173
+ for (const pool of ordered) {
174
+ if (row[pool.column] != null)
175
+ continue;
176
+ 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)];
177
+ row[pool.column] = chosen.id;
178
+ for (const column of wanted)
179
+ if (column !== pool.column && row[column] == null && chosen[column] != null)
180
+ row[column] = chosen[column];
181
+ }
182
+ return row;
183
+ });
184
+ }
185
+ async function generateRecords(model) {
186
+ const records = [], relations = await relationColumns(model);
145
187
  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);
188
+ const record = await generateRecord(model.attributes, model.name, i === 0), fixture = model.fixtures[i], withRelations = { ...record, ...relations[i] ?? {} };
189
+ records.push(fixture ? { ...withRelations, ...fixtureToColumns(fixture) } : withRelations);
148
190
  }
149
191
  return records;
150
192
  }
@@ -180,7 +222,7 @@ async function seedModel(model, options) {
180
222
  };
181
223
  }
182
224
  }
183
- const records = await generateRecords(model, options.verbose);
225
+ const records = await generateRecords(model);
184
226
  if (records.length === 0)
185
227
  return {
186
228
  model: model.name,
@@ -189,12 +231,6 @@ async function seedModel(model, options) {
189
231
  success: !0,
190
232
  duration: Date.now() - startTime
191
233
  };
192
- if (options.fresh)
193
- try {
194
- await db.deleteFrom(model.table).execute();
195
- if (options.verbose)
196
- log.info(` Truncated table: ${model.table}`);
197
- } catch {}
198
234
  const batchSize = 100;
199
235
  let inserted = 0;
200
236
  for (let i = 0;i < records.length; i += batchSize) {
@@ -225,16 +261,44 @@ async function seedModel(model, options) {
225
261
  };
226
262
  }
227
263
  }
264
+ function parentModels(model) {
265
+ const belongsTo = model.model.belongsTo;
266
+ if (Array.isArray(belongsTo))
267
+ return belongsTo.map((entry) => typeof entry === "string" ? entry : String(entry?.model ?? "")).filter(Boolean);
268
+ if (belongsTo && typeof belongsTo === "object")
269
+ return Object.values(belongsTo).map((value) => String(value?.model ?? value)).filter(Boolean);
270
+ return [];
271
+ }
272
+ async function clearTables(models, verbose) {
273
+ for (const model of [...models].reverse())
274
+ try {
275
+ await db.deleteFrom(model.table).execute();
276
+ if (verbose)
277
+ log.info(` Truncated table: ${model.table}`);
278
+ } catch (error) {
279
+ const message = error instanceof Error ? error.message : String(error);
280
+ if (/no such table|doesn't exist|does not exist/i.test(message))
281
+ continue;
282
+ throw Error(`Could not empty ${model.table} before seeding: ${message}`);
283
+ }
284
+ }
228
285
  function sortModelsByDependencies(models) {
229
- const priority = {
230
- User: 0,
231
- Team: 1,
232
- Project: 2
286
+ const byName = new Map(models.map((model) => [model.name, model])), ordered = [], state = new Map, visit = (model) => {
287
+ const status = state.get(model.name);
288
+ if (status === "done" || status === "visiting")
289
+ return;
290
+ state.set(model.name, "visiting");
291
+ for (const parentName of parentModels(model)) {
292
+ const parent = byName.get(parentName);
293
+ if (parent && parent !== model)
294
+ visit(parent);
295
+ }
296
+ state.set(model.name, "done");
297
+ ordered.push(model);
233
298
  };
234
- return models.sort((a, b) => {
235
- const priorityA = priority[a.name] ?? 10, priorityB = priority[b.name] ?? 10;
236
- return priorityA - priorityB;
237
- });
299
+ for (const model of models)
300
+ visit(model);
301
+ return ordered;
238
302
  }
239
303
  export async function seed(config = {}) {
240
304
  const startTime = Date.now();
@@ -277,6 +341,8 @@ export async function seed(config = {}) {
277
341
  models = sortModelsByDependencies(models);
278
342
  if (verbose)
279
343
  log.info(`Found ${models.length} seedable model(s)`);
344
+ if (config.fresh)
345
+ await clearTables(models, verbose);
280
346
  const results = [];
281
347
  for (const model of models) {
282
348
  if (verbose)
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/database",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.187",
5
+ "version": "0.70.188",
6
6
  "description": "The Stacks database integration.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -60,15 +60,15 @@
60
60
  "dynamodb-tooling": "^0.3.2"
61
61
  },
62
62
  "devDependencies": {
63
- "@stacksjs/cli": "0.70.187",
64
- "@stacksjs/config": "0.70.187",
65
- "@stacksjs/logging": "0.70.187",
66
- "@stacksjs/router": "0.70.187",
63
+ "@stacksjs/cli": "0.70.188",
64
+ "@stacksjs/config": "0.70.188",
65
+ "@stacksjs/logging": "0.70.188",
66
+ "@stacksjs/router": "0.70.188",
67
67
  "better-dx": "^0.2.17",
68
- "@stacksjs/path": "0.70.187",
69
- "@stacksjs/query-builder": "0.70.187",
70
- "@stacksjs/storage": "0.70.187",
71
- "@stacksjs/strings": "0.70.187",
72
- "@stacksjs/utils": "0.70.187"
68
+ "@stacksjs/path": "0.70.188",
69
+ "@stacksjs/query-builder": "0.70.188",
70
+ "@stacksjs/storage": "0.70.188",
71
+ "@stacksjs/strings": "0.70.188",
72
+ "@stacksjs/utils": "0.70.188"
73
73
  }
74
74
  }