@90soft/parse-server-kit 2.6.0 → 2.8.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/CLAUDE.md +674 -0
- package/README.md +618 -424
- package/dist/database/schema.d.ts +14 -0
- package/dist/database/schema.d.ts.map +1 -1
- package/dist/database/schema.js +2 -1
- package/dist/database/schema.js.map +1 -1
- package/dist/database/versionRegistry.d.ts +35 -0
- package/dist/database/versionRegistry.d.ts.map +1 -0
- package/dist/database/versionRegistry.js +153 -0
- package/dist/database/versionRegistry.js.map +1 -0
- package/dist/database/versionedMongoAdapter.d.ts +51 -0
- package/dist/database/versionedMongoAdapter.d.ts.map +1 -0
- package/dist/database/versionedMongoAdapter.js +131 -0
- package/dist/database/versionedMongoAdapter.js.map +1 -0
- package/dist/decorators/cloudRegistry.d.ts.map +1 -1
- package/dist/decorators/cloudRegistry.js +5 -0
- package/dist/decorators/cloudRegistry.js.map +1 -1
- package/dist/decorators/triggerDecorator.d.ts +8 -0
- package/dist/decorators/triggerDecorator.d.ts.map +1 -1
- package/dist/decorators/triggerDecorator.js +10 -1
- package/dist/decorators/triggerDecorator.js.map +1 -1
- package/dist/decorators/triggerRegistry.d.ts.map +1 -1
- package/dist/decorators/triggerRegistry.js +52 -8
- package/dist/decorators/triggerRegistry.js.map +1 -1
- package/dist/decorators/types/triggerTypes.d.ts +1 -1
- package/dist/decorators/types/triggerTypes.d.ts.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -2
- package/dist/index.js.map +1 -1
- package/dist/middleware/middleware.d.ts.map +1 -1
- package/dist/middleware/middleware.js +34 -1
- package/dist/middleware/middleware.js.map +1 -1
- package/dist/middleware/rateLimit.d.ts.map +1 -1
- package/dist/middleware/rateLimit.js +7 -1
- package/dist/middleware/rateLimit.js.map +1 -1
- package/dist/swagger/index.d.ts +5 -1
- package/dist/swagger/index.d.ts.map +1 -1
- package/dist/swagger/index.js +32 -8
- package/dist/swagger/index.js.map +1 -1
- package/dist/transactions/ambientSession.d.ts +39 -0
- package/dist/transactions/ambientSession.d.ts.map +1 -0
- package/dist/transactions/ambientSession.js +88 -0
- package/dist/transactions/ambientSession.js.map +1 -0
- package/dist/transactions/context.d.ts +86 -0
- package/dist/transactions/context.d.ts.map +1 -0
- package/dist/transactions/context.js +124 -0
- package/dist/transactions/context.js.map +1 -0
- package/dist/utils/dynamicImport.d.ts +22 -1
- package/dist/utils/dynamicImport.d.ts.map +1 -1
- package/dist/utils/dynamicImport.js +13 -4
- package/dist/utils/dynamicImport.js.map +1 -1
- package/dist/utils/helper.d.ts +10 -1
- package/dist/utils/helper.d.ts.map +1 -1
- package/dist/utils/helper.js +10 -1
- package/dist/utils/helper.js.map +1 -1
- package/package.json +76 -54
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
# @90soft/parse-server-kit — Complete Reference
|
|
2
|
+
|
|
3
|
+
Decorator-driven toolkit for Parse Server. **v2.8.0**
|
|
4
|
+
|
|
5
|
+
This document is the full API contract. Every signature here is taken from the
|
|
6
|
+
source, not paraphrased. If you are generating code against this library, read
|
|
7
|
+
[Silent failures](#silent-failures) first — most of the ways this library goes
|
|
8
|
+
wrong produce no error at all.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Contents
|
|
13
|
+
|
|
14
|
+
- [Requirements](#requirements) · [Boot order](#boot-order)
|
|
15
|
+
- [Models](#models) — `@ParseClass`, `@ParseField`, `BaseModel`
|
|
16
|
+
- [Cloud functions](#cloud-functions) — `@CloudFunction`, `@Route`
|
|
17
|
+
- [Triggers](#triggers) · [Cron](#cron)
|
|
18
|
+
- [Transactions](#transactions) · [Optimistic locking](#optimistic-locking)
|
|
19
|
+
- [ACL](#acl) · [Middleware](#middleware) · [Indexes](#indexes)
|
|
20
|
+
- [Schema](#schema) · [Validation](#validation) · [Swagger](#swagger)
|
|
21
|
+
- [Utilities](#utilities) · [Constants](#constants) · [Types](#types)
|
|
22
|
+
- [Silent failures](#silent-failures) · [Version matrix](#version-matrix)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Requirements
|
|
27
|
+
|
|
28
|
+
**tsconfig — `experimentalDecorators` is mandatory:**
|
|
29
|
+
|
|
30
|
+
```jsonc
|
|
31
|
+
{"compilerOptions": {"experimentalDecorators": true}}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This library uses **legacy** decorators. TypeScript 5 defaults to standard
|
|
35
|
+
(TC39) decorators, which are a different feature. Without the flag, every
|
|
36
|
+
decorator misbehaves. `emitDecoratorMetadata` is **not** needed — the library
|
|
37
|
+
reads no `design:type` metadata.
|
|
38
|
+
|
|
39
|
+
**`Parse` is a global.** The library never imports `parse`; it reads
|
|
40
|
+
`global.Parse` the way cloud code sees it. Install `@types/parse` for the
|
|
41
|
+
type declarations. Do not declare your own global `Parse` namespace — it will
|
|
42
|
+
collide.
|
|
43
|
+
|
|
44
|
+
**Peers:** `parse >=5`, `express >=4`, `reflect-metadata >=0.1.13`.
|
|
45
|
+
**Optional:** `parse-server >=8.3`, `node-cron >=3`, `swagger-ui-express >=4`,
|
|
46
|
+
`@types/parse >=3`.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Boot order
|
|
51
|
+
|
|
52
|
+
Order is load-bearing. This sequence is the contract:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
// 1. Models FIRST — @ParseClass calls Parse.Object.registerSubclass at
|
|
56
|
+
// decorator-evaluation time, so Parse must already be global.
|
|
57
|
+
importFiles(join(__dirname, 'cloudCode/models'));
|
|
58
|
+
importFiles(join(__dirname, 'cloudCode/modules'));
|
|
59
|
+
import './cloudCode/cron'; // @Cron definitions
|
|
60
|
+
|
|
61
|
+
// 2. Parse Server
|
|
62
|
+
const parseServer = await initializeParseServer();
|
|
63
|
+
|
|
64
|
+
// 3. Middleware — this order matters
|
|
65
|
+
app.use(removeResultMiddleware);
|
|
66
|
+
app.use(cors());
|
|
67
|
+
app.use(process.env.mountPath, validateEntityRoutes);
|
|
68
|
+
app.use(conditionalJsonMiddleware);
|
|
69
|
+
app.use(process.env.mountPath, restrictRoutes);
|
|
70
|
+
|
|
71
|
+
// 4. Mount
|
|
72
|
+
app.use(process.env.mountPath, parseServer.app);
|
|
73
|
+
|
|
74
|
+
// 5. Registries — AFTER mount
|
|
75
|
+
CloudFunctionRegistry.initialize(); // also runs RouteRegistry + version check
|
|
76
|
+
TriggerRegistry.initialize();
|
|
77
|
+
CronRegistry.initialize();
|
|
78
|
+
|
|
79
|
+
// 6. Docs
|
|
80
|
+
setupSwagger(app, {title: 'My API', version: '1.0.0'});
|
|
81
|
+
|
|
82
|
+
// 7. After listen
|
|
83
|
+
await applyAllIndexes(parseServer);
|
|
84
|
+
await applyMongoValidators(parseServer);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Models
|
|
90
|
+
|
|
91
|
+
### `@ParseClass(className, options?)`
|
|
92
|
+
|
|
93
|
+
Class decorator. Registers the Parse subclass, CLP, ACL template, compound
|
|
94
|
+
indexes; auto-registers the model with Swagger and flushes any pending triggers.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
interface ParseClassOptions {
|
|
98
|
+
clp?: classLevelPermissions;
|
|
99
|
+
protectedFields?: {[role: string]: string[]};
|
|
100
|
+
ACL?: ClassAclTemplate;
|
|
101
|
+
description?: string; // Swagger
|
|
102
|
+
compoundIndexes?: CompoundIndex[];
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
@ParseClass('Product', {
|
|
108
|
+
clp: {
|
|
109
|
+
find: {[roleKey(UserRoles.ADMIN)]: true},
|
|
110
|
+
get: {[roleKey(UserRoles.ADMIN)]: true},
|
|
111
|
+
create: {[roleKey(UserRoles.ADMIN)]: true},
|
|
112
|
+
update: {[roleKey(UserRoles.ADMIN)]: true},
|
|
113
|
+
delete: {[roleKey(UserRoles.ADMIN)]: true},
|
|
114
|
+
count: {[roleKey(UserRoles.ADMIN)]: true},
|
|
115
|
+
},
|
|
116
|
+
compoundIndexes: [{fields: ['status', 'createdAt'], unique: false}],
|
|
117
|
+
})
|
|
118
|
+
export default class Product extends BaseModel { ... }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
A `Parse.Role` subclass is detected and **not** registered via
|
|
122
|
+
`registerSubclass` (Parse rejects that).
|
|
123
|
+
|
|
124
|
+
### `@ParseField(options)`
|
|
125
|
+
|
|
126
|
+
Property decorator. Defines a getter/setter on the prototype mapping to
|
|
127
|
+
`Parse.Object.get/set`, so `product.name` **is** the supported API — not
|
|
128
|
+
`product.get('name')`.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
interface ParseFieldOptions {
|
|
132
|
+
type: 'String' | 'Number' | 'Boolean' | 'Date' | 'Object' | 'Array'
|
|
133
|
+
| 'GeoPoint' | 'File' | 'Bytes' | 'Polygon' | 'Pointer' | 'Relation';
|
|
134
|
+
required?: boolean;
|
|
135
|
+
targetClass?: string; // REQUIRED for Pointer / Relation
|
|
136
|
+
description?: string; // Swagger
|
|
137
|
+
indexName?: string; // override generated index name
|
|
138
|
+
min?: number; max?: number; // Number only
|
|
139
|
+
minLength?: number; maxLength?: number; // String only
|
|
140
|
+
enum?: string[]; // String only
|
|
141
|
+
pattern?: string; // String only, valid RegExp
|
|
142
|
+
geo?: boolean; // GeoPoint only → 2dsphere index
|
|
143
|
+
ttlSeconds?: number; // Date only → TTL index
|
|
144
|
+
index?: boolean | 1 | -1; // mutually exclusive with `unique`
|
|
145
|
+
unique?: boolean; // mutually exclusive with `index`
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Options are validated at import time** and throw immediately — a Pointer with
|
|
150
|
+
no `targetClass`, `min` on a String, an invalid regex, `geo` combined with
|
|
151
|
+
`index`, a negative `ttlSeconds`. Failures happen at boot, not in production.
|
|
152
|
+
|
|
153
|
+
`index` and `unique` are mutually exclusive in the type system. `geo` and
|
|
154
|
+
`ttlSeconds` cannot be combined with either.
|
|
155
|
+
|
|
156
|
+
### `BaseModel`
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
class BaseModel extends Parse.Object {
|
|
160
|
+
protected static EXCLUDED_POINTER_CLASSES: string[]; // default ['IMG','File']
|
|
161
|
+
static pointer<T>(id: string): InstanceType<T>;
|
|
162
|
+
static fromParams<T>(params: any): InstanceType<T>;
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
- **`Model.pointer(id)`** — a reference by id, no fetch.
|
|
167
|
+
- **`Model.fromParams(req.params)`** — builds an instance from request params,
|
|
168
|
+
reading `@ParseField` metadata to convert Pointers, Arrays-of-Pointers, Dates
|
|
169
|
+
and GeoPoints. Use this in create/update rather than manual `set()`.
|
|
170
|
+
- Pointer fields whose `targetClass` is in `EXCLUDED_POINTER_CLASSES` are
|
|
171
|
+
**skipped** (`IMG`, `File` need their own handling).
|
|
172
|
+
- An Array field converts to pointers **only if `targetClass` is set**.
|
|
173
|
+
- A Pointer given `null` or `{}` is set to `null` (explicit clear).
|
|
174
|
+
|
|
175
|
+
`Parse.Query` returns `Parse.Object`, not your typed class — cast the result
|
|
176
|
+
(`row as Product`) or direct property access is unavailable.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Cloud functions
|
|
181
|
+
|
|
182
|
+
### `@CloudFunction(config)`
|
|
183
|
+
|
|
184
|
+
Static-method decorator. Registers the function and wraps it with role checking.
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
interface RouteConfig {
|
|
188
|
+
methods: HttpMethod[]; // 'GET'|'POST'|'PUT'|'DELETE'|'PATCH'
|
|
189
|
+
requiresAuth?: boolean;
|
|
190
|
+
rateLimit?: {windowMs: number; max: number};
|
|
191
|
+
validation?: Parse.Cloud.Validator;
|
|
192
|
+
description?: string;
|
|
193
|
+
requireRoles?: string[];
|
|
194
|
+
requireAllRoles?: boolean; // default false = ANY role
|
|
195
|
+
customErrorMessage?: string;
|
|
196
|
+
swagger?: {summary?; description?; tags?; deprecated?; responses?};
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`requireRoles` is enforced by the wrapper before your body runs: no user →
|
|
201
|
+
`OBJECT_NOT_FOUND 'Authentication required'`; wrong roles →
|
|
202
|
+
`OPERATION_FORBIDDEN`. `requireAllRoles: true` demands every listed role.
|
|
203
|
+
|
|
204
|
+
**GET params arrive as strings** — `validateEntityRoutes` merges the query
|
|
205
|
+
string into the body. Declare them `{type: String}` and convert:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
const limit = Number(req.params.limit) || 10;
|
|
209
|
+
const active = req.params.isActive === 'true';
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### `@ProtectedCloudFunction(config?)`
|
|
213
|
+
|
|
214
|
+
`@CloudFunction` with `methods: ['POST']` and `validation: {requireUser: true}`
|
|
215
|
+
pre-applied; your config overrides.
|
|
216
|
+
|
|
217
|
+
### `@Route(ModelOrString)`
|
|
218
|
+
|
|
219
|
+
Class decorator mapping methods to REST routes. Captures **both** prototype and
|
|
220
|
+
static method names.
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
@Route(Product) // → /api/products/* (kebab-plural of the JS class name)
|
|
224
|
+
@Route('menu-items') // → /api/menu-items/*
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The route is `/{prefix}/{methodName}` — **the method name is the route**, matched
|
|
228
|
+
against the class's real method list, so there is no string parsing and no
|
|
229
|
+
collision between `getProduct` and `getProductCategory`.
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
createProduct → POST /api/products/createProduct
|
|
233
|
+
listProducts → GET /api/products/listProducts
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Triggers
|
|
239
|
+
|
|
240
|
+
All are static-method decorators taking `{description?, validation?}`.
|
|
241
|
+
|
|
242
|
+
> **Triggers only register if the class also has `@ParseClass`.** They park in
|
|
243
|
+
> metadata until `@ParseClass` names the class and flushes them. On a plain
|
|
244
|
+
> class they are silently never registered.
|
|
245
|
+
|
|
246
|
+
| Decorator | Registers as |
|
|
247
|
+
|---|---|
|
|
248
|
+
| `@BeforeSave` `@AfterSave` | `Parse.Cloud.beforeSave(className, …)` |
|
|
249
|
+
| `@BeforeDelete` `@AfterDelete` | `beforeDelete` / `afterDelete` |
|
|
250
|
+
| `@BeforeFind` `@AfterFind` | `beforeFind` / `afterFind` |
|
|
251
|
+
| `@BeforeLogin` `@AfterLogin` `@AfterLogout` | auth triggers (no className) |
|
|
252
|
+
| `@BeforePasswordResetRequest` | `beforePasswordResetRequest` — parse-server 8.5+ |
|
|
253
|
+
| `@BeforeSaveFile` `@AfterSaveFile` | `beforeSave(Parse.File, …)` |
|
|
254
|
+
| `@BeforeDeleteFile` `@AfterDeleteFile` | `beforeDelete(Parse.File, …)` |
|
|
255
|
+
| `@BeforeFindFile` `@AfterFindFile` | `beforeFind(Parse.File, …)` — 8.1+ |
|
|
256
|
+
| `@BeforeSaveConfig` `@AfterSaveConfig` | `beforeSave(Parse.Config, …)` — 7.3+ |
|
|
257
|
+
| `@BeforeConnect` `@BeforeSubscribe` `@AfterEvent` | LiveQuery (`afterEvent` → `afterLiveQueryEvent`) |
|
|
258
|
+
|
|
259
|
+
File and Config triggers pass the **class itself**, not a name. parse-server
|
|
260
|
+
removed the old `beforeSaveFile()` style methods; this library handles the
|
|
261
|
+
translation.
|
|
262
|
+
|
|
263
|
+
One trigger per `className:type` — a second registration warns and overwrites.
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
@ParseClass('Product', {...})
|
|
267
|
+
export default class Product extends BaseModel {
|
|
268
|
+
@BeforeSave()
|
|
269
|
+
static async onBeforeSave(req: Parse.Cloud.BeforeSaveRequest<Product>) {
|
|
270
|
+
if (!req.object.get('name')) throw new Parse.Error(142, 'Name is required');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Cron
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
interface CronConfig {
|
|
281
|
+
schedule: string; // cron expression, validated at initialize()
|
|
282
|
+
description?: string;
|
|
283
|
+
enabled?: boolean; // default true
|
|
284
|
+
timezone?: string; // default 'UTC'
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
class Jobs {
|
|
290
|
+
@Cron({schedule: CronSchedule.DAILY_MIDNIGHT, description: 'Cleanup'})
|
|
291
|
+
static async cleanup() { ... }
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
`CronSchedule`: `EVERY_MINUTE`, `EVERY_5_MINUTES`, `EVERY_10_MINUTES`,
|
|
296
|
+
`EVERY_15_MINUTES`, `EVERY_30_MINUTES`, `EVERY_HOUR`, `EVERY_2_HOURS`,
|
|
297
|
+
`EVERY_6_HOURS`, `EVERY_12_HOURS`, `DAILY_MIDNIGHT`, `DAILY_NOON`,
|
|
298
|
+
`WEEKLY_SUNDAY`, `WEEKLY_MONDAY`, `MONTHLY_FIRST`, `YEARLY`.
|
|
299
|
+
|
|
300
|
+
`CronRegistry`: `initialize()`, `getJobs()`, `getJob(name)`, `stopJob(name)`,
|
|
301
|
+
`startJob(name)`, `stopAll()`, `runNow(name)`. Without `node-cron` installed,
|
|
302
|
+
`initialize()` warns and skips — jobs never run.
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Transactions
|
|
307
|
+
|
|
308
|
+
MongoDB only. Requires a **replica set** and parse-server `directAccess: true`.
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
const parseServer = new ParseServer({
|
|
312
|
+
databaseAdapter: createVersionedMongoAdapter({
|
|
313
|
+
uri: process.env.DATABASE_URI, collectionPrefix: '', mongoOptions: {},
|
|
314
|
+
}),
|
|
315
|
+
directAccess: true, // REQUIRED — see below
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
function Transactional(): MethodDecorator;
|
|
321
|
+
function withTransaction<T>(body: () => Promise<T>): Promise<T>;
|
|
322
|
+
function inTransaction(): boolean;
|
|
323
|
+
function currentSession(): unknown;
|
|
324
|
+
const CONFLICT = 5001;
|
|
325
|
+
const CONFLICT_MESSAGE: string;
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### ⚠️ Decorator order
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
@CloudFunction({...}) // MUST be above — sees the wrapped method
|
|
332
|
+
@Transactional() // MUST be below — does the wrapping
|
|
333
|
+
static async submitJob(req) { ... }
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Decorators apply bottom-up and `@CloudFunction` captures the method when
|
|
337
|
+
applied. Reversed, the registry keeps the **unwrapped** method and the
|
|
338
|
+
transaction **never opens** — no error, no log.
|
|
339
|
+
|
|
340
|
+
### Semantics
|
|
341
|
+
|
|
342
|
+
- Every `save()` / `destroy()` / query inside the body joins automatically
|
|
343
|
+
(`AsyncLocalStorage` follows the call chain). No session threading.
|
|
344
|
+
- **Nested calls join** the outer transaction; the outermost caller commits.
|
|
345
|
+
- **The body may re-run** (up to 3 attempts) on a transient conflict, so it must
|
|
346
|
+
be safe to repeat. After 3 losses the caller gets `CONFLICT`.
|
|
347
|
+
- Each concurrent request gets its **own** session — unlike Parse Server's
|
|
348
|
+
built-in transaction, which is `DatabaseController`-global.
|
|
349
|
+
- Never joined: `_SCHEMA`, `_Idempotency`, `_Hooks`, `_JobStatus`,
|
|
350
|
+
`_GlobalConfig` — schema creation must survive a rollback.
|
|
351
|
+
- Unfiltered `count()` reads **outside** the transaction (MongoDB refuses
|
|
352
|
+
`count` inside one).
|
|
353
|
+
|
|
354
|
+
Without `directAccess`, a `save()` in cloud code becomes an internal HTTP
|
|
355
|
+
request landing in a fresh async context, and writes **outside** the
|
|
356
|
+
transaction. That failure has no symptom.
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## Optimistic locking
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
@ParseClass('Job')
|
|
364
|
+
class Job extends BaseModel {
|
|
365
|
+
@ParseVersionField() // declares the Number field itself — no @ParseField
|
|
366
|
+
version!: number;
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
That is the entire feature. No endpoint reads or writes the field:
|
|
371
|
+
|
|
372
|
+
- every object **read** carries the version it was read at; every `save()` /
|
|
373
|
+
`saveAll()` asserts it automatically;
|
|
374
|
+
- the adapter moves the assertion into the write's **filter** and `$inc`s the
|
|
375
|
+
field, so the next reader gets a fresh number;
|
|
376
|
+
- a lost race is refused with `CONFLICT` (5001); a genuinely missing row still
|
|
377
|
+
reads as missing;
|
|
378
|
+
- creates get version `1` from the adapter — callers never supply one;
|
|
379
|
+
- an object built from a bare id (never read) has nothing to assert and is
|
|
380
|
+
**not protected**.
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
try {
|
|
384
|
+
await job.save(null, {useMasterKey: true});
|
|
385
|
+
} catch (error) {
|
|
386
|
+
if (error instanceof Parse.Error && error.code === CONFLICT) {
|
|
387
|
+
// Reload, re-apply, retry — or surface CONFLICT_MESSAGE, already
|
|
388
|
+
// written for the end user.
|
|
389
|
+
}
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
`VERSION_CONFLICT` === `CONFLICT` — a lost lock and a lost transaction race are
|
|
395
|
+
the same event to the person on the screen.
|
|
396
|
+
|
|
397
|
+
`VersionRegistry`: `fieldFor(className)`, `isVersioned(className)`,
|
|
398
|
+
`classNames()`, `adapterIsInstalled()`, `verify()`.
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## ACL
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
function implementACL(
|
|
406
|
+
params: {
|
|
407
|
+
publicRead?: boolean;
|
|
408
|
+
publicWrite?: boolean;
|
|
409
|
+
roleRules?: {role: string; read?: boolean; write?: boolean}[];
|
|
410
|
+
excludedRoles?: string[];
|
|
411
|
+
owner?: {user?: string | any; read?: boolean; write?: boolean}[];
|
|
412
|
+
},
|
|
413
|
+
existingACL?: Parse.ACL
|
|
414
|
+
): Parse.ACL;
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**It takes one params object and RETURNS an ACL.** It does not take the object
|
|
418
|
+
as an argument.
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
// CORRECT
|
|
422
|
+
product.setACL(implementACL({
|
|
423
|
+
roleRules: [
|
|
424
|
+
{role: UserRoles.ADMIN, read: true, write: true},
|
|
425
|
+
{role: UserRoles.EMPLOYEE, read: true},
|
|
426
|
+
],
|
|
427
|
+
publicRead: status === 'active',
|
|
428
|
+
owner: [{user: userId, read: true, write: true}],
|
|
429
|
+
}));
|
|
430
|
+
|
|
431
|
+
// WRONG — no such signature, will not compile
|
|
432
|
+
implementACL(product, {readRoles: [...], writeRoles: [...]});
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
function syncImageAcl(parent: Parse.Object, fields: string[], acl?: Parse.ACL): void;
|
|
437
|
+
function cloneAcl(acl: Parse.ACL): Parse.ACL;
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
`syncImageAcl` copies the parent's ACL onto nested image pointers (single or
|
|
441
|
+
array) so they follow the parent's visibility. Call it **after** setting the
|
|
442
|
+
parent's final ACL and **before** saving. For status-derived visibility set in a
|
|
443
|
+
`beforeSave` trigger, pass `publicRead` into `implementACL` at the call site —
|
|
444
|
+
`syncImageAcl` runs before the trigger.
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## Middleware
|
|
449
|
+
|
|
450
|
+
| Export | Purpose |
|
|
451
|
+
|---|---|
|
|
452
|
+
| `validateEntityRoutes` | Resolves `/api/{entity}/{action}` → cloud function; enforces the declared HTTP method (405); applies `rateLimit`; merges GET query into body; rewrites to `/functions/{name}` as POST |
|
|
453
|
+
| `restrictRoutes` | Blocks `/classes`, `/schemas`, `/batch`. Allows `/health`, `/serverInfo`, `/files`, registered prefixes, `/functions`. Master key bypasses |
|
|
454
|
+
| `removeResultMiddleware` | Unwraps Parse's `{result: …}` response |
|
|
455
|
+
| `conditionalJsonMiddleware` | `express.json({limit:'10mb', type:['text/plain']})`, skipping `{mountPath}/files` |
|
|
456
|
+
| `validateFunctionRoutes` | Legacy `/functions/*` validation |
|
|
457
|
+
| `checkRateLimit(req,res,name,cfg)` | In-process token bucket; returns `false` after sending 429 |
|
|
458
|
+
|
|
459
|
+
**Master key** is read from the `X-Parse-Master-Key` header (preferred) or the
|
|
460
|
+
request body (deprecated, warns once). Rate limiting is **per process** — no
|
|
461
|
+
enforcement across instances.
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## Indexes
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
function applyAllIndexes(parseServerInstance?: any): Promise<void>;
|
|
469
|
+
const applyUniqueIndexes = applyAllIndexes; // permanent alias
|
|
470
|
+
function getUniqueIndexes(): UniqueIndexInfo[];
|
|
471
|
+
function getCompoundIndexes(): CompoundIndexInfo[];
|
|
472
|
+
function getFieldIndexes(): FieldIndexInfo[];
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Call after `listen()`. Creates, from decorator metadata:
|
|
476
|
+
|
|
477
|
+
| Source | Index |
|
|
478
|
+
|---|---|
|
|
479
|
+
| `@ParseField({unique: true})` | unique (drops a conflicting non-unique index first) |
|
|
480
|
+
| `@ParseField({index: true \| 1 \| -1})` | B-tree asc/desc |
|
|
481
|
+
| `@ParseField({geo: true})` on GeoPoint | 2dsphere |
|
|
482
|
+
| `@ParseField({ttlSeconds: N})` on Date | TTL |
|
|
483
|
+
| `@ParseClass({compoundIndexes})` | compound |
|
|
484
|
+
|
|
485
|
+
```ts
|
|
486
|
+
interface CompoundIndex {
|
|
487
|
+
fields: string[];
|
|
488
|
+
unique?: boolean;
|
|
489
|
+
name?: string;
|
|
490
|
+
sparse?: boolean;
|
|
491
|
+
partialFilterNulls?: boolean; // only index docs where all fields exist
|
|
492
|
+
fieldTypes?: Record<string, 1|-1|'text'|'2dsphere'|'hashed'>;
|
|
493
|
+
options?: Record<string, unknown>; // e.g. default_language
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
Existing indexes (codes 85/86) are reported, not treated as errors. If the
|
|
498
|
+
adapter can't be reached it prints the equivalent `db.x.createIndex(...)`
|
|
499
|
+
commands. MongoDB allows only **one text index per collection**.
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## Schema
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
function createSchemaConfig(options?: SchemaConfigOptions): {...};
|
|
507
|
+
|
|
508
|
+
interface SchemaConfigOptions {
|
|
509
|
+
adminRole?: string; // default 'SuperAdmin'
|
|
510
|
+
lockSchemas?: boolean; // default false
|
|
511
|
+
strict?: boolean; // default true
|
|
512
|
+
recreateModifiedFields?: boolean; // default false — DANGEROUS
|
|
513
|
+
deleteExtraFields?: boolean; // default false — DANGEROUS
|
|
514
|
+
keepUnknownIndexes?: boolean; // default TRUE — parse-server 8.3+
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Builds the `schema` option from every registered `@ParseClass`, plus a `_Role`
|
|
519
|
+
definition locked to `adminRole`.
|
|
520
|
+
|
|
521
|
+
`keepUnknownIndexes` defaults to **true** because `applyAllIndexes` creates
|
|
522
|
+
indexes out-of-band; parse-server's default (`false`) drops indexes it cannot
|
|
523
|
+
account for during schema migration. **Requires parse-server 8.3+** — it refuses
|
|
524
|
+
to start on an unrecognised option.
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## Validation
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
function validateObject(object: Parse.Object): {valid: boolean; errors: {field, message, value?}[]};
|
|
532
|
+
function validateOrThrow(object: Parse.Object): void; // throws Parse.Error VALIDATION_ERROR
|
|
533
|
+
function getValidators(): {className, validator}[];
|
|
534
|
+
function applyMongoValidators(parseServerInstance?: any): Promise<void>;
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
Checks `required`, `min`/`max`, `minLength`/`maxLength`, `enum`, `pattern` from
|
|
538
|
+
`@ParseField`. `validateOrThrow` is intended for `@BeforeSave`.
|
|
539
|
+
`applyMongoValidators` pushes the same constraints into MongoDB `$jsonSchema`
|
|
540
|
+
validators (`validationLevel: 'moderate'`, `validationAction: 'error'`), so the
|
|
541
|
+
database enforces them even for writes that bypass your code.
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
## Swagger
|
|
546
|
+
|
|
547
|
+
```ts
|
|
548
|
+
function setupSwagger(app: Express, config: SwaggerConfig, path?: string): void; // default '/api-docs'
|
|
549
|
+
function generateSwaggerSpec(config: SwaggerConfig): object;
|
|
550
|
+
function getSwaggerJson(config: SwaggerConfig): string;
|
|
551
|
+
|
|
552
|
+
interface SwaggerConfig {
|
|
553
|
+
title: string; version: string;
|
|
554
|
+
description?: string; basePath?: string; host?: string; schemes?: string[];
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
Models and endpoints are registered automatically from `@ParseClass` /
|
|
559
|
+
`@CloudFunction` — no annotation needed. Endpoints document their real `@Route`
|
|
560
|
+
path. **GET/HEAD emit query `parameters`; everything else emits a JSON
|
|
561
|
+
`requestBody`** (a browser cannot send a GET body). Security schemes derive from
|
|
562
|
+
`requireUser` / `requireMaster` / `requireRoles`.
|
|
563
|
+
|
|
564
|
+
The spec is always served at `{path}/json`. Without `swagger-ui-express`
|
|
565
|
+
installed, the browser UI is skipped with a warning; the spec still serves.
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## Utilities
|
|
570
|
+
|
|
571
|
+
```ts
|
|
572
|
+
function catchError<T>(p: Promise<T>): Promise<[undefined, T] | [Error]>;
|
|
573
|
+
function getUserRoles(user: Parse.User): Promise<string[]>;
|
|
574
|
+
function getUsersRoles(users: Parse.User[]): Promise<Map<string, string[]>>;
|
|
575
|
+
function importFiles(dir: string, options?: {extensions?: string[]; verbose?: boolean}): void;
|
|
576
|
+
function generateRandomPassword(length?, includeNumbers?, includeSymbols?): string;
|
|
577
|
+
function generateRandomString(length?: number): string;
|
|
578
|
+
function generateRandomInteger(length: number): string;
|
|
579
|
+
function sleep(ms: number): Promise<unknown>;
|
|
580
|
+
function formatCount(num: any, locale?: string): string; // compact notation
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
**`catchError` is the error convention** — use it for all async work rather than
|
|
584
|
+
`try/catch` around `await`:
|
|
585
|
+
|
|
586
|
+
```ts
|
|
587
|
+
const [err, saved] = await catchError(product.save(null, {sessionToken}));
|
|
588
|
+
if (err) throw err;
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
**`importFiles` defaults to `.js` only.** Under ts-node/tsx pass
|
|
592
|
+
`{extensions: ['.js', '.ts']}` or nothing is imported at all. Never point it at
|
|
593
|
+
a tree holding both compiled and source copies — every class registers twice.
|
|
594
|
+
|
|
595
|
+
`getUsersRoles` costs one query per **role** (capped at 100), not per user.
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
599
|
+
## Constants
|
|
600
|
+
|
|
601
|
+
```ts
|
|
602
|
+
enum UserRoles {ADMIN = 'SuperAdmin', EMPLOYEE = 'Employee'}
|
|
603
|
+
function roleKey(role: UserRoles): `role:${UserRoles}`;
|
|
604
|
+
const MAX_QUERY_LIMIT = 10000;
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
Use `roleKey(UserRoles.ADMIN)` for CLP/ACL keys — never hardcode
|
|
608
|
+
`'role:SuperAdmin'`.
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
## Types
|
|
613
|
+
|
|
614
|
+
`ClassNameType`, `CLPParamsOption`, `classLevelPermissions`, `ClassAclTemplate`,
|
|
615
|
+
`AclTemplatePermissions`, `ProtectedFields`, `CompoundIndex`, `IndexFieldType`,
|
|
616
|
+
`Fields`, `Indexes`, `SchemaDefinition`, `HttpMethod`, `RouteConfig`,
|
|
617
|
+
`CloudFunctionMetadata`, `SwaggerDocConfig`, `TriggerType`, `TriggerMetadata`,
|
|
618
|
+
`TriggerConfig`, `CronConfig`, `CronJobMetadata`, `RoleString`,
|
|
619
|
+
`ParseClassOptions`, `ParseFieldOptions`, `AllowedFieldType`,
|
|
620
|
+
`SwaggerModelSchema`, `SwaggerPropertySchema`, `SwaggerFunctionSchema`,
|
|
621
|
+
`SchemaConfigOptions`, `TransactionalAdapter`, `VersionedMongoAdapter`,
|
|
622
|
+
`MongoAdapterConstructor`.
|
|
623
|
+
|
|
624
|
+
```ts
|
|
625
|
+
interface AuthRole {id: string; name: string}
|
|
626
|
+
interface MultiLangs {ar?: string; en?: string}
|
|
627
|
+
interface Filter {key: string; value: string|number|string[];
|
|
628
|
+
type: 'string'|'min'|'max'|'array'|'text'|'dropdown'}
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
**Registration hooks** (for custom integrations):
|
|
632
|
+
`onClassRegistered`, `onFieldRegistered`, `onFunctionRegistered`.
|
|
633
|
+
Also exported: `getSchemaDefinition(target)`, `classNames`.
|
|
634
|
+
|
|
635
|
+
---
|
|
636
|
+
|
|
637
|
+
## Silent failures
|
|
638
|
+
|
|
639
|
+
Every item here produces **no error**. This is the list to check when something
|
|
640
|
+
"doesn't work" but nothing is logged.
|
|
641
|
+
|
|
642
|
+
| Symptom | Cause |
|
|
643
|
+
|---|---|
|
|
644
|
+
| Decorators do nothing / odd behaviour | `experimentalDecorators` not set — TS 5 defaults to standard decorators |
|
|
645
|
+
| Transaction never opens | `@Transactional()` placed **above** `@CloudFunction` instead of below |
|
|
646
|
+
| Writes land outside the transaction | parse-server `directAccess` not `true` |
|
|
647
|
+
| No models, empty schema, no routes | `importFiles` ran over `.ts` sources with the default `['.js']` |
|
|
648
|
+
| Every class registered twice | `importFiles` pointed at a tree with both compiled and source copies |
|
|
649
|
+
| Trigger never fires | Declared on a class without `@ParseClass` — pending triggers are never flushed |
|
|
650
|
+
| Unique indexes vanish after restart | `keepUnknownIndexes` false, or parse-server < 8.3 |
|
|
651
|
+
| Pointer field ignored by `fromParams` | `targetClass` missing, or it is `IMG`/`File` (excluded by design) |
|
|
652
|
+
| Array of pointers stays raw | Array field has no `targetClass` |
|
|
653
|
+
| Direct property access unavailable | Query result not cast to the typed model |
|
|
654
|
+
| Cron jobs never run | `node-cron` not installed — `initialize()` warns and skips |
|
|
655
|
+
| Object not protected by version field | Built from a bare id, never read, so there is nothing to assert |
|
|
656
|
+
|
|
657
|
+
Three of these now **do** log as of v2.8.0: `@ParseVersionField` with no adapter
|
|
658
|
+
(or on Postgres), and a Parse SDK where `_getSaveJSON` has moved. Check the boot
|
|
659
|
+
output for `[Versioning]`.
|
|
660
|
+
|
|
661
|
+
---
|
|
662
|
+
|
|
663
|
+
## Version matrix
|
|
664
|
+
|
|
665
|
+
| | Requirement |
|
|
666
|
+
|---|---|
|
|
667
|
+
| Node | ≥ 18 for this package; **≥ 20.19** with parse-server 9 |
|
|
668
|
+
| parse-server | ≥ 8.3 (optional peer) — `keepUnknownIndexes`, adapter internals |
|
|
669
|
+
| MongoDB | ≥ 7.0.16 for parse-server 9; **replica set** for transactions |
|
|
670
|
+
| Parse SDK | ≥ 5; verified on 5.3.0 and 8.6.0 |
|
|
671
|
+
| TypeScript | any, with `experimentalDecorators: true` |
|
|
672
|
+
|
|
673
|
+
**Transactions and `@ParseVersionField` are MongoDB-only.** On Postgres the
|
|
674
|
+
version field is declared but never enforced.
|