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,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso ORM - Repository
|
|
3
|
+
* Repository factory for CRUD operations
|
|
4
|
+
* @module core/orm/repository
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { createQueryBuilder } = require('./query-builder');
|
|
8
|
+
const { loadRelations } = require('./eager-loader');
|
|
9
|
+
const {
|
|
10
|
+
applyInsertModifiers,
|
|
11
|
+
applyUpdateModifiers,
|
|
12
|
+
getSoftDeleteData,
|
|
13
|
+
createScopeContext,
|
|
14
|
+
applyScopes,
|
|
15
|
+
} = require('./scopes');
|
|
16
|
+
const { ensureArray } = require('./utils');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a repository for a model
|
|
20
|
+
* @param {import('./types').ModelDefinition} model - Model definition
|
|
21
|
+
* @param {import('knex').Knex|import('knex').Knex.Transaction} knex - Knex instance
|
|
22
|
+
* @param {import('./types').ScopeContext} [initialContext] - Initial scope context
|
|
23
|
+
* @returns {import('./types').Repository}
|
|
24
|
+
*/
|
|
25
|
+
function createRepository(model, knex, initialContext) {
|
|
26
|
+
const scopeContext = initialContext || createScopeContext();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get base query builder
|
|
30
|
+
* @returns {import('knex').Knex.QueryBuilder}
|
|
31
|
+
*/
|
|
32
|
+
function baseQuery() {
|
|
33
|
+
let qb = knex(model.table);
|
|
34
|
+
return applyScopes(qb, scopeContext, model);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Find a record by primary key
|
|
39
|
+
* @param {number|string} id - Primary key value
|
|
40
|
+
* @param {import('./types').FindOptions} [options={}] - Find options
|
|
41
|
+
* @returns {Promise<Object|null>}
|
|
42
|
+
*/
|
|
43
|
+
async function findById(id, options = {}) {
|
|
44
|
+
const { with: withs = [], select } = options;
|
|
45
|
+
|
|
46
|
+
let qb = baseQuery().where(model.primaryKey, id);
|
|
47
|
+
|
|
48
|
+
if (select && select.length > 0) {
|
|
49
|
+
qb = qb.select(select);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const record = await qb.first();
|
|
53
|
+
|
|
54
|
+
if (!record) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Load relations if requested
|
|
59
|
+
if (withs.length > 0) {
|
|
60
|
+
await loadRelations([record], ensureArray(withs), model, knex, scopeContext);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return record;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Find a single record matching conditions
|
|
68
|
+
* @param {Object} conditions - Where conditions
|
|
69
|
+
* @param {import('./types').FindOptions} [options={}] - Find options
|
|
70
|
+
* @returns {Promise<Object|null>}
|
|
71
|
+
*/
|
|
72
|
+
async function findOne(conditions, options = {}) {
|
|
73
|
+
const { with: withs = [], select } = options;
|
|
74
|
+
|
|
75
|
+
let qb = baseQuery();
|
|
76
|
+
|
|
77
|
+
for (const [key, value] of Object.entries(conditions)) {
|
|
78
|
+
qb = qb.where(key, value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (select && select.length > 0) {
|
|
82
|
+
qb = qb.select(select);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const record = await qb.first();
|
|
86
|
+
|
|
87
|
+
if (!record) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Load relations if requested
|
|
92
|
+
if (withs.length > 0) {
|
|
93
|
+
await loadRelations([record], ensureArray(withs), model, knex, scopeContext);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return record;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Find all records
|
|
101
|
+
* @param {import('./types').FindOptions} [options={}] - Find options
|
|
102
|
+
* @returns {Promise<Object[]>}
|
|
103
|
+
*/
|
|
104
|
+
async function findAll(options = {}) {
|
|
105
|
+
const { with: withs = [], select } = options;
|
|
106
|
+
|
|
107
|
+
let qb = baseQuery();
|
|
108
|
+
|
|
109
|
+
if (select && select.length > 0) {
|
|
110
|
+
qb = qb.select(select);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const records = await qb;
|
|
114
|
+
|
|
115
|
+
// Load relations if requested
|
|
116
|
+
if (withs.length > 0 && records.length > 0) {
|
|
117
|
+
await loadRelations(records, ensureArray(withs), model, knex, scopeContext);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return records;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Create a new record
|
|
125
|
+
* @param {Object} data - Record data
|
|
126
|
+
* @returns {Promise<Object>}
|
|
127
|
+
*/
|
|
128
|
+
async function create(data) {
|
|
129
|
+
// Validate with Zod schema (partial for insert - allows auto fields to be missing)
|
|
130
|
+
const validated = model.schema.partial().parse(data);
|
|
131
|
+
|
|
132
|
+
// Apply insert modifiers (timestamps, tenant)
|
|
133
|
+
const insertData = applyInsertModifiers(validated, scopeContext, model);
|
|
134
|
+
|
|
135
|
+
// Insert and return the record
|
|
136
|
+
const [id] = await knex(model.table).insert(insertData).returning(model.primaryKey);
|
|
137
|
+
|
|
138
|
+
// For databases that don't support returning (SQLite), id might be the row itself
|
|
139
|
+
const insertedId = typeof id === 'object' ? id[model.primaryKey] : id;
|
|
140
|
+
|
|
141
|
+
// Fetch the created record
|
|
142
|
+
return findById(insertedId);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Create multiple records
|
|
147
|
+
* @param {Object[]} dataArray - Array of record data
|
|
148
|
+
* @returns {Promise<Object[]>}
|
|
149
|
+
*/
|
|
150
|
+
async function createMany(dataArray) {
|
|
151
|
+
const records = [];
|
|
152
|
+
|
|
153
|
+
for (const data of dataArray) {
|
|
154
|
+
const record = await create(data);
|
|
155
|
+
records.push(record);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return records;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Update a record by primary key
|
|
163
|
+
* @param {number|string} id - Primary key value
|
|
164
|
+
* @param {Object} data - Data to update
|
|
165
|
+
* @returns {Promise<Object|null>}
|
|
166
|
+
*/
|
|
167
|
+
async function update(id, data) {
|
|
168
|
+
// Validate with Zod schema (partial for update)
|
|
169
|
+
const validated = model.schema.partial().parse(data);
|
|
170
|
+
|
|
171
|
+
// Apply update modifiers (timestamps)
|
|
172
|
+
const updateData = applyUpdateModifiers(validated, model);
|
|
173
|
+
|
|
174
|
+
// Update the record
|
|
175
|
+
const updated = await baseQuery()
|
|
176
|
+
.where(model.primaryKey, id)
|
|
177
|
+
.update(updateData);
|
|
178
|
+
|
|
179
|
+
if (updated === 0) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Fetch and return the updated record
|
|
184
|
+
return findById(id);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Update records matching conditions
|
|
189
|
+
* @param {Object} conditions - Where conditions
|
|
190
|
+
* @param {Object} data - Data to update
|
|
191
|
+
* @returns {Promise<number>} Number of updated records
|
|
192
|
+
*/
|
|
193
|
+
async function updateWhere(conditions, data) {
|
|
194
|
+
// Validate with Zod schema (partial for update)
|
|
195
|
+
const validated = model.schema.partial().parse(data);
|
|
196
|
+
|
|
197
|
+
// Apply update modifiers (timestamps)
|
|
198
|
+
const updateData = applyUpdateModifiers(validated, model);
|
|
199
|
+
|
|
200
|
+
let qb = baseQuery();
|
|
201
|
+
|
|
202
|
+
for (const [key, value] of Object.entries(conditions)) {
|
|
203
|
+
qb = qb.where(key, value);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return qb.update(updateData);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Delete a record by primary key (soft delete if enabled)
|
|
211
|
+
* @param {number|string} id - Primary key value
|
|
212
|
+
* @returns {Promise<boolean>}
|
|
213
|
+
*/
|
|
214
|
+
async function del(id) {
|
|
215
|
+
// Soft delete if enabled
|
|
216
|
+
if (model.scopes.softDelete) {
|
|
217
|
+
const updated = await baseQuery()
|
|
218
|
+
.where(model.primaryKey, id)
|
|
219
|
+
.update(getSoftDeleteData());
|
|
220
|
+
return updated > 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Hard delete
|
|
224
|
+
const deleted = await baseQuery()
|
|
225
|
+
.where(model.primaryKey, id)
|
|
226
|
+
.delete();
|
|
227
|
+
return deleted > 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Force delete a record (bypass soft delete)
|
|
232
|
+
* @param {number|string} id - Primary key value
|
|
233
|
+
* @returns {Promise<boolean>}
|
|
234
|
+
*/
|
|
235
|
+
async function forceDelete(id) {
|
|
236
|
+
// Use raw query to bypass soft delete scope
|
|
237
|
+
const deleted = await knex(model.table)
|
|
238
|
+
.where(model.primaryKey, id)
|
|
239
|
+
.delete();
|
|
240
|
+
return deleted > 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Restore a soft-deleted record
|
|
245
|
+
* @param {number|string} id - Primary key value
|
|
246
|
+
* @returns {Promise<Object|null>}
|
|
247
|
+
*/
|
|
248
|
+
async function restore(id) {
|
|
249
|
+
if (!model.scopes.softDelete) {
|
|
250
|
+
throw new Error(`Model "${model.name}" does not have soft delete enabled`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Update directly without scopes (to find trashed record)
|
|
254
|
+
const updated = await knex(model.table)
|
|
255
|
+
.where(model.primaryKey, id)
|
|
256
|
+
.whereNotNull('deleted_at')
|
|
257
|
+
.update({ deleted_at: null });
|
|
258
|
+
|
|
259
|
+
if (updated === 0) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return findById(id);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get a query builder for this model
|
|
268
|
+
* @returns {import('./query-builder').QueryBuilder}
|
|
269
|
+
*/
|
|
270
|
+
function query() {
|
|
271
|
+
return createQueryBuilder(model, knex, scopeContext);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Execute a raw query
|
|
276
|
+
* @param {string} sql - SQL query
|
|
277
|
+
* @param {Array} [bindings=[]] - Query bindings
|
|
278
|
+
* @returns {Promise<Object[]>}
|
|
279
|
+
*/
|
|
280
|
+
async function raw(sql, bindings = []) {
|
|
281
|
+
const result = await knex.raw(sql, bindings);
|
|
282
|
+
// Normalize result across database drivers
|
|
283
|
+
return result.rows || result;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Count records
|
|
288
|
+
* @param {Object} [conditions={}] - Where conditions
|
|
289
|
+
* @returns {Promise<number>}
|
|
290
|
+
*/
|
|
291
|
+
async function count(conditions = {}) {
|
|
292
|
+
let qb = baseQuery();
|
|
293
|
+
|
|
294
|
+
for (const [key, value] of Object.entries(conditions)) {
|
|
295
|
+
qb = qb.where(key, value);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const result = await qb.count('* as count').first();
|
|
299
|
+
return parseInt(result?.count || 0, 10);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Check if a record exists
|
|
304
|
+
* @param {Object} conditions - Where conditions
|
|
305
|
+
* @returns {Promise<boolean>}
|
|
306
|
+
*/
|
|
307
|
+
async function exists(conditions) {
|
|
308
|
+
const cnt = await count(conditions);
|
|
309
|
+
return cnt > 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Get query builder with relations to eager load
|
|
314
|
+
* Helper for common pattern: query().with(...).list()
|
|
315
|
+
* @param {string[]} relations - Relations to load
|
|
316
|
+
* @returns {import('./query-builder').QueryBuilder}
|
|
317
|
+
*/
|
|
318
|
+
function withRelations(...relations) {
|
|
319
|
+
return query().with(...relations);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
findById,
|
|
324
|
+
findOne,
|
|
325
|
+
findAll,
|
|
326
|
+
create,
|
|
327
|
+
createMany,
|
|
328
|
+
update,
|
|
329
|
+
updateWhere,
|
|
330
|
+
delete: del,
|
|
331
|
+
forceDelete,
|
|
332
|
+
restore,
|
|
333
|
+
query,
|
|
334
|
+
raw,
|
|
335
|
+
count,
|
|
336
|
+
exists,
|
|
337
|
+
with: withRelations,
|
|
338
|
+
// Expose model for introspection
|
|
339
|
+
model,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
module.exports = {
|
|
344
|
+
createRepository,
|
|
345
|
+
};
|
|
346
|
+
|