@90soft/parse-server-kit 2.0.0 → 2.0.1
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
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
# @90soft/parse-server-kit
|
|
2
|
+
|
|
3
|
+
Decorator-driven development kit for Parse Server. Write models and cloud functions with TypeScript decorators — no boilerplate.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @90soft/parse-server-kit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Peer dependencies: `parse`, `reflect-metadata`, `express`
|
|
12
|
+
Optional: `node-cron` (for @Cron), `swagger-ui-express` (for Swagger)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### 1. Define a Model
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { ParseClass, ParseField, BaseModel } from '@90soft/parse-server-kit';
|
|
22
|
+
import { UserRoles, roleKey } from '@90soft/parse-server-kit';
|
|
23
|
+
|
|
24
|
+
@ParseClass('Product', {
|
|
25
|
+
clp: {
|
|
26
|
+
find: { [roleKey(UserRoles.ADMIN)]: true, [roleKey(UserRoles.EMPLOYEE)]: true },
|
|
27
|
+
get: { [roleKey(UserRoles.ADMIN)]: true, [roleKey(UserRoles.EMPLOYEE)]: true },
|
|
28
|
+
create: { [roleKey(UserRoles.ADMIN)]: true, [roleKey(UserRoles.EMPLOYEE)]: true },
|
|
29
|
+
update: { [roleKey(UserRoles.ADMIN)]: true, [roleKey(UserRoles.EMPLOYEE)]: true },
|
|
30
|
+
delete: { [roleKey(UserRoles.ADMIN)]: true },
|
|
31
|
+
count: { [roleKey(UserRoles.ADMIN)]: true, [roleKey(UserRoles.EMPLOYEE)]: true },
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
export default class Product extends BaseModel {
|
|
35
|
+
@ParseField({ type: 'String', required: true })
|
|
36
|
+
name!: string;
|
|
37
|
+
|
|
38
|
+
@ParseField({ type: 'Number', required: true, min: 0 })
|
|
39
|
+
price!: number;
|
|
40
|
+
|
|
41
|
+
@ParseField({ type: 'String', enum: ['active', 'draft', 'archived'] })
|
|
42
|
+
status!: string;
|
|
43
|
+
|
|
44
|
+
@ParseField({ type: 'Pointer', targetClass: 'Category', required: true })
|
|
45
|
+
category!: any;
|
|
46
|
+
|
|
47
|
+
@ParseField({ type: 'Boolean' })
|
|
48
|
+
available!: boolean;
|
|
49
|
+
|
|
50
|
+
@ParseField({ type: 'Date' })
|
|
51
|
+
createdDate!: Date;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 2. Write Cloud Functions
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { CloudFunction, Route, catchError, UserRoles } from '@90soft/parse-server-kit';
|
|
59
|
+
import Product from '../../models/Product';
|
|
60
|
+
import User from '../../models/User';
|
|
61
|
+
|
|
62
|
+
@Route(Product) // → /api/products/create, /api/products/list, etc.
|
|
63
|
+
class ProductFunctions {
|
|
64
|
+
|
|
65
|
+
@CloudFunction({
|
|
66
|
+
methods: ['POST'],
|
|
67
|
+
validation: { requireUser: true, fields: { name: { required: true } } },
|
|
68
|
+
})
|
|
69
|
+
static async createProduct(req: Parse.Cloud.FunctionRequest) {
|
|
70
|
+
const user = req.user! as User;
|
|
71
|
+
const product = Product.fromParams(req.params);
|
|
72
|
+
const [err, saved] = await catchError(
|
|
73
|
+
product.save(null, { sessionToken: user.getSessionToken() })
|
|
74
|
+
);
|
|
75
|
+
if (err) throw err;
|
|
76
|
+
return saved;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@CloudFunction({
|
|
80
|
+
methods: ['GET'],
|
|
81
|
+
validation: { requireUser: true, fields: { skip: { type: String }, limit: { type: String } } },
|
|
82
|
+
})
|
|
83
|
+
static async listProducts(req: Parse.Cloud.FunctionRequest) {
|
|
84
|
+
const query = new Parse.Query(Product);
|
|
85
|
+
query.include('category');
|
|
86
|
+
const skip = Number(req.params.skip) || 0;
|
|
87
|
+
const limit = Number(req.params.limit) || 10;
|
|
88
|
+
query.skip(skip).limit(limit);
|
|
89
|
+
|
|
90
|
+
if (req.params.search) {
|
|
91
|
+
const search = req.params.search;
|
|
92
|
+
query.matches('name', new RegExp(search, 'i'));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const [err, results] = await catchError(query.find({ sessionToken: req.user!.getSessionToken() }));
|
|
96
|
+
if (err) throw err;
|
|
97
|
+
|
|
98
|
+
const [countErr, count] = await catchError(query.count({ sessionToken: req.user!.getSessionToken() }));
|
|
99
|
+
return { results, count: countErr ? 0 : count };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@CloudFunction({
|
|
103
|
+
methods: ['POST'],
|
|
104
|
+
validation: { requireUser: true, fields: { id: { required: true } } },
|
|
105
|
+
})
|
|
106
|
+
static async getProduct(req: Parse.Cloud.FunctionRequest) {
|
|
107
|
+
const query = new Parse.Query(Product);
|
|
108
|
+
query.include('category');
|
|
109
|
+
const [err, product] = await catchError(query.get(req.params.id, { sessionToken: req.user!.getSessionToken() }));
|
|
110
|
+
if (err) throw err;
|
|
111
|
+
return product;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@CloudFunction({
|
|
115
|
+
methods: ['POST'],
|
|
116
|
+
validation: { requireUser: true, fields: { id: { required: true } } },
|
|
117
|
+
})
|
|
118
|
+
static async updateProduct(req: Parse.Cloud.FunctionRequest) {
|
|
119
|
+
const user = req.user! as User;
|
|
120
|
+
const product = Product.fromParams(req.params);
|
|
121
|
+
const [err, saved] = await catchError(
|
|
122
|
+
product.save(null, { sessionToken: user.getSessionToken() })
|
|
123
|
+
);
|
|
124
|
+
if (err) throw err;
|
|
125
|
+
return saved;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@CloudFunction({
|
|
129
|
+
methods: ['POST'],
|
|
130
|
+
validation: { requireUser: true, fields: { id: { required: true } } },
|
|
131
|
+
})
|
|
132
|
+
static async deleteProduct(req: Parse.Cloud.FunctionRequest) {
|
|
133
|
+
const query = new Parse.Query(Product);
|
|
134
|
+
const [err, product] = await catchError(query.get(req.params.id, { useMasterKey: true }));
|
|
135
|
+
if (err) throw err;
|
|
136
|
+
const [delErr] = await catchError(product!.destroy({ useMasterKey: true }));
|
|
137
|
+
if (delErr) throw delErr;
|
|
138
|
+
return { success: true };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 3. Add Triggers
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { ParseClass, ParseField, BaseModel, BeforeSave, AfterDelete } from '@90soft/parse-server-kit';
|
|
147
|
+
|
|
148
|
+
@ParseClass('Product', { /* clp... */ })
|
|
149
|
+
export default class Product extends BaseModel {
|
|
150
|
+
@ParseField({ type: 'String', required: true })
|
|
151
|
+
name!: string;
|
|
152
|
+
|
|
153
|
+
@BeforeSave()
|
|
154
|
+
static async onBeforeSave(req: Parse.Cloud.BeforeSaveRequest<Product>) {
|
|
155
|
+
// Validate or modify before saving
|
|
156
|
+
if (!req.object.get('name')) {
|
|
157
|
+
throw new Parse.Error(142, 'Name is required');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@AfterDelete()
|
|
162
|
+
static async onAfterDelete(req: Parse.Cloud.AfterDeleteRequest<Product>) {
|
|
163
|
+
// Cleanup after deletion
|
|
164
|
+
console.log(`Product ${req.object.id} deleted`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### 4. Add Cron Jobs
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import { Cron, CronSchedule } from '@90soft/parse-server-kit';
|
|
173
|
+
|
|
174
|
+
class MyCronJobs {
|
|
175
|
+
@Cron({ schedule: CronSchedule.DAILY_MIDNIGHT, description: 'Cleanup expired sessions' })
|
|
176
|
+
static async cleanupSessions() {
|
|
177
|
+
// job logic
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@Cron({ schedule: '*/30 * * * *', description: 'Sync data every 30 minutes' })
|
|
181
|
+
static async syncData() {
|
|
182
|
+
// job logic
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 5. Server Setup (app.ts)
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import express from 'express';
|
|
191
|
+
import {
|
|
192
|
+
CloudFunctionRegistry, TriggerRegistry, CronRegistry,
|
|
193
|
+
importFiles, catchError, applyMongoValidators,
|
|
194
|
+
validateEntityRoutes, restrictRoutes, removeResultMiddleware,
|
|
195
|
+
conditionalJsonMiddleware, setupSwagger,
|
|
196
|
+
} from '@90soft/parse-server-kit';
|
|
197
|
+
|
|
198
|
+
const app = express();
|
|
199
|
+
|
|
200
|
+
// 1. Load models (must be before Parse Server init)
|
|
201
|
+
importFiles(join(__dirname, 'cloudCode/models'));
|
|
202
|
+
|
|
203
|
+
// 2. Init Parse Server
|
|
204
|
+
const parseServer = await initializeParseServer();
|
|
205
|
+
|
|
206
|
+
// 3. Middleware
|
|
207
|
+
app.use(removeResultMiddleware);
|
|
208
|
+
app.use(cors());
|
|
209
|
+
app.use(process.env.mountPath, validateEntityRoutes);
|
|
210
|
+
app.use(conditionalJsonMiddleware);
|
|
211
|
+
app.use(process.env.mountPath, restrictRoutes);
|
|
212
|
+
|
|
213
|
+
// 4. Mount Parse Server
|
|
214
|
+
app.use(process.env.mountPath, parseServer.app);
|
|
215
|
+
|
|
216
|
+
// 5. Initialize registries (after Parse Server mount)
|
|
217
|
+
CloudFunctionRegistry.initialize(); // also initializes RouteRegistry
|
|
218
|
+
TriggerRegistry.initialize();
|
|
219
|
+
CronRegistry.initialize();
|
|
220
|
+
|
|
221
|
+
// 6. Swagger docs
|
|
222
|
+
setupSwagger(app, { title: 'My API', version: '1.0.0' });
|
|
223
|
+
|
|
224
|
+
// 7. Start server
|
|
225
|
+
server.listen(1337);
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## API Reference
|
|
231
|
+
|
|
232
|
+
### Decorators
|
|
233
|
+
|
|
234
|
+
| Decorator | Target | Description |
|
|
235
|
+
|---|---|---|
|
|
236
|
+
| `@ParseClass(name, options)` | Class | Registers a Parse model with CLP, ACL, indexes |
|
|
237
|
+
| `@ParseField(options)` | Property | Defines a field with type, validation, index |
|
|
238
|
+
| `@CloudFunction(config)` | Static method | Registers a cloud function with HTTP method, validation, roles |
|
|
239
|
+
| `@ProtectedCloudFunction(config)` | Static method | Same as @CloudFunction but requires auth by default |
|
|
240
|
+
| `@Route(ModelOrString)` | Class | Maps cloud functions to `/api/{entity}/{action}` routes |
|
|
241
|
+
| `@Cron(config)` | Static method | Registers a cron job with schedule |
|
|
242
|
+
| `@BeforeSave(config?)` | Static method | Before save trigger |
|
|
243
|
+
| `@AfterSave(config?)` | Static method | After save trigger |
|
|
244
|
+
| `@BeforeDelete(config?)` | Static method | Before delete trigger |
|
|
245
|
+
| `@AfterDelete(config?)` | Static method | After delete trigger |
|
|
246
|
+
| `@BeforeFind(config?)` | Static method | Before find trigger |
|
|
247
|
+
| `@AfterFind(config?)` | Static method | After find trigger |
|
|
248
|
+
| `@BeforeLogin(config?)` | Static method | Before login trigger |
|
|
249
|
+
| `@AfterLogin(config?)` | Static method | After login trigger |
|
|
250
|
+
| `@AfterLogout(config?)` | Static method | After logout trigger |
|
|
251
|
+
|
|
252
|
+
### @ParseField Options
|
|
253
|
+
|
|
254
|
+
| Option | Type | Description |
|
|
255
|
+
|---|---|---|
|
|
256
|
+
| `type` | `'String' \| 'Number' \| 'Boolean' \| 'Date' \| 'Pointer' \| 'Array' \| ...` | Field type |
|
|
257
|
+
| `required` | `boolean` | Whether the field is required |
|
|
258
|
+
| `targetClass` | `string` | Target class for Pointer/Relation |
|
|
259
|
+
| `index` | `boolean \| 1 \| -1` | Create an index |
|
|
260
|
+
| `unique` | `boolean` | Create a unique index |
|
|
261
|
+
| `min` / `max` | `number` | Number range validation |
|
|
262
|
+
| `minLength` / `maxLength` | `number` | String length validation |
|
|
263
|
+
| `enum` | `string[]` | Allowed values for String fields |
|
|
264
|
+
| `pattern` | `string` | Regex pattern for String fields |
|
|
265
|
+
| `description` | `string` | Swagger documentation |
|
|
266
|
+
|
|
267
|
+
### @Route
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
// Import model class — auto-generates route from className
|
|
271
|
+
@Route(Product) // → /api/products/*
|
|
272
|
+
|
|
273
|
+
// Custom string — full control over route prefix
|
|
274
|
+
@Route('menu-items') // → /api/menu-items/*
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Routes are automatically mapped:
|
|
278
|
+
```
|
|
279
|
+
createProduct → POST /api/products/create
|
|
280
|
+
getProduct → POST /api/products/get
|
|
281
|
+
listProducts → GET /api/products/list
|
|
282
|
+
updateProduct → POST /api/products/update
|
|
283
|
+
deleteProduct → POST /api/products/delete
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### BaseModel
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
// Create from request params (auto-maps fields, handles pointers)
|
|
290
|
+
const product = Product.fromParams(req.params);
|
|
291
|
+
|
|
292
|
+
// Create a pointer reference by ID
|
|
293
|
+
const category = Category.pointer('abc123');
|
|
294
|
+
|
|
295
|
+
// Override excluded pointer classes (default: ['IMG', 'File'])
|
|
296
|
+
class MyModel extends BaseModel {
|
|
297
|
+
protected static EXCLUDED_POINTER_CLASSES = ['IMG', 'File', 'CustomFile'];
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### ACL
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
import { implementACL, UserRoles } from '@90soft/parse-server-kit';
|
|
305
|
+
|
|
306
|
+
obj.setACL(implementACL({
|
|
307
|
+
roleRules: [
|
|
308
|
+
{ role: UserRoles.ADMIN, read: true, write: true },
|
|
309
|
+
{ role: UserRoles.EMPLOYEE, read: true },
|
|
310
|
+
],
|
|
311
|
+
owner: [
|
|
312
|
+
{ user: userId, read: true, write: true },
|
|
313
|
+
],
|
|
314
|
+
}));
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Utilities
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
import {
|
|
321
|
+
catchError, // Wraps promises: const [err, data] = await catchError(promise)
|
|
322
|
+
getUserRoles, // Get role names for a user
|
|
323
|
+
getUsersRoles, // Batch get roles for multiple users
|
|
324
|
+
importFiles, // Auto-import all .js files in a directory
|
|
325
|
+
generateRandomPassword,
|
|
326
|
+
generateRandomString,
|
|
327
|
+
sleep,
|
|
328
|
+
formatCount,
|
|
329
|
+
} from '@90soft/parse-server-kit';
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Middleware
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
import {
|
|
336
|
+
validateEntityRoutes, // Maps /api/{entity}/{action} → cloud functions
|
|
337
|
+
restrictRoutes, // Blocks /classes, /schemas, etc.
|
|
338
|
+
removeResultMiddleware, // Unwraps Parse {result: ...} wrapper
|
|
339
|
+
conditionalJsonMiddleware, // JSON parsing with master key extraction
|
|
340
|
+
validateFunctionRoutes, // Legacy /functions/* validation
|
|
341
|
+
} from '@90soft/parse-server-kit';
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Swagger
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
import { setupSwagger, SwaggerRegistry } from '@90soft/parse-server-kit';
|
|
348
|
+
|
|
349
|
+
// Auto-generates API docs from @ParseClass and @CloudFunction metadata
|
|
350
|
+
setupSwagger(app, {
|
|
351
|
+
title: 'My API',
|
|
352
|
+
version: '1.0.0',
|
|
353
|
+
description: 'Auto-generated docs',
|
|
354
|
+
});
|
|
355
|
+
// Access at: http://localhost:1337/api-docs
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Hooks (for integrations)
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
import { onClassRegistered, onFieldRegistered, onFunctionRegistered } from '@90soft/parse-server-kit';
|
|
362
|
+
|
|
363
|
+
// Runs when @ParseClass is applied
|
|
364
|
+
onClassRegistered((className, constructor, fields, options) => {
|
|
365
|
+
// Register with Swagger, setup triggers, etc.
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Runs when @CloudFunction is applied
|
|
369
|
+
onFunctionRegistered((name, config, target) => {
|
|
370
|
+
// Register with Swagger docs
|
|
371
|
+
});
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Constants
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
import { UserRoles, roleKey, MAX_QUERY_LIMIT, CronSchedule } from '@90soft/parse-server-kit';
|
|
378
|
+
|
|
379
|
+
UserRoles.ADMIN // 'SuperAdmin'
|
|
380
|
+
UserRoles.EMPLOYEE // 'Employee'
|
|
381
|
+
roleKey(UserRoles.ADMIN) // 'role:SuperAdmin'
|
|
382
|
+
MAX_QUERY_LIMIT // 10000
|
|
383
|
+
|
|
384
|
+
CronSchedule.EVERY_HOUR // '0 * * * *'
|
|
385
|
+
CronSchedule.DAILY_MIDNIGHT // '0 0 * * *'
|
|
386
|
+
CronSchedule.WEEKLY_MONDAY // '0 0 * * 1'
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Validation
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
import { validateObject, validateOrThrow, applyMongoValidators } from '@90soft/parse-server-kit';
|
|
393
|
+
|
|
394
|
+
// Validate against @ParseField constraints (min, max, enum, pattern, etc.)
|
|
395
|
+
const result = validateObject(parseObject);
|
|
396
|
+
// { valid: false, errors: [{ field: 'price', message: 'must be at least 0' }] }
|
|
397
|
+
|
|
398
|
+
// Throw Parse.Error if invalid (use in BeforeSave triggers)
|
|
399
|
+
validateOrThrow(parseObject);
|
|
400
|
+
|
|
401
|
+
// Apply MongoDB schema validators from decorator metadata
|
|
402
|
+
await applyMongoValidators(parseServerInstance);
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## File Structure Convention
|
|
408
|
+
|
|
409
|
+
```
|
|
410
|
+
backend/src/cloudCode/
|
|
411
|
+
├── models/
|
|
412
|
+
│ └── Product.ts ← @ParseClass + @ParseField + triggers
|
|
413
|
+
├── modules/
|
|
414
|
+
│ └── Product/
|
|
415
|
+
│ └── functions.ts ← @Route + @CloudFunction
|
|
416
|
+
├── cron.ts ← @Cron jobs
|
|
417
|
+
├── main.ts ← importFiles for models + modules
|
|
418
|
+
└── decorator/
|
|
419
|
+
└── setupHooks.ts ← connects package hooks to Swagger + Triggers
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
## License
|
|
423
|
+
|
|
424
|
+
MIT
|
|
@@ -1,36 +1,41 @@
|
|
|
1
1
|
import express, { NextFunction, Request, Response } from 'express';
|
|
2
2
|
/**
|
|
3
|
-
* Validates
|
|
4
|
-
*
|
|
5
|
-
* Entity routes: /api/students/create → resolves to 'createStudent'
|
|
6
|
-
* Legacy routes: /api/functions/createStudent → resolves directly
|
|
7
|
-
*
|
|
8
|
-
* Converts all requests to POST for Parse Server internal handling.
|
|
3
|
+
* Validates legacy /functions/* routes.
|
|
4
|
+
* Skips if already handled by validateEntityRoutes.
|
|
9
5
|
*/
|
|
10
6
|
export declare function validateFunctionRoutes(req: Request, res: Response, next: NextFunction): void | express.Response<any, Record<string, any>>;
|
|
11
7
|
/**
|
|
12
8
|
* Validates entity-based routes: /api/{entity}/{action}
|
|
13
|
-
* Resolves
|
|
14
|
-
*
|
|
9
|
+
* Resolves to a cloud function name and rewrites the URL for Parse Server.
|
|
10
|
+
*
|
|
11
|
+
* Flow:
|
|
12
|
+
* 1. Skip CORS preflight (OPTIONS)
|
|
13
|
+
* 2. Skip non-entity routes (pass to next middleware)
|
|
14
|
+
* 3. Resolve route → cloud function name
|
|
15
|
+
* 4. Validate HTTP method
|
|
16
|
+
* 5. Rewrite URL to /functions/{name} with query string preserved
|
|
17
|
+
* 6. Merge GET query params into body (Parse reads from body)
|
|
18
|
+
* 7. Convert to POST for Parse Server
|
|
15
19
|
*/
|
|
16
20
|
export declare function validateEntityRoutes(req: Request, res: Response, next: NextFunction): void | express.Response<any, Record<string, any>>;
|
|
17
21
|
/**
|
|
18
|
-
* Extracts master key from the request body during JSON parsing
|
|
22
|
+
* Extracts master key from the request body during JSON parsing.
|
|
23
|
+
* Used as the `verify` callback in express.json().
|
|
19
24
|
*/
|
|
20
25
|
export declare function extractMasterKey(req: any, res: Response, buf: Buffer, encoding: BufferEncoding): void;
|
|
21
26
|
/**
|
|
22
|
-
* Restricts access to
|
|
23
|
-
* Only
|
|
24
|
-
* /
|
|
27
|
+
* Restricts access to internal Parse Server endpoints.
|
|
28
|
+
* Only entity routes, system routes, and rewritten /functions routes pass through.
|
|
29
|
+
* Direct /classes, /schemas, /batch access is blocked.
|
|
25
30
|
*/
|
|
26
|
-
export declare function restrictRoutes(req: any, res: Response, next: NextFunction): void
|
|
31
|
+
export declare function restrictRoutes(req: any, res: Response, next: NextFunction): void | express.Response<any, Record<string, any>>;
|
|
27
32
|
/**
|
|
28
|
-
* Conditionally applies JSON parsing middleware, skipping file routes
|
|
33
|
+
* Conditionally applies JSON parsing middleware, skipping file upload routes.
|
|
29
34
|
*/
|
|
30
35
|
export declare function conditionalJsonMiddleware(req: Request, res: Response, next: NextFunction): void;
|
|
31
36
|
/**
|
|
32
|
-
* Removes the
|
|
33
|
-
* Applies to
|
|
37
|
+
* Removes the {result: ...} wrapper from Parse Server cloud function responses.
|
|
38
|
+
* Applies to entity routes and rewritten /functions routes.
|
|
34
39
|
*/
|
|
35
40
|
export declare function removeResultMiddleware(req: any, res: any, next: any): any;
|
|
36
41
|
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/middleware/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,EAAE,EAAC,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAC,MAAM,SAAS,CAAC;AAKjE
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/middleware/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,EAAE,EAAC,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAC,MAAM,SAAS,CAAC;AAKjE;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,qDAoBnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,qDA0CnB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,QAAQ,EACb,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,cAAc,QAUzB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,qDAwBzE;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,QAWnB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,OAmBnE"}
|
|
@@ -13,14 +13,14 @@ const express_1 = __importDefault(require("express"));
|
|
|
13
13
|
const cloudRegistry_1 = require("../decorators/cloudRegistry");
|
|
14
14
|
const routeDecorator_1 = require("../decorators/routeDecorator");
|
|
15
15
|
/**
|
|
16
|
-
* Validates
|
|
17
|
-
*
|
|
18
|
-
* Entity routes: /api/students/create → resolves to 'createStudent'
|
|
19
|
-
* Legacy routes: /api/functions/createStudent → resolves directly
|
|
20
|
-
*
|
|
21
|
-
* Converts all requests to POST for Parse Server internal handling.
|
|
16
|
+
* Validates legacy /functions/* routes.
|
|
17
|
+
* Skips if already handled by validateEntityRoutes.
|
|
22
18
|
*/
|
|
23
19
|
function validateFunctionRoutes(req, res, next) {
|
|
20
|
+
// Skip if already handled by entity route middleware
|
|
21
|
+
if (req._entityRouteHandled) {
|
|
22
|
+
return next();
|
|
23
|
+
}
|
|
24
24
|
const functionName = req.path.split('/')[1];
|
|
25
25
|
const metadata = cloudRegistry_1.CloudFunctionRegistry.getFunction(functionName);
|
|
26
26
|
if (!metadata) {
|
|
@@ -29,17 +29,32 @@ function validateFunctionRoutes(req, res, next) {
|
|
|
29
29
|
if (!metadata.config.methods.includes(req.method)) {
|
|
30
30
|
return res.status(405).json({ message: 'Method not allowed' });
|
|
31
31
|
}
|
|
32
|
-
req.method = 'POST';
|
|
32
|
+
req.method = 'POST';
|
|
33
33
|
return next();
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
36
|
* Validates entity-based routes: /api/{entity}/{action}
|
|
37
|
-
* Resolves
|
|
38
|
-
*
|
|
37
|
+
* Resolves to a cloud function name and rewrites the URL for Parse Server.
|
|
38
|
+
*
|
|
39
|
+
* Flow:
|
|
40
|
+
* 1. Skip CORS preflight (OPTIONS)
|
|
41
|
+
* 2. Skip non-entity routes (pass to next middleware)
|
|
42
|
+
* 3. Resolve route → cloud function name
|
|
43
|
+
* 4. Validate HTTP method
|
|
44
|
+
* 5. Rewrite URL to /functions/{name} with query string preserved
|
|
45
|
+
* 6. Merge GET query params into body (Parse reads from body)
|
|
46
|
+
* 7. Convert to POST for Parse Server
|
|
39
47
|
*/
|
|
40
48
|
function validateEntityRoutes(req, res, next) {
|
|
41
|
-
// Build route path from URL: /students/create
|
|
42
49
|
const routePath = req.path;
|
|
50
|
+
// Skip CORS preflight — let cors() middleware handle them
|
|
51
|
+
if (req.method === 'OPTIONS') {
|
|
52
|
+
return next();
|
|
53
|
+
}
|
|
54
|
+
// Only handle requests that match a registered entity prefix
|
|
55
|
+
if (!routeDecorator_1.RouteRegistry.isRegisteredPrefix(routePath)) {
|
|
56
|
+
return next();
|
|
57
|
+
}
|
|
43
58
|
const functionName = routeDecorator_1.RouteRegistry.resolve(routePath);
|
|
44
59
|
if (!functionName) {
|
|
45
60
|
return res.status(404).json({ message: 'Route not found' });
|
|
@@ -52,12 +67,20 @@ function validateEntityRoutes(req, res, next) {
|
|
|
52
67
|
return res.status(405).json({ message: 'Method not allowed' });
|
|
53
68
|
}
|
|
54
69
|
// Rewrite URL to /functions/{functionName} so Parse Server handles it
|
|
55
|
-
|
|
56
|
-
|
|
70
|
+
// Preserve query string for GET requests
|
|
71
|
+
const queryString = req.originalUrl.split('?')[1];
|
|
72
|
+
req.url = `/functions/${functionName}${queryString ? '?' + queryString : ''}`;
|
|
73
|
+
// Merge GET query params into body — Parse Server reads params from body
|
|
74
|
+
if (req.method === 'GET' && req.query && Object.keys(req.query).length > 0) {
|
|
75
|
+
req.body = { ...req.body, ...req.query };
|
|
76
|
+
}
|
|
77
|
+
req.method = 'POST';
|
|
78
|
+
req._entityRouteHandled = true;
|
|
57
79
|
return next();
|
|
58
80
|
}
|
|
59
81
|
/**
|
|
60
|
-
* Extracts master key from the request body during JSON parsing
|
|
82
|
+
* Extracts master key from the request body during JSON parsing.
|
|
83
|
+
* Used as the `verify` callback in express.json().
|
|
61
84
|
*/
|
|
62
85
|
function extractMasterKey(req, res, buf, encoding) {
|
|
63
86
|
try {
|
|
@@ -71,9 +94,9 @@ function extractMasterKey(req, res, buf, encoding) {
|
|
|
71
94
|
}
|
|
72
95
|
}
|
|
73
96
|
/**
|
|
74
|
-
* Restricts access to
|
|
75
|
-
* Only
|
|
76
|
-
* /
|
|
97
|
+
* Restricts access to internal Parse Server endpoints.
|
|
98
|
+
* Only entity routes, system routes, and rewritten /functions routes pass through.
|
|
99
|
+
* Direct /classes, /schemas, /batch access is blocked.
|
|
77
100
|
*/
|
|
78
101
|
function restrictRoutes(req, res, next) {
|
|
79
102
|
const systemRoutes = ['/health', '/serverInfo', '/files'];
|
|
@@ -89,18 +112,17 @@ function restrictRoutes(req, res, next) {
|
|
|
89
112
|
if (routeDecorator_1.RouteRegistry.isRegisteredPrefix(req.path)) {
|
|
90
113
|
return next();
|
|
91
114
|
}
|
|
92
|
-
// Allow /functions for internal
|
|
115
|
+
// Allow /functions for internal use (rewritten by validateEntityRoutes)
|
|
93
116
|
if (req.path.startsWith('/functions')) {
|
|
94
117
|
return next();
|
|
95
118
|
}
|
|
96
|
-
|
|
119
|
+
return res.status(403).json({ message: 'Route not allowed' });
|
|
97
120
|
}
|
|
98
121
|
/**
|
|
99
|
-
* Conditionally applies JSON parsing middleware, skipping file routes
|
|
122
|
+
* Conditionally applies JSON parsing middleware, skipping file upload routes.
|
|
100
123
|
*/
|
|
101
124
|
function conditionalJsonMiddleware(req, res, next) {
|
|
102
125
|
if (req.path.startsWith(`${process.env.mountPath}/files`)) {
|
|
103
|
-
console.log('Skipping JSON middleware for /files route');
|
|
104
126
|
return next();
|
|
105
127
|
}
|
|
106
128
|
express_1.default.json({
|
|
@@ -110,12 +132,11 @@ function conditionalJsonMiddleware(req, res, next) {
|
|
|
110
132
|
})(req, res, next);
|
|
111
133
|
}
|
|
112
134
|
/**
|
|
113
|
-
* Removes the
|
|
114
|
-
* Applies to
|
|
135
|
+
* Removes the {result: ...} wrapper from Parse Server cloud function responses.
|
|
136
|
+
* Applies to entity routes and rewritten /functions routes.
|
|
115
137
|
*/
|
|
116
138
|
function removeResultMiddleware(req, res, next) {
|
|
117
139
|
const mountPath = process.env.mountPath || '/api';
|
|
118
|
-
// Check if this is a registered entity route or a rewritten /functions route
|
|
119
140
|
const pathAfterMount = req.path.replace(mountPath, '');
|
|
120
141
|
const isEntityRoute = routeDecorator_1.RouteRegistry.isRegisteredPrefix(pathAfterMount);
|
|
121
142
|
const isFunctionRoute = req.path.includes('/functions');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/middleware/middleware.ts"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/middleware/middleware.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@90soft/parse-server-kit",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Complete decorator-driven development kit for Parse Server — models, cloud functions, triggers, cron, routing, ACL, Swagger, and more.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|