meadow-endpoints 4.0.7 → 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.
- 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 -19
- package/source/endpoints/update/Meadow-Operation-Update.js +9 -1
- package/test/MeadowEndpoints_basic_tests.js +1586 -37
|
@@ -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.
|