@classytic/mongokit 1.0.0

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,225 @@
1
+ import mongoose from 'mongoose';
2
+ import createError from 'http-errors';
3
+ import * as createActions from './actions/create.js';
4
+ import * as readActions from './actions/read.js';
5
+ import * as updateActions from './actions/update.js';
6
+ import * as deleteActions from './actions/delete.js';
7
+ import * as aggregateActions from './actions/aggregate.js';
8
+
9
+ export class Repository {
10
+ constructor(Model, plugins = []) {
11
+ this.Model = Model;
12
+ this.model = Model.modelName;
13
+ this._hooks = new Map();
14
+ plugins.forEach(plugin => this.use(plugin));
15
+ }
16
+
17
+ use(plugin) {
18
+ if (typeof plugin === 'function') {
19
+ plugin(this);
20
+ } else if (plugin && typeof plugin.apply === 'function') {
21
+ plugin.apply(this);
22
+ }
23
+ return this;
24
+ }
25
+
26
+ on(event, listener) {
27
+ if (!this._hooks.has(event)) {
28
+ this._hooks.set(event, []);
29
+ }
30
+ this._hooks.get(event).push(listener);
31
+ return this;
32
+ }
33
+
34
+ emit(event, data) {
35
+ const listeners = this._hooks.get(event) || [];
36
+ listeners.forEach(listener => listener(data));
37
+ }
38
+
39
+ async create(data, options = {}) {
40
+ const context = await this._buildContext('create', { data, ...options });
41
+
42
+ try {
43
+ const result = await createActions.create(this.Model, context.data, options);
44
+ this.emit('after:create', { context, result });
45
+ return result;
46
+ } catch (error) {
47
+ this.emit('error:create', { context, error });
48
+ throw this._handleError(error);
49
+ }
50
+ }
51
+
52
+ async createMany(dataArray, options = {}) {
53
+ const context = await this._buildContext('createMany', { dataArray, ...options });
54
+
55
+ try {
56
+ const result = await createActions.createMany(this.Model, context.dataArray || dataArray, options);
57
+ this.emit('after:createMany', { context, result });
58
+ return result;
59
+ } catch (error) {
60
+ this.emit('error:createMany', { context, error });
61
+ throw this._handleError(error);
62
+ }
63
+ }
64
+
65
+ async getById(id, options = {}) {
66
+ const context = await this._buildContext('getById', { id, ...options });
67
+ return readActions.getById(this.Model, id, context);
68
+ }
69
+
70
+ async getByQuery(query, options = {}) {
71
+ const context = await this._buildContext('getByQuery', { query, ...options });
72
+ return readActions.getByQuery(this.Model, query, context);
73
+ }
74
+
75
+ async getAll(queryParams = {}, options = {}) {
76
+ const context = await this._buildContext('getAll', { queryParams, ...options });
77
+
78
+ const {
79
+ pagination = { page: 1, limit: 10 },
80
+ search,
81
+ sort = '-createdAt',
82
+ filters = {},
83
+ } = context.queryParams || queryParams;
84
+
85
+ let query = { ...filters };
86
+ if (search) query.$text = { $search: search };
87
+
88
+ const paginateOptions = {
89
+ page: parseInt(pagination.page, 10),
90
+ limit: parseInt(pagination.limit, 10),
91
+ sort: this._parseSort(sort),
92
+ populate: this._parsePopulate(context.populate || options.populate),
93
+ select: context.select || options.select,
94
+ lean: context.lean ?? options.lean ?? true,
95
+ session: options.session,
96
+ };
97
+
98
+ if (!this.Model.paginate) {
99
+ throw createError(500, `Model ${this.model} missing paginate plugin`);
100
+ }
101
+
102
+ return this.Model.paginate(query, paginateOptions);
103
+ }
104
+
105
+ async getOrCreate(query, createData, options = {}) {
106
+ return readActions.getOrCreate(this.Model, query, createData, options);
107
+ }
108
+
109
+ async count(query = {}, options = {}) {
110
+ return readActions.count(this.Model, query, options);
111
+ }
112
+
113
+ async exists(query, options = {}) {
114
+ return readActions.exists(this.Model, query, options);
115
+ }
116
+
117
+ async update(id, data, options = {}) {
118
+ const context = await this._buildContext('update', { id, data, ...options });
119
+
120
+ try {
121
+ const result = await updateActions.update(this.Model, id, context.data, context);
122
+ this.emit('after:update', { context, result });
123
+ return result;
124
+ } catch (error) {
125
+ this.emit('error:update', { context, error });
126
+ throw this._handleError(error);
127
+ }
128
+ }
129
+
130
+ async delete(id, options = {}) {
131
+ const context = await this._buildContext('delete', { id, ...options });
132
+
133
+ try {
134
+ const result = await deleteActions.deleteById(this.Model, id, options);
135
+ this.emit('after:delete', { context, result });
136
+ return result;
137
+ } catch (error) {
138
+ this.emit('error:delete', { context, error });
139
+ throw this._handleError(error);
140
+ }
141
+ }
142
+
143
+ async aggregate(pipeline, options = {}) {
144
+ return aggregateActions.aggregate(this.Model, pipeline, options);
145
+ }
146
+
147
+ async aggregatePaginate(pipeline, options = {}) {
148
+ return aggregateActions.aggregatePaginate(this.Model, pipeline, options);
149
+ }
150
+
151
+ async distinct(field, query = {}, options = {}) {
152
+ return aggregateActions.distinct(this.Model, field, query, options);
153
+ }
154
+
155
+ async withTransaction(callback) {
156
+ const session = await mongoose.startSession();
157
+ session.startTransaction();
158
+ try {
159
+ const result = await callback(session);
160
+ await session.commitTransaction();
161
+ return result;
162
+ } catch (error) {
163
+ await session.abortTransaction();
164
+ throw error;
165
+ } finally {
166
+ session.endSession();
167
+ }
168
+ }
169
+
170
+ async _executeQuery(buildQuery) {
171
+ const operation = buildQuery.name || 'custom';
172
+ const context = await this._buildContext(operation, {});
173
+
174
+ try {
175
+ const result = await buildQuery(this.Model);
176
+ this.emit(`after:${operation}`, { context, result });
177
+ return result;
178
+ } catch (error) {
179
+ this.emit(`error:${operation}`, { context, error });
180
+ throw this._handleError(error);
181
+ }
182
+ }
183
+
184
+ async _buildContext(operation, options) {
185
+ const context = { operation, model: this.model, ...options };
186
+ const event = `before:${operation}`;
187
+ const hooks = this._hooks.get(event) || [];
188
+
189
+ for (const hook of hooks) {
190
+ await hook(context);
191
+ }
192
+
193
+ return context;
194
+ }
195
+
196
+ _parseSort(sort) {
197
+ if (!sort) return { createdAt: -1 };
198
+ if (typeof sort === 'object') return sort;
199
+
200
+ const sortOrder = sort.startsWith('-') ? -1 : 1;
201
+ const sortField = sort.startsWith('-') ? sort.substring(1) : sort;
202
+ return { [sortField]: sortOrder };
203
+ }
204
+
205
+ _parsePopulate(populate) {
206
+ if (!populate) return [];
207
+ if (typeof populate === 'string') return populate.split(',').map(p => p.trim());
208
+ if (Array.isArray(populate)) return populate.map(p => (typeof p === 'string' ? p.trim() : p));
209
+ return [populate];
210
+ }
211
+
212
+ _handleError(error) {
213
+ if (error instanceof mongoose.Error.ValidationError) {
214
+ const messages = Object.values(error.errors).map(err => err.message);
215
+ return createError(400, `Validation Error: ${messages.join(', ')}`);
216
+ }
217
+ if (error instanceof mongoose.Error.CastError) {
218
+ return createError(400, `Invalid ${error.path}: ${error.value}`);
219
+ }
220
+ if (error.status && error.message) return error;
221
+ return createError(500, error.message || 'Internal Server Error');
222
+ }
223
+ }
224
+
225
+ export default Repository;
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Aggregate Actions
3
+ * MongoDB aggregation pipeline operations
4
+ */
5
+
6
+ /**
7
+ * Execute aggregation pipeline
8
+ */
9
+ export async function aggregate(Model, pipeline, options = {}) {
10
+ const aggregation = Model.aggregate(pipeline);
11
+
12
+ if (options.session) {
13
+ aggregation.session(options.session);
14
+ }
15
+
16
+ return aggregation.exec();
17
+ }
18
+
19
+ /**
20
+ * Aggregate with pagination
21
+ */
22
+ export async function aggregatePaginate(Model, pipeline, options = {}) {
23
+ const { page = 1, limit = 10 } = options;
24
+
25
+ return Model.aggregatePaginate(Model.aggregate(pipeline), {
26
+ page: parseInt(page, 10),
27
+ limit: parseInt(limit, 10),
28
+ });
29
+ }
30
+
31
+ /**
32
+ * Group by field
33
+ */
34
+ export async function groupBy(Model, field, options = {}) {
35
+ const pipeline = [
36
+ { $group: { _id: `$${field}`, count: { $sum: 1 } } },
37
+ { $sort: { count: -1 } },
38
+ ];
39
+
40
+ if (options.limit) {
41
+ pipeline.push({ $limit: options.limit });
42
+ }
43
+
44
+ return aggregate(Model, pipeline, options);
45
+ }
46
+
47
+ /**
48
+ * Count by field values
49
+ */
50
+ export async function countBy(Model, field, query = {}, options = {}) {
51
+ const pipeline = [];
52
+
53
+ if (Object.keys(query).length > 0) {
54
+ pipeline.push({ $match: query });
55
+ }
56
+
57
+ pipeline.push(
58
+ { $group: { _id: `$${field}`, count: { $sum: 1 } } },
59
+ { $sort: { count: -1 } }
60
+ );
61
+
62
+ return aggregate(Model, pipeline, options);
63
+ }
64
+
65
+ /**
66
+ * Lookup (join) with another collection
67
+ */
68
+ export async function lookup(Model, {
69
+ from,
70
+ localField,
71
+ foreignField,
72
+ as,
73
+ pipeline = [],
74
+ query = {},
75
+ options = {}
76
+ }) {
77
+ const aggPipeline = [];
78
+
79
+ if (Object.keys(query).length > 0) {
80
+ aggPipeline.push({ $match: query });
81
+ }
82
+
83
+ aggPipeline.push({
84
+ $lookup: {
85
+ from,
86
+ localField,
87
+ foreignField,
88
+ as,
89
+ ...(pipeline.length > 0 ? { pipeline } : {}),
90
+ },
91
+ });
92
+
93
+ return aggregate(Model, aggPipeline, options);
94
+ }
95
+
96
+ /**
97
+ * Unwind array field
98
+ */
99
+ export async function unwind(Model, field, options = {}) {
100
+ const pipeline = [
101
+ {
102
+ $unwind: {
103
+ path: `$${field}`,
104
+ preserveNullAndEmptyArrays: options.preserveEmpty !== false,
105
+ },
106
+ },
107
+ ];
108
+
109
+ return aggregate(Model, pipeline, options);
110
+ }
111
+
112
+ /**
113
+ * Facet search (multiple aggregations in one query)
114
+ */
115
+ export async function facet(Model, facets, options = {}) {
116
+ const pipeline = [{ $facet: facets }];
117
+
118
+ return aggregate(Model, pipeline, options);
119
+ }
120
+
121
+ /**
122
+ * Get distinct values
123
+ */
124
+ export async function distinct(Model, field, query = {}, options = {}) {
125
+ return Model.distinct(field, query).session(options.session);
126
+ }
127
+
128
+ /**
129
+ * Calculate sum
130
+ */
131
+ export async function sum(Model, field, query = {}, options = {}) {
132
+ const pipeline = [];
133
+
134
+ if (Object.keys(query).length > 0) {
135
+ pipeline.push({ $match: query });
136
+ }
137
+
138
+ pipeline.push({
139
+ $group: {
140
+ _id: null,
141
+ total: { $sum: `$${field}` },
142
+ },
143
+ });
144
+
145
+ const result = await aggregate(Model, pipeline, options);
146
+ return result[0]?.total || 0;
147
+ }
148
+
149
+ /**
150
+ * Calculate average
151
+ */
152
+ export async function average(Model, field, query = {}, options = {}) {
153
+ const pipeline = [];
154
+
155
+ if (Object.keys(query).length > 0) {
156
+ pipeline.push({ $match: query });
157
+ }
158
+
159
+ pipeline.push({
160
+ $group: {
161
+ _id: null,
162
+ average: { $avg: `$${field}` },
163
+ },
164
+ });
165
+
166
+ const result = await aggregate(Model, pipeline, options);
167
+ return result[0]?.average || 0;
168
+ }
169
+
170
+ /**
171
+ * Min/Max
172
+ */
173
+ export async function minMax(Model, field, query = {}, options = {}) {
174
+ const pipeline = [];
175
+
176
+ if (Object.keys(query).length > 0) {
177
+ pipeline.push({ $match: query });
178
+ }
179
+
180
+ pipeline.push({
181
+ $group: {
182
+ _id: null,
183
+ min: { $min: `$${field}` },
184
+ max: { $max: `$${field}` },
185
+ },
186
+ });
187
+
188
+ const result = await aggregate(Model, pipeline, options);
189
+ return result[0] || { min: null, max: null };
190
+ }
191
+
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Create Actions
3
+ * Pure functions for document creation
4
+ */
5
+
6
+ /**
7
+ * Create single document
8
+ */
9
+ export async function create(Model, data, options = {}) {
10
+ const document = new Model(data);
11
+ await document.save({ session: options.session });
12
+ return document;
13
+ }
14
+
15
+ /**
16
+ * Create multiple documents
17
+ */
18
+ export async function createMany(Model, dataArray, options = {}) {
19
+ return Model.insertMany(dataArray, {
20
+ session: options.session,
21
+ ordered: options.ordered !== false,
22
+ });
23
+ }
24
+
25
+ /**
26
+ * Create with defaults (useful for initialization)
27
+ */
28
+ export async function createDefault(Model, overrides = {}, options = {}) {
29
+ const defaults = {};
30
+
31
+ // Extract defaults from schema
32
+ Model.schema.eachPath((path, schemaType) => {
33
+ if (schemaType.options.default !== undefined && path !== '_id') {
34
+ defaults[path] = typeof schemaType.options.default === 'function'
35
+ ? schemaType.options.default()
36
+ : schemaType.options.default;
37
+ }
38
+ });
39
+
40
+ return create(Model, { ...defaults, ...overrides }, options);
41
+ }
42
+
43
+ /**
44
+ * Upsert (create or update)
45
+ */
46
+ export async function upsert(Model, query, data, options = {}) {
47
+ return Model.findOneAndUpdate(
48
+ query,
49
+ { $setOnInsert: data },
50
+ {
51
+ upsert: true,
52
+ new: true,
53
+ runValidators: true,
54
+ session: options.session,
55
+ }
56
+ );
57
+ }
58
+
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Delete Actions
3
+ * Pure functions for document deletion
4
+ */
5
+
6
+ import createError from 'http-errors';
7
+
8
+ /**
9
+ * Delete by ID
10
+ */
11
+ export async function deleteById(Model, id, options = {}) {
12
+ const document = await Model.findByIdAndDelete(id).session(options.session);
13
+
14
+ if (!document) {
15
+ throw createError(404, 'Document not found');
16
+ }
17
+
18
+ return { success: true, message: 'Deleted successfully' };
19
+ }
20
+
21
+ /**
22
+ * Delete many documents
23
+ */
24
+ export async function deleteMany(Model, query, options = {}) {
25
+ const result = await Model.deleteMany(query).session(options.session);
26
+
27
+ return {
28
+ success: true,
29
+ count: result.deletedCount,
30
+ message: 'Deleted successfully',
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Delete by query
36
+ */
37
+ export async function deleteByQuery(Model, query, options = {}) {
38
+ const document = await Model.findOneAndDelete(query).session(options.session);
39
+
40
+ if (!document && options.throwOnNotFound !== false) {
41
+ throw createError(404, 'Document not found');
42
+ }
43
+
44
+ return { success: true, message: 'Deleted successfully' };
45
+ }
46
+
47
+ /**
48
+ * Soft delete (set deleted flag)
49
+ */
50
+ export async function softDelete(Model, id, options = {}) {
51
+ const document = await Model.findByIdAndUpdate(
52
+ id,
53
+ {
54
+ deleted: true,
55
+ deletedAt: new Date(),
56
+ deletedBy: options.userId,
57
+ },
58
+ { new: true, session: options.session }
59
+ );
60
+
61
+ if (!document) {
62
+ throw createError(404, 'Document not found');
63
+ }
64
+
65
+ return { success: true, message: 'Soft deleted successfully' };
66
+ }
67
+
68
+ /**
69
+ * Restore soft deleted document
70
+ */
71
+ export async function restore(Model, id, options = {}) {
72
+ const document = await Model.findByIdAndUpdate(
73
+ id,
74
+ {
75
+ deleted: false,
76
+ deletedAt: null,
77
+ deletedBy: null,
78
+ },
79
+ { new: true, session: options.session }
80
+ );
81
+
82
+ if (!document) {
83
+ throw createError(404, 'Document not found');
84
+ }
85
+
86
+ return { success: true, message: 'Restored successfully' };
87
+ }
88
+
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Repository Actions
3
+ * Modular, composable data access operations
4
+ */
5
+
6
+ export * as create from './create.js';
7
+ export * as read from './read.js';
8
+ export * as update from './update.js';
9
+ export * as deleteActions from './delete.js';
10
+ export * as aggregate from './aggregate.js';
11
+