meadow-endpoints 4.0.5 → 4.0.9

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.
Files changed (35) hide show
  1. package/CONTRIBUTING.md +50 -0
  2. package/Dockerfile_LUXURYCode +1 -39
  3. package/README.md +172 -51
  4. package/dist/indoctrinate_content_staging/Indoctrinate-Catalog-AppData.json +4548 -0
  5. package/docs/README.md +326 -0
  6. package/docs/_sidebar.md +37 -0
  7. package/docs/crud/README.md +220 -0
  8. package/docs/crud/count.md +168 -0
  9. package/docs/crud/create.md +194 -0
  10. package/docs/crud/delete.md +237 -0
  11. package/docs/crud/read.md +324 -0
  12. package/docs/crud/schema.md +349 -0
  13. package/docs/crud/update.md +203 -0
  14. package/docs/css/docuserve.css +73 -0
  15. package/docs/index.html +39 -0
  16. package/docs/retold-catalog.json +177 -0
  17. package/docs/retold-keyword-index.json +6720 -0
  18. package/package.json +24 -19
  19. package/source/Meadow-Endpoints.js +8 -1
  20. package/source/controller/Meadow-Endpoints-Controller-Base.js +14 -7
  21. package/source/controller/components/Meadow-Endpoints-Controller-BehaviorInjection.js +4 -1
  22. package/source/controller/components/Meadow-Endpoints-Controller-Error.js +4 -1
  23. package/source/controller/components/Meadow-Endpoints-Controller-Log.js +4 -1
  24. package/source/controller/utility/Meadow-Endpoints-Filter-Parser.js +13 -204
  25. package/source/controller/utility/Meadow-Endpoints-Session-Marshaler.js +10 -6
  26. package/source/controller/utility/Meadow-Endpoints-Stream-RecordArray.js +5 -2
  27. package/source/endpoints/delete/Meadow-Endpoint-Undelete.js +2 -2
  28. package/source/endpoints/read/Meadow-Endpoint-ReadDistinctList.js +3 -1
  29. package/source/endpoints/read/Meadow-Endpoint-ReadLiteList.js +3 -1
  30. package/source/endpoints/read/Meadow-Endpoint-ReadSelectList.js +3 -1
  31. package/source/endpoints/read/Meadow-Endpoint-Reads.js +3 -1
  32. package/source/endpoints/read/Meadow-Endpoint-ReadsBy.js +3 -1
  33. package/source/endpoints/update/Meadow-Operation-Update.js +9 -1
  34. package/test/MeadowEndpoints_basic_tests.js +1580 -65
  35. package/tsconfig.json +13 -0
