Skip to content

Commit 550ccad

Browse files
committed
docs: fix blocking issues in Cloud Code Adapter spec
- Add beforePasswordResetRequest to TriggerName - Replace file/config trigger names with virtual className pattern (@file, @config, @connect) matching triggers.js internals - Add applicationId scoping (one CloudCodeManager per app on Config) - Add GlobalConfigRouter.js to consumer migration table - Add missing API methods (triggerExists, getJobs, runQueryTrigger, runFileTrigger, runGlobalConfigTrigger, runLiveQueryEventHandlers) - Clarify validators/rate-limiting only for LegacyAdapter - Document crash recovery ownership (manager calls unregisterAll)
1 parent 11b7293 commit 550ccad

1 file changed

Lines changed: 48 additions & 14 deletions

File tree

docs/superpowers/specs/2026-03-16-cloud-code-adapter-design.md

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,16 @@ Parse Server's cloud code system (`Parse.Cloud.define`, `Parse.Cloud.beforeSave`
2626
| Hot reload | Startup-only for v1 | Simpler implementation; can be added later |
2727
| Registry API | Adapters only (no public registry) | Clean boundary, single integration point |
2828
| Implementation location | In parse-server directly | Core server functionality |
29-
| Webhook key | Explicitly configured (required) | No auto-generation, no persistence question |
29+
| Webhook key | Explicitly configured (required) | No auto-generation, no persistence question. Diverges from proposal which offered auto-generation. |
3030
| Language | TypeScript | Type safety throughout |
3131
| Architecture | Replace triggers.js entirely | CloudCodeManager becomes single source of truth |
32+
| applicationId scoping | One CloudCodeManager per app | Stored on `Config`, mirrors existing `_triggerStore[applicationId]` pattern |
3233

3334
## 3. Architecture
3435

3536
### 3.1 CloudCodeManager — The New Core
3637

37-
`CloudCodeManager` replaces `triggers.js` as the single source of truth for all hook registration, lookup, and execution.
38+
`CloudCodeManager` replaces `triggers.js` as the single source of truth for all hook registration, lookup, and execution. One instance exists per `applicationId`, stored on the app's `Config` object.
3839

3940
```typescript
4041
class CloudCodeManager {
@@ -50,21 +51,30 @@ class CloudCodeManager {
5051
defineFunction(source: string, name: string, handler: CloudFunctionHandler, validator?: ValidatorHandler): void;
5152
defineTrigger(source: string, className: string, triggerName: TriggerName, handler: CloudTriggerHandler, validator?: ValidatorHandler): void;
5253
defineJob(source: string, name: string, handler: CloudJobHandler): void;
54+
defineLiveQueryHandler(source: string, handler: LiveQueryHandler): void;
5355
unregisterAll(source: string): void;
5456

5557
// Lookup (consumed by routers, rest of Parse Server)
56-
getFunction(name: string, applicationId: string): CloudFunctionHandler | undefined;
57-
getTrigger(className: string, triggerType: string, applicationId: string): CloudTriggerHandler | undefined;
58-
getJob(name: string, applicationId: string): CloudJobHandler | undefined;
59-
getFunctionNames(applicationId: string): string[];
60-
getValidator(functionName: string, applicationId: string): ValidatorHandler | undefined;
61-
62-
// Execution (replaces maybeRunTrigger, maybeRunValidator)
58+
getFunction(name: string): CloudFunctionHandler | undefined;
59+
getTrigger(className: string, triggerType: string): CloudTriggerHandler | undefined;
60+
triggerExists(className: string, triggerType: string): boolean;
61+
getJob(name: string): CloudJobHandler | undefined;
62+
getJobs(): Map<string, CloudJobHandler>;
63+
getFunctionNames(): string[];
64+
getValidator(functionName: string): ValidatorHandler | undefined;
65+
66+
// Execution (replaces maybeRunTrigger, maybeRunValidator, and specialized variants)
6367
async runTrigger(triggerType: string, auth: Auth, parseObject: ParseObject, ...): Promise<any>;
68+
async runQueryTrigger(triggerType: string, className: string, query: any, ...): Promise<any>;
69+
async runFileTrigger(triggerType: string, file: any, ...): Promise<any>;
70+
async runGlobalConfigTrigger(triggerType: string, config: any, ...): Promise<any>;
6471
async runValidator(request: any, functionName: string, auth: Auth): Promise<void>;
72+
async runLiveQueryEventHandlers(data: any): void;
6573
}
6674
```
6775

76+
Since the manager is scoped per-app, lookup methods no longer need an `applicationId` parameter.
77+
6878
### 3.2 HookStore
6979

7080
Typed internal structure replacing `Object.create(null)` pattern:
@@ -106,18 +116,31 @@ interface CloudCodeRegistry {
106116
defineFunction(name: string, handler: CloudFunctionHandler, validator?: ValidatorHandler): void;
107117
defineTrigger(className: string, triggerName: TriggerName, handler: CloudTriggerHandler, validator?: ValidatorHandler): void;
108118
defineJob(name: string, handler: CloudJobHandler): void;
119+
defineLiveQueryHandler(handler: LiveQueryHandler): void;
109120
}
110121

