@classytic/mongokit 1.0.1 → 2.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.
- package/README.md +564 -157
- package/package.json +20 -12
- package/src/Repository.js +296 -225
- package/src/actions/aggregate.js +266 -191
- package/src/actions/create.js +59 -58
- package/src/actions/delete.js +88 -88
- package/src/actions/index.js +11 -11
- package/src/actions/read.js +188 -155
- package/src/actions/update.js +176 -172
- package/src/hooks/lifecycle.js +146 -146
- package/src/index.js +71 -60
- package/src/pagination/PaginationEngine.js +348 -0
- package/src/pagination/utils/cursor.js +119 -0
- package/src/pagination/utils/filter.js +42 -0
- package/src/pagination/utils/limits.js +82 -0
- package/src/pagination/utils/sort.js +101 -0
- package/src/plugins/aggregate-helpers.plugin.js +71 -71
- package/src/plugins/audit-log.plugin.js +60 -60
- package/src/plugins/batch-operations.plugin.js +66 -66
- package/src/plugins/field-filter.plugin.js +27 -27
- package/src/plugins/index.js +19 -19
- package/src/plugins/method-registry.plugin.js +140 -140
- package/src/plugins/mongo-operations.plugin.js +317 -313
- package/src/plugins/soft-delete.plugin.js +46 -46
- package/src/plugins/subdocument.plugin.js +66 -66
- package/src/plugins/timestamp.plugin.js +19 -19
- package/src/plugins/validation-chain.plugin.js +145 -145
- package/src/types.d.ts +87 -0
- package/src/utils/error.js +12 -0
- package/src/utils/field-selection.js +156 -156
- package/src/utils/index.js +12 -12
- package/types/Repository.d.ts +95 -0
- package/types/Repository.d.ts.map +1 -0
- package/types/actions/aggregate.d.ts +112 -0
- package/types/actions/aggregate.d.ts.map +1 -0
- package/types/actions/create.d.ts +21 -0
- package/types/actions/create.d.ts.map +1 -0
- package/types/actions/delete.d.ts +37 -0
- package/types/actions/delete.d.ts.map +1 -0
- package/types/actions/index.d.ts +6 -113
- package/types/actions/index.d.ts.map +1 -0
- package/types/actions/read.d.ts +135 -0
- package/types/actions/read.d.ts.map +1 -0
- package/types/actions/update.d.ts +58 -0
- package/types/actions/update.d.ts.map +1 -0
- package/types/hooks/lifecycle.d.ts +44 -0
- package/types/hooks/lifecycle.d.ts.map +1 -0
- package/types/index.d.ts +25 -96
- package/types/index.d.ts.map +1 -0
- package/types/pagination/PaginationEngine.d.ts +386 -0
- package/types/pagination/PaginationEngine.d.ts.map +1 -0
- package/types/pagination/utils/cursor.d.ts +40 -0
- package/types/pagination/utils/cursor.d.ts.map +1 -0
- package/types/pagination/utils/filter.d.ts +28 -0
- package/types/pagination/utils/filter.d.ts.map +1 -0
- package/types/pagination/utils/limits.d.ts +64 -0
- package/types/pagination/utils/limits.d.ts.map +1 -0
- package/types/pagination/utils/sort.d.ts +41 -0
- package/types/pagination/utils/sort.d.ts.map +1 -0
- package/types/plugins/aggregate-helpers.plugin.d.ts +6 -0
- package/types/plugins/aggregate-helpers.plugin.d.ts.map +1 -0
- package/types/plugins/audit-log.plugin.d.ts +6 -0
- package/types/plugins/audit-log.plugin.d.ts.map +1 -0
- package/types/plugins/batch-operations.plugin.d.ts +6 -0
- package/types/plugins/batch-operations.plugin.d.ts.map +1 -0
- package/types/plugins/field-filter.plugin.d.ts +6 -0
- package/types/plugins/field-filter.plugin.d.ts.map +1 -0
- package/types/plugins/index.d.ts +11 -88
- package/types/plugins/index.d.ts.map +1 -0
- package/types/plugins/method-registry.plugin.d.ts +3 -0
- package/types/plugins/method-registry.plugin.d.ts.map +1 -0
- package/types/plugins/mongo-operations.plugin.d.ts +4 -0
- package/types/plugins/mongo-operations.plugin.d.ts.map +1 -0
- package/types/plugins/soft-delete.plugin.d.ts +6 -0
- package/types/plugins/soft-delete.plugin.d.ts.map +1 -0
- package/types/plugins/subdocument.plugin.d.ts +6 -0
- package/types/plugins/subdocument.plugin.d.ts.map +1 -0
- package/types/plugins/timestamp.plugin.d.ts +6 -0
- package/types/plugins/timestamp.plugin.d.ts.map +1 -0
- package/types/plugins/validation-chain.plugin.d.ts +31 -0
- package/types/plugins/validation-chain.plugin.d.ts.map +1 -0
- package/types/utils/error.d.ts +11 -0
- package/types/utils/error.d.ts.map +1 -0
- package/types/utils/field-selection.d.ts +9 -0
- package/types/utils/field-selection.d.ts.map +1 -0
- package/types/utils/index.d.ts +2 -24
- package/types/utils/index.d.ts.map +1 -0
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
export const softDeletePlugin = (options = {}) => ({
|
|
2
|
-
name: 'softDelete',
|
|
3
|
-
|
|
4
|
-
apply(repo) {
|
|
5
|
-
const deletedField = options.deletedField || 'deletedAt';
|
|
6
|
-
const deletedByField = options.deletedByField || 'deletedBy';
|
|
7
|
-
|
|
8
|
-
repo.on('before:delete', async (context) => {
|
|
9
|
-
if (options.soft !== false) {
|
|
10
|
-
const updateData = {
|
|
11
|
-
[deletedField]: new Date(),
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
if (context.user) {
|
|
15
|
-
updateData[deletedByField] = context.user._id || context.user.id;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
await repo.Model.findByIdAndUpdate(context.id, updateData, { session: context.session });
|
|
19
|
-
|
|
20
|
-
context.softDeleted = true;
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
repo.on('before:getAll', (context) => {
|
|
25
|
-
if (!context.includeDeleted && options.soft !== false) {
|
|
26
|
-
const queryParams = context.queryParams || {};
|
|
27
|
-
queryParams.filters = {
|
|
28
|
-
...(queryParams.filters || {}),
|
|
29
|
-
[deletedField]: { $exists: false },
|
|
30
|
-
};
|
|
31
|
-
context.queryParams = queryParams;
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
repo.on('before:getById', (context) => {
|
|
36
|
-
if (!context.includeDeleted && options.soft !== false) {
|
|
37
|
-
context.query = {
|
|
38
|
-
...(context.query || {}),
|
|
39
|
-
[deletedField]: { $exists: false },
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
export default softDeletePlugin;
|
|
1
|
+
export const softDeletePlugin = (options = {}) => ({
|
|
2
|
+
name: 'softDelete',
|
|
3
|
+
|
|
4
|
+
apply(repo) {
|
|
5
|
+
const deletedField = options.deletedField || 'deletedAt';
|
|
6
|
+
const deletedByField = options.deletedByField || 'deletedBy';
|
|
7
|
+
|
|
8
|
+
repo.on('before:delete', async (context) => {
|
|
9
|
+
if (options.soft !== false) {
|
|
10
|
+
const updateData = {
|
|
11
|
+
[deletedField]: new Date(),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
if (context.user) {
|
|
15
|
+
updateData[deletedByField] = context.user._id || context.user.id;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
await repo.Model.findByIdAndUpdate(context.id, updateData, { session: context.session });
|
|
19
|
+
|
|
20
|
+
context.softDeleted = true;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
repo.on('before:getAll', (context) => {
|
|
25
|
+
if (!context.includeDeleted && options.soft !== false) {
|
|
26
|
+
const queryParams = context.queryParams || {};
|
|
27
|
+
queryParams.filters = {
|
|
28
|
+
...(queryParams.filters || {}),
|
|
29
|
+
[deletedField]: { $exists: false },
|
|
30
|
+
};
|
|
31
|
+
context.queryParams = queryParams;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
repo.on('before:getById', (context) => {
|
|
36
|
+
if (!context.includeDeleted && options.soft !== false) {
|
|
37
|
+
context.query = {
|
|
38
|
+
...(context.query || {}),
|
|
39
|
+
[deletedField]: { $exists: false },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export default softDeletePlugin;
|
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subdocument Plugin
|
|
3
|
-
* Adds subdocument array operations
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import createError from '
|
|
7
|
-
|
|
8
|
-
export const subdocumentPlugin = () => ({
|
|
9
|
-
name: 'subdocument',
|
|
10
|
-
|
|
11
|
-
apply(repo) {
|
|
12
|
-
if (!repo.registerMethod) {
|
|
13
|
-
throw new Error('subdocumentPlugin requires methodRegistryPlugin');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Add subdocument to array
|
|
18
|
-
*/
|
|
19
|
-
repo.registerMethod('addSubdocument', async function (parentId, arrayPath, subData, options = {}) {
|
|
20
|
-
return this.update(parentId, { $push: { [arrayPath]: subData } }, options);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Get subdocument from array
|
|
25
|
-
*/
|
|
26
|
-
repo.registerMethod('getSubdocument', async function (parentId, arrayPath, subId, options = {}) {
|
|
27
|
-
return this._executeQuery(async (Model) => {
|
|
28
|
-
const parent = await Model.findById(parentId).session(options.session).exec();
|
|
29
|
-
if (!parent) throw createError(404, 'Parent not found');
|
|
30
|
-
|
|
31
|
-
const sub = parent[arrayPath].id(subId);
|
|
32
|
-
if (!sub) throw createError(404, 'Subdocument not found');
|
|
33
|
-
|
|
34
|
-
return options.lean ? sub.toObject() : sub;
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Update subdocument in array
|
|
40
|
-
*/
|
|
41
|
-
repo.registerMethod('updateSubdocument', async function (parentId, arrayPath, subId, updateData, options = {}) {
|
|
42
|
-
return this._executeQuery(async (Model) => {
|
|
43
|
-
const query = { _id: parentId, [`${arrayPath}._id`]: subId };
|
|
44
|
-
const update = { $set: { [`${arrayPath}.$`]: { ...updateData, _id: subId } } };
|
|
45
|
-
|
|
46
|
-
const result = await Model.findOneAndUpdate(query, update, {
|
|
47
|
-
new: true,
|
|
48
|
-
runValidators: true,
|
|
49
|
-
session: options.session,
|
|
50
|
-
}).exec();
|
|
51
|
-
|
|
52
|
-
if (!result) throw createError(404, 'Parent or subdocument not found');
|
|
53
|
-
return result;
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Delete subdocument from array
|
|
59
|
-
*/
|
|
60
|
-
repo.registerMethod('deleteSubdocument', async function (parentId, arrayPath, subId, options = {}) {
|
|
61
|
-
return this.update(parentId, { $pull: { [arrayPath]: { _id: subId } } }, options);
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
export default subdocumentPlugin;
|
|
1
|
+
/**
|
|
2
|
+
* Subdocument Plugin
|
|
3
|
+
* Adds subdocument array operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createError } from '../utils/error.js';
|
|
7
|
+
|
|
8
|
+
export const subdocumentPlugin = () => ({
|
|
9
|
+
name: 'subdocument',
|
|
10
|
+
|
|
11
|
+
apply(repo) {
|
|
12
|
+
if (!repo.registerMethod) {
|
|
13
|
+
throw new Error('subdocumentPlugin requires methodRegistryPlugin');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Add subdocument to array
|
|
18
|
+
*/
|
|
19
|
+
repo.registerMethod('addSubdocument', async function (parentId, arrayPath, subData, options = {}) {
|
|
20
|
+
return this.update(parentId, { $push: { [arrayPath]: subData } }, options);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get subdocument from array
|
|
25
|
+
*/
|
|
26
|
+
repo.registerMethod('getSubdocument', async function (parentId, arrayPath, subId, options = {}) {
|
|
27
|
+
return this._executeQuery(async (Model) => {
|
|
28
|
+
const parent = await Model.findById(parentId).session(options.session).exec();
|
|
29
|
+
if (!parent) throw createError(404, 'Parent not found');
|
|
30
|
+
|
|
31
|
+
const sub = parent[arrayPath].id(subId);
|
|
32
|
+
if (!sub) throw createError(404, 'Subdocument not found');
|
|
33
|
+
|
|
34
|
+
return options.lean ? sub.toObject() : sub;
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Update subdocument in array
|
|
40
|
+
*/
|
|
41
|
+
repo.registerMethod('updateSubdocument', async function (parentId, arrayPath, subId, updateData, options = {}) {
|
|
42
|
+
return this._executeQuery(async (Model) => {
|
|
43
|
+
const query = { _id: parentId, [`${arrayPath}._id`]: subId };
|
|
44
|
+
const update = { $set: { [`${arrayPath}.$`]: { ...updateData, _id: subId } } };
|
|
45
|
+
|
|
46
|
+
const result = await Model.findOneAndUpdate(query, update, {
|
|
47
|
+
new: true,
|
|
48
|
+
runValidators: true,
|
|
49
|
+
session: options.session,
|
|
50
|
+
}).exec();
|
|
51
|
+
|
|
52
|
+
if (!result) throw createError(404, 'Parent or subdocument not found');
|
|
53
|
+
return result;
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Delete subdocument from array
|
|
59
|
+
*/
|
|
60
|
+
repo.registerMethod('deleteSubdocument', async function (parentId, arrayPath, subId, options = {}) {
|
|
61
|
+
return this.update(parentId, { $pull: { [arrayPath]: { _id: subId } } }, options);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export default subdocumentPlugin;
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
export const timestampPlugin = () => ({
|
|
2
|
-
name: 'timestamp',
|
|
3
|
-
|
|
4
|
-
apply(repo) {
|
|
5
|
-
repo.on('before:create', (context) => {
|
|
6
|
-
if (!context.data) return;
|
|
7
|
-
const now = new Date();
|
|
8
|
-
if (!context.data.createdAt) context.data.createdAt = now;
|
|
9
|
-
if (!context.data.updatedAt) context.data.updatedAt = now;
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
repo.on('before:update', (context) => {
|
|
13
|
-
if (!context.data) return;
|
|
14
|
-
context.data.updatedAt = new Date();
|
|
15
|
-
});
|
|
16
|
-
},
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
export default timestampPlugin;
|
|
1
|
+
export const timestampPlugin = () => ({
|
|
2
|
+
name: 'timestamp',
|
|
3
|
+
|
|
4
|
+
apply(repo) {
|
|
5
|
+
repo.on('before:create', (context) => {
|
|
6
|
+
if (!context.data) return;
|
|
7
|
+
const now = new Date();
|
|
8
|
+
if (!context.data.createdAt) context.data.createdAt = now;
|
|
9
|
+
if (!context.data.updatedAt) context.data.updatedAt = now;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
repo.on('before:update', (context) => {
|
|
13
|
+
if (!context.data) return;
|
|
14
|
+
context.data.updatedAt = new Date();
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export default timestampPlugin;
|
|
@@ -1,145 +1,145 @@
|
|
|
1
|
-
import createError from '
|
|
2
|
-
|
|
3
|
-
export const validationChainPlugin = (validators = [], options = {}) => {
|
|
4
|
-
const { stopOnFirstError = true } = options;
|
|
5
|
-
|
|
6
|
-
validators.forEach((v, idx) => {
|
|
7
|
-
if (!v.name || typeof v.name !== 'string') {
|
|
8
|
-
throw new Error(`Validator at index ${idx} missing 'name' (string)`);
|
|
9
|
-
}
|
|
10
|
-
if (typeof v.validate !== 'function') {
|
|
11
|
-
throw new Error(`Validator '${v.name}' missing 'validate' function`);
|
|
12
|
-
}
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
const validatorsByOperation = { create: [], update: [], delete: [], createMany: [] };
|
|
16
|
-
const allOperationsValidators = [];
|
|
17
|
-
|
|
18
|
-
validators.forEach(v => {
|
|
19
|
-
if (!v.operations || v.operations.length === 0) {
|
|
20
|
-
allOperationsValidators.push(v);
|
|
21
|
-
} else {
|
|
22
|
-
v.operations.forEach(op => {
|
|
23
|
-
if (validatorsByOperation[op]) {
|
|
24
|
-
validatorsByOperation[op].push(v);
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
name: 'validation-chain',
|
|
32
|
-
|
|
33
|
-
apply(repo) {
|
|
34
|
-
const getValidatorsForOperation = (operation) => {
|
|
35
|
-
const specific = validatorsByOperation[operation] || [];
|
|
36
|
-
return [...allOperationsValidators, ...specific];
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const runValidators = async (operation, context) => {
|
|
40
|
-
const validators = getValidatorsForOperation(operation);
|
|
41
|
-
const errors = [];
|
|
42
|
-
|
|
43
|
-
for (const validator of validators) {
|
|
44
|
-
try {
|
|
45
|
-
await validator.validate(context, repo);
|
|
46
|
-
} catch (error) {
|
|
47
|
-
if (stopOnFirstError) {
|
|
48
|
-
throw error;
|
|
49
|
-
}
|
|
50
|
-
errors.push({
|
|
51
|
-
validator: validator.name,
|
|
52
|
-
error: error.message || String(error)
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (errors.length > 0) {
|
|
58
|
-
const error = createError(
|
|
59
|
-
400,
|
|
60
|
-
`Validation failed: ${errors.map(e => `[${e.validator}] ${e.error}`).join('; ')}`
|
|
61
|
-
);
|
|
62
|
-
error.validationErrors = errors;
|
|
63
|
-
throw error;
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
repo.on('before:create', async (context) => await runValidators('create', context));
|
|
68
|
-
repo.on('before:createMany', async (context) => await runValidators('createMany', context));
|
|
69
|
-
repo.on('before:update', async (context) => await runValidators('update', context));
|
|
70
|
-
repo.on('before:delete', async (context) => await runValidators('delete', context));
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Block operation if condition is true
|
|
77
|
-
* @param {string} name - Validator name
|
|
78
|
-
* @param {string[]} operations - Operations to block on
|
|
79
|
-
* @param {Function} condition - Condition function (context) => boolean
|
|
80
|
-
* @param {string} errorMessage - Error message to throw
|
|
81
|
-
* @example blockIf('block-library', ['delete'], ctx => ctx.data.managed, 'Cannot delete managed records')
|
|
82
|
-
*/
|
|
83
|
-
export const blockIf = (name, operations, condition, errorMessage) => ({
|
|
84
|
-
name,
|
|
85
|
-
operations,
|
|
86
|
-
validate: (context) => {
|
|
87
|
-
if (condition(context)) {
|
|
88
|
-
throw createError(403, errorMessage);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
export const requireField = (field, operations = ['create']) => ({
|
|
94
|
-
name: `require-${field}`,
|
|
95
|
-
operations,
|
|
96
|
-
validate: (context) => {
|
|
97
|
-
if (!context.data || context.data[field] === undefined || context.data[field] === null) {
|
|
98
|
-
throw createError(400, `Field '${field}' is required`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
export const autoInject = (field, getter, operations = ['create']) => ({
|
|
104
|
-
name: `auto-inject-${field}`,
|
|
105
|
-
operations,
|
|
106
|
-
validate: (context) => {
|
|
107
|
-
if (context.data && !(field in context.data)) {
|
|
108
|
-
const value = getter(context);
|
|
109
|
-
if (value !== null && value !== undefined) {
|
|
110
|
-
context.data[field] = value;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
export const immutableField = (field) => ({
|
|
117
|
-
name: `immutable-${field}`,
|
|
118
|
-
operations: ['update'],
|
|
119
|
-
validate: (context) => {
|
|
120
|
-
if (context.data && field in context.data) {
|
|
121
|
-
throw createError(400, `Field '${field}' cannot be modified`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
export const uniqueField = (field, errorMessage) => ({
|
|
127
|
-
name: `unique-${field}`,
|
|
128
|
-
operations: ['create', 'update'],
|
|
129
|
-
validate: async (context, repo) => {
|
|
130
|
-
if (!context.data || !context.data[field]) return;
|
|
131
|
-
|
|
132
|
-
const query = { [field]: context.data[field] };
|
|
133
|
-
const existing = await repo.getByQuery(query, {
|
|
134
|
-
select: '_id',
|
|
135
|
-
lean: true,
|
|
136
|
-
throwOnNotFound: false
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
if (existing && existing._id.toString() !== context.id?.toString()) {
|
|
140
|
-
throw createError(409, errorMessage || `${field} already exists`);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
export default validationChainPlugin;
|
|
1
|
+
import { createError } from '../utils/error.js';
|
|
2
|
+
|
|
3
|
+
export const validationChainPlugin = (validators = [], options = {}) => {
|
|
4
|
+
const { stopOnFirstError = true } = options;
|
|
5
|
+
|
|
6
|
+
validators.forEach((v, idx) => {
|
|
7
|
+
if (!v.name || typeof v.name !== 'string') {
|
|
8
|
+
throw new Error(`Validator at index ${idx} missing 'name' (string)`);
|
|
9
|
+
}
|
|
10
|
+
if (typeof v.validate !== 'function') {
|
|
11
|
+
throw new Error(`Validator '${v.name}' missing 'validate' function`);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const validatorsByOperation = { create: [], update: [], delete: [], createMany: [] };
|
|
16
|
+
const allOperationsValidators = [];
|
|
17
|
+
|
|
18
|
+
validators.forEach(v => {
|
|
19
|
+
if (!v.operations || v.operations.length === 0) {
|
|
20
|
+
allOperationsValidators.push(v);
|
|
21
|
+
} else {
|
|
22
|
+
v.operations.forEach(op => {
|
|
23
|
+
if (validatorsByOperation[op]) {
|
|
24
|
+
validatorsByOperation[op].push(v);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
name: 'validation-chain',
|
|
32
|
+
|
|
33
|
+
apply(repo) {
|
|
34
|
+
const getValidatorsForOperation = (operation) => {
|
|
35
|
+
const specific = validatorsByOperation[operation] || [];
|
|
36
|
+
return [...allOperationsValidators, ...specific];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const runValidators = async (operation, context) => {
|
|
40
|
+
const validators = getValidatorsForOperation(operation);
|
|
41
|
+
const errors = [];
|
|
42
|
+
|
|
43
|
+
for (const validator of validators) {
|
|
44
|
+
try {
|
|
45
|
+
await validator.validate(context, repo);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (stopOnFirstError) {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
errors.push({
|
|
51
|
+
validator: validator.name,
|
|
52
|
+
error: error.message || String(error)
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (errors.length > 0) {
|
|
58
|
+
const error = /** @type {Error & {status: number, validationErrors: any[]}} */ (createError(
|
|
59
|
+
400,
|
|
60
|
+
`Validation failed: ${errors.map(e => `[${e.validator}] ${e.error}`).join('; ')}`
|
|
61
|
+
));
|
|
62
|
+
error.validationErrors = errors;
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
repo.on('before:create', async (context) => await runValidators('create', context));
|
|
68
|
+
repo.on('before:createMany', async (context) => await runValidators('createMany', context));
|
|
69
|
+
repo.on('before:update', async (context) => await runValidators('update', context));
|
|
70
|
+
repo.on('before:delete', async (context) => await runValidators('delete', context));
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Block operation if condition is true
|
|
77
|
+
* @param {string} name - Validator name
|
|
78
|
+
* @param {string[]} operations - Operations to block on
|
|
79
|
+
* @param {Function} condition - Condition function (context) => boolean
|
|
80
|
+
* @param {string} errorMessage - Error message to throw
|
|
81
|
+
* @example blockIf('block-library', ['delete'], ctx => ctx.data.managed, 'Cannot delete managed records')
|
|
82
|
+
*/
|
|
83
|
+
export const blockIf = (name, operations, condition, errorMessage) => ({
|
|
84
|
+
name,
|
|
85
|
+
operations,
|
|
86
|
+
validate: (context) => {
|
|
87
|
+
if (condition(context)) {
|
|
88
|
+
throw createError(403, errorMessage);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export const requireField = (field, operations = ['create']) => ({
|
|
94
|
+
name: `require-${field}`,
|
|
95
|
+
operations,
|
|
96
|
+
validate: (context) => {
|
|
97
|
+
if (!context.data || context.data[field] === undefined || context.data[field] === null) {
|
|
98
|
+
throw createError(400, `Field '${field}' is required`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export const autoInject = (field, getter, operations = ['create']) => ({
|
|
104
|
+
name: `auto-inject-${field}`,
|
|
105
|
+
operations,
|
|
106
|
+
validate: (context) => {
|
|
107
|
+
if (context.data && !(field in context.data)) {
|
|
108
|
+
const value = getter(context);
|
|
109
|
+
if (value !== null && value !== undefined) {
|
|
110
|
+
context.data[field] = value;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
export const immutableField = (field) => ({
|
|
117
|
+
name: `immutable-${field}`,
|
|
118
|
+
operations: ['update'],
|
|
119
|
+
validate: (context) => {
|
|
120
|
+
if (context.data && field in context.data) {
|
|
121
|
+
throw createError(400, `Field '${field}' cannot be modified`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
export const uniqueField = (field, errorMessage) => ({
|
|
127
|
+
name: `unique-${field}`,
|
|
128
|
+
operations: ['create', 'update'],
|
|
129
|
+
validate: async (context, repo) => {
|
|
130
|
+
if (!context.data || !context.data[field]) return;
|
|
131
|
+
|
|
132
|
+
const query = { [field]: context.data[field] };
|
|
133
|
+
const existing = await repo.getByQuery(query, {
|
|
134
|
+
select: '_id',
|
|
135
|
+
lean: true,
|
|
136
|
+
throwOnNotFound: false
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (existing && existing._id.toString() !== context.id?.toString()) {
|
|
140
|
+
throw createError(409, errorMessage || `${field} already exists`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
export default validationChainPlugin;
|
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for MongoKit
|
|
3
|
+
* Used for type checking and auto-completion
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Model, Document, PopulateOptions, ClientSession, Types } from 'mongoose';
|
|
7
|
+
|
|
8
|
+
// Re-export mongoose ObjectId
|
|
9
|
+
export type ObjectId = Types.ObjectId;
|
|
10
|
+
|
|
11
|
+
// Pagination configuration
|
|
12
|
+
export interface PaginationConfig {
|
|
13
|
+
defaultLimit?: number;
|
|
14
|
+
maxLimit?: number;
|
|
15
|
+
maxPage?: number;
|
|
16
|
+
deepPageThreshold?: number;
|
|
17
|
+
cursorVersion?: number;
|
|
18
|
+
useEstimatedCount?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Pagination options
|
|
22
|
+
export interface OffsetPaginationOptions {
|
|
23
|
+
filters?: Record<string, any>;
|
|
24
|
+
sort?: Record<string, 1 | -1>;
|
|
25
|
+
page?: number;
|
|
26
|
+
limit?: number;
|
|
27
|
+
select?: string | string[];
|
|
28
|
+
populate?: string | string[] | PopulateOptions | PopulateOptions[];
|
|
29
|
+
lean?: boolean;
|
|
30
|
+
session?: ClientSession;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface KeysetPaginationOptions {
|
|
34
|
+
filters?: Record<string, any>;
|
|
35
|
+
sort: Record<string, 1 | -1>;
|
|
36
|
+
after?: string;
|
|
37
|
+
limit?: number;
|
|
38
|
+
select?: string | string[] | Record<string, any>;
|
|
39
|
+
populate?: string | string[] | PopulateOptions | PopulateOptions[];
|
|
40
|
+
lean?: boolean;
|
|
41
|
+
session?: ClientSession;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface AggregatePaginationOptions {
|
|
45
|
+
pipeline?: any[];
|
|
46
|
+
page?: number;
|
|
47
|
+
limit?: number;
|
|
48
|
+
session?: ClientSession;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Pagination result types
|
|
52
|
+
export interface OffsetPaginationResult<T = any> {
|
|
53
|
+
method: 'offset';
|
|
54
|
+
docs: T[];
|
|
55
|
+
page: number;
|
|
56
|
+
limit: number;
|
|
57
|
+
total: number;
|
|
58
|
+
pages: number;
|
|
59
|
+
hasNext: boolean;
|
|
60
|
+
hasPrev: boolean;
|
|
61
|
+
warning?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface KeysetPaginationResult<T = any> {
|
|
65
|
+
method: 'keyset';
|
|
66
|
+
docs: T[];
|
|
67
|
+
limit: number;
|
|
68
|
+
hasMore: boolean;
|
|
69
|
+
next: string | null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface AggregatePaginationResult<T = any> {
|
|
73
|
+
method: 'aggregate';
|
|
74
|
+
docs: T[];
|
|
75
|
+
page: number;
|
|
76
|
+
limit: number;
|
|
77
|
+
total: number;
|
|
78
|
+
pages: number;
|
|
79
|
+
hasNext: boolean;
|
|
80
|
+
hasPrev: boolean;
|
|
81
|
+
warning?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type PaginationResult<T = any> =
|
|
85
|
+
| OffsetPaginationResult<T>
|
|
86
|
+
| KeysetPaginationResult<T>
|
|
87
|
+
| AggregatePaginationResult<T>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates an error with HTTP status code
|
|
3
|
+
*
|
|
4
|
+
* @param {number} status - HTTP status code
|
|
5
|
+
* @param {string} message - Error message
|
|
6
|
+
* @returns {Error & {status: number}} Error with status property
|
|
7
|
+
*/
|
|
8
|
+
export function createError(status, message) {
|
|
9
|
+
const error = /** @type {Error & {status: number}} */ (new Error(message));
|
|
10
|
+
error.status = status;
|
|
11
|
+
return error;
|
|
12
|
+
}
|