meadow-endpoints 4.0.17 → 4.0.19
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/dist/meadow-endpoints.js +2195 -228
- package/dist/meadow-endpoints.js.map +1 -1
- package/dist/meadow-endpoints.min.js +21 -16
- package/dist/meadow-endpoints.min.js.map +1 -1
- package/package.json +5 -5
- package/source/Meadow-Endpoints.js +10 -0
- package/source/endpoints/update/Meadow-Endpoint-Update.js +17 -1
- package/source/endpoints/upsert/Meadow-Endpoint-BulkUpsert.js +25 -1
- package/source/endpoints/upsert/Meadow-Endpoint-BulkUpsertDetailed.js +96 -0
- package/source/endpoints/upsert/Meadow-Operation-Upsert.js +15 -0
- package/test/MeadowEndpoints_basic_tests.js +552 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meadow-endpoints",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.19",
|
|
4
4
|
"description": "Automatic API endpoints for Meadow data.",
|
|
5
5
|
"main": "source/Meadow-Endpoints.js",
|
|
6
6
|
"scripts": {
|
|
@@ -52,12 +52,12 @@
|
|
|
52
52
|
"better-sqlite3": "^12.9.0",
|
|
53
53
|
"chance": "^1.1.13",
|
|
54
54
|
"gulp-util": "^3.0.8",
|
|
55
|
-
"meadow-connection-sqlite": "^1.0.
|
|
55
|
+
"meadow-connection-sqlite": "^1.0.19",
|
|
56
56
|
"mysql2": "^3.22.1",
|
|
57
57
|
"orator-serviceserver-restify": "^2.0.10",
|
|
58
58
|
"papaparse": "^5.5.3",
|
|
59
59
|
"pict-docuserve": "^0.1.5",
|
|
60
|
-
"quackage": "^1.
|
|
60
|
+
"quackage": "^1.2.3",
|
|
61
61
|
"supertest": "^7.2.2",
|
|
62
62
|
"typescript": "^5.9.3",
|
|
63
63
|
"why-is-node-running": "^3.2.2"
|
|
@@ -67,9 +67,9 @@
|
|
|
67
67
|
"fable": "^3.1.71",
|
|
68
68
|
"fable-serviceproviderbase": "^3.0.19",
|
|
69
69
|
"JSONStream": "^1.3.5",
|
|
70
|
-
"meadow": "^2.0.
|
|
70
|
+
"meadow": "^2.0.38",
|
|
71
71
|
"meadow-filter": "^1.0.10",
|
|
72
|
-
"orator": "^6.
|
|
72
|
+
"orator": "^6.1.1",
|
|
73
73
|
"underscore": "^1.13.8"
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -82,6 +82,7 @@ class MeadowEndpoints
|
|
|
82
82
|
|
|
83
83
|
Upsert: require('./endpoints/upsert/Meadow-Endpoint-Upsert.js'),
|
|
84
84
|
Upserts: require('./endpoints/upsert/Meadow-Endpoint-BulkUpsert.js'),
|
|
85
|
+
UpsertsDetailed: require('./endpoints/upsert/Meadow-Endpoint-BulkUpsertDetailed.js'),
|
|
85
86
|
|
|
86
87
|
Delete: require('./endpoints/delete/Meadow-Endpoint-Delete.js'),
|
|
87
88
|
Undelete: require('./endpoints/delete/Meadow-Endpoint-Undelete.js'),
|
|
@@ -211,6 +212,15 @@ class MeadowEndpoints
|
|
|
211
212
|
this.connectRoute(pServiceServer, 'putWithBodyParser', `s`, this._Endpoints.Updates, `the internal behavior _Endpoints.Updates`);
|
|
212
213
|
this.connectRoute(pServiceServer, 'putWithBodyParser', `/Upsert`, this._Endpoints.Upsert, `the internal behavior _Endpoints.Upsert`);
|
|
213
214
|
this.connectRoute(pServiceServer, 'putWithBodyParser', `/Upserts`, this._Endpoints.Upserts, `the internal behavior _Endpoints.Upserts`);
|
|
215
|
+
this.connectRoute(pServiceServer, 'putWithBodyParser', `/Upserts/Detailed`, this._Endpoints.UpsertsDetailed, `the internal behavior _Endpoints.UpsertsDetailed`);
|
|
216
|
+
// REST-idiomatic PUT/PATCH-by-id: the URL path holds the primary key,
|
|
217
|
+
// matching GET/:IDRecord and DELETE/:IDRecord. The Update endpoint
|
|
218
|
+
// merges pRequest.params.IDRecord into the body so the row is updated
|
|
219
|
+
// in-place — no GET → DELETE → INSERT dance, primary key preserved.
|
|
220
|
+
// Registered AFTER the literal /Upsert(s) routes so those win on the
|
|
221
|
+
// :IDRecord match.
|
|
222
|
+
this.connectRoute(pServiceServer, 'putWithBodyParser', `/:IDRecord`, this._Endpoints.Update, `the internal behavior _Endpoints.Update (by-id PUT)`);
|
|
223
|
+
this.connectRoute(pServiceServer, 'patchWithBodyParser', `/:IDRecord`, this._Endpoints.Update, `the internal behavior _Endpoints.Update (by-id PATCH)`);
|
|
214
224
|
}
|
|
215
225
|
if (this._EnabledBehaviorSets.Delete)
|
|
216
226
|
{
|
|
@@ -12,10 +12,26 @@ const doAPIEndpointUpdate = function(pRequest, pResponse, fNext)
|
|
|
12
12
|
[
|
|
13
13
|
(fStageComplete) =>
|
|
14
14
|
{
|
|
15
|
-
if (typeof(pRequest.body) !== 'object')
|
|
15
|
+
if (typeof(pRequest.body) !== 'object' || pRequest.body === null)
|
|
16
16
|
{
|
|
17
17
|
return fStageComplete(this.ErrorHandler.getError('Record update failure - a valid record is required.', 400));
|
|
18
18
|
}
|
|
19
|
+
|
|
20
|
+
// PUT|PATCH /Entity/:IDRecord puts the primary key in the URL path
|
|
21
|
+
// (REST-idiomatic, parallels GET/:IDRecord and DELETE/:IDRecord).
|
|
22
|
+
// When present, the URL ID is authoritative and overrides anything
|
|
23
|
+
// in the body. This is what lets clients update-in-place without
|
|
24
|
+
// the GET → DELETE → INSERT churn that loses the primary key.
|
|
25
|
+
if (pRequest.params && pRequest.params.IDRecord)
|
|
26
|
+
{
|
|
27
|
+
let tmpURLID = pRequest.params.IDRecord;
|
|
28
|
+
if (typeof(tmpURLID) === 'string' && /^-?\d+$/.test(tmpURLID))
|
|
29
|
+
{
|
|
30
|
+
tmpURLID = Number(tmpURLID);
|
|
31
|
+
}
|
|
32
|
+
pRequest.body[this.DAL.defaultIdentifier] = tmpURLID;
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
if (pRequest.body[this.DAL.defaultIdentifier] < 1)
|
|
20
36
|
{
|
|
21
37
|
return fStageComplete(this.ErrorHandler.getError('Record update failure - a valid record ID is required in the passed-in record.', 400));
|
|
@@ -40,11 +40,35 @@ const doAPIEndpointUpserts = function(pRequest, pResponse, fNext)
|
|
|
40
40
|
fBehaviorInjector(`UpsertBulk-PostOperation`),
|
|
41
41
|
(fStageComplete) =>
|
|
42
42
|
{
|
|
43
|
+
// Surface per-row error counts to the caller via response
|
|
44
|
+
// headers BEFORE streaming the success array. Without these,
|
|
45
|
+
// callers can only see "N records came back" and have no
|
|
46
|
+
// signal that other records were silently dropped (e.g.
|
|
47
|
+
// from a NOT NULL constraint or a column-too-long error).
|
|
48
|
+
// We keep the response body shape stable (bare array of
|
|
49
|
+
// upserted records) for back-compat — the headers are the
|
|
50
|
+
// non-breaking surface for the failure count + total.
|
|
51
|
+
let tmpInputCount = (tmpRequestState.BulkRecords && tmpRequestState.BulkRecords.length) || 0;
|
|
52
|
+
let tmpErrorCount = (tmpRequestState.ErrorRecords && tmpRequestState.ErrorRecords.length) || 0;
|
|
53
|
+
let tmpUpsertedCount = (tmpRequestState.UpsertedRecords && tmpRequestState.UpsertedRecords.length) || 0;
|
|
54
|
+
try
|
|
55
|
+
{
|
|
56
|
+
pResponse.header('X-Meadow-Upsert-Total', String(tmpInputCount));
|
|
57
|
+
pResponse.header('X-Meadow-Upsert-Succeeded', String(tmpUpsertedCount));
|
|
58
|
+
pResponse.header('X-Meadow-Upsert-Errored', String(tmpErrorCount));
|
|
59
|
+
}
|
|
60
|
+
catch (pHdrErr) { /* response.header may not exist on all servers; degrade silently */ }
|
|
61
|
+
|
|
43
62
|
return this.doStreamRecordArray(pResponse, marshalLiteList.call(this, tmpRequestState.UpsertedRecords, pRequest), fStageComplete);
|
|
44
63
|
},
|
|
45
64
|
(fStageComplete) =>
|
|
46
65
|
{
|
|
47
|
-
|
|
66
|
+
let tmpUpsertedCount = (tmpRequestState.UpsertedRecords && tmpRequestState.UpsertedRecords.length) || 0;
|
|
67
|
+
let tmpErrorCount = (tmpRequestState.ErrorRecords && tmpRequestState.ErrorRecords.length) || 0;
|
|
68
|
+
let tmpMessage = (tmpErrorCount > 0)
|
|
69
|
+
? `Bulk upsert complete -- ${tmpUpsertedCount} records succeeded, ${tmpErrorCount} errored`
|
|
70
|
+
: `Bulk upsert complete -- ${tmpUpsertedCount} records processed`;
|
|
71
|
+
this.log.requestCompletedSuccessfully(pRequest, tmpRequestState, tmpMessage);
|
|
48
72
|
return fStageComplete();
|
|
49
73
|
}
|
|
50
74
|
], (pError) =>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meadow Endpoint - Upsert a set of Records, with detailed envelope response.
|
|
3
|
+
*
|
|
4
|
+
* Same upsert pipeline as Meadow-Endpoint-BulkUpsert.js, but the
|
|
5
|
+
* response body is an envelope:
|
|
6
|
+
*
|
|
7
|
+
* {
|
|
8
|
+
* "Counts": { "Total": N, "Succeeded": K, "Errored": M },
|
|
9
|
+
* "UpsertedRecords": [ ... lite-marshaled successes ... ],
|
|
10
|
+
* "ErrorRecords": [ { "Record": {...}, "Operation": "...", "Error": "..." }, ... ]
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* The vanilla `/Upserts` endpoint streams a bare array of successes for
|
|
14
|
+
* back-compat; this `/Upserts/Detailed` variant trades that compatibility
|
|
15
|
+
* for full per-row error visibility. Same X-Meadow-Upsert-* response
|
|
16
|
+
* headers are set on both.
|
|
17
|
+
*/
|
|
18
|
+
const doUpsert = require('./Meadow-Operation-Upsert.js');
|
|
19
|
+
|
|
20
|
+
const marshalLiteList = require('../read/Meadow-Marshal-LiteList.js');
|
|
21
|
+
|
|
22
|
+
const doAPIEndpointUpsertsDetailed = function(pRequest, pResponse, fNext)
|
|
23
|
+
{
|
|
24
|
+
let tmpRequestState = this.initializeRequestState(pRequest, 'UpsertBulkDetailed');
|
|
25
|
+
let fBehaviorInjector = (pBehaviorHash) => { return (fStageComplete) => { this.BehaviorInjection.runBehavior(pBehaviorHash, this, pRequest, tmpRequestState, fStageComplete); }; };
|
|
26
|
+
|
|
27
|
+
tmpRequestState.CreatedRecords = [];
|
|
28
|
+
tmpRequestState.UpdatedRecords = [];
|
|
29
|
+
tmpRequestState.UpsertedRecords = [];
|
|
30
|
+
tmpRequestState.ErrorRecords = [];
|
|
31
|
+
|
|
32
|
+
this.waterfall(
|
|
33
|
+
[
|
|
34
|
+
(fStageComplete) =>
|
|
35
|
+
{
|
|
36
|
+
if (!Array.isArray(pRequest.body))
|
|
37
|
+
{
|
|
38
|
+
return fStageComplete(this.ErrorHandler.getError(`Record bulk upsert (detailed) failure - a valid array of records is required.`, 500));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
tmpRequestState.BulkRecords = pRequest.body;
|
|
42
|
+
|
|
43
|
+
return fStageComplete();
|
|
44
|
+
},
|
|
45
|
+
fBehaviorInjector(`UpsertBulk-PreOperation`),
|
|
46
|
+
(fStageComplete) =>
|
|
47
|
+
{
|
|
48
|
+
this.eachLimit(tmpRequestState.BulkRecords, 1,
|
|
49
|
+
(pRecord, fCallback) =>
|
|
50
|
+
{
|
|
51
|
+
doUpsert.call(this, pRecord, pRequest, tmpRequestState, pResponse, fCallback);
|
|
52
|
+
}, fStageComplete);
|
|
53
|
+
},
|
|
54
|
+
fBehaviorInjector(`UpsertBulk-PostOperation`),
|
|
55
|
+
(fStageComplete) =>
|
|
56
|
+
{
|
|
57
|
+
let tmpInputCount = (tmpRequestState.BulkRecords && tmpRequestState.BulkRecords.length) || 0;
|
|
58
|
+
let tmpErrorCount = (tmpRequestState.ErrorRecords && tmpRequestState.ErrorRecords.length) || 0;
|
|
59
|
+
let tmpUpsertedCount = (tmpRequestState.UpsertedRecords && tmpRequestState.UpsertedRecords.length) || 0;
|
|
60
|
+
|
|
61
|
+
// Mirror the X-Meadow-Upsert-* headers from /Upserts so a
|
|
62
|
+
// caller can dispatch to either endpoint and read the same
|
|
63
|
+
// summary surface. The body envelope adds the per-row
|
|
64
|
+
// detail; the headers stay the structured summary.
|
|
65
|
+
try
|
|
66
|
+
{
|
|
67
|
+
pResponse.header('X-Meadow-Upsert-Total', String(tmpInputCount));
|
|
68
|
+
pResponse.header('X-Meadow-Upsert-Succeeded', String(tmpUpsertedCount));
|
|
69
|
+
pResponse.header('X-Meadow-Upsert-Errored', String(tmpErrorCount));
|
|
70
|
+
}
|
|
71
|
+
catch (pHdrErr) { /* response.header may not exist on all servers; degrade silently */ }
|
|
72
|
+
|
|
73
|
+
let tmpEnvelope = {
|
|
74
|
+
Counts: { Total: tmpInputCount, Succeeded: tmpUpsertedCount, Errored: tmpErrorCount },
|
|
75
|
+
UpsertedRecords: marshalLiteList.call(this, tmpRequestState.UpsertedRecords, pRequest),
|
|
76
|
+
ErrorRecords: tmpRequestState.ErrorRecords || []
|
|
77
|
+
};
|
|
78
|
+
pResponse.send(tmpEnvelope);
|
|
79
|
+
return fStageComplete();
|
|
80
|
+
},
|
|
81
|
+
(fStageComplete) =>
|
|
82
|
+
{
|
|
83
|
+
let tmpUpsertedCount = (tmpRequestState.UpsertedRecords && tmpRequestState.UpsertedRecords.length) || 0;
|
|
84
|
+
let tmpErrorCount = (tmpRequestState.ErrorRecords && tmpRequestState.ErrorRecords.length) || 0;
|
|
85
|
+
let tmpMessage = (tmpErrorCount > 0)
|
|
86
|
+
? `Bulk upsert (detailed) complete -- ${tmpUpsertedCount} records succeeded, ${tmpErrorCount} errored`
|
|
87
|
+
: `Bulk upsert (detailed) complete -- ${tmpUpsertedCount} records processed`;
|
|
88
|
+
this.log.requestCompletedSuccessfully(pRequest, tmpRequestState, tmpMessage);
|
|
89
|
+
return fStageComplete();
|
|
90
|
+
}
|
|
91
|
+
], (pError) =>
|
|
92
|
+
{
|
|
93
|
+
return this.ErrorHandler.handleErrorIfSet(pRequest, tmpRequestState, pResponse, pError, fNext);
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
module.exports = doAPIEndpointUpsertsDetailed;
|
|
@@ -126,6 +126,21 @@ const doUpsert = function(pRecordToUpsert, pRequest, pRequestState, pResponse, f
|
|
|
126
126
|
if (pError)
|
|
127
127
|
{
|
|
128
128
|
tmpRequestState.Record.Error = pError;
|
|
129
|
+
// Surface per-row failures back to the bulk caller. The
|
|
130
|
+
// parent BulkUpsert endpoint reads this array to count
|
|
131
|
+
// errors and signal partial-success to clients via a
|
|
132
|
+
// response header. Without this push, individual upsert
|
|
133
|
+
// failures vanish silently and the bulk response only
|
|
134
|
+
// reflects what survived.
|
|
135
|
+
if (tmpRequestState.ParentRequestState && Array.isArray(tmpRequestState.ParentRequestState.ErrorRecords))
|
|
136
|
+
{
|
|
137
|
+
let tmpErrorMessage = (pError && (pError.message || pError.Error)) ? (pError.message || pError.Error) : String(pError);
|
|
138
|
+
tmpRequestState.ParentRequestState.ErrorRecords.push({
|
|
139
|
+
Record: tmpRequestState.Record,
|
|
140
|
+
Operation: tmpRequestState.Operation || 'Unknown',
|
|
141
|
+
Error: tmpErrorMessage
|
|
142
|
+
});
|
|
143
|
+
}
|
|
129
144
|
}
|
|
130
145
|
return fCallback();
|
|
131
146
|
});
|