webspresso 0.0.6 → 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 +494 -0
- package/bin/webspresso.js +255 -0
- package/core/applySchema.js +1 -0
- package/core/compileSchema.js +1 -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 +24 -3
- package/src/plugin-manager.js +1 -0
- package/utils/schemaCache.js +1 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso ORM - Query Builder
|
|
3
|
+
* Fluent query builder wrapping Knex
|
|
4
|
+
* @module core/orm/query-builder
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { applyScopes, createScopeContext } = require('./scopes');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a new query builder
|
|
11
|
+
* @param {import('./types').ModelDefinition} model - Model definition
|
|
12
|
+
* @param {import('knex').Knex|import('knex').Knex.Transaction} knex - Knex instance or transaction
|
|
13
|
+
* @param {import('./types').ScopeContext} [initialContext] - Initial scope context
|
|
14
|
+
* @returns {QueryBuilder}
|
|
15
|
+
*/
|
|
16
|
+
function createQueryBuilder(model, knex, initialContext) {
|
|
17
|
+
return new QueryBuilder(model, knex, initialContext);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* QueryBuilder class
|
|
22
|
+
* Provides a fluent interface for building SQL queries
|
|
23
|
+
*/
|
|
24
|
+
class QueryBuilder {
|
|
25
|
+
/**
|
|
26
|
+
* @param {import('./types').ModelDefinition} model
|
|
27
|
+
* @param {import('knex').Knex|import('knex').Knex.Transaction} knex
|
|
28
|
+
* @param {import('./types').ScopeContext} [initialContext]
|
|
29
|
+
*/
|
|
30
|
+
constructor(model, knex, initialContext) {
|
|
31
|
+
this.model = model;
|
|
32
|
+
this.knex = knex;
|
|
33
|
+
this.scopeContext = initialContext || createScopeContext();
|
|
34
|
+
|
|
35
|
+
/** @type {import('./types').QueryState} */
|
|
36
|
+
this.state = {
|
|
37
|
+
wheres: [],
|
|
38
|
+
orderBys: [],
|
|
39
|
+
selects: [],
|
|
40
|
+
withs: [],
|
|
41
|
+
limitValue: undefined,
|
|
42
|
+
offsetValue: undefined,
|
|
43
|
+
scopeContext: this.scopeContext,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Select specific columns
|
|
49
|
+
* @param {...string} columns - Columns to select
|
|
50
|
+
* @returns {this}
|
|
51
|
+
*/
|
|
52
|
+
select(...columns) {
|
|
53
|
+
this.state.selects.push(...columns.flat());
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Add a WHERE clause
|
|
59
|
+
* @param {string|Object} columnOrConditions - Column name or conditions object
|
|
60
|
+
* @param {string|*} [operatorOrValue] - Operator or value
|
|
61
|
+
* @param {*} [value] - Value (if operator provided)
|
|
62
|
+
* @returns {this}
|
|
63
|
+
*/
|
|
64
|
+
where(columnOrConditions, operatorOrValue, value) {
|
|
65
|
+
// Object form: where({ column: value, ... })
|
|
66
|
+
if (typeof columnOrConditions === 'object' && columnOrConditions !== null) {
|
|
67
|
+
for (const [col, val] of Object.entries(columnOrConditions)) {
|
|
68
|
+
this.state.wheres.push({
|
|
69
|
+
column: col,
|
|
70
|
+
operator: '=',
|
|
71
|
+
value: val,
|
|
72
|
+
boolean: 'and',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Three-argument form: where('column', '>', value)
|
|
79
|
+
if (value !== undefined) {
|
|
80
|
+
this.state.wheres.push({
|
|
81
|
+
column: columnOrConditions,
|
|
82
|
+
operator: operatorOrValue,
|
|
83
|
+
value,
|
|
84
|
+
boolean: 'and',
|
|
85
|
+
});
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Two-argument form: where('column', value)
|
|
90
|
+
this.state.wheres.push({
|
|
91
|
+
column: columnOrConditions,
|
|
92
|
+
operator: '=',
|
|
93
|
+
value: operatorOrValue,
|
|
94
|
+
boolean: 'and',
|
|
95
|
+
});
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Add an OR WHERE clause
|
|
101
|
+
* @param {string|Object} columnOrConditions
|
|
102
|
+
* @param {string|*} [operatorOrValue]
|
|
103
|
+
* @param {*} [value]
|
|
104
|
+
* @returns {this}
|
|
105
|
+
*/
|
|
106
|
+
orWhere(columnOrConditions, operatorOrValue, value) {
|
|
107
|
+
const startIndex = this.state.wheres.length;
|
|
108
|
+
this.where(columnOrConditions, operatorOrValue, value);
|
|
109
|
+
|
|
110
|
+
// Mark the new clauses as OR
|
|
111
|
+
for (let i = startIndex; i < this.state.wheres.length; i++) {
|
|
112
|
+
this.state.wheres[i].boolean = 'or';
|
|
113
|
+
}
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Add a WHERE IN clause
|
|
119
|
+
* @param {string} column - Column name
|
|
120
|
+
* @param {Array} values - Values array
|
|
121
|
+
* @returns {this}
|
|
122
|
+
*/
|
|
123
|
+
whereIn(column, values) {
|
|
124
|
+
this.state.wheres.push({
|
|
125
|
+
column,
|
|
126
|
+
operator: 'in',
|
|
127
|
+
value: values,
|
|
128
|
+
boolean: 'and',
|
|
129
|
+
});
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Add a WHERE NOT IN clause
|
|
135
|
+
* @param {string} column - Column name
|
|
136
|
+
* @param {Array} values - Values array
|
|
137
|
+
* @returns {this}
|
|
138
|
+
*/
|
|
139
|
+
whereNotIn(column, values) {
|
|
140
|
+
this.state.wheres.push({
|
|
141
|
+
column,
|
|
142
|
+
operator: 'not in',
|
|
143
|
+
value: values,
|
|
144
|
+
boolean: 'and',
|
|
145
|
+
});
|
|
146
|
+
return this;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Add a WHERE NULL clause
|
|
151
|
+
* @param {string} column - Column name
|
|
152
|
+
* @returns {this}
|
|
153
|
+
*/
|
|
154
|
+
whereNull(column) {
|
|
155
|
+
this.state.wheres.push({
|
|
156
|
+
column,
|
|
157
|
+
operator: 'is null',
|
|
158
|
+
value: null,
|
|
159
|
+
boolean: 'and',
|
|
160
|
+
});
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Add a WHERE NOT NULL clause
|
|
166
|
+
* @param {string} column - Column name
|
|
167
|
+
* @returns {this}
|
|
168
|
+
*/
|
|
169
|
+
whereNotNull(column) {
|
|
170
|
+
this.state.wheres.push({
|
|
171
|
+
column,
|
|
172
|
+
operator: 'is not null',
|
|
173
|
+
value: null,
|
|
174
|
+
boolean: 'and',
|
|
175
|
+
});
|
|
176
|
+
return this;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Add ORDER BY clause
|
|
181
|
+
* @param {string} column - Column name
|
|
182
|
+
* @param {'asc'|'desc'} [direction='asc'] - Sort direction
|
|
183
|
+
* @returns {this}
|
|
184
|
+
*/
|
|
185
|
+
orderBy(column, direction = 'asc') {
|
|
186
|
+
this.state.orderBys.push({ column, direction });
|
|
187
|
+
return this;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Set LIMIT
|
|
192
|
+
* @param {number} limit - Limit value
|
|
193
|
+
* @returns {this}
|
|
194
|
+
*/
|
|
195
|
+
limit(limit) {
|
|
196
|
+
this.state.limitValue = limit;
|
|
197
|
+
return this;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Set OFFSET
|
|
202
|
+
* @param {number} offset - Offset value
|
|
203
|
+
* @returns {this}
|
|
204
|
+
*/
|
|
205
|
+
offset(offset) {
|
|
206
|
+
this.state.offsetValue = offset;
|
|
207
|
+
return this;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Add relations to eager load
|
|
212
|
+
* @param {...string} relations - Relation names
|
|
213
|
+
* @returns {this}
|
|
214
|
+
*/
|
|
215
|
+
with(...relations) {
|
|
216
|
+
this.state.withs.push(...relations.flat());
|
|
217
|
+
return this;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Include soft-deleted records
|
|
222
|
+
* @returns {this}
|
|
223
|
+
*/
|
|
224
|
+
withTrashed() {
|
|
225
|
+
this.scopeContext.withTrashed = true;
|
|
226
|
+
return this;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Only include soft-deleted records
|
|
231
|
+
* @returns {this}
|
|
232
|
+
*/
|
|
233
|
+
onlyTrashed() {
|
|
234
|
+
this.scopeContext.onlyTrashed = true;
|
|
235
|
+
return this;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Set tenant context
|
|
240
|
+
* @param {*} tenantId - Tenant ID
|
|
241
|
+
* @returns {this}
|
|
242
|
+
*/
|
|
243
|
+
forTenant(tenantId) {
|
|
244
|
+
this.scopeContext.tenantId = tenantId;
|
|
245
|
+
return this;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Build the Knex query
|
|
250
|
+
* @returns {import('knex').Knex.QueryBuilder}
|
|
251
|
+
*/
|
|
252
|
+
toKnex() {
|
|
253
|
+
let qb = this.knex(this.model.table);
|
|
254
|
+
|
|
255
|
+
// Apply global scopes
|
|
256
|
+
qb = applyScopes(qb, this.scopeContext, this.model);
|
|
257
|
+
|
|
258
|
+
// Apply selects
|
|
259
|
+
if (this.state.selects.length > 0) {
|
|
260
|
+
qb = qb.select(this.state.selects);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Apply wheres
|
|
264
|
+
for (const where of this.state.wheres) {
|
|
265
|
+
const method = where.boolean === 'or' ? 'orWhere' : 'where';
|
|
266
|
+
|
|
267
|
+
switch (where.operator) {
|
|
268
|
+
case 'in':
|
|
269
|
+
qb = where.boolean === 'or'
|
|
270
|
+
? qb.orWhereIn(where.column, where.value)
|
|
271
|
+
: qb.whereIn(where.column, where.value);
|
|
272
|
+
break;
|
|
273
|
+
case 'not in':
|
|
274
|
+
qb = where.boolean === 'or'
|
|
275
|
+
? qb.orWhereNotIn(where.column, where.value)
|
|
276
|
+
: qb.whereNotIn(where.column, where.value);
|
|
277
|
+
break;
|
|
278
|
+
case 'is null':
|
|
279
|
+
qb = where.boolean === 'or'
|
|
280
|
+
? qb.orWhereNull(where.column)
|
|
281
|
+
: qb.whereNull(where.column);
|
|
282
|
+
break;
|
|
283
|
+
case 'is not null':
|
|
284
|
+
qb = where.boolean === 'or'
|
|
285
|
+
? qb.orWhereNotNull(where.column)
|
|
286
|
+
: qb.whereNotNull(where.column);
|
|
287
|
+
break;
|
|
288
|
+
default:
|
|
289
|
+
qb = qb[method](where.column, where.operator, where.value);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Apply order by
|
|
294
|
+
for (const orderBy of this.state.orderBys) {
|
|
295
|
+
qb = qb.orderBy(orderBy.column, orderBy.direction);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Apply limit
|
|
299
|
+
if (this.state.limitValue !== undefined) {
|
|
300
|
+
qb = qb.limit(this.state.limitValue);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Apply offset
|
|
304
|
+
if (this.state.offsetValue !== undefined) {
|
|
305
|
+
qb = qb.offset(this.state.offsetValue);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return qb;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Execute query and return first result
|
|
313
|
+
* @returns {Promise<Object|null>}
|
|
314
|
+
*/
|
|
315
|
+
async first() {
|
|
316
|
+
const qb = this.toKnex().first();
|
|
317
|
+
const result = await qb;
|
|
318
|
+
return result || null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Execute query and return all results
|
|
323
|
+
* @returns {Promise<Object[]>}
|
|
324
|
+
*/
|
|
325
|
+
async list() {
|
|
326
|
+
return this.toKnex();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Execute query and return count
|
|
331
|
+
* @returns {Promise<number>}
|
|
332
|
+
*/
|
|
333
|
+
async count() {
|
|
334
|
+
const result = await this.toKnex().count('* as count').first();
|
|
335
|
+
return parseInt(result?.count || 0, 10);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Check if any records exist
|
|
340
|
+
* @returns {Promise<boolean>}
|
|
341
|
+
*/
|
|
342
|
+
async exists() {
|
|
343
|
+
const count = await this.count();
|
|
344
|
+
return count > 0;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Execute query with pagination
|
|
349
|
+
* @param {number} [page=1] - Page number (1-indexed)
|
|
350
|
+
* @param {number} [perPage=15] - Items per page
|
|
351
|
+
* @returns {Promise<import('./types').PaginatedResult>}
|
|
352
|
+
*/
|
|
353
|
+
async paginate(page = 1, perPage = 15) {
|
|
354
|
+
// Clone state for count query
|
|
355
|
+
const countQb = this.toKnex().clone();
|
|
356
|
+
|
|
357
|
+
// Get total count
|
|
358
|
+
const countResult = await countQb.count('* as count').first();
|
|
359
|
+
const total = parseInt(countResult?.count || 0, 10);
|
|
360
|
+
|
|
361
|
+
// Get paginated data
|
|
362
|
+
const offset = (page - 1) * perPage;
|
|
363
|
+
const data = await this.toKnex().limit(perPage).offset(offset);
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
data,
|
|
367
|
+
total,
|
|
368
|
+
page,
|
|
369
|
+
perPage,
|
|
370
|
+
totalPages: Math.ceil(total / perPage),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Delete matching records
|
|
376
|
+
* @returns {Promise<number>} Number of deleted records
|
|
377
|
+
*/
|
|
378
|
+
async delete() {
|
|
379
|
+
return this.toKnex().delete();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Update matching records
|
|
384
|
+
* @param {Object} data - Data to update
|
|
385
|
+
* @returns {Promise<number>} Number of updated records
|
|
386
|
+
*/
|
|
387
|
+
async update(data) {
|
|
388
|
+
return this.toKnex().update(data);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Get the relations to eager load
|
|
393
|
+
* @returns {string[]}
|
|
394
|
+
*/
|
|
395
|
+
getWiths() {
|
|
396
|
+
return [...this.state.withs];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Get the current scope context
|
|
401
|
+
* @returns {import('./types').ScopeContext}
|
|
402
|
+
*/
|
|
403
|
+
getScopeContext() {
|
|
404
|
+
return { ...this.scopeContext };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Clone the query builder
|
|
409
|
+
* @returns {QueryBuilder}
|
|
410
|
+
*/
|
|
411
|
+
clone() {
|
|
412
|
+
const cloned = new QueryBuilder(this.model, this.knex, { ...this.scopeContext });
|
|
413
|
+
cloned.state = {
|
|
414
|
+
wheres: [...this.state.wheres],
|
|
415
|
+
orderBys: [...this.state.orderBys],
|
|
416
|
+
selects: [...this.state.selects],
|
|
417
|
+
withs: [...this.state.withs],
|
|
418
|
+
limitValue: this.state.limitValue,
|
|
419
|
+
offsetValue: this.state.offsetValue,
|
|
420
|
+
scopeContext: { ...this.state.scopeContext },
|
|
421
|
+
};
|
|
422
|
+
return cloned;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
module.exports = {
|
|
427
|
+
createQueryBuilder,
|
|
428
|
+
QueryBuilder,
|
|
429
|
+
};
|
|
430
|
+
|