@@ -0,0 +1,168 @@
1
+ # Count Endpoints
2
+
3
+ > Aggregate record counts with filtering
4
+
5
+ The Count endpoints return the number of records matching given criteria. They support the same filter expressions as the Read endpoints and provide field-specific counting via the CountBy variant.
6
+
7
+ ## Routes
8
+
9
+ | Method | Route | Description |
10
+ |--------|-------|-------------|
11
+ | GET | `/{v}/{entity}/s/Count` | Total record count |
12
+ | GET | `/{v}/{entity}/s/Count/FilteredTo/:Filter` | Filtered record count |
13
+ | GET | `/{v}/{entity}/s/Count/By/:ByField/:ByValue` | Count by field value |
14
+
15
+ ## Count
16
+
17
+ **Request:** `GET /1.0/Book/s/Count`
18
+
19
+ **Response:**
20
+
21
+ ```json
22
+ {
23
+ "Count": 157
24
+ }
25
+ ```
26
+
27
+ ### Filtered Count
28
+
29
+ **Request:** `GET /1.0/Book/s/Count/FilteredTo/Author~Herbert`
30
+
31
+ **Response:**
32
+
33
+ ```json
34
+ {
35
+ "Count": 12
36
+ }
37
+ ```
38
+
39
+ ## CountBy
40
+
41
+ **Request:** `GET /1.0/Book/s/Count/By/Author/Frank Herbert`
42
+
43
+ **Response:**
44
+
45
+ ```json
46
+ {
47
+ "Count": 6
48
+ }
49
+ ```
50
+
51
+ ## Count Request Lifecycle
52
+
53
+ ```
54
+ GET /{v}/{entity}/s/Count
55
+ |
56
+ v
57
+ Build query, parse filter expression (if FilteredTo)
58
+ |
59
+ v
60
+ [Count-QueryConfiguration] <-- add security filters
61
+ |
62
+ v
63
+ DAL.doCount(query)
64
+ |
65
+ v
66
+ Send response ({Count: N})
67
+ ```
68
+
69
+ ## CountBy Request Lifecycle
70
+
71
+ ```
72
+ GET /{v}/{entity}/s/Count/By/:ByField/:ByValue
73
+ |
74
+ v
75
+ Build query, add field = value filter
76
+ |
77
+ v
78
+ [CountBy-QueryConfiguration] <-- add security filters
79
+ |
80
+ v
81
+ DAL.doCount(query)
82
+ |
83
+ v
84
+ Send response ({Count: N})
85
+ ```
86
+
87
+ ## Behavior Injection Points
88
+
89
+ ### Count-QueryConfiguration
90
+
91
+ Runs after any filter expression is parsed and applied, but before the DAL count executes. Use this to add security filters so counts reflect only the records the user has access to.
92
+
93
+ ```javascript
94
+ tmpEndpoints.controller.BehaviorInjection.setBehavior('Count-QueryConfiguration',
95
+ (pRequest, pRequestState, fCallback) =>
96
+ {
97
+ // Multi-tenant security: restrict count to user's customer
98
+ if (pRequestState.SessionData.UserRoleIndex < 5)
99
+ {
100
+ pRequestState.Query.addFilter('IDCustomer',
101
+ pRequestState.SessionData.CustomerID);
102
+ }
103
+ return fCallback();
104
+ });
105
+ ```
106
+
107
+ ### CountBy-QueryConfiguration
108
+
109
+ Runs after the field value filter is applied on CountBy requests. Operates the same as `Count-QueryConfiguration` but is specific to the `CountBy` endpoint, allowing different behavior for field-specific counts.
110
+
111
+ ```javascript
112
+ tmpEndpoints.controller.BehaviorInjection.setBehavior('CountBy-QueryConfiguration',
113
+ (pRequest, pRequestState, fCallback) =>
114
+ {
115
+ // Add tenant filter for CountBy as well
116
+ pRequestState.Query.addFilter('IDCustomer',
117
+ pRequestState.SessionData.CustomerID);
118
+
119
+ // Log which field is being counted
120
+ _Fable.log.info(`CountBy: ${pRequest.params.ByField} = ${pRequest.params.ByValue}`);
121
+
122
+ return fCallback();
123
+ });
124
+ ```
125
+
126
+ ## Filter Expressions
127
+
128
+ Count supports the same filter syntax as Read endpoints:
129
+
130
+ ```
131
+ GET /1.0/Book/s/Count/FilteredTo/Author~Herbert
132
+ GET /1.0/Book/s/Count/FilteredTo/PublishYear>2000
133
+ GET /1.0/Book/s/Count/FilteredTo/Author~Herbert;PublishYear>2000
134
+ ```
135
+
136
+ | Operator | Meaning |
137
+ |----------|---------|
138
+ | `=` | Equals |
139
+ | `!=` | Not equals |
140
+ | `>` | Greater than |
141
+ | `<` | Less than |
142
+ | `>=` | Greater than or equal |
143
+ | `<=` | Less than or equal |
144
+ | `~` | Contains (LIKE) |
145
+
146
+ ## Real-World Example: Dashboard Metrics
147
+
148
+ ```javascript
149
+ // Set up security filtering for count operations
150
+ tmpEndpoints.controller.BehaviorInjection.setBehavior('Count-QueryConfiguration',
151
+ (pRequest, pRequestState, fCallback) =>
152
+ {
153
+ // Non-admin users only see their customer's records
154
+ if (pRequestState.SessionData.UserRoleIndex < 5)
155
+ {
156
+ pRequestState.Query.addFilter('IDCustomer',
157
+ pRequestState.SessionData.CustomerID);
158
+ }
159
+ return fCallback();
160
+ });
161
+
162
+ // The frontend can then call:
163
+ // GET /1.0/Book/s/Count -> total books
164
+ // GET /1.0/Book/s/Count/FilteredTo/Status=Draft -> draft books
165
+ // GET /1.0/Book/s/Count/By/Author/Frank Herbert -> books by author
166
+ //
167
+ // All counts will automatically be filtered by the user's customer ID.
168
+ ```
@@ -0,0 +1,194 @@
1
+ # Create Endpoints
2
+
3
+ > Insert new records through REST with full lifecycle hooks
4
+
5
+ The Create endpoints handle single-record and bulk-record creation. Both variants use the same underlying `Meadow-Operation-Create` function, which runs behavior injection hooks around the actual DAL create call.
6
+
7
+ ## Routes
8
+
9
+ | Method | Route | Description |
10
+ |--------|-------|-------------|
11
+ | POST | `/{v}/{entity}` | Create a single record |
12
+ | POST | `/{v}/{entity}/s` | Bulk create (array of records) |
13
+
14
+ ## Single Create
15
+
16
+ **Request:** `POST /1.0/Book`
17
+
18
+ ```json
19
+ {
20
+ "Title": "Dune",
21
+ "Author": "Frank Herbert"
22
+ }
23
+ ```
24
+
25
+ **Response:** The complete created record, including auto-generated fields:
26
+
27
+ ```json
28
+ {
29
+ "IDBook": 42,
30
+ "GUIDBook": "0x12ab34cd...",
31
+ "Title": "Dune",
32
+ "Author": "Frank Herbert",
33
+ "CreateDate": "2024-01-15T10:30:00.000Z",
34
+ "CreatingIDUser": 1,
35
+ "UpdateDate": "2024-01-15T10:30:00.000Z",
36
+ "UpdatingIDUser": 1,
37
+ "Deleted": 0
38
+ }
39
+ ```
40
+
41
+ ## Bulk Create
42
+
43
+ **Request:** `POST /1.0/Book/s`
44
+
45
+ Send an array of records in the request body:
46
+
47
+ ```json
48
+ [
49
+ { "Title": "Dune", "Author": "Frank Herbert" },
50
+ { "Title": "Neuromancer", "Author": "William Gibson" },
51
+ { "Title": "Snow Crash", "Author": "Neal Stephenson" }
52
+ ]
53
+ ```
54
+
55
+ **Response:** An array of created records. Each record in the array is processed through the full Create operation lifecycle individually, so all behavior hooks fire for each record.
56
+
57
+ ## Request Lifecycle
58
+
59
+ The Create operation delegates to `Meadow-Operation-Create`, which runs the following waterfall:
60
+
61
+ ```
62
+ POST /{v}/{entity}
63
+ |
64
+ v
65
+ Validate request body is an object
66
+ |
67
+ v
68
+ Set IDCustomer from session (if schema has IDCustomer)
69
+ |
70
+ v
71
+ [Create-PreOperation] <-- validate, authorize, transform
72
+ |
73
+ v
74
+ Build query: addRecord(record), setIDUser(SessionData.UserID)
75
+ |
76
+ v
77
+ [Create-QueryConfiguration] <-- modify query before execution
78
+ |
79
+ v
80
+ DAL.doCreate(query) <-- insert + re-read the new record
81
+ |
82
+ v
83
+ [Create-PostOperation] <-- audit, notify, enrich
84
+ |
85
+ v
86
+ Send response (created record)
87
+ ```
88
+
89
+ For bulk creates, each record in the array passes through this same lifecycle independently. Errors on individual records do not halt the batch -- the error is attached to that record's response entry.
90
+
91
+ ## Behavior Injection Points
92
+
93
+ ### Create-PreOperation
94
+
95
+ Runs **before** the query is built. The record to be created is available on `pRequestState.RecordToCreate`. Use this hook for validation, authorization, or to modify the record before it is persisted.
96
+
97
+ ```javascript
98
+ tmpEndpoints.controller.BehaviorInjection.setBehavior('Create-PreOperation',
99
+ (pRequest, pRequestState, fCallback) =>
100
+ {
101
+ // Validate required fields
102
+ if (!pRequestState.RecordToCreate.Title)
103
+ {
104
+ return fCallback({ Code: 400, Message: 'Title is required' });
105
+ }
106
+
107
+ // Set defaults
108
+ if (!pRequestState.RecordToCreate.Status)
109
+ {
110
+ pRequestState.RecordToCreate.Status = 'Draft';
111
+ }
112
+
113
+ return fCallback();
114
+ });
115
+ ```
116
+
117
+ ### Create-QueryConfiguration
118
+
119
+ Runs **after** the FoxHound query is configured with the record and user ID, but **before** `DAL.doCreate()` executes. Use this to modify the query object directly.
120
+
121
+ ```javascript
122
+ tmpEndpoints.controller.BehaviorInjection.setBehavior('Create-QueryConfiguration',
123
+ (pRequest, pRequestState, fCallback) =>
124
+ {
125
+ // Add extra query configuration
126
+ pRequestState.Query.setLogLevel(5);
127
+ return fCallback();
128
+ });
129
+ ```
130
+
131
+ ### Create-PostOperation
132
+
133
+ Runs **after** the record has been created and re-read from the database. The complete record (including auto-generated fields like identity and timestamps) is available on `pRequestState.Record`.
134
+
135
+ ```javascript
136
+ tmpEndpoints.controller.BehaviorInjection.setBehavior('Create-PostOperation',
137
+ (pRequest, pRequestState, fCallback) =>
138
+ {
139
+ // Log an audit entry
140
+ let tmpAuditRecord = {
141
+ Action: 'Create',
142
+ EntityType: 'Book',
143
+ EntityID: pRequestState.Record.IDBook,
144
+ UserID: pRequestState.SessionData.UserID
145
+ };
146
+ _Fable.log.info(`Book created: ${JSON.stringify(tmpAuditRecord)}`);
147
+ return fCallback();
148
+ });
149
+ ```
150
+
151
+ ## Automatic Field Handling
152
+
153
+ The Create operation automatically handles several fields:
154
+
155
+ | Field | Behavior |
156
+ |-------|----------|
157
+ | `IDCustomer` | Set from `SessionData.CustomerID` if the schema includes it and the field is not already set |
158
+ | `CreatingIDUser` | Set by the DAL from `Query.IDUser` (which comes from `SessionData.UserID`) |
159
+ | `CreateDate` | Set automatically by the DAL |
160
+ | `UpdatingIDUser` | Set by the DAL (same as CreatingIDUser on create) |
161
+ | `UpdateDate` | Set automatically by the DAL (same as CreateDate on create) |
162
+ | `Deleted` | Defaults to `0` |
163
+
164
+ ## Error Handling
165
+
166
+ Errors returned to `fCallback` in any behavior hook halt the waterfall. The error object should include a `Code` (HTTP status) and `Message`:
167
+
168
+ ```javascript
169
+ return fCallback({ Code: 400, Message: 'Validation failed: Title is required' });
170
+ ```
171
+
172
+ For bulk creates, individual record errors are captured in the response without stopping the remaining records. Each errored record will include an `Error` property in its response entry.
173
+
174
+ ## Real-World Example: Authorization Check
175
+
176
+ ```javascript
177
+ tmpEndpoints.controller.BehaviorInjection.setBehavior('Create-PreOperation',
178
+ (pRequest, pRequestState, fCallback) =>
179
+ {
180
+ // Only managers (role index >= 3) can create books
181
+ if (pRequestState.SessionData.UserRoleIndex < 3)
182
+ {
183
+ return fCallback({
184
+ Code: 403,
185
+ Message: 'Insufficient permissions to create records'
186
+ });
187
+ }
188
+
189
+ // Force the customer ID to match the session
190
+ pRequestState.RecordToCreate.IDCustomer = pRequestState.SessionData.CustomerID;
191
+
192
+ return fCallback();
193
+ });
194
+ ```
@@ -0,0 +1,237 @@
1
+ # Delete Endpoints
2
+
3
+ > Soft-delete and restore records with full authorization hooks
4
+
5
+ The Delete endpoints handle record deletion and undeletion. By default, Meadow uses soft deletes -- setting a `Deleted` column to `1` rather than removing the row. The delete lifecycle includes a pre-read of the target record, providing a checkpoint for authorization before the delete executes.
6
+
7
+ ## Routes
8
+
9
+ | Method | Route | Description |
10
+ |--------|-------|-------------|
11
+ | DELETE | `/{v}/{entity}/:IDRecord` | Delete a record by URL parameter |
12
+ | DELETE | `/{v}/{entity}` | Delete a record (ID in request body) |
13
+ | GET | `/{v}/{entity}/Undelete/:IDRecord` | Undelete a soft-deleted record |
14
+
15
+ ## Delete
16
+
17
+ **Request:** `DELETE /1.0/Book/42`
18
+
19
+ Or with the ID in the request body:
20
+
21
+ ```
22
+ DELETE /1.0/Book
23
+ Content-Type: application/json
24
+
25
+ { "IDBook": 42 }
26
+ ```
27
+
28
+ **Response:** A count of deleted records:
29
+
30
+ ```json
31
+ {
32
+ "Count": 1
33
+ }
34
+ ```
35
+
36
+ ## Undelete
37
+
38
+ **Request:** `GET /1.0/Book/Undelete/42`
39
+
40
+ **Response:** A count of undeleted records:
41
+
42
+ ```json
43
+ {
44
+ "Count": 1
45
+ }
46
+ ```
47
+
48
+ The Undelete endpoint verifies that the schema includes a `Deleted` column and that the target record actually has `Deleted = 1` before restoring it.
49
+
50
+ ## Delete Request Lifecycle
51
+
52
+ ```
53
+ DELETE /{v}/{entity}/:IDRecord
54
+ |
55
+ v
56
+ Extract record ID (from URL param or request body)
57
+ Validate ID > 0
58
+ |
59
+ v
60
+ Build query with ID filter, set IDUser from session
61
+ |
62
+ v
63
+ [Delete-QueryConfiguration] <-- modify query before record load
64
+ |
65
+ v
66
+ DAL.doRead(query) <-- load the record for security checks
67
+ |
68
+ v
69
+ [Delete-PreOperation] <-- authorize, validate, prevent
70
+ |
71
+ v
72
+ DAL.doDelete(query) <-- execute soft-delete
73
+ |
74
+ v
75
+ [Delete-PostOperation] <-- audit, cascade, cleanup
76
+ |
77
+ v
78
+ Send response ({Count: N})
79
+ ```
80
+
81
+ ## Undelete Request Lifecycle
82
+
83
+ ```
84
+ GET /{v}/{entity}/Undelete/:IDRecord
85
+ |
86
+ v
87
+ Extract record ID, validate ID > 0
88
+ |
89
+ v
90
+ Verify schema has a 'Deleted' type column
91
+ |
92
+ v
93
+ Build query: filter by ID + Deleted = 1
94
+ |
95
+ v
96
+ DAL.doRead(query) <-- find the deleted record
97
+ |
98
+ v
99
+ [Undelete-PreOperation] <-- authorize the restore
100
+ |
101
+ v
102
+ DAL.doUndelete(query) <-- set Deleted = 0
103
+ |
104
+ v
105
+ [Undelete-PostOperation] <-- audit, notify
106
+ |
107
+ v
108
+ Send response ({Count: N})
109
+ ```
110
+
111
+ ## Behavior Injection Points
112
+
113
+ ### Delete-QueryConfiguration
114
+
115
+ Runs after the initial query is built with the record ID filter and user ID, but before the record is loaded from the database. Use this to add additional query constraints.
116
+
117
+ ```javascript
118
+ tmpEndpoints.controller.BehaviorInjection.setBehavior('Delete-QueryConfiguration',
119
+ (pRequest, pRequestState, fCallback) =>
120
+ {
121
+ // Add tenant filter to prevent cross-tenant deletes
122
+ pRequestState.Query.addFilter('IDCustomer',
123
+ pRequestState.SessionData.CustomerID);
124
+ return fCallback();
125
+ });
126
+ ```
127
+
128
+ ### Delete-PreOperation
129
+
130
+ Runs after the target record is loaded from the database, but before the delete executes. The record is available on `pRequestState.Record`. This is the primary hook for authorization -- you can inspect the record's ownership, status, or relationships before allowing the delete.
131
+
132
+ ```javascript
133
+ tmpEndpoints.controller.BehaviorInjection.setBehavior('Delete-PreOperation',
134
+ (pRequest, pRequestState, fCallback) =>
135
+ {
136
+ // Only the record creator or admins can delete
137
+ if (pRequestState.Record.CreatingIDUser !== pRequestState.SessionData.UserID &&
138
+ pRequestState.SessionData.UserRoleIndex < 5)
139
+ {
140
+ return fCallback({
141
+ Code: 403,
142
+ Message: 'Only the record creator or an admin can delete this record'
143
+ });
144
+ }
145
+
146
+ // Prevent deletion of published records
147
+ if (pRequestState.Record.Status === 'Published')
148
+ {
149
+ return fCallback({
150
+ Code: 409,
151
+ Message: 'Cannot delete a published record. Unpublish it first.'
152
+ });
153
+ }
154
+
155
+ return fCallback();
156
+ });
157
+ ```
158
+
159
+ ### Delete-PostOperation
160
+
161
+ Runs after the record has been deleted (soft-delete). Use this for audit logging, cascading deletes to related entities, or sending notifications.
162
+
163
+ ```javascript
164
+ tmpEndpoints.controller.BehaviorInjection.setBehavior('Delete-PostOperation',
165
+ (pRequest, pRequestState, fCallback) =>
166
+ {
167
+ // Log the deletion
168
+ _Fable.log.info(`Record ${pRequestState.IDRecord} deleted by user ${pRequestState.SessionData.UserID}`);
169
+
170
+ // Cascade soft-delete to child records
171
+ let tmpChildQuery = _ChapterDAL.query;
172
+ tmpChildQuery.addFilter('IDBook', pRequestState.IDRecord);
173
+ _ChapterDAL.doReads(tmpChildQuery,
174
+ (pError, pQuery, pRecords) =>
175
+ {
176
+ if (pRecords && pRecords.length > 0)
177
+ {
178
+ // Soft-delete each child record
179
+ _Fable.log.info(`Cascading delete to ${pRecords.length} chapters`);
180
+ }
181
+ return fCallback();
182
+ });
183
+ });
184
+ ```
185
+
186
+ ### Undelete-PreOperation
187
+
188
+ Runs after the deleted record is located (verified to exist with `Deleted = 1`), before the undelete executes. The record is available on `pRequestState.Record`.
189
+
190
+ ```javascript
191
+ tmpEndpoints.controller.BehaviorInjection.setBehavior('Undelete-PreOperation',
192
+ (pRequest, pRequestState, fCallback) =>
193
+ {
194
+ // Only admins can undelete
195
+ if (pRequestState.SessionData.UserRoleIndex < 5)
196
+ {
197
+ return fCallback({
198
+ Code: 403,
199
+ Message: 'Only administrators can restore deleted records'
200
+ });
201
+ }
202
+ return fCallback();
203
+ });
204
+ ```
205
+
206
+ ### Undelete-PostOperation
207
+
208
+ Runs after the record has been restored. Use this for audit logging or re-enabling related records.
209
+
210
+ ```javascript
211
+ tmpEndpoints.controller.BehaviorInjection.setBehavior('Undelete-PostOperation',
212
+ (pRequest, pRequestState, fCallback) =>
213
+ {
214
+ _Fable.log.info(`Record ${pRequest.params.IDRecord} undeleted by user ${pRequestState.SessionData.UserID}`);
215
+ return fCallback();
216
+ });
217
+ ```
218
+
219
+ ## Soft Delete vs Hard Delete
220
+
221
+ Meadow's delete is always a soft delete -- it sets the `Deleted` column to `1`. This means:
222
+
223
+ - **Deleted records are excluded from normal reads** -- the DAL automatically filters `Deleted = 0` on Read and Reads operations
224
+ - **Deleted records can be restored** via the Undelete endpoint
225
+ - **No data is permanently lost** through the standard API
226
+
227
+ If your schema does not include a `Deleted` column, the Undelete endpoint will return a `500` error with the message "No undelete bit on record."
228
+
229
+ ## Record ID Resolution
230
+
231
+ The Delete endpoint accepts the record ID from two sources, checked in order:
232
+
233
+ 1. **URL parameter:** `DELETE /1.0/Book/42` -- `pRequest.params.IDRecord`
234
+ 2. **Request body (number):** `{ "IDBook": 42 }` -- `pRequest.body[defaultIdentifier]`
235
+ 3. **Request body (string):** `{ "IDBook": "42" }` -- string-to-number conversion
236
+
237
+ If no valid ID is found (ID < 1), the endpoint returns a `500` error.