meadow-endpoints 4.0.7 → 4.0.10
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/CONTRIBUTING.md +50 -0
- package/README.md +172 -51
- package/dist/indoctrinate_content_staging/Indoctrinate-Catalog-AppData.json +4548 -0
- package/docs/README.md +326 -0
- package/docs/_sidebar.md +37 -0
- package/docs/crud/README.md +220 -0
- package/docs/crud/count.md +168 -0
- package/docs/crud/create.md +194 -0
- package/docs/crud/delete.md +237 -0
- package/docs/crud/read.md +324 -0
- package/docs/crud/schema.md +349 -0
- package/docs/crud/update.md +203 -0
- package/docs/css/docuserve.css +73 -0
- package/docs/index.html +39 -0
- package/docs/retold-catalog.json +177 -0
- package/docs/retold-keyword-index.json +6720 -0
- package/package.json +21 -28
- package/source/endpoints/update/Meadow-Operation-Update.js +9 -1
- package/test/MeadowEndpoints_basic_tests.js +1586 -37
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# Read Endpoints
|
|
2
|
+
|
|
3
|
+
> Retrieve records with pagination, filtering, and projections
|
|
4
|
+
|
|
5
|
+
The Read endpoint group is the largest in Meadow Endpoints, covering single-record reads, paginated lists, filtered queries, select lists for dropdowns, lite projections, and distinct value queries. All list-based endpoints share the `Reads-QueryConfiguration` behavior injection point.
|
|
6
|
+
|
|
7
|
+
## Routes
|
|
8
|
+
|
|
9
|
+
### Single Record
|
|
10
|
+
|
|
11
|
+
| Method | Route | Description |
|
|
12
|
+
|--------|-------|-------------|
|
|
13
|
+
| GET | `/{v}/{entity}/:IDRecord` | Read a record by its integer ID |
|
|
14
|
+
| GET | `/{v}/{entity}/By/:GUIDRecord` | Read a record by its GUID |
|
|
15
|
+
|
|
16
|
+
### Record Lists
|
|
17
|
+
|
|
18
|
+
| Method | Route | Description |
|
|
19
|
+
|--------|-------|-------------|
|
|
20
|
+
| GET | `/{v}/{entity}/s` | Read records (default cap: 250) |
|
|
21
|
+
| GET | `/{v}/{entity}/s/:Begin/:Cap` | Read with pagination |
|
|
22
|
+
| GET | `/{v}/{entity}/s/FilteredTo/:Filter` | Read with filter expression |
|
|
23
|
+
| GET | `/{v}/{entity}/s/FilteredTo/:Filter/:Begin/:Cap` | Filtered read with pagination |
|
|
24
|
+
| GET | `/{v}/{entity}/s/By/:ByField/:ByValue` | Read by exact field match |
|
|
25
|
+
| GET | `/{v}/{entity}/s/By/:ByField/:ByValue/:Begin/:Cap` | Field match with pagination |
|
|
26
|
+
|
|
27
|
+
### Aggregation
|
|
28
|
+
|
|
29
|
+
| Method | Route | Description |
|
|
30
|
+
|--------|-------|-------------|
|
|
31
|
+
| GET | `/{v}/{entity}/Max/:ColumnName` | Maximum value for a column |
|
|
32
|
+
|
|
33
|
+
### Specialized Projections
|
|
34
|
+
|
|
35
|
+
| Method | Route | Description |
|
|
36
|
+
|--------|-------|-------------|
|
|
37
|
+
| GET | `/{v}/{entity}/Select` | Select list ({Hash, Value} pairs) |
|
|
38
|
+
| GET | `/{v}/{entity}/Select/:Begin/:Cap` | Paginated select list |
|
|
39
|
+
| GET | `/{v}/{entity}/Select/FilteredTo/:Filter` | Filtered select list |
|
|
40
|
+
| GET | `/{v}/{entity}/Select/FilteredTo/:Filter/:Begin/:Cap` | Filtered paginated select list |
|
|
41
|
+
| GET | `/{v}/{entity}/s/Lite` | Lite list (ID + GUID + name only) |
|
|
42
|
+
| GET | `/{v}/{entity}/s/Lite/:Begin/:Cap` | Paginated lite list |
|
|
43
|
+
| GET | `/{v}/{entity}/s/LiteExtended/:ExtraColumns` | Lite list with extra columns |
|
|
44
|
+
| GET | `/{v}/{entity}/s/Distinct/:Columns` | Distinct values for columns |
|
|
45
|
+
| GET | `/{v}/{entity}/s/Distinct/:Columns/:Begin/:Cap` | Paginated distinct list |
|
|
46
|
+
| GET | `/{v}/{entity}/s/Distinct/:Columns/FilteredTo/:Filter` | Filtered distinct list |
|
|
47
|
+
|
|
48
|
+
## Single Read
|
|
49
|
+
|
|
50
|
+
**Request:** `GET /1.0/Book/42`
|
|
51
|
+
|
|
52
|
+
**Response:** A single record object:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"IDBook": 42,
|
|
57
|
+
"GUIDBook": "0x12ab34cd...",
|
|
58
|
+
"Title": "Dune",
|
|
59
|
+
"Author": "Frank Herbert",
|
|
60
|
+
"CreateDate": "2024-01-15T10:30:00.000Z",
|
|
61
|
+
"CreatingIDUser": 1,
|
|
62
|
+
"UpdateDate": "2024-01-15T10:30:00.000Z",
|
|
63
|
+
"UpdatingIDUser": 1,
|
|
64
|
+
"Deleted": 0
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Single Read Lifecycle
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
GET /{v}/{entity}/:IDRecord
|
|
72
|
+
|
|
|
73
|
+
v
|
|
74
|
+
Initialize Query
|
|
75
|
+
|
|
|
76
|
+
v
|
|
77
|
+
[Read-PreOperation] <-- authorize, modify criteria
|
|
78
|
+
|
|
|
79
|
+
v
|
|
80
|
+
Apply filter: IDRecord or GUIDRecord
|
|
81
|
+
|
|
|
82
|
+
v
|
|
83
|
+
[Read-QueryConfiguration] <-- add extra filters, joins
|
|
84
|
+
|
|
|
85
|
+
v
|
|
86
|
+
DAL.doRead(query)
|
|
87
|
+
|
|
|
88
|
+
v
|
|
89
|
+
[Read-PostOperation] <-- transform, redact fields
|
|
90
|
+
|
|
|
91
|
+
v
|
|
92
|
+
Send response (single record)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Record List (Reads)
|
|
96
|
+
|
|
97
|
+
**Request:** `GET /1.0/Book/s/0/25`
|
|
98
|
+
|
|
99
|
+
**Response:** A newline-delimited JSON stream of records. Each record is a complete JSON object separated by newlines. This streaming approach efficiently handles large recordsets.
|
|
100
|
+
|
|
101
|
+
### Reads Lifecycle
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
GET /{v}/{entity}/s/:Begin/:Cap
|
|
105
|
+
|
|
|
106
|
+
v
|
|
107
|
+
Build query with pagination (Begin, Cap)
|
|
108
|
+
Parse filter expression (if FilteredTo)
|
|
109
|
+
|
|
|
110
|
+
v
|
|
111
|
+
[Reads-QueryConfiguration] <-- add security filters, modify query
|
|
112
|
+
|
|
|
113
|
+
v
|
|
114
|
+
DAL.doReads(query)
|
|
115
|
+
|
|
|
116
|
+
v
|
|
117
|
+
[Reads-PostOperation] <-- transform recordset, aggregate
|
|
118
|
+
|
|
|
119
|
+
v
|
|
120
|
+
Stream response (newline-delimited JSON)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Pagination
|
|
124
|
+
|
|
125
|
+
All list endpoints support pagination via `Begin` and `Cap` URL parameters:
|
|
126
|
+
|
|
127
|
+
- **Begin** - Zero-based starting index (skip this many records)
|
|
128
|
+
- **Cap** - Maximum number of records to return
|
|
129
|
+
|
|
130
|
+
If no `Cap` is provided, the default is `250` (configurable via `MeadowDefaultMaxCap` in Fable settings).
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
GET /1.0/Book/s -> records 0-249 (default)
|
|
134
|
+
GET /1.0/Book/s/0/10 -> first 10 records
|
|
135
|
+
GET /1.0/Book/s/10/10 -> records 10-19
|
|
136
|
+
GET /1.0/Book/s/100/50 -> records 100-149
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Filter Expressions
|
|
140
|
+
|
|
141
|
+
The `FilteredTo` parameter accepts filter expressions parsed by meadow-filter:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
GET /1.0/Book/s/FilteredTo/Author~Herbert
|
|
145
|
+
GET /1.0/Book/s/FilteredTo/Title=Dune
|
|
146
|
+
GET /1.0/Book/s/FilteredTo/IDBook>10
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
| Operator | Meaning |
|
|
150
|
+
|----------|---------|
|
|
151
|
+
| `=` | Equals |
|
|
152
|
+
| `!=` | Not equals |
|
|
153
|
+
| `>` | Greater than |
|
|
154
|
+
| `<` | Less than |
|
|
155
|
+
| `>=` | Greater than or equal |
|
|
156
|
+
| `<=` | Less than or equal |
|
|
157
|
+
| `~` | Contains (LIKE) |
|
|
158
|
+
|
|
159
|
+
Multiple filters can be combined with semicolons:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
GET /1.0/Book/s/FilteredTo/Author~Herbert;Title~Dune
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## ReadsBy
|
|
166
|
+
|
|
167
|
+
The `ReadsBy` endpoint filters by an exact field match:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
GET /1.0/Book/s/By/Author/Frank Herbert
|
|
171
|
+
GET /1.0/Book/s/By/Author/Frank Herbert/0/10
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
When the `ByValue` parameter is an array (passed via query string), it generates an `IN` filter:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
GET /1.0/Book/s/By/IDBook/[1,2,3]
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## ReadMax
|
|
181
|
+
|
|
182
|
+
Returns the maximum value for a given column:
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
GET /1.0/Book/Max/IDBook
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Select List
|
|
189
|
+
|
|
190
|
+
Returns `{Hash, Value}` pairs suitable for populating dropdowns. The `Hash` is the record's default identifier, and the `Value` is generated from a template:
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
GET /1.0/Book/Select
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Response:**
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
[
|
|
200
|
+
{ "Hash": 1, "Value": "Book #1" },
|
|
201
|
+
{ "Hash": 2, "Value": "Book #2" }
|
|
202
|
+
]
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The `Value` template defaults to `{Entity} #{ID}` but can be customized via `BehaviorInjection.processTemplate('SelectList', ...)`.
|
|
206
|
+
|
|
207
|
+
## Lite List
|
|
208
|
+
|
|
209
|
+
Returns only the identity columns (ID, GUID, and name/label columns) for a compact response:
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
GET /1.0/Book/s/Lite
|
|
213
|
+
GET /1.0/Book/s/Lite/0/50
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Extended Lite List
|
|
217
|
+
|
|
218
|
+
Include additional columns by name:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
GET /1.0/Book/s/LiteExtended/Author,PublishYear
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Distinct List
|
|
225
|
+
|
|
226
|
+
Returns distinct values for the specified columns:
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
GET /1.0/Book/s/Distinct/Author
|
|
230
|
+
GET /1.0/Book/s/Distinct/Author,Publisher/0/100
|
|
231
|
+
GET /1.0/Book/s/Distinct/Author/FilteredTo/PublishYear>2000
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Behavior Injection Points
|
|
235
|
+
|
|
236
|
+
### Read-PreOperation
|
|
237
|
+
|
|
238
|
+
Runs before filter criteria are applied on single-record reads. The query object is initialized but no filters are set yet.
|
|
239
|
+
|
|
240
|
+
```javascript
|
|
241
|
+
tmpEndpoints.controller.BehaviorInjection.setBehavior('Read-PreOperation',
|
|
242
|
+
(pRequest, pRequestState, fCallback) =>
|
|
243
|
+
{
|
|
244
|
+
// Check if user is authenticated
|
|
245
|
+
if (!pRequestState.SessionData.LoggedIn)
|
|
246
|
+
{
|
|
247
|
+
return fCallback({ Code: 401, Message: 'Authentication required' });
|
|
248
|
+
}
|
|
249
|
+
return fCallback();
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Read-QueryConfiguration
|
|
254
|
+
|
|
255
|
+
Runs after the ID/GUID filter is applied on single-record reads. Use this to add security filters or additional query constraints.
|
|
256
|
+
|
|
257
|
+
```javascript
|
|
258
|
+
tmpEndpoints.controller.BehaviorInjection.setBehavior('Read-QueryConfiguration',
|
|
259
|
+
(pRequest, pRequestState, fCallback) =>
|
|
260
|
+
{
|
|
261
|
+
// Restrict reads to user's own customer
|
|
262
|
+
pRequestState.Query.addFilter('IDCustomer',
|
|
263
|
+
pRequestState.SessionData.CustomerID);
|
|
264
|
+
return fCallback();
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Read-PostOperation
|
|
269
|
+
|
|
270
|
+
Runs after the record is retrieved from the database. Use this to transform the result, redact sensitive fields, or load related data.
|
|
271
|
+
|
|
272
|
+
```javascript
|
|
273
|
+
tmpEndpoints.controller.BehaviorInjection.setBehavior('Read-PostOperation',
|
|
274
|
+
(pRequest, pRequestState, fCallback) =>
|
|
275
|
+
{
|
|
276
|
+
// Redact sensitive information
|
|
277
|
+
delete pRequestState.Record.InternalNotes;
|
|
278
|
+
|
|
279
|
+
// Add computed fields
|
|
280
|
+
pRequestState.Record.DisplayName =
|
|
281
|
+
`${pRequestState.Record.Title} by ${pRequestState.Record.Author}`;
|
|
282
|
+
|
|
283
|
+
return fCallback();
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Reads-QueryConfiguration
|
|
288
|
+
|
|
289
|
+
Shared across all list-based read endpoints: Reads, ReadsBy, ReadSelectList, ReadLiteList, ReadDistinctList, and ReadMax. Runs after pagination and filter expressions are applied, before the DAL call.
|
|
290
|
+
|
|
291
|
+
```javascript
|
|
292
|
+
tmpEndpoints.controller.BehaviorInjection.setBehavior('Reads-QueryConfiguration',
|
|
293
|
+
(pRequest, pRequestState, fCallback) =>
|
|
294
|
+
{
|
|
295
|
+
// Multi-tenant security: always filter by customer
|
|
296
|
+
if (pRequestState.SessionData.UserRoleIndex < 5)
|
|
297
|
+
{
|
|
298
|
+
pRequestState.Query.addFilter('IDCustomer',
|
|
299
|
+
pRequestState.SessionData.CustomerID);
|
|
300
|
+
}
|
|
301
|
+
return fCallback();
|
|
302
|
+
});
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Reads-PostOperation
|
|
306
|
+
|
|
307
|
+
Runs after the recordset is retrieved on Reads and ReadsBy endpoints. Use this to transform the array of records before they are streamed to the client.
|
|
308
|
+
|
|
309
|
+
```javascript
|
|
310
|
+
tmpEndpoints.controller.BehaviorInjection.setBehavior('Reads-PostOperation',
|
|
311
|
+
(pRequest, pRequestState, fCallback) =>
|
|
312
|
+
{
|
|
313
|
+
// Strip internal fields from all records
|
|
314
|
+
for (let i = 0; i < pRequestState.Records.length; i++)
|
|
315
|
+
{
|
|
316
|
+
delete pRequestState.Records[i].InternalNotes;
|
|
317
|
+
}
|
|
318
|
+
return fCallback();
|
|
319
|
+
});
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Streaming Responses
|
|
323
|
+
|
|
324
|
+
All list endpoints use `doStreamRecordArray()` to send records as newline-delimited JSON. This approach avoids buffering large result sets in memory and allows the client to begin processing records before the full response is complete.
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# Schema Endpoints
|
|
2
|
+
|
|
3
|
+
> Serve metadata, default records, and validation with customization hooks
|
|
4
|
+
|
|
5
|
+
The Schema endpoints expose entity metadata over REST: the JSON Schema definition, an empty default record for new-record forms, and record validation. Each endpoint has both pre-operation and post-operation hooks, making them fully customizable.
|
|
6
|
+
|
|
7
|
+
## Routes
|
|
8
|
+
|
|
9
|
+
| Method | Route | Description |
|
|
10
|
+
|--------|-------|-------------|
|
|
11
|
+
| GET | `/{v}/{entity}/Schema` | Return the JSON Schema for the entity |
|
|
12
|
+
| GET | `/{v}/{entity}/Schema/New` | Return an empty default record |
|
|
13
|
+
| POST | `/{v}/{entity}/Schema/Validate` | Validate a record against the schema |
|
|
14
|
+
|
|
15
|
+
## Schema
|
|
16
|
+
|
|
17
|
+
**Request:** `GET /1.0/Book/Schema`
|
|
18
|
+
|
|
19
|
+
**Response:** The entity's JSON Schema definition:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"title": "Book",
|
|
24
|
+
"type": "object",
|
|
25
|
+
"properties": {
|
|
26
|
+
"IDBook": { "type": "integer" },
|
|
27
|
+
"GUIDBook": { "type": "string" },
|
|
28
|
+
"Title": { "type": "string", "maxLength": 255 },
|
|
29
|
+
"Author": { "type": "string", "maxLength": 128 },
|
|
30
|
+
"CreateDate": { "type": "string", "format": "date-time" },
|
|
31
|
+
"CreatingIDUser": { "type": "integer" },
|
|
32
|
+
"UpdateDate": { "type": "string", "format": "date-time" },
|
|
33
|
+
"UpdatingIDUser": { "type": "integer" },
|
|
34
|
+
"Deleted": { "type": "integer" }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## New (Default Record)
|
|
40
|
+
|
|
41
|
+
**Request:** `GET /1.0/Book/Schema/New`
|
|
42
|
+
|
|
43
|
+
**Response:** An empty record with default values populated from the schema:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"IDBook": 0,
|
|
48
|
+
"GUIDBook": "",
|
|
49
|
+
"Title": "",
|
|
50
|
+
"Author": "",
|
|
51
|
+
"CreateDate": "",
|
|
52
|
+
"CreatingIDUser": 0,
|
|
53
|
+
"UpdateDate": "",
|
|
54
|
+
"UpdatingIDUser": 0,
|
|
55
|
+
"Deleted": 0
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Validate
|
|
60
|
+
|
|
61
|
+
**Request:** `POST /1.0/Book/Schema/Validate`
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"IDBook": 0,
|
|
66
|
+
"Title": "Dune",
|
|
67
|
+
"Author": "Frank Herbert"
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Response:** The validation result from the Meadow schema's `validateObject()` method. Returns the validation outcome indicating whether the record conforms to the schema constraints.
|
|
72
|
+
|
|
73
|
+
## Schema Request Lifecycle
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
GET /{v}/{entity}/Schema
|
|
77
|
+
|
|
|
78
|
+
v
|
|
79
|
+
[Schema-PreOperation] <-- authorize, customize
|
|
80
|
+
|
|
|
81
|
+
v
|
|
82
|
+
Load JSON Schema from DAL (if not set during PreOperation)
|
|
83
|
+
|
|
|
84
|
+
v
|
|
85
|
+
[Schema-PostOperation] <-- filter properties, add metadata
|
|
86
|
+
|
|
|
87
|
+
v
|
|
88
|
+
Send response (JSON Schema)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## New Request Lifecycle
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
GET /{v}/{entity}/Schema/New
|
|
95
|
+
|
|
|
96
|
+
v
|
|
97
|
+
[New-PreOperation] <-- set custom default record
|
|
98
|
+
|
|
|
99
|
+
v
|
|
100
|
+
Build default record from schema (if not set during PreOperation)
|
|
101
|
+
|
|
|
102
|
+
v
|
|
103
|
+
[New-PostOperation] <-- populate computed defaults
|
|
104
|
+
|
|
|
105
|
+
v
|
|
106
|
+
Send response (default record)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Validate Request Lifecycle
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
POST /{v}/{entity}/Schema/Validate
|
|
113
|
+
|
|
|
114
|
+
v
|
|
115
|
+
[Validate-PreOperation] <-- add custom validation rules
|
|
116
|
+
|
|
|
117
|
+
v
|
|
118
|
+
Validate request body is an object
|
|
119
|
+
|
|
|
120
|
+
v
|
|
121
|
+
Run schema validation: DAL.schemaFull.validateObject(record)
|
|
122
|
+
|
|
|
123
|
+
v
|
|
124
|
+
[Validate-PostOperation] <-- augment validation result
|
|
125
|
+
|
|
|
126
|
+
v
|
|
127
|
+
Send response (validation result)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Behavior Injection Points
|
|
131
|
+
|
|
132
|
+
### Schema-PreOperation
|
|
133
|
+
|
|
134
|
+
Runs before the JSON Schema is loaded. You can set `pRequest.JSONSchema` during this hook to provide a custom schema instead of the default.
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
tmpEndpoints.controller.BehaviorInjection.setBehavior('Schema-PreOperation',
|
|
138
|
+
(pRequest, pRequestState, fCallback) =>
|
|
139
|
+
{
|
|
140
|
+
// Require authentication to view schema
|
|
141
|
+
if (!pRequestState.SessionData.LoggedIn)
|
|
142
|
+
{
|
|
143
|
+
return fCallback({ Code: 401, Message: 'Authentication required' });
|
|
144
|
+
}
|
|
145
|
+
return fCallback();
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Schema-PostOperation
|
|
150
|
+
|
|
151
|
+
Runs after the JSON Schema is loaded onto `pRequestState.JSONSchema`. Use this to modify the schema before it is sent -- for example, to remove internal columns or add UI hints.
|
|
152
|
+
|
|
153
|
+
```javascript
|
|
154
|
+
tmpEndpoints.controller.BehaviorInjection.setBehavior('Schema-PostOperation',
|
|
155
|
+
(pRequest, pRequestState, fCallback) =>
|
|
156
|
+
{
|
|
157
|
+
// Remove internal fields from the public schema
|
|
158
|
+
if (pRequestState.JSONSchema && pRequestState.JSONSchema.properties)
|
|
159
|
+
{
|
|
160
|
+
delete pRequestState.JSONSchema.properties.InternalNotes;
|
|
161
|
+
delete pRequestState.JSONSchema.properties.InternalStatus;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Add UI metadata
|
|
165
|
+
if (pRequestState.JSONSchema)
|
|
166
|
+
{
|
|
167
|
+
pRequestState.JSONSchema.uiHints = {
|
|
168
|
+
primaryField: 'Title',
|
|
169
|
+
searchableFields: ['Title', 'Author']
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return fCallback();
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### New-PreOperation
|
|
178
|
+
|
|
179
|
+
Runs before the default record is generated. You can set `pRequestState.EmptyEntityRecord` during this hook to provide a custom default object instead of the schema-generated one.
|
|
180
|
+
|
|
181
|
+
```javascript
|
|
182
|
+
tmpEndpoints.controller.BehaviorInjection.setBehavior('New-PreOperation',
|
|
183
|
+
(pRequest, pRequestState, fCallback) =>
|
|
184
|
+
{
|
|
185
|
+
// Provide a custom default record with pre-populated values
|
|
186
|
+
pRequestState.EmptyEntityRecord = {
|
|
187
|
+
IDBook: 0,
|
|
188
|
+
GUIDBook: '',
|
|
189
|
+
Title: '',
|
|
190
|
+
Author: '',
|
|
191
|
+
Status: 'Draft',
|
|
192
|
+
IDCustomer: pRequestState.SessionData.CustomerID,
|
|
193
|
+
CreatingIDUser: pRequestState.SessionData.UserID
|
|
194
|
+
};
|
|
195
|
+
return fCallback();
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### New-PostOperation
|
|
200
|
+
|
|
201
|
+
Runs after the default record is generated (or custom default is set). Use this to add computed defaults or session-specific values.
|
|
202
|
+
|
|
203
|
+
```javascript
|
|
204
|
+
tmpEndpoints.controller.BehaviorInjection.setBehavior('New-PostOperation',
|
|
205
|
+
(pRequest, pRequestState, fCallback) =>
|
|
206
|
+
{
|
|
207
|
+
// Set session-specific defaults
|
|
208
|
+
pRequestState.EmptyEntityRecord.IDCustomer =
|
|
209
|
+
pRequestState.SessionData.CustomerID;
|
|
210
|
+
pRequestState.EmptyEntityRecord.CreatingIDUser =
|
|
211
|
+
pRequestState.SessionData.UserID;
|
|
212
|
+
|
|
213
|
+
// Add a generated reference number
|
|
214
|
+
pRequestState.EmptyEntityRecord.ReferenceNumber =
|
|
215
|
+
`BK-${Date.now()}`;
|
|
216
|
+
|
|
217
|
+
return fCallback();
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Validate-PreOperation
|
|
222
|
+
|
|
223
|
+
Runs before validation is performed. Use this to add custom validation logic or modify the record before schema validation.
|
|
224
|
+
|
|
225
|
+
```javascript
|
|
226
|
+
tmpEndpoints.controller.BehaviorInjection.setBehavior('Validate-PreOperation',
|
|
227
|
+
(pRequest, pRequestState, fCallback) =>
|
|
228
|
+
{
|
|
229
|
+
// Normalize data before validation
|
|
230
|
+
if (pRequest.body && pRequest.body.Title)
|
|
231
|
+
{
|
|
232
|
+
pRequest.body.Title = pRequest.body.Title.trim();
|
|
233
|
+
}
|
|
234
|
+
return fCallback();
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Validate-PostOperation
|
|
239
|
+
|
|
240
|
+
Runs after schema validation completes. The validation result is on `pRequestState.RecordValidation`. Use this to add custom validation rules beyond what the JSON Schema checks.
|
|
241
|
+
|
|
242
|
+
```javascript
|
|
243
|
+
tmpEndpoints.controller.BehaviorInjection.setBehavior('Validate-PostOperation',
|
|
244
|
+
(pRequest, pRequestState, fCallback) =>
|
|
245
|
+
{
|
|
246
|
+
// Add custom business rule validation
|
|
247
|
+
if (pRequestState.Record && !pRequestState.Record.Author)
|
|
248
|
+
{
|
|
249
|
+
// Augment validation result with custom error
|
|
250
|
+
if (!pRequestState.RecordValidation.errors)
|
|
251
|
+
{
|
|
252
|
+
pRequestState.RecordValidation.errors = [];
|
|
253
|
+
}
|
|
254
|
+
pRequestState.RecordValidation.errors.push({
|
|
255
|
+
field: 'Author',
|
|
256
|
+
message: 'Author is required for all books'
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return fCallback();
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Complete Hook Example: Protected Schema with Custom Defaults
|
|
265
|
+
|
|
266
|
+
This example shows all six schema hooks working together to provide a customized, secure schema experience:
|
|
267
|
+
|
|
268
|
+
```javascript
|
|
269
|
+
const tmpEndpoints = new libMeadowEndpoints(tmpBookMeadow);
|
|
270
|
+
|
|
271
|
+
// Protect schema access
|
|
272
|
+
tmpEndpoints.controller.BehaviorInjection.setBehavior('Schema-PreOperation',
|
|
273
|
+
(pRequest, pRequestState, fCallback) =>
|
|
274
|
+
{
|
|
275
|
+
if (!pRequestState.SessionData.LoggedIn)
|
|
276
|
+
{
|
|
277
|
+
return fCallback({ Code: 401, Message: 'Login required' });
|
|
278
|
+
}
|
|
279
|
+
return fCallback();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Clean up schema for API consumers
|
|
283
|
+
tmpEndpoints.controller.BehaviorInjection.setBehavior('Schema-PostOperation',
|
|
284
|
+
(pRequest, pRequestState, fCallback) =>
|
|
285
|
+
{
|
|
286
|
+
// Hide internal tracking fields
|
|
287
|
+
let tmpHiddenFields = ['CreatingIDUser', 'UpdatingIDUser', 'Deleted'];
|
|
288
|
+
tmpHiddenFields.forEach((pField) =>
|
|
289
|
+
{
|
|
290
|
+
if (pRequestState.JSONSchema.properties)
|
|
291
|
+
{
|
|
292
|
+
delete pRequestState.JSONSchema.properties[pField];
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
return fCallback();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Provide smart defaults for new records
|
|
299
|
+
tmpEndpoints.controller.BehaviorInjection.setBehavior('New-PostOperation',
|
|
300
|
+
(pRequest, pRequestState, fCallback) =>
|
|
301
|
+
{
|
|
302
|
+
pRequestState.EmptyEntityRecord.IDCustomer =
|
|
303
|
+
pRequestState.SessionData.CustomerID;
|
|
304
|
+
pRequestState.EmptyEntityRecord.Status = 'Draft';
|
|
305
|
+
return fCallback();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Normalize before validation
|
|
309
|
+
tmpEndpoints.controller.BehaviorInjection.setBehavior('Validate-PreOperation',
|
|
310
|
+
(pRequest, pRequestState, fCallback) =>
|
|
311
|
+
{
|
|
312
|
+
if (pRequest.body && typeof(pRequest.body.Title) === 'string')
|
|
313
|
+
{
|
|
314
|
+
pRequest.body.Title = pRequest.body.Title.trim();
|
|
315
|
+
}
|
|
316
|
+
return fCallback();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Add business rules after schema validation
|
|
320
|
+
tmpEndpoints.controller.BehaviorInjection.setBehavior('Validate-PostOperation',
|
|
321
|
+
(pRequest, pRequestState, fCallback) =>
|
|
322
|
+
{
|
|
323
|
+
if (pRequestState.Record)
|
|
324
|
+
{
|
|
325
|
+
if (!pRequestState.Record.Title || pRequestState.Record.Title.length < 1)
|
|
326
|
+
{
|
|
327
|
+
if (!pRequestState.RecordValidation.errors)
|
|
328
|
+
{
|
|
329
|
+
pRequestState.RecordValidation.errors = [];
|
|
330
|
+
}
|
|
331
|
+
pRequestState.RecordValidation.errors.push({
|
|
332
|
+
field: 'Title',
|
|
333
|
+
message: 'Title cannot be empty'
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return fCallback();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
tmpEndpoints.connectRoutes(_Fable.Orator.serviceServer);
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Use Cases
|
|
344
|
+
|
|
345
|
+
| Endpoint | Common Use Cases |
|
|
346
|
+
|----------|-----------------|
|
|
347
|
+
| **Schema** | Form generation, API documentation, client-side validation setup |
|
|
348
|
+
| **New** | Pre-populating create forms with defaults, setting tenant-specific values |
|
|
349
|
+
| **Validate** | Pre-submit validation, import data verification, API input checking |
|