backend-manager 5.5.4 → 5.6.0
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/CHANGELOG.md +21 -4
- package/CLAUDE.md +35 -11
- package/README.md +6 -5
- package/docs/build-system.md +27 -0
- package/docs/cli-logs.md +2 -2
- package/docs/common-mistakes.md +1 -1
- package/docs/consent.md +4 -2
- package/docs/email-system.md +28 -2
- package/docs/environment-detection.md +1 -1
- package/docs/firestore.md +134 -0
- package/docs/logging.md +27 -0
- package/docs/migration.md +107 -0
- package/docs/routes.md +110 -14
- package/docs/schemas.md +121 -25
- package/docs/test-boot-layer.md +65 -0
- package/docs/{testing.md → test-framework.md} +150 -19
- package/docs/usage-rate-limiting.md +15 -0
- package/package.json +3 -2
- package/src/cli/commands/base-command.js +5 -5
- package/src/cli/commands/logs.js +2 -2
- package/src/cli/commands/serve.js +3 -3
- package/src/cli/commands/test.js +140 -0
- package/src/cli/commands/watch.js +4 -4
- package/src/defaults/CLAUDE.md +11 -0
- package/src/defaults/test/README.md +15 -0
- package/src/defaults/test/_init.js +1 -1
- package/src/manager/libraries/email/data/blocked-local-patterns.js +0 -2
- package/src/manager/libraries/email/data/custom-disposable-domains.json +47 -1
- package/src/manager/libraries/email/data/disposable-domains.json +3 -0
- package/src/manager/libraries/email/data/typo-domains.js +83 -0
- package/src/manager/libraries/email/validation.js +74 -8
- package/src/manager/libraries/email/validation.test.js +125 -0
- package/src/test/fixtures/firebase-project/.firebaserc +5 -0
- package/src/test/fixtures/firebase-project/database.rules.json +6 -0
- package/src/test/fixtures/firebase-project/firebase.json +49 -0
- package/src/test/fixtures/firebase-project/firestore.indexes.json +4 -0
- package/src/test/fixtures/firebase-project/firestore.rules +10 -0
- package/src/test/fixtures/firebase-project/functions/backend-manager-config.json +24 -0
- package/src/test/fixtures/firebase-project/functions/index.js +10 -0
- package/src/test/fixtures/firebase-project/functions/package.json +15 -0
- package/src/test/fixtures/firebase-project/public/index.html +5 -0
- package/src/test/fixtures/firebase-project/storage.rules +8 -0
- package/src/test/runner.js +16 -4
- package/test/boot/emulator-boots.js +37 -0
- package/test/rules/user.js +35 -30
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Migration
|
|
2
|
+
|
|
3
|
+
Procedures for migrating old BEM consumer projects to the current format: environment variables (Part 1), legacy code patterns (Part 2), and routes/schemas (Part 3).
|
|
4
|
+
|
|
5
|
+
## Part 1: Environment Variable Migration
|
|
6
|
+
|
|
7
|
+
Convert old config formats (runtime config / nested JSON) into individual top-level environment variables in `functions/.env`.
|
|
8
|
+
|
|
9
|
+
### Key Mapping
|
|
10
|
+
|
|
11
|
+
| Old Path | New ENV Key |
|
|
12
|
+
|----------|-------------|
|
|
13
|
+
| `backend_manager.key` or `backendmanager.key` | `BACKEND_MANAGER_KEY` |
|
|
14
|
+
| `backend_manager.namespace` or `backendmanager.namespace` | `BACKEND_MANAGER_NAMESPACE` |
|
|
15
|
+
| `github.key` or `github.token` | `GITHUB_TOKEN` |
|
|
16
|
+
| `openai.key` or `openai.api_key` | `OPENAI_API_KEY` |
|
|
17
|
+
| `paypal.client_id` | `PAYPAL_CLIENT_ID` |
|
|
18
|
+
| `paypal.client_secret` | `PAYPAL_CLIENT_SECRET` |
|
|
19
|
+
| `stripe.secret_key` or `stripe.key` | `STRIPE_SECRET_KEY` |
|
|
20
|
+
| `chargebee.site` | `CHARGEBEE_SITE` |
|
|
21
|
+
| `chargebee.api_key` or `chargebee.key` | `CHARGEBEE_API_KEY` |
|
|
22
|
+
| `coinbase.api_key` or `coinbase.key` | `COINBASE_API_KEY` |
|
|
23
|
+
| `cloudflare.token` or `cloudflare.key` | `CLOUDFLARE_TOKEN` |
|
|
24
|
+
| `recaptcha.secret_key` or `recaptcha.key` | `RECAPTCHA_SECRET_KEY` |
|
|
25
|
+
| `sendgrid.api_key` or `sendgrid.key` | `SENDGRID_API_KEY` |
|
|
26
|
+
| `beehiiv.api_key` or `beehiiv.key` | `BEEHIIV_API_KEY` |
|
|
27
|
+
| `zerobounce.api_key` or `zerobounce.key` | `ZEROBOUNCE_API_KEY` |
|
|
28
|
+
|
|
29
|
+
### Steps
|
|
30
|
+
|
|
31
|
+
1. **Check for config sources**: first `functions/.runtimeconfig.json` (parse JSON); else a `RUNTIME_CONFIG` variable inside `functions/.env` (parse the object inside).
|
|
32
|
+
2. **Extract key-value pairs** using the mapping table.
|
|
33
|
+
3. **Backup existing `.env`** as `functions/.env.backup` if it exists.
|
|
34
|
+
4. **Check existing `.env` for conflicts**: skip existing keys and warn.
|
|
35
|
+
5. **Write/update `functions/.env`**: each mapped key as a top-level variable.
|
|
36
|
+
6. **Delete source files**: remove `functions/.runtimeconfig.json` if it existed.
|
|
37
|
+
7. **Update `functions/backend-manager-config.json`**: remove the deprecated `mailchimp` key entirely; update `brand` to the nested structure `{ name, url, contact: { email }, images: { brandmark, wordmark, combomark } }`; set `github.user` to `"itw-creative-works"`.
|
|
38
|
+
8. Update `functions/.nvmrc` to `v22/*` and `functions/package.json` `engines.node` to `"22"`.
|
|
39
|
+
9. Clean up `functions/.gitignore` duplicates.
|
|
40
|
+
|
|
41
|
+
## Part 2: Legacy Code Migration
|
|
42
|
+
|
|
43
|
+
Search all `.js` files under `functions/` for legacy config reads and convert to `process.env`:
|
|
44
|
+
|
|
45
|
+
- `Manager.config.*` → `process.env.KEY_NAME`
|
|
46
|
+
- `RUNTIME_CONFIG` → individual `process.env` vars
|
|
47
|
+
- `functions.config()` → `process.env` vars
|
|
48
|
+
|
|
49
|
+
| Old Pattern | New Pattern |
|
|
50
|
+
|-------------|-------------|
|
|
51
|
+
| `Manager.config.github.key` | `process.env.GITHUB_TOKEN` |
|
|
52
|
+
| `Manager.config.sendgrid.key` | `process.env.SENDGRID_API_KEY` |
|
|
53
|
+
| `Manager.config.stripe.secret_key` | `process.env.STRIPE_SECRET_KEY` |
|
|
54
|
+
| `Manager.config.openai.key` | `process.env.OPENAI_API_KEY` |
|
|
55
|
+
| `Manager.config.paypal.client_id` | `process.env.PAYPAL_CLIENT_ID` |
|
|
56
|
+
| `Manager.config.paypal.client_secret` | `process.env.PAYPAL_CLIENT_SECRET` |
|
|
57
|
+
| `Manager.config.chargebee.site` | `process.env.CHARGEBEE_SITE` |
|
|
58
|
+
| `Manager.config.chargebee.api_key` | `process.env.CHARGEBEE_API_KEY` |
|
|
59
|
+
| `Manager.config.coinbase.api_key` | `process.env.COINBASE_API_KEY` |
|
|
60
|
+
| `Manager.config.cloudflare.token` | `process.env.CLOUDFLARE_TOKEN` |
|
|
61
|
+
| `Manager.config.recaptcha.secret_key` | `process.env.RECAPTCHA_SECRET_KEY` |
|
|
62
|
+
| `Manager.config.beehiiv.api_key` | `process.env.BEEHIIV_API_KEY` |
|
|
63
|
+
| `Manager.config.zerobounce.api_key` | `process.env.ZEROBOUNCE_API_KEY` |
|
|
64
|
+
| `Manager.config.backend_manager.key` | `process.env.BACKEND_MANAGER_KEY` |
|
|
65
|
+
| `Manager.config.backend_manager.namespace` | `process.env.BACKEND_MANAGER_NAMESPACE` |
|
|
66
|
+
|
|
67
|
+
## Part 3: Route/Schema Migration
|
|
68
|
+
|
|
69
|
+
**IMPORTANT:** Only migrate routes that already use the middleware system in `functions/index.js`:
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
// MIGRATE these (uses Manager.Middleware)
|
|
73
|
+
.https.onRequest((req, res) => Manager.Middleware(req, res).run('example'));
|
|
74
|
+
|
|
75
|
+
// DO NOT migrate these (old manual route loading)
|
|
76
|
+
.https.onRequest(async (req, res) => {
|
|
77
|
+
return new (require(`${__dirname}/routes/example/index.js`))().main(Manager, req, res);
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Old → New Format
|
|
82
|
+
|
|
83
|
+
**Route:** constructor pattern → context-object export ([routes.md](routes.md)):
|
|
84
|
+
|
|
85
|
+
- `routes/example/index.js` → `routes/example/post.js` (or the appropriate method file)
|
|
86
|
+
- Remove the constructor; use `module.exports = async ({ Manager, assistant, analytics, usage, user, settings, libraries, utilities }) => {}`
|
|
87
|
+
|
|
88
|
+
**Schema:** wrapped tiers → flat ([schemas.md](schemas.md)):
|
|
89
|
+
|
|
90
|
+
- `schemas/example/index.js` → `schemas/example/post.js`
|
|
91
|
+
- Remove the `['defaults']:` wrapper; flatten the structure (plan adjustments move INSIDE the function, branching on `user`)
|
|
92
|
+
- Change the signature to the context object: `({ assistant, user, data, method, headers, geolocation, client })`
|
|
93
|
+
- Remove `value: undefined` noise
|
|
94
|
+
|
|
95
|
+
| Aspect | Old Format | New Format |
|
|
96
|
+
|--------|-----------|------------|
|
|
97
|
+
| Route export | `module.exports = Route` (constructor) | `module.exports = async ({ ... }) => {}` |
|
|
98
|
+
| Self reference | `const self = this;` | Not needed |
|
|
99
|
+
| File naming | `index.js` | `get.js`, `post.js`, `put.js`, `delete.js` |
|
|
100
|
+
| Schema wrapper | `['defaults']: { ... }` | Flat structure (no wrapper) |
|
|
101
|
+
| Schema params | `(assistant)` | `({ assistant, user, data, ... })` context object |
|
|
102
|
+
|
|
103
|
+
## See also
|
|
104
|
+
|
|
105
|
+
- [routes.md](routes.md) — the current route format being migrated TO
|
|
106
|
+
- [schemas.md](schemas.md) — the current schema contract
|
|
107
|
+
- [environment-detection.md](environment-detection.md) — env var conventions
|
package/docs/routes.md
CHANGED
|
@@ -36,35 +36,131 @@ module.exports = Module;
|
|
|
36
36
|
|
|
37
37
|
## New Route (Consumer Project)
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
Routes live at `functions/routes/{path}/{method}.js` — BEM routes requests to the matching method file (`get.js` / `post.js` / `put.js` / `delete.js`), falling back to `index.js` if no method-specific file exists.
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
function Route() {}
|
|
43
|
-
|
|
44
|
-
Route.prototype.main = async function (assistant) {
|
|
45
|
-
const Manager = assistant.Manager;
|
|
46
|
-
const usage = assistant.usage;
|
|
47
|
-
const user = assistant.usage.user;
|
|
48
|
-
const analytics = assistant.analytics;
|
|
49
|
-
const settings = assistant.settings;
|
|
41
|
+
A route exports an **async function receiving a context object** (built by the middleware — see `src/manager/helpers/middleware.js`):
|
|
50
42
|
|
|
51
|
-
|
|
43
|
+
```javascript
|
|
44
|
+
/**
|
|
45
|
+
* POST /items - Create a new item
|
|
46
|
+
*/
|
|
47
|
+
module.exports = async ({ Manager, assistant, analytics, usage, user, settings, libraries, utilities }) => {
|
|
52
48
|
if (!user.authenticated) {
|
|
53
49
|
return assistant.respond('Authentication required', { code: 401 });
|
|
54
50
|
}
|
|
55
51
|
|
|
52
|
+
const { admin } = libraries;
|
|
53
|
+
const firestore = admin.firestore();
|
|
54
|
+
|
|
56
55
|
// Track usage
|
|
57
56
|
await usage.validate('requests');
|
|
58
57
|
usage.increment('requests');
|
|
59
58
|
await usage.update();
|
|
60
59
|
|
|
61
|
-
//
|
|
62
|
-
|
|
60
|
+
// settings strings are whitespace-trimmed by middleware; HTML is preserved.
|
|
61
|
+
// Call utilities.sanitize() at the HTML-insertion site, or opt the whole
|
|
62
|
+
// route in to middleware HTML strip via { sanitize: true } on .run().
|
|
63
|
+
const id = settings.id; // auto-generated by the schema (see schemas.md)
|
|
64
|
+
|
|
65
|
+
await firestore.doc(`items/${id}`).set({ id, owner: user.auth.uid, ...settings });
|
|
66
|
+
|
|
67
|
+
return assistant.respond({ id });
|
|
63
68
|
};
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Context object fields: `Manager`, `assistant`, `user` (from `assistant.getUser()`), `usage`, `settings` (schema-resolved), `analytics`, `libraries`, `utilities`.
|
|
72
|
+
|
|
73
|
+
### CRUD method files
|
|
64
74
|
|
|
65
|
-
|
|
75
|
+
Every resource endpoint follows proper CRUD with **method-specific files** (plural-noun route names — `items`, not `item`):
|
|
76
|
+
|
|
77
|
+
| Method | File | Purpose | Path |
|
|
78
|
+
|--------|------|---------|------|
|
|
79
|
+
| GET | `get.js` | List all or fetch one | `/items` or `/items/{id}` |
|
|
80
|
+
| POST | `post.js` | Create new | `/items` |
|
|
81
|
+
| PUT | `put.js` | Update existing | `/items/{id}` |
|
|
82
|
+
| DELETE | `delete.js` | Delete existing | `/items/{id}` |
|
|
83
|
+
|
|
84
|
+
**GET routes exist for external API consumers.** The dashboard/frontend reads data directly from Firestore (faster, cheaper, no cold starts). POST/PUT/DELETE still go through Cloud Functions for server-side validation, usage tracking, and analytics.
|
|
85
|
+
|
|
86
|
+
**PUT and DELETE must verify ownership** before mutating:
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
const existing = docSnap.data();
|
|
90
|
+
|
|
91
|
+
if (existing.owner !== user.auth.uid) {
|
|
92
|
+
return assistant.respond('Not authorized', { code: 403 });
|
|
93
|
+
}
|
|
66
94
|
```
|
|
67
95
|
|
|
96
|
+
Immutable fields (`id`, `owner`, `stats`, `metadata.created`) should NOT be editable via PUT. ID generation (POST) and ID-from-path extraction (GET/PUT/DELETE) happen in the **schema**, not the route — see [schemas.md](schemas.md).
|
|
97
|
+
|
|
98
|
+
### Key route patterns
|
|
99
|
+
|
|
100
|
+
- Short-circuit returns for auth/validation checks
|
|
101
|
+
- `settings` contains the parsed + validated request data (from schemas)
|
|
102
|
+
- `assistant.respond()` for ALL responses (success and error)
|
|
103
|
+
- `admin.firestore().doc('collection/id')` shorthand for Firestore access ([firestore.md](firestore.md))
|
|
104
|
+
- Timestamps under `metadata.{created,updated}` ([firestore.md](firestore.md#document-metadata))
|
|
105
|
+
|
|
106
|
+
### Functions entry point (`functions/index.js`)
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
const Manager = (new (require('backend-manager'))).init(exports, {
|
|
110
|
+
setupFunctionsIdentity: false,
|
|
111
|
+
});
|
|
112
|
+
const { functions } = Manager.libraries;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @route /items
|
|
116
|
+
*
|
|
117
|
+
* @method GET /items - List all items
|
|
118
|
+
* @method GET /items/:itemId - Get single item
|
|
119
|
+
* @method POST /items - Create a new item
|
|
120
|
+
* @method PUT /items/:itemId - Update an item
|
|
121
|
+
* @method DELETE /items/:itemId - Delete an item
|
|
122
|
+
*/
|
|
123
|
+
exports.items = functions
|
|
124
|
+
.runWith({ memory: '256MB', timeoutSeconds: 120 })
|
|
125
|
+
.https.onRequest((req, res) => Manager.Middleware(req, res).run('items'));
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The schema defaults to the route name (`.run('items')` loads `functions/schemas/items/`); pass `{ schema: 'custom' }` only when the schema path differs.
|
|
129
|
+
|
|
130
|
+
For operations that don't fit CRUD (e.g. `/items/:itemId/export`), add an **action sub-path** handled within the same Cloud Function (parsed from the request path) or as a separate function if resource needs differ significantly.
|
|
131
|
+
|
|
132
|
+
### firebase.json routing
|
|
133
|
+
|
|
134
|
+
Make routes public with rewrites. Use the bracket syntax so sub-paths like `/items/{id}` route correctly:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"hosting": {
|
|
139
|
+
"rewrites": [
|
|
140
|
+
{ "source": "{/items,/items/**}", "function": "items" }
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**CRITICAL**: Without the `/**` wildcard, requests to `/items/{id}` won't reach the function.
|
|
147
|
+
|
|
148
|
+
**CRITICAL: Rewrite order matters — first match wins.** When multiple functions share a path prefix, the most specific routes MUST come first and the catch-all last, otherwise it swallows all sub-routes:
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"hosting": {
|
|
153
|
+
"rewrites": [
|
|
154
|
+
{ "source": "{/agents/*/chat,/agents/*/chat/**}", "function": "agentsChat" },
|
|
155
|
+
{ "source": "/agents/*/conversations/**", "function": "agentsConversations" },
|
|
156
|
+
{ "source": "{/agents,/agents/**}", "function": "agents" }
|
|
157
|
+
]
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Order: most specific → least specific → catch-all.
|
|
163
|
+
|
|
68
164
|
## New Event Handler
|
|
69
165
|
|
|
70
166
|
Create `src/manager/functions/core/events/{type}/{event}.js`:
|
package/docs/schemas.md
CHANGED
|
@@ -1,39 +1,135 @@
|
|
|
1
1
|
# Schemas
|
|
2
2
|
|
|
3
|
-
Schemas define and validate the payload your routes accept.
|
|
3
|
+
Schemas define and validate the payload your routes accept. They live at `functions/schemas/{name}/{method}.js` — the resolver loads the method-specific file first (`get.js` / `post.js` / …), falling back to `{name}/index.js` (see `src/manager/helpers/settings.js`). The schema name defaults to the route name (`.run('items')` ↔ `functions/schemas/items/`).
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Schema function contract
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
A schema exports a function that receives a **context object** and returns a **flat schema object**. Plan-based adjustments happen INSIDE the function (there is no tier system):
|
|
8
8
|
|
|
9
9
|
```javascript
|
|
10
|
-
module.exports =
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
},
|
|
20
|
-
numericField: {
|
|
21
|
-
types: ['number'],
|
|
22
|
-
default: 10,
|
|
23
|
-
min: 1,
|
|
24
|
-
max: 100,
|
|
25
|
-
},
|
|
10
|
+
module.exports = ({ assistant, user, data, method, headers, geolocation, client }) => {
|
|
11
|
+
const planId = user?.subscription?.product?.id || 'basic';
|
|
12
|
+
const isPremium = planId !== 'basic';
|
|
13
|
+
|
|
14
|
+
const schema = {
|
|
15
|
+
name: {
|
|
16
|
+
types: ['string'],
|
|
17
|
+
default: '',
|
|
18
|
+
required: true,
|
|
26
19
|
},
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
20
|
+
limit: {
|
|
21
|
+
types: ['number'],
|
|
22
|
+
default: 10,
|
|
23
|
+
min: 1,
|
|
24
|
+
max: isPremium ? 1000 : 100, // plan-based limit
|
|
32
25
|
},
|
|
33
26
|
};
|
|
27
|
+
|
|
28
|
+
// Premium-only field (not present for basic users)
|
|
29
|
+
if (isPremium) {
|
|
30
|
+
schema.premiumOnlyField = {
|
|
31
|
+
types: ['string'],
|
|
32
|
+
default: 'premium-feature',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return schema;
|
|
34
37
|
};
|
|
35
38
|
```
|
|
36
39
|
|
|
40
|
+
Context fields: `assistant`, `user` (resolved user), `data` (raw request data), `method`, `headers`, `geolocation`, `client`.
|
|
41
|
+
|
|
42
|
+
## Field Properties
|
|
43
|
+
|
|
44
|
+
- `types` — array of allowed types: `['string']`, `['number']`, `['boolean']`, `['object']`, `['array']`, `['any']`, or multiple (`['string', 'number']`)
|
|
45
|
+
- `default` — default value if not provided; may be a function (`default: () => ...`)
|
|
46
|
+
- `value` — force-set value (ignores user input — e.g. auto-generated IDs)
|
|
47
|
+
- `required` — `true`/`false` or a function `(assistant, settings, options) => bool`. **NEVER combine with `default`** — see the footgun below
|
|
48
|
+
- `min` / `max` — validation bounds (string length, number range, array length); numbers clamp
|
|
49
|
+
- `clean` — a RegExp (matched chars removed) or function `(value) => cleaned`
|
|
50
|
+
- `sanitize` — per-field opt-out (`false`) for HTML sanitization; only meaningful when the route opts in via `Manager.Middleware(req, res).run('route', { sanitize: true })`. See [sanitization.md](sanitization.md)
|
|
51
|
+
|
|
52
|
+
### ⚠️ `required` + `default` footgun
|
|
53
|
+
|
|
54
|
+
BEM checks `required` against the ORIGINAL request value, before defaults apply — so `required: true` on a field with a `default` throws `Required key {field} is missing in settings` before the default is ever used. For fields that must be non-empty but have a derived default (like path-extracted IDs), use `min: 1` instead.
|
|
55
|
+
|
|
56
|
+
## ID Generation (POST — Create)
|
|
57
|
+
|
|
58
|
+
IDs are auto-generated in the **schema**, NOT in the route. Use `value` to force-generate via BEM's built-in `randomId()` (14-char nanoid, 62-char alphabet, no `-` or `_`):
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
id: {
|
|
62
|
+
types: ['string'],
|
|
63
|
+
value: () => assistant.Manager.Utilities().randomId(),
|
|
64
|
+
},
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The route just reads `settings.id` — no ID generation logic needed.
|
|
68
|
+
|
|
69
|
+
## ID Extraction from Path (GET, PUT, DELETE)
|
|
70
|
+
|
|
71
|
+
For single-item operations, extract the ID from the URL path in the **schema**:
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
// /items/{id} → split('/') = ['', 'items', '{id}'] → index 2
|
|
75
|
+
id: {
|
|
76
|
+
types: ['string'],
|
|
77
|
+
default: (assistant.request.path || '').split('/')[2] || '',
|
|
78
|
+
min: 1, // enforce non-empty (NOT required: true — see footgun above)
|
|
79
|
+
max: 128,
|
|
80
|
+
},
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
For GET list endpoints, omit `min` so an empty ID means "list":
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
id: {
|
|
87
|
+
types: ['string'],
|
|
88
|
+
default: (assistant.request.path || '').split('/')[2] || '',
|
|
89
|
+
},
|
|
90
|
+
limit: { types: ['number'], default: 20, min: 1, max: 100 },
|
|
91
|
+
startAfter: { types: ['string'], default: '' },
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Dynamic Schemas
|
|
95
|
+
|
|
96
|
+
Schemas can branch on request data (e.g. different fields per type):
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
module.exports = ({ assistant, data }) => {
|
|
100
|
+
const type = data?.type || '';
|
|
101
|
+
|
|
102
|
+
const schema = {
|
|
103
|
+
name: { types: ['string'], default: undefined, required: true },
|
|
104
|
+
type: { types: ['string'], default: undefined, required: true },
|
|
105
|
+
options: {},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
switch (type) {
|
|
109
|
+
case 'url':
|
|
110
|
+
schema.options = { url: { types: ['string'], default: undefined, required: true } };
|
|
111
|
+
break;
|
|
112
|
+
case 'text':
|
|
113
|
+
schema.options = { text: { types: ['string'], default: undefined, required: true } };
|
|
114
|
+
break;
|
|
115
|
+
default:
|
|
116
|
+
schema.options = {};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return schema;
|
|
120
|
+
};
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Reference Implementation
|
|
124
|
+
|
|
125
|
+
The comprehensive test schema exercising every field option (types, function defaults, forced `value`, conditional `required`, min/max clamping, `clean` regex + function, nested objects, plan-based fields): [`src/manager/schemas/test/schema/post.js`](../src/manager/schemas/test/schema/post.js).
|
|
126
|
+
|
|
37
127
|
## Field Sanitization
|
|
38
128
|
|
|
39
|
-
Middleware always trims whitespace on string fields. HTML sanitization is **opt-in per route** — `Manager.Middleware(req, res).run('my-route', { sanitize: true })`. When opted in, fields can individually opt back out with `sanitize: false`. See [
|
|
129
|
+
Middleware always trims whitespace on string fields. HTML sanitization is **opt-in per route** — `Manager.Middleware(req, res).run('my-route', { sanitize: true })`. When opted in, fields can individually opt back out with `sanitize: false`. See [sanitization.md](sanitization.md).
|
|
130
|
+
|
|
131
|
+
## See also
|
|
132
|
+
|
|
133
|
+
- [routes.md](routes.md) — the routes consuming `settings`
|
|
134
|
+
- [sanitization.md](sanitization.md) — trim vs HTML-strip behavior
|
|
135
|
+
- [test-framework.md](test-framework.md) — schema tests (`test/routes/test/schema.js`)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Test Framework — Boot Layer
|
|
2
|
+
|
|
3
|
+
The `boot` layer is BEM's framework self-test. When `npx mgr test` runs from the backend-manager repo itself (no `firebase.json` in cwd), the runner boots a **bundled fixture Firebase project**, brings up the emulator against it, and runs the `test/boot/` smoke suite. It replaces the old "you can't run tests from the framework repo" gap with a deterministic pass/fail. BEM's equivalent of BXM's `BXM_TEST_BOOT_PROJECT` boot layer and UJM's `UJ_TEST_BOOT_PROJECT` site-boot layer.
|
|
4
|
+
|
|
5
|
+
## What boot tests verify
|
|
6
|
+
|
|
7
|
+
Things that ONLY break when the whole self-test path assembles correctly:
|
|
8
|
+
- The Firebase emulator boots against the fixture (functions, firestore, auth, hosting, …)
|
|
9
|
+
- The fixture's `functions/index.js` runs `Manager.init()` inside the emulator's functions runtime — i.e. the **local** `backend-manager` (symlinked in) loads and wires the built-in `bm_api` function
|
|
10
|
+
- The hosting rewrite routes `/backend-manager/**` → `bm_api`
|
|
11
|
+
- A health request returns `200` with the fixture's `projectId` (`demo-backend-manager`) and the live `bemVersion`
|
|
12
|
+
|
|
13
|
+
If the boot smoke passes, the framework at minimum *boots a consumer backend end-to-end* — catching a class of integration breaks (broken `Manager.init`, mis-wired `bm_api`, bad hosting rewrites) that no single handler test covers.
|
|
14
|
+
|
|
15
|
+
## Test file shape
|
|
16
|
+
|
|
17
|
+
Same `{ description, type, tests }` contract as every other BEM suite — the boot suite is just scoped to the `test/boot/` directory and gated to self-test runs:
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
module.exports = {
|
|
21
|
+
description: 'Boot smoke — fixture emulator + bm_api reachable',
|
|
22
|
+
type: 'group',
|
|
23
|
+
timeout: 30000,
|
|
24
|
+
tests: [
|
|
25
|
+
{
|
|
26
|
+
name: 'bm_api-health-responds-over-hosting-rewrite',
|
|
27
|
+
async run({ http, assert }) {
|
|
28
|
+
const response = await http.get('backend-manager/test/health');
|
|
29
|
+
assert.isSuccess(response, 'bm_api /test/health should respond through the emulator');
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## The bundled fixture project
|
|
37
|
+
|
|
38
|
+
`src/test/fixtures/firebase-project/` — a minimal, committed BEM consumer backend:
|
|
39
|
+
|
|
40
|
+
- `firebase.json` + `.firebaserc` — a **`demo-` project** (`demo-backend-manager`) so the emulator NEVER touches real Firebase; emulator ports from `DEFAULT_EMULATOR_PORTS`; hosting rewrite to `bm_api`.
|
|
41
|
+
- `functions/index.js` — the one-line `Manager.init()` bootstrap (mirrors a real consumer).
|
|
42
|
+
- `functions/package.json` + `backend-manager-config.json` — fake brand/config (no real secrets).
|
|
43
|
+
- `firestore.rules` / `storage.rules` / `database.rules.json` / `firestore.indexes.json` — minimal locked rules (`bm_api` uses the Admin SDK, which bypasses rules).
|
|
44
|
+
|
|
45
|
+
**Runtime-only, gitignored** (never committed): before boot, the test command symlinks the local `backend-manager` (+ `firebase-admin`/`firebase-functions` from BEM's own `node_modules`) into the fixture's `functions/node_modules`, injects the fixture admin keys into the env, and generates a **throwaway RSA `service-account.json`** (emulator-only — a `demo-` project never authenticates against Google). All of this lives in `setupSelfTest()` / `linkFixtureDeps()` / `ensureFixtureServiceAccount()` in [src/cli/commands/test.js](../src/cli/commands/test.js).
|
|
46
|
+
|
|
47
|
+
## `BEM_TEST_BOOT_PROJECT`
|
|
48
|
+
|
|
49
|
+
| Env | Purpose |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `BEM_TEST_BOOT_PROJECT` | Root of a Firebase project to boot instead of the bundled fixture. Auto-set to `src/test/fixtures/firebase-project` when BEM tests itself; set it explicitly to self-test against a **real consumer** (e.g. `ultimate-jekyll-backend`) without `cd`-ing into it. |
|
|
52
|
+
|
|
53
|
+
## What happens in a consumer run
|
|
54
|
+
|
|
55
|
+
The `boot/` smoke is **excluded from real-consumer runs** (`runner.js` `discoverTests` skips `boot/` unless `isFrameworkSelfTest`) — it targets the bundled fixture, so it would be redundant noise in a consumer's suite. Consumers run the full `routes`/`events`/`rules` suites against their own emulator as normal.
|
|
56
|
+
|
|
57
|
+
## Why this exists
|
|
58
|
+
|
|
59
|
+
BEM has no pure-logic test layer — every `routes`/`events`/`rules` suite needs a live emulator + a real project, so they run against a real consumer. The boot layer fills the remaining gap: a fast, self-contained smoke proving the framework still boots a consumer backend from the repo itself. It's the BEM analog of "does the extension load?" (BXM) / "does the site boot?" (UJM).
|
|
60
|
+
|
|
61
|
+
## See also
|
|
62
|
+
|
|
63
|
+
- [test-framework.md](test-framework.md) — overall harness, running/filtering, context object, assertions, auth levels
|
|
64
|
+
- [logging.md](logging.md) — `functions/*.log` files (the emulator/test logs the boot run writes)
|
|
65
|
+
- [environment-detection.md](environment-detection.md) — `Manager.isTesting()` and the environment signals
|