webspresso 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +564 -0
- package/bin/webspresso.js +255 -0
- package/core/applySchema.js +49 -0
- package/core/compileSchema.js +69 -0
- package/core/orm/eager-loader.js +232 -0
- package/core/orm/index.js +148 -0
- package/core/orm/migrations/index.js +205 -0
- package/core/orm/migrations/scaffold.js +312 -0
- package/core/orm/model.js +178 -0
- package/core/orm/query-builder.js +430 -0
- package/core/orm/repository.js +346 -0
- package/core/orm/schema-helpers.js +416 -0
- package/core/orm/scopes.js +183 -0
- package/core/orm/seeder.js +585 -0
- package/core/orm/transaction.js +69 -0
- package/core/orm/types.js +237 -0
- package/core/orm/utils.js +127 -0
- package/index.js +13 -1
- package/package.json +29 -5
- package/src/plugin-manager.js +2 -0
- package/utils/schemaCache.js +60 -0
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto Seeder
|
|
3
|
+
* Generate fake data based on Zod schema metadata
|
|
4
|
+
* Uses @faker-js/faker for data generation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { getModel, getAllModels } = require('./model');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} SeederOptions
|
|
11
|
+
* @property {number} [count=10] - Number of records to create
|
|
12
|
+
* @property {Object} [overrides={}] - Override specific fields
|
|
13
|
+
* @property {Object} [with={}] - Create related records { posts: 3 }
|
|
14
|
+
* @property {Object} [generators={}] - Custom generators per field
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} FactoryDefinition
|
|
19
|
+
* @property {Object} model - Model definition
|
|
20
|
+
* @property {Object} [defaults={}] - Default overrides
|
|
21
|
+
* @property {Object} [generators={}] - Custom field generators
|
|
22
|
+
* @property {Object} [states={}] - Named state modifiers
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a seeder instance
|
|
27
|
+
* @param {Object} faker - Faker instance (@faker-js/faker)
|
|
28
|
+
* @param {Object} knex - Knex instance
|
|
29
|
+
* @returns {Object} Seeder API
|
|
30
|
+
*/
|
|
31
|
+
function createSeeder(faker, knex) {
|
|
32
|
+
if (!faker) {
|
|
33
|
+
throw new Error('Faker instance is required. Install @faker-js/faker and pass it to createSeeder.');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @type {Map<string, FactoryDefinition>} */
|
|
37
|
+
const factories = new Map();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Generate fake value based on column metadata
|
|
41
|
+
* @param {string} columnName - Column name
|
|
42
|
+
* @param {Object} meta - Column metadata
|
|
43
|
+
* @returns {*} Generated value
|
|
44
|
+
*/
|
|
45
|
+
function generateValue(columnName, meta) {
|
|
46
|
+
// Skip auto-generated fields
|
|
47
|
+
if (meta.autoIncrement || meta.auto === 'create' || meta.auto === 'update') {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Handle nullable with 10% chance of null
|
|
52
|
+
if (meta.nullable && faker.datatype.boolean({ probability: 0.1 })) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Use default if defined (50% chance)
|
|
57
|
+
if (meta.default !== undefined && faker.datatype.boolean({ probability: 0.5 })) {
|
|
58
|
+
return meta.default;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Smart field detection by name
|
|
62
|
+
const lowerName = columnName.toLowerCase();
|
|
63
|
+
|
|
64
|
+
// Email detection
|
|
65
|
+
if (lowerName.includes('email')) {
|
|
66
|
+
return faker.internet.email().toLowerCase();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Name detection
|
|
70
|
+
if (lowerName === 'name' || lowerName === 'full_name' || lowerName === 'fullname') {
|
|
71
|
+
return faker.person.fullName();
|
|
72
|
+
}
|
|
73
|
+
if (lowerName === 'first_name' || lowerName === 'firstname') {
|
|
74
|
+
return faker.person.firstName();
|
|
75
|
+
}
|
|
76
|
+
if (lowerName === 'last_name' || lowerName === 'lastname') {
|
|
77
|
+
return faker.person.lastName();
|
|
78
|
+
}
|
|
79
|
+
if (lowerName === 'username' || lowerName === 'user_name') {
|
|
80
|
+
return faker.internet.username().toLowerCase();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Title/Slug detection
|
|
84
|
+
if (lowerName === 'title') {
|
|
85
|
+
return faker.lorem.sentence({ min: 3, max: 8 }).slice(0, -1);
|
|
86
|
+
}
|
|
87
|
+
if (lowerName === 'slug') {
|
|
88
|
+
return faker.lorem.slug({ min: 2, max: 4 });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Content detection
|
|
92
|
+
if (lowerName === 'content' || lowerName === 'body' || lowerName === 'description') {
|
|
93
|
+
return faker.lorem.paragraphs({ min: 1, max: 3 });
|
|
94
|
+
}
|
|
95
|
+
if (lowerName === 'bio' || lowerName === 'about') {
|
|
96
|
+
return faker.lorem.paragraph();
|
|
97
|
+
}
|
|
98
|
+
if (lowerName === 'summary' || lowerName === 'excerpt') {
|
|
99
|
+
return faker.lorem.sentence();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// URL detection
|
|
103
|
+
if (lowerName.includes('url') || lowerName.includes('link')) {
|
|
104
|
+
return faker.internet.url();
|
|
105
|
+
}
|
|
106
|
+
if (lowerName === 'avatar' || lowerName === 'image' || lowerName === 'photo') {
|
|
107
|
+
return faker.image.avatar();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Phone detection
|
|
111
|
+
if (lowerName.includes('phone') || lowerName.includes('tel')) {
|
|
112
|
+
return faker.phone.number();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Address detection
|
|
116
|
+
if (lowerName === 'address' || lowerName === 'street') {
|
|
117
|
+
return faker.location.streetAddress();
|
|
118
|
+
}
|
|
119
|
+
if (lowerName === 'city') {
|
|
120
|
+
return faker.location.city();
|
|
121
|
+
}
|
|
122
|
+
if (lowerName === 'country') {
|
|
123
|
+
return faker.location.country();
|
|
124
|
+
}
|
|
125
|
+
if (lowerName === 'zip' || lowerName === 'zipcode' || lowerName === 'postal_code') {
|
|
126
|
+
return faker.location.zipCode();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Company detection
|
|
130
|
+
if (lowerName === 'company' || lowerName === 'company_name') {
|
|
131
|
+
return faker.company.name();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Price/Amount detection
|
|
135
|
+
if (lowerName.includes('price') || lowerName.includes('amount') || lowerName.includes('cost')) {
|
|
136
|
+
return parseFloat(faker.commerce.price({ min: 10, max: 1000 }));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Count/Quantity detection
|
|
140
|
+
if (lowerName.includes('count') || lowerName.includes('quantity') || lowerName.includes('qty')) {
|
|
141
|
+
return faker.number.int({ min: 1, max: 100 });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Status detection
|
|
145
|
+
if (lowerName === 'status') {
|
|
146
|
+
return faker.helpers.arrayElement(['active', 'inactive', 'pending']);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Generate by type
|
|
150
|
+
return generateByType(meta);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Generate value by column type
|
|
155
|
+
* @param {Object} meta - Column metadata
|
|
156
|
+
* @returns {*} Generated value
|
|
157
|
+
*/
|
|
158
|
+
function generateByType(meta) {
|
|
159
|
+
switch (meta.type) {
|
|
160
|
+
case 'bigint':
|
|
161
|
+
case 'integer':
|
|
162
|
+
return faker.number.int({ min: 1, max: 10000 });
|
|
163
|
+
|
|
164
|
+
case 'float':
|
|
165
|
+
return faker.number.float({ min: 0, max: 1000, fractionDigits: 2 });
|
|
166
|
+
|
|
167
|
+
case 'decimal':
|
|
168
|
+
const precision = meta.precision || 10;
|
|
169
|
+
const scale = meta.scale || 2;
|
|
170
|
+
const max = Math.pow(10, precision - scale) - 1;
|
|
171
|
+
return parseFloat(faker.number.float({ min: 0, max, fractionDigits: scale }).toFixed(scale));
|
|
172
|
+
|
|
173
|
+
case 'boolean':
|
|
174
|
+
return faker.datatype.boolean();
|
|
175
|
+
|
|
176
|
+
case 'date':
|
|
177
|
+
return faker.date.past().toISOString().split('T')[0];
|
|
178
|
+
|
|
179
|
+
case 'datetime':
|
|
180
|
+
case 'timestamp':
|
|
181
|
+
return faker.date.past().toISOString();
|
|
182
|
+
|
|
183
|
+
case 'uuid':
|
|
184
|
+
return faker.string.uuid();
|
|
185
|
+
|
|
186
|
+
case 'json':
|
|
187
|
+
return { key: faker.lorem.word(), value: faker.lorem.sentence() };
|
|
188
|
+
|
|
189
|
+
case 'enum':
|
|
190
|
+
if (meta.enumValues && meta.enumValues.length > 0) {
|
|
191
|
+
return faker.helpers.arrayElement(meta.enumValues);
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
|
|
195
|
+
case 'text':
|
|
196
|
+
return faker.lorem.paragraphs({ min: 1, max: 3 });
|
|
197
|
+
|
|
198
|
+
case 'string':
|
|
199
|
+
default:
|
|
200
|
+
const maxLength = meta.maxLength || 255;
|
|
201
|
+
const text = faker.lorem.sentence();
|
|
202
|
+
return text.length > maxLength ? text.substring(0, maxLength) : text;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Generate a single record for a model
|
|
208
|
+
* @param {Object} model - Model definition
|
|
209
|
+
* @param {Object} overrides - Field overrides
|
|
210
|
+
* @param {Object} generators - Custom generators
|
|
211
|
+
* @returns {Object} Generated record
|
|
212
|
+
*/
|
|
213
|
+
function generateRecord(model, overrides = {}, generators = {}) {
|
|
214
|
+
const record = {};
|
|
215
|
+
|
|
216
|
+
if (!model.columns) {
|
|
217
|
+
throw new Error(`Model "${model.name}" has no columns defined`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const [columnName, meta] of model.columns) {
|
|
221
|
+
// Skip if overridden
|
|
222
|
+
if (columnName in overrides) {
|
|
223
|
+
record[columnName] = overrides[columnName];
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Use custom generator if provided
|
|
228
|
+
if (columnName in generators) {
|
|
229
|
+
const generator = generators[columnName];
|
|
230
|
+
record[columnName] = typeof generator === 'function' ? generator(faker) : generator;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Skip foreign keys (will be handled by relations)
|
|
235
|
+
if (meta.references) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Generate value
|
|
240
|
+
const value = generateValue(columnName, meta);
|
|
241
|
+
if (value !== undefined) {
|
|
242
|
+
record[columnName] = value;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return record;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Define a factory for a model
|
|
251
|
+
* @param {string|Object} modelOrName - Model or model name
|
|
252
|
+
* @param {Object} options - Factory options
|
|
253
|
+
* @returns {Object} Factory builder
|
|
254
|
+
*/
|
|
255
|
+
function defineFactory(modelOrName, options = {}) {
|
|
256
|
+
const model = typeof modelOrName === 'string' ? getModel(modelOrName) : modelOrName;
|
|
257
|
+
|
|
258
|
+
if (!model) {
|
|
259
|
+
throw new Error(`Model not found: ${modelOrName}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const factory = {
|
|
263
|
+
model,
|
|
264
|
+
defaults: options.defaults || {},
|
|
265
|
+
generators: options.generators || {},
|
|
266
|
+
states: options.states || {},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
factories.set(model.name, factory);
|
|
270
|
+
|
|
271
|
+
return createFactoryBuilder(factory);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get factory for a model
|
|
276
|
+
* @param {string|Object} modelOrName - Model or model name
|
|
277
|
+
* @returns {Object} Factory builder
|
|
278
|
+
*/
|
|
279
|
+
function factory(modelOrName) {
|
|
280
|
+
const modelName = typeof modelOrName === 'string' ? modelOrName : modelOrName.name;
|
|
281
|
+
|
|
282
|
+
let factoryDef = factories.get(modelName);
|
|
283
|
+
|
|
284
|
+
// Auto-create factory if not defined
|
|
285
|
+
if (!factoryDef) {
|
|
286
|
+
const model = typeof modelOrName === 'string' ? getModel(modelOrName) : modelOrName;
|
|
287
|
+
if (!model) {
|
|
288
|
+
throw new Error(`Model not found: ${modelName}`);
|
|
289
|
+
}
|
|
290
|
+
factoryDef = { model, defaults: {}, generators: {}, states: {} };
|
|
291
|
+
factories.set(modelName, factoryDef);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return createFactoryBuilder(factoryDef);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Create factory builder with fluent API
|
|
299
|
+
* @param {FactoryDefinition} factoryDef - Factory definition
|
|
300
|
+
* @returns {Object} Factory builder
|
|
301
|
+
*/
|
|
302
|
+
function createFactoryBuilder(factoryDef) {
|
|
303
|
+
let currentOverrides = { ...factoryDef.defaults };
|
|
304
|
+
let currentGenerators = { ...factoryDef.generators };
|
|
305
|
+
let currentRelations = {};
|
|
306
|
+
let activeStates = [];
|
|
307
|
+
|
|
308
|
+
const builder = {
|
|
309
|
+
/**
|
|
310
|
+
* Apply a named state
|
|
311
|
+
* @param {string} stateName - State name
|
|
312
|
+
* @returns {Object} Builder
|
|
313
|
+
*/
|
|
314
|
+
state(stateName) {
|
|
315
|
+
if (factoryDef.states[stateName]) {
|
|
316
|
+
activeStates.push(stateName);
|
|
317
|
+
const stateConfig = factoryDef.states[stateName];
|
|
318
|
+
if (typeof stateConfig === 'function') {
|
|
319
|
+
const stateOverrides = stateConfig(faker);
|
|
320
|
+
currentOverrides = { ...currentOverrides, ...stateOverrides };
|
|
321
|
+
} else {
|
|
322
|
+
currentOverrides = { ...currentOverrides, ...stateConfig };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return builder;
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Override specific fields
|
|
330
|
+
* @param {Object} overrides - Field overrides
|
|
331
|
+
* @returns {Object} Builder
|
|
332
|
+
*/
|
|
333
|
+
override(overrides) {
|
|
334
|
+
currentOverrides = { ...currentOverrides, ...overrides };
|
|
335
|
+
return builder;
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Add custom generators
|
|
340
|
+
* @param {Object} generators - Custom generators
|
|
341
|
+
* @returns {Object} Builder
|
|
342
|
+
*/
|
|
343
|
+
generators(generators) {
|
|
344
|
+
currentGenerators = { ...currentGenerators, ...generators };
|
|
345
|
+
return builder;
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Create related records
|
|
350
|
+
* @param {string} relationName - Relation name
|
|
351
|
+
* @param {number} [count=1] - Number of related records
|
|
352
|
+
* @returns {Object} Builder
|
|
353
|
+
*/
|
|
354
|
+
with(relationName, count = 1) {
|
|
355
|
+
currentRelations[relationName] = count;
|
|
356
|
+
return builder;
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Generate record(s) without saving
|
|
361
|
+
* @param {number} [count=1] - Number of records
|
|
362
|
+
* @returns {Object|Object[]} Generated record(s)
|
|
363
|
+
*/
|
|
364
|
+
make(count = 1) {
|
|
365
|
+
const records = [];
|
|
366
|
+
for (let i = 0; i < count; i++) {
|
|
367
|
+
records.push(generateRecord(factoryDef.model, currentOverrides, currentGenerators));
|
|
368
|
+
}
|
|
369
|
+
return count === 1 ? records[0] : records;
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Create and save record(s)
|
|
374
|
+
* @param {number} [count=1] - Number of records
|
|
375
|
+
* @returns {Promise<Object|Object[]>} Created record(s)
|
|
376
|
+
*/
|
|
377
|
+
async create(count = 1) {
|
|
378
|
+
const records = [];
|
|
379
|
+
const model = factoryDef.model;
|
|
380
|
+
|
|
381
|
+
for (let i = 0; i < count; i++) {
|
|
382
|
+
const record = generateRecord(model, currentOverrides, currentGenerators);
|
|
383
|
+
|
|
384
|
+
// Handle belongsTo relations first (need parent IDs)
|
|
385
|
+
if (model.relations) {
|
|
386
|
+
for (const [relName, relation] of Object.entries(model.relations)) {
|
|
387
|
+
if (relation.type === 'belongsTo' && relation.foreignKey) {
|
|
388
|
+
// Check if foreign key is already set
|
|
389
|
+
if (record[relation.foreignKey]) continue;
|
|
390
|
+
|
|
391
|
+
// Check if we should create related or skip
|
|
392
|
+
const relatedModel = relation.model();
|
|
393
|
+
if (relatedModel) {
|
|
394
|
+
const parent = await factory(relatedModel).create();
|
|
395
|
+
record[relation.foreignKey] = parent[relatedModel.primaryKey || 'id'];
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Insert record
|
|
402
|
+
const [insertedId] = await knex(model.table).insert(record);
|
|
403
|
+
const primaryKey = model.primaryKey || 'id';
|
|
404
|
+
const id = record[primaryKey] || insertedId;
|
|
405
|
+
|
|
406
|
+
// Fetch the created record
|
|
407
|
+
const created = await knex(model.table).where(primaryKey, id).first();
|
|
408
|
+
|
|
409
|
+
// Handle hasMany relations
|
|
410
|
+
if (model.relations) {
|
|
411
|
+
for (const [relName, count] of Object.entries(currentRelations)) {
|
|
412
|
+
const relation = model.relations[relName];
|
|
413
|
+
if (relation && relation.type === 'hasMany') {
|
|
414
|
+
const relatedModel = relation.model();
|
|
415
|
+
if (relatedModel) {
|
|
416
|
+
const children = await factory(relatedModel)
|
|
417
|
+
.override({ [relation.foreignKey]: created[primaryKey] })
|
|
418
|
+
.create(count);
|
|
419
|
+
created[relName] = Array.isArray(children) ? children : [children];
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
records.push(created);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return count === 1 ? records[0] : records;
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Create records in a transaction
|
|
433
|
+
* @param {number} [count=1] - Number of records
|
|
434
|
+
* @returns {Promise<Object|Object[]>} Created record(s)
|
|
435
|
+
*/
|
|
436
|
+
async createInTransaction(count = 1) {
|
|
437
|
+
return knex.transaction(async (trx) => {
|
|
438
|
+
const originalKnex = knex;
|
|
439
|
+
// Temporarily replace knex with transaction
|
|
440
|
+
const records = [];
|
|
441
|
+
const model = factoryDef.model;
|
|
442
|
+
|
|
443
|
+
for (let i = 0; i < count; i++) {
|
|
444
|
+
const record = generateRecord(model, currentOverrides, currentGenerators);
|
|
445
|
+
|
|
446
|
+
// Handle belongsTo relations
|
|
447
|
+
if (model.relations) {
|
|
448
|
+
for (const [relName, relation] of Object.entries(model.relations)) {
|
|
449
|
+
if (relation.type === 'belongsTo' && relation.foreignKey) {
|
|
450
|
+
if (record[relation.foreignKey]) continue;
|
|
451
|
+
const relatedModel = relation.model();
|
|
452
|
+
if (relatedModel) {
|
|
453
|
+
const parentRecord = generateRecord(relatedModel, {}, {});
|
|
454
|
+
const [parentId] = await trx(relatedModel.table).insert(parentRecord);
|
|
455
|
+
record[relation.foreignKey] = parentRecord[relatedModel.primaryKey || 'id'] || parentId;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const [insertedId] = await trx(model.table).insert(record);
|
|
462
|
+
const primaryKey = model.primaryKey || 'id';
|
|
463
|
+
const id = record[primaryKey] || insertedId;
|
|
464
|
+
const created = await trx(model.table).where(primaryKey, id).first();
|
|
465
|
+
records.push(created);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return count === 1 ? records[0] : records;
|
|
469
|
+
});
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
return builder;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Seed a model with records
|
|
478
|
+
* @param {string|Object} modelOrName - Model or model name
|
|
479
|
+
* @param {number} count - Number of records
|
|
480
|
+
* @param {Object} [options={}] - Seed options
|
|
481
|
+
* @returns {Promise<Object[]>} Created records
|
|
482
|
+
*/
|
|
483
|
+
async function seed(modelOrName, count, options = {}) {
|
|
484
|
+
const builder = factory(modelOrName);
|
|
485
|
+
|
|
486
|
+
if (options.overrides) {
|
|
487
|
+
builder.override(options.overrides);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (options.generators) {
|
|
491
|
+
builder.generators(options.generators);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (options.state) {
|
|
495
|
+
builder.state(options.state);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const records = await builder.create(count);
|
|
499
|
+
return Array.isArray(records) ? records : [records];
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Run multiple seeders
|
|
504
|
+
* @param {Object[]} seeders - Array of seeder configs
|
|
505
|
+
* @returns {Promise<Object>} Results by model name
|
|
506
|
+
*/
|
|
507
|
+
async function run(seeders) {
|
|
508
|
+
const results = {};
|
|
509
|
+
|
|
510
|
+
for (const seederConfig of seeders) {
|
|
511
|
+
const { model, count = 10, ...options } = seederConfig;
|
|
512
|
+
const modelName = typeof model === 'string' ? model : model.name;
|
|
513
|
+
results[modelName] = await seed(model, count, options);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return results;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Truncate table(s)
|
|
521
|
+
* @param {string|string[]|Object|Object[]} models - Model(s) to truncate
|
|
522
|
+
* @returns {Promise<void>}
|
|
523
|
+
*/
|
|
524
|
+
async function truncate(models) {
|
|
525
|
+
const modelList = Array.isArray(models) ? models : [models];
|
|
526
|
+
|
|
527
|
+
for (const modelOrName of modelList) {
|
|
528
|
+
const model = typeof modelOrName === 'string' ? getModel(modelOrName) : modelOrName;
|
|
529
|
+
if (model) {
|
|
530
|
+
await knex(model.table).truncate();
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Clear all tables (respecting foreign key order)
|
|
537
|
+
* @returns {Promise<void>}
|
|
538
|
+
*/
|
|
539
|
+
async function clearAll() {
|
|
540
|
+
const allModels = getAllModels();
|
|
541
|
+
const tables = [];
|
|
542
|
+
|
|
543
|
+
// Collect tables with dependency info
|
|
544
|
+
for (const [name, model] of allModels) {
|
|
545
|
+
const deps = [];
|
|
546
|
+
if (model.relations) {
|
|
547
|
+
for (const relation of Object.values(model.relations)) {
|
|
548
|
+
if (relation.type === 'belongsTo') {
|
|
549
|
+
const relatedModel = relation.model();
|
|
550
|
+
if (relatedModel) {
|
|
551
|
+
deps.push(relatedModel.table);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
tables.push({ table: model.table, deps });
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Sort by dependencies (tables with no deps first)
|
|
560
|
+
tables.sort((a, b) => a.deps.length - b.deps.length);
|
|
561
|
+
|
|
562
|
+
// Reverse to delete children first
|
|
563
|
+
for (const { table } of tables.reverse()) {
|
|
564
|
+
try {
|
|
565
|
+
await knex(table).del();
|
|
566
|
+
} catch (e) {
|
|
567
|
+
// Ignore errors (table might not exist)
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return {
|
|
573
|
+
defineFactory,
|
|
574
|
+
factory,
|
|
575
|
+
seed,
|
|
576
|
+
run,
|
|
577
|
+
truncate,
|
|
578
|
+
clearAll,
|
|
579
|
+
// Expose faker for custom generators
|
|
580
|
+
faker,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
module.exports = { createSeeder };
|
|
585
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso ORM - Transaction
|
|
3
|
+
* Transaction wrapper with scoped repositories
|
|
4
|
+
* @module core/orm/transaction
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { createRepository } = require('./repository');
|
|
8
|
+
const { createScopeContext } = require('./scopes');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a transaction context
|
|
12
|
+
* @param {import('knex').Knex.Transaction} trx - Knex transaction
|
|
13
|
+
* @param {import('./types').ScopeContext} [scopeContext] - Scope context
|
|
14
|
+
* @returns {import('./types').TransactionContext}
|
|
15
|
+
*/
|
|
16
|
+
function createTransactionContext(trx, scopeContext) {
|
|
17
|
+
const context = scopeContext || createScopeContext();
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
trx,
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a repository bound to this transaction
|
|
24
|
+
* @param {import('./types').ModelDefinition} model - Model definition
|
|
25
|
+
* @returns {import('./types').Repository}
|
|
26
|
+
*/
|
|
27
|
+
createRepository(model) {
|
|
28
|
+
return createRepository(model, trx, context);
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Set tenant context for this transaction
|
|
33
|
+
* @param {*} tenantId - Tenant ID
|
|
34
|
+
* @returns {this}
|
|
35
|
+
*/
|
|
36
|
+
forTenant(tenantId) {
|
|
37
|
+
context.tenantId = tenantId;
|
|
38
|
+
return this;
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get the scope context
|
|
43
|
+
* @returns {import('./types').ScopeContext}
|
|
44
|
+
*/
|
|
45
|
+
getScopeContext() {
|
|
46
|
+
return { ...context };
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Run a callback within a transaction
|
|
53
|
+
* @param {import('knex').Knex} knex - Knex instance
|
|
54
|
+
* @param {function(import('./types').TransactionContext): Promise<*>} callback - Transaction callback
|
|
55
|
+
* @param {import('./types').ScopeContext} [scopeContext] - Scope context
|
|
56
|
+
* @returns {Promise<*>} Result of callback
|
|
57
|
+
*/
|
|
58
|
+
async function runTransaction(knex, callback, scopeContext) {
|
|
59
|
+
return knex.transaction(async (trx) => {
|
|
60
|
+
const ctx = createTransactionContext(trx, scopeContext);
|
|
61
|
+
return callback(ctx);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
createTransactionContext,
|
|
67
|
+
runTransaction,
|
|
68
|
+
};
|
|
69
|
+
|