@90soft/parse-server-kit
v2.4.0
Published
Complete decorator-driven development kit for Parse Server — models, cloud functions, triggers, cron, routing, ACL, Swagger, and more.
Maintainers
Readme
@90soft/parse-server-kit
Decorator-driven development kit for Parse Server. Write models and cloud functions with TypeScript decorators — no boilerplate.
Install
npm install @90soft/parse-server-kitPeer dependencies: parse, reflect-metadata, express
Optional: node-cron (for @Cron), swagger-ui-express (for Swagger)
Quick Start
1. Define a Model
import { ParseClass, ParseField, BaseModel } from '@90soft/parse-server-kit';
import { UserRoles, roleKey } from '@90soft/parse-server-kit';
@ParseClass('Product', {
clp: {
find: { [roleKey(UserRoles.ADMIN)]: true, [roleKey(UserRoles.EMPLOYEE)]: true },
get: { [roleKey(UserRoles.ADMIN)]: true, [roleKey(UserRoles.EMPLOYEE)]: true },
create: { [roleKey(UserRoles.ADMIN)]: true, [roleKey(UserRoles.EMPLOYEE)]: true },
update: { [roleKey(UserRoles.ADMIN)]: true, [roleKey(UserRoles.EMPLOYEE)]: true },
delete: { [roleKey(UserRoles.ADMIN)]: true },
count: { [roleKey(UserRoles.ADMIN)]: true, [roleKey(UserRoles.EMPLOYEE)]: true },
},
})
export default class Product extends BaseModel {
@ParseField({ type: 'String', required: true })
name!: string;
@ParseField({ type: 'Number', required: true, min: 0 })
price!: number;
@ParseField({ type: 'String', enum: ['active', 'draft', 'archived'] })
status!: string;
@ParseField({ type: 'Pointer', targetClass: 'Category', required: true })
category!: any;
@ParseField({ type: 'Boolean' })
available!: boolean;
@ParseField({ type: 'Date' })
createdDate!: Date;
}2. Write Cloud Functions
import { CloudFunction, Route, catchError, UserRoles } from '@90soft/parse-server-kit';
import Product from '../../models/Product';
import User from '../../models/User';
@Route(Product) // → /api/products/create, /api/products/list, etc.
class ProductFunctions {
@CloudFunction({
methods: ['POST'],
validation: { requireUser: true, fields: { name: { required: true } } },
})
static async createProduct(req: Parse.Cloud.FunctionRequest) {
const user = req.user! as User;
const product = Product.fromParams(req.params);
const [err, saved] = await catchError(
product.save(null, { sessionToken: user.getSessionToken() })
);
if (err) throw err;
return saved;
}
@CloudFunction({
methods: ['GET'],
validation: { requireUser: true, fields: { skip: { type: String }, limit: { type: String } } },
})
static async listProducts(req: Parse.Cloud.FunctionRequest) {
const query = new Parse.Query(Product);
query.include('category');
const skip = Number(req.params.skip) || 0;
const limit = Number(req.params.limit) || 10;
query.skip(skip).limit(limit);
if (req.params.search) {
const search = req.params.search;
query.matches('name', new RegExp(search, 'i'));
}
const [err, results] = await catchError(query.find({ sessionToken: req.user!.getSessionToken() }));
if (err) throw err;
const [countErr, count] = await catchError(query.count({ sessionToken: req.user!.getSessionToken() }));
return { results, count: countErr ? 0 : count };
}
@CloudFunction({
methods: ['POST'],
validation: { requireUser: true, fields: { id: { required: true } } },
})
static async getProduct(req: Parse.Cloud.FunctionRequest) {
const query = new Parse.Query(Product);
query.include('category');
const [err, product] = await catchError(query.get(req.params.id, { sessionToken: req.user!.getSessionToken() }));
if (err) throw err;
return product;
}
@CloudFunction({
methods: ['POST'],
validation: { requireUser: true, fields: { id: { required: true } } },
})
static async updateProduct(req: Parse.Cloud.FunctionRequest) {
const user = req.user! as User;
const product = Product.fromParams(req.params);
const [err, saved] = await catchError(
product.save(null, { sessionToken: user.getSessionToken() })
);
if (err) throw err;
return saved;
}
@CloudFunction({
methods: ['POST'],
validation: { requireUser: true, fields: { id: { required: true } } },
})
static async deleteProduct(req: Parse.Cloud.FunctionRequest) {
const query = new Parse.Query(Product);
const [err, product] = await catchError(query.get(req.params.id, { useMasterKey: true }));
if (err) throw err;
const [delErr] = await catchError(product!.destroy({ useMasterKey: true }));
if (delErr) throw delErr;
return { success: true };
}
}3. Add Triggers
import { ParseClass, ParseField, BaseModel, BeforeSave, AfterDelete } from '@90soft/parse-server-kit';
@ParseClass('Product', { /* clp... */ })
export default class Product extends BaseModel {
@ParseField({ type: 'String', required: true })
name!: string;
@BeforeSave()
static async onBeforeSave(req: Parse.Cloud.BeforeSaveRequest<Product>) {
// Validate or modify before saving
if (!req.object.get('name')) {
throw new Parse.Error(142, 'Name is required');
}
}
@AfterDelete()
static async onAfterDelete(req: Parse.Cloud.AfterDeleteRequest<Product>) {
// Cleanup after deletion
console.log(`Product ${req.object.id} deleted`);
}
}4. Add Cron Jobs
import { Cron, CronSchedule } from '@90soft/parse-server-kit';
class MyCronJobs {
@Cron({ schedule: CronSchedule.DAILY_MIDNIGHT, description: 'Cleanup expired sessions' })
static async cleanupSessions() {
// job logic
}
@Cron({ schedule: '*/30 * * * *', description: 'Sync data every 30 minutes' })
static async syncData() {
// job logic
}
}5. Server Setup (app.ts)
import express from 'express';
import {
CloudFunctionRegistry, TriggerRegistry, CronRegistry,
importFiles, catchError, applyMongoValidators,
validateEntityRoutes, restrictRoutes, removeResultMiddleware,
conditionalJsonMiddleware, setupSwagger,
} from '@90soft/parse-server-kit';
const app = express();
// 1. Load models (must be before Parse Server init)
importFiles(join(__dirname, 'cloudCode/models'));
// 2. Init Parse Server
const parseServer = await initializeParseServer();
// 3. Middleware
app.use(removeResultMiddleware);
app.use(cors());
app.use(process.env.mountPath, validateEntityRoutes);
app.use(conditionalJsonMiddleware);
app.use(process.env.mountPath, restrictRoutes);
// 4. Mount Parse Server
app.use(process.env.mountPath, parseServer.app);
// 5. Initialize registries (after Parse Server mount)
CloudFunctionRegistry.initialize(); // also initializes RouteRegistry
TriggerRegistry.initialize();
CronRegistry.initialize();
// 6. Swagger docs
setupSwagger(app, { title: 'My API', version: '1.0.0' });
// 7. Start server
server.listen(1337);API Reference
Decorators
| Decorator | Target | Description |
|---|---|---|
| @ParseClass(name, options) | Class | Registers a Parse model with CLP, ACL, indexes |
| @ParseField(options) | Property | Defines a field with type, validation, index |
| @CloudFunction(config) | Static method | Registers a cloud function with HTTP method, validation, roles |
| @ProtectedCloudFunction(config) | Static method | Same as @CloudFunction but requires auth by default |
| @Route(ModelOrString) | Class | Maps cloud functions to /api/{entity}/{action} routes |
| @Cron(config) | Static method | Registers a cron job with schedule |
| @BeforeSave(config?) | Static method | Before save trigger |
| @AfterSave(config?) | Static method | After save trigger |
| @BeforeDelete(config?) | Static method | Before delete trigger |
| @AfterDelete(config?) | Static method | After delete trigger |
| @BeforeFind(config?) | Static method | Before find trigger |
| @AfterFind(config?) | Static method | After find trigger |
| @BeforeLogin(config?) | Static method | Before login trigger |
| @AfterLogin(config?) | Static method | After login trigger |
| @AfterLogout(config?) | Static method | After logout trigger |
@ParseField Options
| Option | Type | Description |
|---|---|---|
| type | 'String' \| 'Number' \| 'Boolean' \| 'Date' \| 'Pointer' \| 'Array' \| ... | Field type |
| required | boolean | Whether the field is required |
| targetClass | string | Target class for Pointer/Relation |
| index | boolean \| 1 \| -1 | Create an index |
| unique | boolean | Create a unique index |
| min / max | number | Number range validation |
| minLength / maxLength | number | String length validation |
| enum | string[] | Allowed values for String fields |
| pattern | string | Regex pattern for String fields |
| description | string | Swagger documentation |
@Route
// Import model class — auto-generates route from className
@Route(Product) // → /api/products/*
// Custom string — full control over route prefix
@Route('menu-items') // → /api/menu-items/*Routes are automatically mapped:
createProduct → POST /api/products/create
getProduct → POST /api/products/get
listProducts → GET /api/products/list
updateProduct → POST /api/products/update
deleteProduct → POST /api/products/deleteBaseModel
// Create from request params (auto-maps fields, handles pointers)
const product = Product.fromParams(req.params);
// Create a pointer reference by ID
const category = Category.pointer('abc123');
// Override excluded pointer classes (default: ['IMG', 'File'])
class MyModel extends BaseModel {
protected static EXCLUDED_POINTER_CLASSES = ['IMG', 'File', 'CustomFile'];
}ACL
import { implementACL, UserRoles } from '@90soft/parse-server-kit';
obj.setACL(implementACL({
roleRules: [
{ role: UserRoles.ADMIN, read: true, write: true },
{ role: UserRoles.EMPLOYEE, read: true },
],
owner: [
{ user: userId, read: true, write: true },
],
}));Utilities
import {
catchError, // Wraps promises: const [err, data] = await catchError(promise)
getUserRoles, // Get role names for a user
getUsersRoles, // Batch get roles for multiple users
importFiles, // Auto-import all .js files in a directory
generateRandomPassword,
generateRandomString,
sleep,
formatCount,
} from '@90soft/parse-server-kit';Middleware
import {
validateEntityRoutes, // Maps /api/{entity}/{action} → cloud functions
restrictRoutes, // Blocks /classes, /schemas, etc.
removeResultMiddleware, // Unwraps Parse {result: ...} wrapper
conditionalJsonMiddleware, // JSON parsing with master key extraction
validateFunctionRoutes, // Legacy /functions/* validation
} from '@90soft/parse-server-kit';Swagger
import { setupSwagger, SwaggerRegistry } from '@90soft/parse-server-kit';
// Auto-generates API docs from @ParseClass and @CloudFunction metadata
setupSwagger(app, {
title: 'My API',
version: '1.0.0',
description: 'Auto-generated docs',
});
// Access at: http://localhost:1337/api-docsHooks (for integrations)
import { onClassRegistered, onFieldRegistered, onFunctionRegistered } from '@90soft/parse-server-kit';
// Runs when @ParseClass is applied
onClassRegistered((className, constructor, fields, options) => {
// Register with Swagger, setup triggers, etc.
});
// Runs when @CloudFunction is applied
onFunctionRegistered((name, config, target) => {
// Register with Swagger docs
});Constants
import { UserRoles, roleKey, MAX_QUERY_LIMIT, CronSchedule } from '@90soft/parse-server-kit';
UserRoles.ADMIN // 'SuperAdmin'
UserRoles.EMPLOYEE // 'Employee'
roleKey(UserRoles.ADMIN) // 'role:SuperAdmin'
MAX_QUERY_LIMIT // 10000
CronSchedule.EVERY_HOUR // '0 * * * *'
CronSchedule.DAILY_MIDNIGHT // '0 0 * * *'
CronSchedule.WEEKLY_MONDAY // '0 0 * * 1'Validation
import { validateObject, validateOrThrow, applyMongoValidators } from '@90soft/parse-server-kit';
// Validate against @ParseField constraints (min, max, enum, pattern, etc.)
const result = validateObject(parseObject);
// { valid: false, errors: [{ field: 'price', message: 'must be at least 0' }] }
// Throw Parse.Error if invalid (use in BeforeSave triggers)
validateOrThrow(parseObject);
// Apply MongoDB schema validators from decorator metadata
await applyMongoValidators(parseServerInstance);File Structure Convention
backend/src/cloudCode/
├── models/
│ └── Product.ts ← @ParseClass + @ParseField + triggers
├── modules/
│ └── Product/
│ └── functions.ts ← @Route + @CloudFunction
├── cron.ts ← @Cron jobs
├── main.ts ← importFiles for models + modules
└── decorator/
└── setupHooks.ts ← connects package hooks to Swagger + TriggersLicense
MIT