111122
type TriggerName =
112123
| 'beforeSave' | 'afterSave'
113124
| 'beforeDelete' | 'afterDelete'
114125
| 'beforeFind' | 'afterFind'
115126
| 'beforeLogin' | 'afterLogin' | 'afterLogout'
116-
| 'beforeConnect' | 'beforeSubscribe' | 'afterEvent'
117-
| 'beforeSaveFile' | 'afterSaveFile'
118-
| 'beforeDeleteFile' | 'afterDeleteFile';
127+
| 'beforePasswordResetRequest'
128+
| 'beforeConnect' | 'beforeSubscribe' | 'afterEvent';
119129
```
120130

131+
**Virtual classNames for special trigger targets:**
132+
133+
File and Config triggers use standard trigger names (`beforeSave`, `afterSave`, etc.) with virtual classNames:
134+
135+
| Target | Virtual className | Example registration |
136+
|--------|-------------------|---------------------|
137+
| `Parse.File` | `@File` | `defineTrigger('@File', 'beforeSave', handler)` |
138+
| `Parse.Config` | `@Config` | `defineTrigger('@Config', 'beforeSave', handler)` |
139+
| `beforeConnect` | `@Connect` | `defineTrigger('@Connect', 'beforeConnect', handler)` |
140+
| `beforeSubscribe` | class name | `defineTrigger('Todo', 'beforeSubscribe', handler)` |
141+
142+
This matches the existing internal storage pattern in `triggers.js` where `getClassName(Parse.File)` returns `'@File'`. The LegacyAdapter maps `Parse.Cloud.beforeSaveFile(handler)` to `defineTrigger('@File', 'beforeSave', handler)`, and `Parse.Cloud.beforeConnect(handler)` to `defineTrigger('@Connect', 'beforeConnect', handler)`.
143+
121144
## 4. Built-in Adapter Implementations
122145

123146
### 4.1 LegacyAdapter
@@ -169,7 +192,7 @@ Wraps `cloudCodeCommand: 'swift run CloudCode'`.
169192
- `initialize()` spawns child process with environment variables, waits for `PARSE_CLOUD_READY:<port>` on stdout, fetches manifest via `GET http://localhost:<port>/`, registers bridge handlers.
170193
- `isHealthy()` calls `GET http://localhost:<port>/health`.
171194
- `shutdown()` sends `SIGTERM`, waits `shutdownTimeout`, then `SIGKILL`.
172-
- Crash recovery: unregisters hooks, restarts with exponential backoff (1s, 2s, 4s, 8s, capped at `maxRestartDelay`).
195+
- Crash recovery: the `CloudCodeManager` calls `unregisterAll(adapter.name)` internally, then the adapter restarts with exponential backoff (1s, 2s, 4s, 8s, capped at `maxRestartDelay`).
173196

174197
**Environment variables passed to child process:**
175198

@@ -269,6 +292,7 @@ function resolveAdapters(options: ParseServerOptions): CloudCodeAdapter[] {
269292
| `RestQuery.js` | `getTrigger`, `maybeRunTrigger` | Import from `CloudCodeManager` |
270293
| `UsersRouter.js` | `getTrigger` (login/logout) | Import from `CloudCodeManager` |
271294
| `FilesRouter.js` | `getTrigger` (file triggers) | Import from `CloudCodeManager` |
295+
| `GlobalConfigRouter.js` | `maybeRunGlobalConfigTrigger` | Import from `CloudCodeManager` |
272296
| `LiveQuery/` | `getTrigger`, `maybeRunTrigger`, connect/subscribe | Import from `CloudCodeManager` |
273297
| `Config.js` | Validates cloud config | Updated for new options |
274298

@@ -307,7 +331,17 @@ class LegacyAdapter implements CloudCodeAdapter {
307331

308332
### 6.4 Utility Functions
309333

310-
Pure data transformation helpers from `triggers.js` (`getRequestObject()`, `getResponseObject()`, `resolveError()`, `toJSONwithObjects()`) move to `src/cloud-code/request-utils.ts`. They have no dependency on the hook store.
334+
Pure data transformation helpers from `triggers.js` move to `src/cloud-code/request-utils.ts`. They have no dependency on the hook store:
335+
336+
- `getRequestObject()`, `getResponseObject()` — build request/response objects for trigger handlers
337+
- `getRequestQueryObject()` — build request for query triggers
338+
- `resolveError()` — normalize error responses
339+
- `toJSONwithObjects()` — serialize with Parse object preservation
340+
- `inflate()` — inflate REST data into Parse Objects (used by `RestWrite.js`)
341+
342+
### 6.5 Validators and Rate Limiting
343+
344+
Validators (including `requireUser`, `requireMaster`, `fields`, `rateLimit`) are supported only through the `LegacyAdapter`. Non-legacy adapters (InProcess, External) handle validation within their own process — Parse Server does not apply server-side validators for hooks registered by these adapters. Rate limiting middleware integration (`addRateLimit`) is handled by the `LegacyAdapter` during `initialize()`, preserving existing behavior.
311345

312346
## 7. Request/Response Bridge
313347

0 commit comments

Comments
 (0)