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.
@@ -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 |