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.
@@ -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
+