@modular-rest/server 1.11.12 → 1.11.14
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/.nvmrc +1 -0
- package/.prettierrc.json +9 -0
- package/.releaserc.json +24 -0
- package/README.md +79 -94
- package/dist/index.js +79 -0
- package/docs/.keep +0 -0
- package/docs/system-access-type.md +26 -0
- package/package.json +58 -45
- package/src/application.ts +206 -0
- package/src/class/cms_trigger.ts +68 -0
- package/src/class/collection_definition.ts +134 -0
- package/src/class/combinator.ts +176 -0
- package/src/class/database_trigger.ts +99 -0
- package/src/class/db_schemas.ts +44 -0
- package/src/class/{directory.js → directory.ts} +40 -18
- package/src/class/paginator.ts +51 -0
- package/src/class/reply.ts +59 -0
- package/src/class/security.ts +250 -0
- package/src/class/trigger_operator.ts +142 -0
- package/src/class/user.ts +199 -0
- package/src/class/validator.ts +123 -0
- package/src/config.ts +122 -0
- package/src/defult-permissions.ts +31 -0
- package/src/events.ts +59 -0
- package/src/helper/data_insertion.ts +94 -0
- package/src/helper/presetup_services.ts +96 -0
- package/src/index.ts +146 -0
- package/src/middlewares.ts +75 -0
- package/src/play-test.ts +8 -0
- package/src/services/data_provider/router.ts +191 -0
- package/src/services/data_provider/service.ts +305 -0
- package/src/services/data_provider/typeCasters.ts +15 -0
- package/src/services/file/db.ts +29 -0
- package/src/services/file/router.ts +88 -0
- package/src/services/file/service.ts +387 -0
- package/src/services/functions/router.ts +34 -0
- package/src/services/functions/service.ts +203 -0
- package/src/services/jwt/router.ts +73 -0
- package/src/services/jwt/service.ts +139 -0
- package/src/services/user_manager/db.ts +87 -0
- package/src/services/user_manager/permissionManager.ts +49 -0
- package/src/services/user_manager/router.ts +193 -0
- package/src/services/user_manager/service.ts +698 -0
- package/tsconfig.json +16 -9
- package/typedoc.mjs +41 -0
- package/LICENSE +0 -21
- package/package-lock.json +0 -1373
- package/src/application.js +0 -239
- package/src/class/cms_trigger.js +0 -20
- package/src/class/collection_definition.js +0 -33
- package/src/class/combinator.js +0 -133
- package/src/class/database_trigger.js +0 -20
- package/src/class/db_schemas.js +0 -18
- package/src/class/paginator.js +0 -31
- package/src/class/reply.js +0 -37
- package/src/class/security.js +0 -141
- package/src/class/trigger_operator.js +0 -39
- package/src/class/user.js +0 -112
- package/src/class/validator.js +0 -91
- package/src/config.js +0 -67
- package/src/events.js +0 -15
- package/src/helper/data_insertion.js +0 -64
- package/src/helper/presetup_services.js +0 -31
- package/src/index.js +0 -66
- package/src/middlewares.js +0 -44
- package/src/services/data_provider/router.js +0 -552
- package/src/services/data_provider/service.js +0 -262
- package/src/services/data_provider/typeCasters.js +0 -10
- package/src/services/file/db.js +0 -29
- package/src/services/file/router.js +0 -92
- package/src/services/file/service.js +0 -231
- package/src/services/functions/router.js +0 -37
- package/src/services/functions/service.js +0 -74
- package/src/services/jwt/router.js +0 -70
- package/src/services/jwt/service.js +0 -37
- package/src/services/user_manager/db.js +0 -83
- package/src/services/user_manager/permissionManager.js +0 -43
- package/src/services/user_manager/router.js +0 -176
- package/src/services/user_manager/service.js +0 -377
- package/types/application.d.ts +0 -97
- package/types/class/cms_trigger.d.ts +0 -24
- package/types/class/collection_definition.d.ts +0 -36
- package/types/class/combinator.d.ts +0 -30
- package/types/class/database_trigger.d.ts +0 -28
- package/types/class/db_schemas.d.ts +0 -2
- package/types/class/directory.d.ts +0 -2
- package/types/class/paginator.d.ts +0 -8
- package/types/class/reply.d.ts +0 -8
- package/types/class/security.d.ts +0 -109
- package/types/class/trigger_operator.d.ts +0 -19
- package/types/class/user.d.ts +0 -24
- package/types/class/validator.d.ts +0 -9
- package/types/config.d.ts +0 -101
- package/types/events.d.ts +0 -7
- package/types/helper/data_insertion.d.ts +0 -4
- package/types/helper/presetup_services.d.ts +0 -5
- package/types/index.d.ts +0 -72
- package/types/middlewares.d.ts +0 -9
- package/types/services/data_provider/router.d.ts +0 -3
- package/types/services/data_provider/service.d.ts +0 -40
- package/types/services/data_provider/typeCasters.d.ts +0 -3
- package/types/services/file/db.d.ts +0 -3
- package/types/services/file/router.d.ts +0 -3
- package/types/services/file/service.d.ts +0 -81
- package/types/services/functions/router.d.ts +0 -3
- package/types/services/functions/service.d.ts +0 -23
- package/types/services/jwt/router.d.ts +0 -3
- package/types/services/jwt/service.d.ts +0 -10
- package/types/services/user_manager/db.d.ts +0 -3
- package/types/services/user_manager/permissionManager.d.ts +0 -3
- package/types/services/user_manager/router.d.ts +0 -3
- package/types/services/user_manager/service.d.ts +0 -131
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import Router from 'koa-router';
|
|
2
|
+
import { validateObject } from '../../class/validator';
|
|
3
|
+
import { create as reply } from '../../class/reply';
|
|
4
|
+
import { Context, Next } from 'koa';
|
|
5
|
+
import { AccessTypes } from './../../class/security';
|
|
6
|
+
import * as DataService from './../data_provider/service';
|
|
7
|
+
import { main as service } from './service';
|
|
8
|
+
import * as middleware from '../../middlewares';
|
|
9
|
+
|
|
10
|
+
const name = 'file';
|
|
11
|
+
const fileRouter = new Router();
|
|
12
|
+
|
|
13
|
+
fileRouter.use('/', middleware.auth, async (ctx: Context, next: Next) => {
|
|
14
|
+
await next();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
fileRouter.post('/', async (ctx: Context) => {
|
|
18
|
+
const body = ctx.request.body;
|
|
19
|
+
|
|
20
|
+
// validate result
|
|
21
|
+
const bodyValidate = validateObject(body, 'tag');
|
|
22
|
+
|
|
23
|
+
// fields validation
|
|
24
|
+
if (!bodyValidate.isValid) {
|
|
25
|
+
ctx.status = 412;
|
|
26
|
+
ctx.body = reply('e', { e: bodyValidate.requires });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Access validation
|
|
31
|
+
const hasAccess = DataService.checkAccess('cms', 'file', AccessTypes.write, body, ctx.state.user);
|
|
32
|
+
if (!hasAccess) {
|
|
33
|
+
ctx.throw(403, 'access denied');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const files = ctx.request.files;
|
|
38
|
+
const file = files && files.file;
|
|
39
|
+
let result;
|
|
40
|
+
|
|
41
|
+
if (!file) {
|
|
42
|
+
ctx.status = 412;
|
|
43
|
+
result = reply('f', { message: 'file field required' });
|
|
44
|
+
} else {
|
|
45
|
+
await service
|
|
46
|
+
.storeFile({
|
|
47
|
+
file: file as any,
|
|
48
|
+
ownerId: ctx.state.user.id,
|
|
49
|
+
tag: body.tag,
|
|
50
|
+
})
|
|
51
|
+
.then(file => {
|
|
52
|
+
result = reply('s', { file });
|
|
53
|
+
})
|
|
54
|
+
.catch(error => {
|
|
55
|
+
ctx.status = 412;
|
|
56
|
+
result = reply('e', error);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
ctx.body = result;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
fileRouter.delete('/', async (ctx: Context) => {
|
|
64
|
+
const body = ctx.request.query;
|
|
65
|
+
// validate
|
|
66
|
+
const bodyValidate = validateObject(body as Record<string, any>, 'id');
|
|
67
|
+
|
|
68
|
+
let result;
|
|
69
|
+
|
|
70
|
+
if (!bodyValidate.isValid) {
|
|
71
|
+
ctx.status = 412;
|
|
72
|
+
result = reply('f', { message: 'some fields required.', error: bodyValidate.requires });
|
|
73
|
+
} else {
|
|
74
|
+
await service
|
|
75
|
+
.removeFile(body.id as string)
|
|
76
|
+
.then(() => {
|
|
77
|
+
result = reply('s');
|
|
78
|
+
})
|
|
79
|
+
.catch(e => {
|
|
80
|
+
ctx.status = 412;
|
|
81
|
+
result = reply('e', { error: e });
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
ctx.body = result;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export { name, fileRouter as main };
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import pathModule from 'path';
|
|
3
|
+
import * as DataProvider from '../data_provider/service';
|
|
4
|
+
import triggerService from '../../class/trigger_operator';
|
|
5
|
+
import { config } from '../../config';
|
|
6
|
+
import { IFile } from '../../class/db_schemas';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Service name constant
|
|
10
|
+
* @constant {string}
|
|
11
|
+
*/
|
|
12
|
+
export const name = 'file';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* File storage detail interface
|
|
16
|
+
* @interface StoredFileDetail
|
|
17
|
+
* @property {string} fileName - Generated unique filename
|
|
18
|
+
* @property {string} fullPath - Full path to the stored file
|
|
19
|
+
* @property {string} fileFormat - File format/extension
|
|
20
|
+
*/
|
|
21
|
+
interface StoredFileDetail {
|
|
22
|
+
fileName: string;
|
|
23
|
+
fullPath: string;
|
|
24
|
+
fileFormat: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* File upload options interface
|
|
29
|
+
* @interface StoreFileOptions
|
|
30
|
+
* @property {Object} file - File details
|
|
31
|
+
* @property {string} file.path - Temporary file path
|
|
32
|
+
* @property {string} file.type - MIME type of the file
|
|
33
|
+
* @property {string} file.name - Original filename
|
|
34
|
+
* @property {number} file.size - File size in bytes
|
|
35
|
+
* @property {string} ownerId - ID of the file owner
|
|
36
|
+
* @property {string} tag - Tag for file organization
|
|
37
|
+
* @property {boolean} [removeFileAfterStore=true] - Whether to remove the temporary file after storage
|
|
38
|
+
*/
|
|
39
|
+
interface StoreFileOptions {
|
|
40
|
+
file: {
|
|
41
|
+
path: string;
|
|
42
|
+
type: string;
|
|
43
|
+
name: string;
|
|
44
|
+
size: number;
|
|
45
|
+
};
|
|
46
|
+
ownerId: string;
|
|
47
|
+
tag: string;
|
|
48
|
+
removeFileAfterStore?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* File service for handling file storage and retrieval.
|
|
53
|
+
*
|
|
54
|
+
* This service provides functionality for storing, retrieving, and managing files.
|
|
55
|
+
* It handles file storage on disk and maintains file metadata in the database.
|
|
56
|
+
* Files are organized by format and tag in the upload directory.
|
|
57
|
+
*/
|
|
58
|
+
class FileService {
|
|
59
|
+
/**
|
|
60
|
+
* @hidden
|
|
61
|
+
*/
|
|
62
|
+
private directory: string | null = null;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @hidden
|
|
66
|
+
*/
|
|
67
|
+
static instance: FileService;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @hidden
|
|
71
|
+
*/
|
|
72
|
+
constructor() {}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @hidden
|
|
76
|
+
*
|
|
77
|
+
* Sets the upload directory for file storage
|
|
78
|
+
* @param {string} directory - Directory path for file storage
|
|
79
|
+
* @throws {Error} If directory is invalid or not writable
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* import { fileService } from '@modular-rest/server';
|
|
83
|
+
*
|
|
84
|
+
* fileService.setUploadDirectory('/path/to/uploads');
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
setUploadDirectory(directory: string): void {
|
|
88
|
+
if (!fs.existsSync(directory)) {
|
|
89
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
this.directory = directory;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @hidden
|
|
96
|
+
*
|
|
97
|
+
* Creates stored file details with unique filename
|
|
98
|
+
* @param {string} fileType - MIME type of the file
|
|
99
|
+
* @param {string} tag - Tag for file organization
|
|
100
|
+
* @returns {StoredFileDetail} Storage details including filename and path
|
|
101
|
+
* @throws {Error} If upload directory is not set
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```typescript
|
|
105
|
+
* import { fileService } from '@modular-rest/server';
|
|
106
|
+
*
|
|
107
|
+
* const details = fileService.createStoredDetail('image/jpeg', 'profile');
|
|
108
|
+
* // Returns: { fileName: '1234567890.jpeg', fullPath: '/uploads/jpeg/profile/1234567890.jpeg', fileFormat: 'jpeg' }
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
createStoredDetail(fileType: string, tag: string): StoredFileDetail {
|
|
112
|
+
const typeParts = fileType.split('/');
|
|
113
|
+
const fileFormat = typeParts[1] || typeParts[0] || 'unknown';
|
|
114
|
+
|
|
115
|
+
const time = new Date().getTime();
|
|
116
|
+
const fileName = `${time}.${fileFormat}`;
|
|
117
|
+
|
|
118
|
+
if (!FileService.instance.directory) {
|
|
119
|
+
throw new Error('Upload directory has not been set');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const fullPath = pathModule.join(FileService.instance.directory, fileFormat, tag, fileName);
|
|
123
|
+
|
|
124
|
+
return { fileName, fullPath, fileFormat };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @hidden
|
|
129
|
+
*
|
|
130
|
+
* Stores a file, removes the temporary file, and saves metadata to database
|
|
131
|
+
* @param {StoreFileOptions} options - File storage options
|
|
132
|
+
* @returns {Promise<IFile>} Promise resolving to stored file document
|
|
133
|
+
* @throws {Error} If upload directory is not set or storage fails
|
|
134
|
+
* @example
|
|
135
|
+
* ```typescript
|
|
136
|
+
* import { fileService } from '@modular-rest/server';
|
|
137
|
+
*
|
|
138
|
+
* const file = await fileService.storeFile({
|
|
139
|
+
* file: {
|
|
140
|
+
* path: '/tmp/upload.jpg',
|
|
141
|
+
* type: 'image/jpeg',
|
|
142
|
+
* name: 'profile.jpg',
|
|
143
|
+
* size: 1024
|
|
144
|
+
* },
|
|
145
|
+
* ownerId: 'user123',
|
|
146
|
+
* tag: 'profile',
|
|
147
|
+
* removeFileAfterStore: true
|
|
148
|
+
* });
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
storeFile({ file, ownerId, tag, removeFileAfterStore = true }: StoreFileOptions): Promise<IFile> {
|
|
152
|
+
if (!FileService.instance.directory) {
|
|
153
|
+
throw new Error('Upload directory has not been set');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let storedFile: StoredFileDetail;
|
|
157
|
+
|
|
158
|
+
return new Promise(async (done, reject) => {
|
|
159
|
+
storedFile = FileService.instance.createStoredDetail(file.type, tag);
|
|
160
|
+
|
|
161
|
+
fs.copyFile(file.path, storedFile.fullPath, (err: Error | null) => {
|
|
162
|
+
if (err) {
|
|
163
|
+
reject(err);
|
|
164
|
+
} else {
|
|
165
|
+
done(null);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// remove temp file
|
|
169
|
+
if (removeFileAfterStore) {
|
|
170
|
+
fs.unlinkSync(file.path);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
})
|
|
174
|
+
.then(() => {
|
|
175
|
+
// Get collection model for access to relative collection
|
|
176
|
+
const CollectionModel = DataProvider.getCollection<IFile>('cms', 'file');
|
|
177
|
+
|
|
178
|
+
if (!CollectionModel) {
|
|
179
|
+
throw new Error('Collection model not found');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const data = {
|
|
183
|
+
owner: ownerId,
|
|
184
|
+
fileName: storedFile.fileName,
|
|
185
|
+
originalName: file.name,
|
|
186
|
+
format: storedFile.fileFormat,
|
|
187
|
+
tag,
|
|
188
|
+
size: file.size,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Create new document
|
|
192
|
+
const doc = new CollectionModel(data);
|
|
193
|
+
|
|
194
|
+
return doc.save().then(savedDoc => {
|
|
195
|
+
triggerService.call('insert-one', 'cms', 'file', {
|
|
196
|
+
query: null,
|
|
197
|
+
queryResult: savedDoc,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return savedDoc;
|
|
201
|
+
});
|
|
202
|
+
})
|
|
203
|
+
.catch(err => {
|
|
204
|
+
// remove stored file
|
|
205
|
+
fs.unlinkSync(storedFile.fullPath);
|
|
206
|
+
|
|
207
|
+
throw err;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @hidden
|
|
213
|
+
*
|
|
214
|
+
* Removes a file from the disk
|
|
215
|
+
* @param {string} path - File path to remove
|
|
216
|
+
* @returns {Promise<void>} Promise resolving when file is removed
|
|
217
|
+
* @throws {Error} If file removal fails
|
|
218
|
+
* @example
|
|
219
|
+
* ```typescript
|
|
220
|
+
* import { fileService } from '@modular-rest/server';
|
|
221
|
+
*
|
|
222
|
+
* await fileService.removeFromDisc('/uploads/jpeg/profile/1234567890.jpeg');
|
|
223
|
+
* ```
|
|
224
|
+
*/
|
|
225
|
+
removeFromDisc(path: string): Promise<void> {
|
|
226
|
+
return new Promise((done, reject) => {
|
|
227
|
+
fs.unlink(path, (err: Error | null) => {
|
|
228
|
+
if (err) reject(err);
|
|
229
|
+
else done();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Removes a file from both database and disk
|
|
236
|
+
*
|
|
237
|
+
* @param {string} fileId - File ID to remove
|
|
238
|
+
* @returns {Promise<void>} Promise resolving when file is removed
|
|
239
|
+
* @throws {Error} If file is not found or removal fails
|
|
240
|
+
* @example
|
|
241
|
+
* ```typescript
|
|
242
|
+
* import { fileService } from '@modular-rest/server';
|
|
243
|
+
*
|
|
244
|
+
* try {
|
|
245
|
+
* await fileService.removeFile('file123');
|
|
246
|
+
* console.log('File removed successfully');
|
|
247
|
+
* } catch (error) {
|
|
248
|
+
* console.error('Failed to remove file:', error);
|
|
249
|
+
* }
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
252
|
+
removeFile(fileId: string): Promise<void> {
|
|
253
|
+
if (!FileService.instance.directory) {
|
|
254
|
+
throw new Error('Upload directory has not been set');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return new Promise(async (done, reject) => {
|
|
258
|
+
const CollectionModel = DataProvider.getCollection<IFile>('cms', 'file');
|
|
259
|
+
|
|
260
|
+
if (!CollectionModel) {
|
|
261
|
+
return reject(new Error('Collection model not found'));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const fileDoc = await CollectionModel.findOne({ _id: fileId }).exec();
|
|
265
|
+
|
|
266
|
+
if (!fileDoc) {
|
|
267
|
+
return reject(new Error('File not found'));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
await CollectionModel.deleteOne({ _id: fileId })
|
|
271
|
+
.exec()
|
|
272
|
+
.then(() => {
|
|
273
|
+
// create file path
|
|
274
|
+
const filePath = pathModule.join(
|
|
275
|
+
FileService.instance.directory as string,
|
|
276
|
+
fileDoc.format,
|
|
277
|
+
fileDoc.tag,
|
|
278
|
+
fileDoc.fileName
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
// Remove file from disc
|
|
282
|
+
return FileService.instance.removeFromDisc(filePath).catch(async err => {
|
|
283
|
+
// Recreate fileDoc if removing file operation has error
|
|
284
|
+
await new CollectionModel(fileDoc).save();
|
|
285
|
+
|
|
286
|
+
throw err;
|
|
287
|
+
});
|
|
288
|
+
})
|
|
289
|
+
.then(() => {
|
|
290
|
+
triggerService.call('remove-one', 'cms', 'file', {
|
|
291
|
+
query: { _id: fileId },
|
|
292
|
+
queryResult: null,
|
|
293
|
+
});
|
|
294
|
+
})
|
|
295
|
+
.then(done)
|
|
296
|
+
.catch(reject);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Retrieves a file document from the database
|
|
302
|
+
*
|
|
303
|
+
* @param {string} fileId - File ID to retrieve
|
|
304
|
+
* @returns {Promise<IFile>} Promise resolving to file document
|
|
305
|
+
* @throws {Error} If collection model is not found or file is not found
|
|
306
|
+
* @example
|
|
307
|
+
* ```typescript
|
|
308
|
+
* import { fileService } from '@modular-rest/server';
|
|
309
|
+
*
|
|
310
|
+
* const fileDoc = await fileService.getFile('file123');
|
|
311
|
+
* console.log('File details:', fileDoc);
|
|
312
|
+
* ```
|
|
313
|
+
*/
|
|
314
|
+
getFile(fileId: string): Promise<IFile> {
|
|
315
|
+
const CollectionModel = DataProvider.getCollection<IFile>('cms', 'file');
|
|
316
|
+
|
|
317
|
+
if (!CollectionModel) {
|
|
318
|
+
throw new Error('Collection model not found');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return CollectionModel.findOne({ _id: fileId })
|
|
322
|
+
.exec()
|
|
323
|
+
.then(doc => {
|
|
324
|
+
if (!doc) {
|
|
325
|
+
throw new Error('File not found');
|
|
326
|
+
}
|
|
327
|
+
return doc;
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Retrieves the public URL link for a file
|
|
333
|
+
*
|
|
334
|
+
* @param {string} fileId - File ID to get link for
|
|
335
|
+
* @returns {Promise<string>} Promise resolving to file URL
|
|
336
|
+
* @throws {Error} If static path root is not defined or file is not found
|
|
337
|
+
* @example
|
|
338
|
+
* ```typescript
|
|
339
|
+
* import { fileService } from '@modular-rest/server';
|
|
340
|
+
*
|
|
341
|
+
* const link = await fileService.getFileLink('file123');
|
|
342
|
+
* // Returns: '/static/jpeg/profile/1234567890.jpeg'
|
|
343
|
+
* ```
|
|
344
|
+
*/
|
|
345
|
+
async getFileLink(fileId: string): Promise<string> {
|
|
346
|
+
const fileDoc = await FileService.instance.getFile(fileId);
|
|
347
|
+
|
|
348
|
+
if (!config.staticPath?.actualPath) {
|
|
349
|
+
throw new Error('Static path root is not defined');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const link =
|
|
353
|
+
config.staticPath.actualPath + `/${fileDoc.format}/${fileDoc.tag}/` + fileDoc.fileName;
|
|
354
|
+
|
|
355
|
+
return link;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Gets the full filesystem path for a file
|
|
360
|
+
*
|
|
361
|
+
* @param {string} fileId - File ID to get path for
|
|
362
|
+
* @returns {Promise<string>} Promise resolving to full file path
|
|
363
|
+
* @throws {Error} If upload directory is not set or file is not found
|
|
364
|
+
* @example
|
|
365
|
+
* ```typescript
|
|
366
|
+
* import { fileService } from '@modular-rest/server';
|
|
367
|
+
*
|
|
368
|
+
* const path = await fileService.getFilePath('file123');
|
|
369
|
+
* // Returns: '/uploads/jpeg/profile/1234567890.jpeg'
|
|
370
|
+
* ```
|
|
371
|
+
*/
|
|
372
|
+
async getFilePath(fileId: string): Promise<string> {
|
|
373
|
+
const { fileName, format, tag } = await FileService.instance.getFile(fileId);
|
|
374
|
+
|
|
375
|
+
if (!FileService.instance.directory) {
|
|
376
|
+
throw new Error('Upload directory has not been set');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return pathModule.join(FileService.instance.directory, format, tag, fileName);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Main file service instance
|
|
385
|
+
* @constant {FileService}
|
|
386
|
+
*/
|
|
387
|
+
export const main = new FileService();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import Router from 'koa-router';
|
|
2
|
+
import * as service from './service';
|
|
3
|
+
import * as middleware from '../../middlewares';
|
|
4
|
+
import { validateObject } from '../../class/validator';
|
|
5
|
+
import { create as reply } from '../../class/reply';
|
|
6
|
+
import { Context, Next } from 'koa';
|
|
7
|
+
|
|
8
|
+
const functionRouter = new Router();
|
|
9
|
+
const name = 'function';
|
|
10
|
+
|
|
11
|
+
functionRouter.use('/', middleware.auth, async (ctx: Context, next: Next) => {
|
|
12
|
+
const body = ctx.request.body;
|
|
13
|
+
const bodyValidated = validateObject(body, 'name args');
|
|
14
|
+
|
|
15
|
+
// fields validation
|
|
16
|
+
if (!bodyValidated.isValid) {
|
|
17
|
+
ctx.throw(412, JSON.stringify(reply('e', { error: bodyValidated.requires })));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
await next();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
functionRouter.post('/run', middleware.auth, async (ctx: Context) => {
|
|
24
|
+
const { name, args } = ctx.request.body;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const result = await service.runFunction(name, args, ctx.state.user);
|
|
28
|
+
ctx.body = JSON.stringify(reply('s', { data: result }));
|
|
29
|
+
} catch (e) {
|
|
30
|
+
ctx.throw(400, JSON.stringify(reply('e', { error: (e as Error).message })));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export { name, functionRouter as main };
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { AccessType } from '../../class/security';
|
|
2
|
+
import { User } from '../../class/user';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Service name constant
|
|
6
|
+
* @constant {string}
|
|
7
|
+
*/
|
|
8
|
+
export const name = 'functions';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Storage for registered functions
|
|
12
|
+
* @private
|
|
13
|
+
*/
|
|
14
|
+
const functions: DefinedFunction[] = [];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Interface for defined functions.
|
|
18
|
+
*
|
|
19
|
+
* @interface DefinedFunction
|
|
20
|
+
* @inline
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* const myFunction: DefinedFunction = {
|
|
24
|
+
* name: 'calculateTotal',
|
|
25
|
+
* permissionTypes: ['user_access'],
|
|
26
|
+
* callback: (args) => args.items.reduce((sum, item) => sum + item.price, 0)
|
|
27
|
+
* };
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export interface DefinedFunction {
|
|
31
|
+
/**
|
|
32
|
+
* Unique name of the function
|
|
33
|
+
* @type {string}
|
|
34
|
+
*/
|
|
35
|
+
name: string;
|
|
36
|
+
/**
|
|
37
|
+
* List of permission types required to run the function
|
|
38
|
+
* @type {AccessType[]}
|
|
39
|
+
*/
|
|
40
|
+
permissionTypes: AccessType[];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The actual function implementation
|
|
44
|
+
* @type {(args: any) => any}
|
|
45
|
+
*/
|
|
46
|
+
callback: (args: any) => any;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* To define a function you need to create a `functions.[js|ts]` in each module of your app and return am array called `functions`, and then define all your functions with calling the `defineFunction` method.
|
|
51
|
+
*
|
|
52
|
+
* The `defineFunction` method serves as a core utility for creating custom functions dynamically. This method allows you to specify various parameters, including the name of the function, the permissions required for access, and the corresponding logic that should be executed when the function is invoked.
|
|
53
|
+
*
|
|
54
|
+
* @summary
|
|
55
|
+
* Define a server function to be called by clients.
|
|
56
|
+
*
|
|
57
|
+
* @param {DefinedFunction} options - The function definition options. See {@link DefinedFunction} for detailed parameter descriptions.
|
|
58
|
+
* @expandType DefinedFunction
|
|
59
|
+
*
|
|
60
|
+
* @returns {Object} The defined function object which system will use to generate a router for the function, generall the client library will use the router to call the function.
|
|
61
|
+
* @throws {Error} If function name already exists, permission types are missing, or callback is invalid
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* Here is an example illustrating how to use the `defineFunction` method effectively:
|
|
65
|
+
* ```typescript
|
|
66
|
+
* // /modules/myModule/functions.ts
|
|
67
|
+
*
|
|
68
|
+
* import { defineFunction } from "@modular-rest/server";
|
|
69
|
+
*
|
|
70
|
+
* const getServerTime = defineFunction({
|
|
71
|
+
* name: "getServerTime",
|
|
72
|
+
* permissionTypes: ["anonymous_access"],
|
|
73
|
+
* callback: (params) => {
|
|
74
|
+
* // return your data only
|
|
75
|
+
* return `
|
|
76
|
+
* Welcome, ${params.username}!
|
|
77
|
+
* The current server time is ${new Date().toLocaleString()}.
|
|
78
|
+
* `;
|
|
79
|
+
*
|
|
80
|
+
* // error handling,
|
|
81
|
+
* // client gets error code 400, and the message
|
|
82
|
+
* // throw new Error('An error occurred');
|
|
83
|
+
* },
|
|
84
|
+
* });
|
|
85
|
+
*
|
|
86
|
+
* module.exports.functions = [getServerTime];
|
|
87
|
+
* ```
|
|
88
|
+
* In this example, we define a function named `getServerTime` that requires the `user` permission type to access. When the function is called, it will return a message containing the current server time and the username of the user who invoked the function.
|
|
89
|
+
*
|
|
90
|
+
* ---
|
|
91
|
+
*
|
|
92
|
+
* By utilizing the `defineFunction` method, developers are empowered to create custom functionality effortlessly within the Modular REST framework, enhancing both the versatility and security of their applications.
|
|
93
|
+
*/
|
|
94
|
+
export function defineFunction(options: DefinedFunction): DefinedFunction {
|
|
95
|
+
// Check if the function already exists
|
|
96
|
+
const existingFunction = functions.find(f => f.name === name);
|
|
97
|
+
if (existingFunction) {
|
|
98
|
+
throw new Error(`Function with name ${name} already exists`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check if the permission types provided
|
|
102
|
+
if (!options.permissionTypes || !options.permissionTypes.length) {
|
|
103
|
+
throw new Error(`Permission types not provided for function ${name}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check if the callback is a function
|
|
107
|
+
if (typeof options.callback !== 'function') {
|
|
108
|
+
throw new Error(`Callback is not a function for function ${name}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Add the function to the list of functions
|
|
112
|
+
return options;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Runs a function by name with arguments and user context
|
|
117
|
+
*
|
|
118
|
+
* @param {string} name - Name of the function to run
|
|
119
|
+
* @param {any} args - Arguments to pass to the function
|
|
120
|
+
* @param {User} user - User attempting to run the function
|
|
121
|
+
* @returns {Promise<any>} Promise resolving to function result
|
|
122
|
+
* @throws {Error} If function not found or user lacks required permissions
|
|
123
|
+
*
|
|
124
|
+
* @private
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```typescript
|
|
128
|
+
* try {
|
|
129
|
+
* const result = await runFunction('calculateTotal', {
|
|
130
|
+
* items: [
|
|
131
|
+
* { price: 10 },
|
|
132
|
+
* { price: 20 }
|
|
133
|
+
* ]
|
|
134
|
+
* }, currentUser);
|
|
135
|
+
* console.log('Total:', result); // 30
|
|
136
|
+
* } catch (error) {
|
|
137
|
+
* console.error('Function execution failed:', error);
|
|
138
|
+
* }
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export function runFunction(name: string, args: any, user: User): Promise<any> {
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
const func = functions.find(f => f.name === name);
|
|
144
|
+
if (!func) {
|
|
145
|
+
return reject(new Error(`Function with name ${name} not found`));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const hasPermission = func.permissionTypes.some(permissionType =>
|
|
149
|
+
user.hasPermission(permissionType)
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (!hasPermission) {
|
|
153
|
+
const userBrief = user.getBrief();
|
|
154
|
+
const userPermissions =
|
|
155
|
+
typeof userBrief.permissionGroup === 'object' && userBrief.permissionGroup
|
|
156
|
+
? userBrief.permissionGroup.allowedAccessTypes
|
|
157
|
+
: 'none';
|
|
158
|
+
|
|
159
|
+
reject(
|
|
160
|
+
new Error(`User does not have permission to run function ${name}:
|
|
161
|
+
Function permissions: ${func.permissionTypes}
|
|
162
|
+
User permissions: ${userPermissions}
|
|
163
|
+
`)
|
|
164
|
+
);
|
|
165
|
+
} else {
|
|
166
|
+
try {
|
|
167
|
+
resolve(func.callback(args));
|
|
168
|
+
} catch (e) {
|
|
169
|
+
reject(e);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Adds a function to the registry, this method is used for internal use only,
|
|
177
|
+
* it will add all defined functions to the registry.
|
|
178
|
+
*
|
|
179
|
+
* @param {DefinedFunction} func - Function to add
|
|
180
|
+
* @throws {Error} If function name already exists
|
|
181
|
+
*
|
|
182
|
+
* @private
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```typescript
|
|
186
|
+
* const myFunction = defineFunction({
|
|
187
|
+
* name: 'myFunction',
|
|
188
|
+
* permissionTypes: ['user_access'],
|
|
189
|
+
* callback: (args) => args.value * 2
|
|
190
|
+
* });
|
|
191
|
+
*
|
|
192
|
+
* addFunction(myFunction);
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
export function addFunction(func: DefinedFunction): void {
|
|
196
|
+
// Check if the function already exists
|
|
197
|
+
const existingFunction = functions.find(f => f.name === func.name);
|
|
198
|
+
if (existingFunction) {
|
|
199
|
+
throw new Error(`Function with name ${func.name} already exists`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
functions.push(func);
|
|
203
|
+
}
|