@objectql/server 1.3.1 → 1.5.0
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/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/dist/adapters/node.js +157 -23
- package/dist/adapters/node.js.map +1 -1
- package/dist/adapters/rest.d.ts +15 -0
- package/dist/adapters/rest.js +252 -0
- package/dist/adapters/rest.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/metadata.d.ts +1 -1
- package/dist/metadata.js +165 -38
- package/dist/metadata.js.map +1 -1
- package/dist/openapi.js +73 -24
- package/dist/openapi.js.map +1 -1
- package/dist/server.d.ts +8 -0
- package/dist/server.js +75 -15
- package/dist/server.js.map +1 -1
- package/dist/studio.d.ts +5 -0
- package/dist/{console.js → studio.js} +35 -23
- package/dist/studio.js.map +1 -0
- package/dist/types.d.ts +49 -1
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -1
- package/package.json +7 -5
- package/src/adapters/node.ts +166 -26
- package/src/adapters/rest.ts +271 -0
- package/src/index.ts +3 -1
- package/src/metadata.ts +180 -38
- package/src/openapi.ts +80 -32
- package/src/server.ts +119 -16
- package/src/{console.ts → studio.ts} +35 -22
- package/src/types.ts +56 -2
- package/test/rest.test.ts +164 -0
- package/tsconfig.json +5 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/console.d.ts +0 -5
- package/dist/console.js.map +0 -1
|
@@ -33,37 +33,49 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.
|
|
36
|
+
exports.createStudioHandler = createStudioHandler;
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
/**
|
|
40
|
-
* Creates a handler to serve the
|
|
40
|
+
* Creates a handler to serve the Studio UI static files.
|
|
41
41
|
*/
|
|
42
|
-
function
|
|
43
|
-
// Try to find the built console files
|
|
44
|
-
const possiblePaths = [
|
|
45
|
-
path.join(__dirname, '../../console/dist'),
|
|
46
|
-
path.join(process.cwd(), 'node_modules/@objectql/console/dist'),
|
|
47
|
-
path.join(process.cwd(), 'packages/console/dist'),
|
|
48
|
-
];
|
|
42
|
+
function createStudioHandler() {
|
|
49
43
|
let distPath = null;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
// 1. Try to resolve from installed package (Standard way)
|
|
45
|
+
try {
|
|
46
|
+
const studioPkg = require.resolve('@objectql/studio/package.json');
|
|
47
|
+
const candidate = path.join(path.dirname(studioPkg), 'dist');
|
|
48
|
+
if (fs.existsSync(candidate)) {
|
|
49
|
+
distPath = candidate;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
// @objectql/studio might not be installed
|
|
54
|
+
}
|
|
55
|
+
// 2. Fallback for local development (Monorepo)
|
|
56
|
+
if (!distPath) {
|
|
57
|
+
const possiblePaths = [
|
|
58
|
+
path.join(__dirname, '../../studio/dist'),
|
|
59
|
+
path.join(process.cwd(), 'packages/studio/dist'),
|
|
60
|
+
];
|
|
61
|
+
for (const p of possiblePaths) {
|
|
62
|
+
if (fs.existsSync(p)) {
|
|
63
|
+
distPath = p;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
54
66
|
}
|
|
55
67
|
}
|
|
56
68
|
return async (req, res) => {
|
|
57
69
|
if (!distPath) {
|
|
58
|
-
// Return placeholder page if
|
|
70
|
+
// Return placeholder page if studio is not built
|
|
59
71
|
const html = getPlaceholderPage();
|
|
60
72
|
res.setHeader('Content-Type', 'text/html');
|
|
61
73
|
res.statusCode = 200;
|
|
62
74
|
res.end(html);
|
|
63
75
|
return;
|
|
64
76
|
}
|
|
65
|
-
// Parse the URL and remove /
|
|
66
|
-
let urlPath = (req.url || '').replace(/^\/
|
|
77
|
+
// Parse the URL and remove /studio prefix
|
|
78
|
+
let urlPath = (req.url || '').replace(/^\/studio/, '') || '/';
|
|
67
79
|
// Default to index.html for SPA routing
|
|
68
80
|
if (urlPath === '/' || !urlPath.includes('.')) {
|
|
69
81
|
urlPath = '/index.html';
|
|
@@ -119,7 +131,7 @@ function getPlaceholderPage() {
|
|
|
119
131
|
<head>
|
|
120
132
|
<meta charset="UTF-8">
|
|
121
133
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
122
|
-
<title>ObjectQL
|
|
134
|
+
<title>ObjectQL Studio</title>
|
|
123
135
|
<style>
|
|
124
136
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
125
137
|
body {
|
|
@@ -155,19 +167,19 @@ function getPlaceholderPage() {
|
|
|
155
167
|
</head>
|
|
156
168
|
<body>
|
|
157
169
|
<div class="container">
|
|
158
|
-
<h1>ObjectQL
|
|
159
|
-
<p>Web-based admin
|
|
170
|
+
<h1>ObjectQL Studio</h1>
|
|
171
|
+
<p>Web-based admin studio for database management</p>
|
|
160
172
|
<div class="info">
|
|
161
173
|
<p style="margin-bottom: 1rem;">
|
|
162
|
-
The
|
|
174
|
+
The studio is available but needs to be built separately.
|
|
163
175
|
</p>
|
|
164
176
|
<p style="font-size: 1rem;">
|
|
165
|
-
To use the full
|
|
166
|
-
<code>cd packages/
|
|
177
|
+
To use the full studio UI, run:<br>
|
|
178
|
+
<code>cd packages/studio && pnpm run build</code>
|
|
167
179
|
</p>
|
|
168
180
|
</div>
|
|
169
181
|
</div>
|
|
170
182
|
</body>
|
|
171
183
|
</html>`;
|
|
172
184
|
}
|
|
173
|
-
//# sourceMappingURL=
|
|
185
|
+
//# sourceMappingURL=studio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"studio.js","sourceRoot":"","sources":["../src/studio.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standardized error codes for ObjectQL API
|
|
3
|
+
*/
|
|
4
|
+
export declare enum ErrorCode {
|
|
5
|
+
INVALID_REQUEST = "INVALID_REQUEST",
|
|
6
|
+
VALIDATION_ERROR = "VALIDATION_ERROR",
|
|
7
|
+
UNAUTHORIZED = "UNAUTHORIZED",
|
|
8
|
+
FORBIDDEN = "FORBIDDEN",
|
|
9
|
+
NOT_FOUND = "NOT_FOUND",
|
|
10
|
+
CONFLICT = "CONFLICT",
|
|
11
|
+
INTERNAL_ERROR = "INTERNAL_ERROR",
|
|
12
|
+
DATABASE_ERROR = "DATABASE_ERROR",
|
|
13
|
+
RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED"
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* AI context for better logging, debugging, and AI processing
|
|
17
|
+
*/
|
|
18
|
+
export interface AIContext {
|
|
19
|
+
intent?: string;
|
|
20
|
+
natural_language?: string;
|
|
21
|
+
use_case?: string;
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* ObjectQL JSON-RPC style request
|
|
26
|
+
*/
|
|
1
27
|
export interface ObjectQLRequest {
|
|
2
28
|
user?: {
|
|
3
29
|
id: string;
|
|
@@ -7,11 +33,33 @@ export interface ObjectQLRequest {
|
|
|
7
33
|
op: 'find' | 'findOne' | 'create' | 'update' | 'delete' | 'count' | 'action';
|
|
8
34
|
object: string;
|
|
9
35
|
args: any;
|
|
36
|
+
ai_context?: AIContext;
|
|
10
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Error details structure
|
|
40
|
+
*/
|
|
41
|
+
export interface ErrorDetails {
|
|
42
|
+
field?: string;
|
|
43
|
+
reason?: string;
|
|
44
|
+
fields?: Record<string, string>;
|
|
45
|
+
required_permission?: string;
|
|
46
|
+
user_roles?: string[];
|
|
47
|
+
retry_after?: number;
|
|
48
|
+
[key: string]: unknown;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* ObjectQL API response
|
|
52
|
+
*/
|
|
11
53
|
export interface ObjectQLResponse {
|
|
12
54
|
data?: any;
|
|
13
55
|
error?: {
|
|
14
|
-
code: string;
|
|
56
|
+
code: ErrorCode | string;
|
|
15
57
|
message: string;
|
|
58
|
+
details?: ErrorDetails;
|
|
59
|
+
};
|
|
60
|
+
meta?: {
|
|
61
|
+
total?: number;
|
|
62
|
+
page?: number;
|
|
63
|
+
per_page?: number;
|
|
16
64
|
};
|
|
17
65
|
}
|
package/dist/types.js
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
// src/types.ts
|
|
2
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.ErrorCode = void 0;
|
|
5
|
+
/**
|
|
6
|
+
* Standardized error codes for ObjectQL API
|
|
7
|
+
*/
|
|
8
|
+
var ErrorCode;
|
|
9
|
+
(function (ErrorCode) {
|
|
10
|
+
ErrorCode["INVALID_REQUEST"] = "INVALID_REQUEST";
|
|
11
|
+
ErrorCode["VALIDATION_ERROR"] = "VALIDATION_ERROR";
|
|
12
|
+
ErrorCode["UNAUTHORIZED"] = "UNAUTHORIZED";
|
|
13
|
+
ErrorCode["FORBIDDEN"] = "FORBIDDEN";
|
|
14
|
+
ErrorCode["NOT_FOUND"] = "NOT_FOUND";
|
|
15
|
+
ErrorCode["CONFLICT"] = "CONFLICT";
|
|
16
|
+
ErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
|
|
17
|
+
ErrorCode["DATABASE_ERROR"] = "DATABASE_ERROR";
|
|
18
|
+
ErrorCode["RATE_LIMIT_EXCEEDED"] = "RATE_LIMIT_EXCEEDED";
|
|
19
|
+
})(ErrorCode || (exports.ErrorCode = ErrorCode = {}));
|
|
3
20
|
//# sourceMappingURL=types.js.map
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA,eAAe;;;AAEf;;GAEG;AACH,IAAY,SAUX;AAVD,WAAY,SAAS;IACjB,gDAAmC,CAAA;IACnC,kDAAqC,CAAA;IACrC,0CAA6B,CAAA;IAC7B,oCAAuB,CAAA;IACvB,oCAAuB,CAAA;IACvB,kCAAqB,CAAA;IACrB,8CAAiC,CAAA;IACjC,8CAAiC,CAAA;IACjC,wDAA2C,CAAA;AAC/C,CAAC,EAVW,SAAS,yBAAT,SAAS,QAUpB"}
|
package/package.json
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectql/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "HTTP Server Adapter for ObjectQL",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"
|
|
9
|
-
"@objectql/
|
|
8
|
+
"js-yaml": "^4.1.1",
|
|
9
|
+
"@objectql/core": "1.5.0",
|
|
10
|
+
"@objectql/types": "1.5.0"
|
|
10
11
|
},
|
|
11
12
|
"devDependencies": {
|
|
12
|
-
"
|
|
13
|
-
"@types/node": "^20.10.0"
|
|
13
|
+
"@types/js-yaml": "^4.0.9",
|
|
14
|
+
"@types/node": "^20.10.0",
|
|
15
|
+
"typescript": "^5.3.0"
|
|
14
16
|
},
|
|
15
17
|
"scripts": {
|
|
16
18
|
"build": "tsc",
|
package/src/adapters/node.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { IObjectQL } from '@objectql/types';
|
|
2
2
|
import { ObjectQLServer } from '../server';
|
|
3
|
-
import { ObjectQLRequest } from '../types';
|
|
3
|
+
import { ObjectQLRequest, ErrorCode } from '../types';
|
|
4
4
|
import { IncomingMessage, ServerResponse } from 'http';
|
|
5
5
|
import { generateOpenAPI } from '../openapi';
|
|
6
6
|
|
|
@@ -21,50 +21,190 @@ export function createNodeHandler(app: IObjectQL) {
|
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
if (req.method !== 'POST') {
|
|
25
|
-
res.statusCode = 405;
|
|
26
|
-
res.end('Method Not Allowed');
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
24
|
const handleRequest = async (json: any) => {
|
|
31
25
|
try {
|
|
32
|
-
//
|
|
26
|
+
// Determine Operation based on JSON or previously derived info
|
|
33
27
|
const qlReq: ObjectQLRequest = {
|
|
34
28
|
op: json.op,
|
|
35
29
|
object: json.object,
|
|
36
30
|
args: json.args,
|
|
37
|
-
user: json.user
|
|
31
|
+
user: json.user,
|
|
32
|
+
ai_context: json.ai_context
|
|
38
33
|
};
|
|
39
34
|
|
|
40
35
|
const result = await server.handle(qlReq);
|
|
41
36
|
|
|
37
|
+
// Determine HTTP status code based on error
|
|
38
|
+
let statusCode = 200;
|
|
39
|
+
if (result.error) {
|
|
40
|
+
switch (result.error.code) {
|
|
41
|
+
case ErrorCode.INVALID_REQUEST:
|
|
42
|
+
case ErrorCode.VALIDATION_ERROR:
|
|
43
|
+
statusCode = 400;
|
|
44
|
+
break;
|
|
45
|
+
case ErrorCode.UNAUTHORIZED:
|
|
46
|
+
statusCode = 401;
|
|
47
|
+
break;
|
|
48
|
+
case ErrorCode.FORBIDDEN:
|
|
49
|
+
statusCode = 403;
|
|
50
|
+
break;
|
|
51
|
+
case ErrorCode.NOT_FOUND:
|
|
52
|
+
statusCode = 404;
|
|
53
|
+
break;
|
|
54
|
+
case ErrorCode.CONFLICT:
|
|
55
|
+
statusCode = 409;
|
|
56
|
+
break;
|
|
57
|
+
case ErrorCode.RATE_LIMIT_EXCEEDED:
|
|
58
|
+
statusCode = 429;
|
|
59
|
+
break;
|
|
60
|
+
default:
|
|
61
|
+
statusCode = 500;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
42
65
|
res.setHeader('Content-Type', 'application/json');
|
|
43
|
-
res.statusCode =
|
|
66
|
+
res.statusCode = statusCode;
|
|
44
67
|
res.end(JSON.stringify(result));
|
|
45
68
|
} catch (e) {
|
|
69
|
+
console.error(e);
|
|
46
70
|
res.statusCode = 500;
|
|
47
|
-
res.end(JSON.stringify({
|
|
71
|
+
res.end(JSON.stringify({
|
|
72
|
+
error: {
|
|
73
|
+
code: ErrorCode.INTERNAL_ERROR,
|
|
74
|
+
message: 'Internal Server Error'
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
48
77
|
}
|
|
49
78
|
};
|
|
50
79
|
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
80
|
+
// Parse URL
|
|
81
|
+
const urlObj = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
82
|
+
const pathName = urlObj.pathname;
|
|
83
|
+
const method = req.method;
|
|
84
|
+
|
|
85
|
+
// 1. JSON-RPC: POST /api/objectql
|
|
86
|
+
if (pathName === '/api/objectql' && method === 'POST') {
|
|
87
|
+
await processBody(req, async (json) => {
|
|
88
|
+
await handleRequest(json);
|
|
89
|
+
}, res);
|
|
90
|
+
return;
|
|
55
91
|
}
|
|
56
92
|
|
|
57
|
-
// 2.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
93
|
+
// 2. REST API: /api/data/:object and /api/data/:object/:id
|
|
94
|
+
// Regex to match /api/data/objectName(/id)?
|
|
95
|
+
const restMatch = pathName.match(/^\/api\/data\/([^/]+)(?:\/(.+))?$/);
|
|
96
|
+
|
|
97
|
+
if (restMatch) {
|
|
98
|
+
const objectName = restMatch[1];
|
|
99
|
+
const id = restMatch[2];
|
|
100
|
+
const query = Object.fromEntries(urlObj.searchParams.entries());
|
|
101
|
+
|
|
102
|
+
if (method === 'GET') {
|
|
103
|
+
// GET /api/data/:object/:id -> findOne
|
|
104
|
+
if (id) {
|
|
105
|
+
await handleRequest({
|
|
106
|
+
op: 'findOne',
|
|
107
|
+
object: objectName,
|
|
108
|
+
args: { filters: [['_id', '=', id]] } // Assuming _id or id mapping
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// GET /api/data/:object -> find (List)
|
|
112
|
+
else {
|
|
113
|
+
// Parse standard params
|
|
114
|
+
const args: any = {};
|
|
115
|
+
if (query.fields) args.fields = (query.fields as string).split(',');
|
|
116
|
+
if (query.top) args.limit = parseInt(query.top as string);
|
|
117
|
+
if (query.skip) args.skip = parseInt(query.skip as string);
|
|
118
|
+
if (query.filter) {
|
|
119
|
+
try {
|
|
120
|
+
args.filters = JSON.parse(query.filter as string);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
// ignore invalid filter json
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
await handleRequest({ op: 'find', object: objectName, args });
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (method === 'POST' && !id) {
|
|
131
|
+
// POST /api/data/:object -> create
|
|
132
|
+
await processBody(req, async (body) => {
|
|
133
|
+
await handleRequest({
|
|
134
|
+
op: 'create',
|
|
135
|
+
object: objectName,
|
|
136
|
+
args: body.data || body // Support enclosed in data or flat
|
|
137
|
+
});
|
|
138
|
+
}, res);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (method === 'PATCH' && id) {
|
|
143
|
+
// PATCH /api/data/:object/:id -> update
|
|
144
|
+
await processBody(req, async (body) => {
|
|
145
|
+
await handleRequest({
|
|
146
|
+
op: 'update',
|
|
147
|
+
object: objectName,
|
|
148
|
+
args: {
|
|
149
|
+
id: id,
|
|
150
|
+
data: body.data || body
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}, res);
|
|
154
|
+
return;
|
|
67
155
|
}
|
|
68
|
-
|
|
156
|
+
|
|
157
|
+
if (method === 'DELETE' && id) {
|
|
158
|
+
// DELETE /api/data/:object/:id -> delete
|
|
159
|
+
await handleRequest({
|
|
160
|
+
op: 'delete',
|
|
161
|
+
object: objectName,
|
|
162
|
+
args: { id: id }
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Fallback or 404
|
|
169
|
+
if (req.method === 'POST') {
|
|
170
|
+
// Fallback for root POSTs if people forget /api/objectql but send to /api something
|
|
171
|
+
await processBody(req, handleRequest, res);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Special case for root: since we accept POST / (RPC), correct response for GET / is 405
|
|
176
|
+
if (pathName === '/') {
|
|
177
|
+
res.setHeader('Allow', 'POST');
|
|
178
|
+
res.statusCode = 405;
|
|
179
|
+
res.end(JSON.stringify({ error: { code: ErrorCode.INVALID_REQUEST, message: 'Method Not Allowed. Use POST for JSON-RPC.' } }));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
res.statusCode = 404;
|
|
184
|
+
res.end(JSON.stringify({ error: { code: ErrorCode.NOT_FOUND, message: 'Not Found' } }));
|
|
69
185
|
};
|
|
70
186
|
}
|
|
187
|
+
|
|
188
|
+
// Helper to process body
|
|
189
|
+
async function processBody(req: IncomingMessage & { body?: any }, callback: (json: any) => Promise<void>, res: ServerResponse) {
|
|
190
|
+
if (req.body && typeof req.body === 'object') {
|
|
191
|
+
return callback(req.body);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let body = '';
|
|
195
|
+
req.on('data', chunk => body += chunk);
|
|
196
|
+
req.on('end', async () => {
|
|
197
|
+
try {
|
|
198
|
+
const json = body ? JSON.parse(body) : {};
|
|
199
|
+
await callback(json);
|
|
200
|
+
} catch (e) {
|
|
201
|
+
res.statusCode = 400;
|
|
202
|
+
res.end(JSON.stringify({
|
|
203
|
+
error: {
|
|
204
|
+
code: 'INVALID_JSON',
|
|
205
|
+
message: 'Invalid JSON body'
|
|
206
|
+
}
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|