webspresso 0.0.6 → 0.0.8
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 +506 -2
- package/bin/webspresso.js +357 -10
- 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,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso ORM - Model Definition
|
|
3
|
+
* Define models and maintain a registry
|
|
4
|
+
* @module core/orm/model
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { extractColumnsFromSchema } = require('./schema-helpers');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Global model registry
|
|
11
|
+
* @type {Map<string, import('./types').ModelDefinition>}
|
|
12
|
+
*/
|
|
13
|
+
const modelRegistry = new Map();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Define a new model
|
|
17
|
+
* @param {import('./types').ModelOptions} options - Model configuration
|
|
18
|
+
* @returns {import('./types').ModelDefinition}
|
|
19
|
+
*/
|
|
20
|
+
function defineModel(options) {
|
|
21
|
+
const {
|
|
22
|
+
name,
|
|
23
|
+
table,
|
|
24
|
+
schema,
|
|
25
|
+
primaryKey = 'id',
|
|
26
|
+
relations = {},
|
|
27
|
+
scopes = {},
|
|
28
|
+
} = options;
|
|
29
|
+
|
|
30
|
+
// Validate required fields
|
|
31
|
+
if (!name || typeof name !== 'string') {
|
|
32
|
+
throw new Error('Model name is required and must be a string');
|
|
33
|
+
}
|
|
34
|
+
if (!table || typeof table !== 'string') {
|
|
35
|
+
throw new Error('Model table is required and must be a string');
|
|
36
|
+
}
|
|
37
|
+
if (!schema || typeof schema.parse !== 'function') {
|
|
38
|
+
throw new Error('Model schema is required and must be a Zod schema');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check for duplicate registration
|
|
42
|
+
if (modelRegistry.has(name)) {
|
|
43
|
+
throw new Error(`Model "${name}" is already defined`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Extract column metadata from schema
|
|
47
|
+
const columns = extractColumnsFromSchema(schema);
|
|
48
|
+
|
|
49
|
+
// Validate relations
|
|
50
|
+
for (const [relationName, relation] of Object.entries(relations)) {
|
|
51
|
+
if (!['belongsTo', 'hasMany', 'hasOne'].includes(relation.type)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Invalid relation type "${relation.type}" for "${relationName}" in model "${name}"`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (typeof relation.model !== 'function') {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Relation "${relationName}" in model "${name}" must have a model function`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (!relation.foreignKey || typeof relation.foreignKey !== 'string') {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Relation "${relationName}" in model "${name}" must have a foreignKey string`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Create model definition
|
|
69
|
+
const model = {
|
|
70
|
+
name,
|
|
71
|
+
table,
|
|
72
|
+
schema,
|
|
73
|
+
primaryKey,
|
|
74
|
+
relations,
|
|
75
|
+
scopes: {
|
|
76
|
+
softDelete: scopes.softDelete || false,
|
|
77
|
+
timestamps: scopes.timestamps || false,
|
|
78
|
+
tenant: scopes.tenant || null,
|
|
79
|
+
},
|
|
80
|
+
columns,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Register model
|
|
84
|
+
modelRegistry.set(name, model);
|
|
85
|
+
|
|
86
|
+
return model;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get a model by name
|
|
91
|
+
* @param {string} name - Model name
|
|
92
|
+
* @returns {import('./types').ModelDefinition|undefined}
|
|
93
|
+
*/
|
|
94
|
+
function getModel(name) {
|
|
95
|
+
return modelRegistry.get(name);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get all registered models
|
|
100
|
+
* @returns {Map<string, import('./types').ModelDefinition>}
|
|
101
|
+
*/
|
|
102
|
+
function getAllModels() {
|
|
103
|
+
return new Map(modelRegistry);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if a model exists
|
|
108
|
+
* @param {string} name - Model name
|
|
109
|
+
* @returns {boolean}
|
|
110
|
+
*/
|
|
111
|
+
function hasModel(name) {
|
|
112
|
+
return modelRegistry.has(name);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Clear the model registry (useful for testing)
|
|
117
|
+
*/
|
|
118
|
+
function clearRegistry() {
|
|
119
|
+
modelRegistry.clear();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Unregister a model by name
|
|
124
|
+
* @param {string} name - Model name
|
|
125
|
+
* @returns {boolean} Whether the model was removed
|
|
126
|
+
*/
|
|
127
|
+
function unregisterModel(name) {
|
|
128
|
+
return modelRegistry.delete(name);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Resolve a relation's model (handles lazy loading)
|
|
133
|
+
* @param {import('./types').RelationDefinition} relation - Relation definition
|
|
134
|
+
* @returns {import('./types').ModelDefinition}
|
|
135
|
+
*/
|
|
136
|
+
function resolveRelationModel(relation) {
|
|
137
|
+
const model = relation.model();
|
|
138
|
+
if (!model || !model.name) {
|
|
139
|
+
throw new Error('Invalid relation model reference');
|
|
140
|
+
}
|
|
141
|
+
return model;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get the foreign key column info for a relation
|
|
146
|
+
* @param {import('./types').ModelDefinition} model - Parent model
|
|
147
|
+
* @param {string} relationName - Relation name
|
|
148
|
+
* @returns {{ localKey: string, foreignKey: string, relatedModel: import('./types').ModelDefinition }}
|
|
149
|
+
*/
|
|
150
|
+
function getRelationKeys(model, relationName) {
|
|
151
|
+
const relation = model.relations[relationName];
|
|
152
|
+
if (!relation) {
|
|
153
|
+
throw new Error(`Relation "${relationName}" not found on model "${model.name}"`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const relatedModel = resolveRelationModel(relation);
|
|
157
|
+
const localKey = relation.localKey || model.primaryKey;
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
localKey,
|
|
161
|
+
foreignKey: relation.foreignKey,
|
|
162
|
+
relatedModel,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = {
|
|
167
|
+
defineModel,
|
|
168
|
+
getModel,
|
|
169
|
+
getAllModels,
|
|
170
|
+
hasModel,
|
|
171
|
+
clearRegistry,
|
|
172
|
+
unregisterModel,
|
|
173
|
+
resolveRelationModel,
|
|
174
|
+
getRelationKeys,
|
|
175
|
+
// Export registry for testing
|
|
176
|
+
_registry: modelRegistry,
|
|
177
|
+
};
|
|
178
|
+
|
|
@@ -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
|
+
|