@objectql/server 1.3.1 → 1.4.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 +19 -0
- package/LICENSE +21 -0
- package/dist/adapters/node.js +69 -9
- 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.js +152 -11
- package/dist/metadata.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 +72 -11
- package/src/adapters/rest.ts +271 -0
- package/src/index.ts +3 -1
- package/src/metadata.ts +166 -11
- 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.tsbuildinfo +1 -1
- package/dist/console.d.ts +0 -5
- package/dist/console.js.map +0 -1
package/src/server.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IObjectQL, ObjectQLContext } from '@objectql/types';
|
|
2
|
-
import { ObjectQLRequest, ObjectQLResponse } from './types';
|
|
2
|
+
import { ObjectQLRequest, ObjectQLResponse, ErrorCode } from './types';
|
|
3
3
|
|
|
4
4
|
export class ObjectQLServer {
|
|
5
5
|
constructor(private app: IObjectQL) {}
|
|
@@ -10,6 +10,17 @@ export class ObjectQLServer {
|
|
|
10
10
|
*/
|
|
11
11
|
async handle(req: ObjectQLRequest): Promise<ObjectQLResponse> {
|
|
12
12
|
try {
|
|
13
|
+
// Log AI context if provided
|
|
14
|
+
if (req.ai_context) {
|
|
15
|
+
console.log('[ObjectQL AI Context]', {
|
|
16
|
+
object: req.object,
|
|
17
|
+
op: req.op,
|
|
18
|
+
intent: req.ai_context.intent,
|
|
19
|
+
natural_language: req.ai_context.natural_language,
|
|
20
|
+
use_case: req.ai_context.use_case
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
// 1. Build Context
|
|
14
25
|
// TODO: integrate with real session/auth
|
|
15
26
|
const contextOptions = {
|
|
@@ -23,10 +34,23 @@ export class ObjectQLServer {
|
|
|
23
34
|
// We need to cast or fix the interface. Assuming 'app' behaves like ObjectQL class.
|
|
24
35
|
const app = this.app as any;
|
|
25
36
|
if (typeof app.createContext !== 'function') {
|
|
26
|
-
|
|
37
|
+
return this.errorResponse(
|
|
38
|
+
ErrorCode.INTERNAL_ERROR,
|
|
39
|
+
"The provided ObjectQL instance does not support createContext."
|
|
40
|
+
);
|
|
27
41
|
}
|
|
28
42
|
|
|
29
43
|
const ctx: ObjectQLContext = app.createContext(contextOptions);
|
|
44
|
+
|
|
45
|
+
// Validate object exists
|
|
46
|
+
const objectConfig = app.getObject(req.object);
|
|
47
|
+
if (!objectConfig) {
|
|
48
|
+
return this.errorResponse(
|
|
49
|
+
ErrorCode.NOT_FOUND,
|
|
50
|
+
`Object '${req.object}' not found`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
30
54
|
const repo = ctx.object(req.object);
|
|
31
55
|
|
|
32
56
|
let result: any;
|
|
@@ -36,7 +60,12 @@ export class ObjectQLServer {
|
|
|
36
60
|
result = await repo.find(req.args);
|
|
37
61
|
break;
|
|
38
62
|
case 'findOne':
|
|
39
|
-
|
|
63
|
+
// Support both string ID and query object
|
|
64
|
+
if (typeof req.args === 'string') {
|
|
65
|
+
result = await repo.findOne({ filters: [['_id', '=', req.args]] });
|
|
66
|
+
} else {
|
|
67
|
+
result = await repo.findOne(req.args);
|
|
68
|
+
}
|
|
40
69
|
break;
|
|
41
70
|
case 'create':
|
|
42
71
|
result = await repo.create(req.args);
|
|
@@ -46,33 +75,107 @@ export class ObjectQLServer {
|
|
|
46
75
|
break;
|
|
47
76
|
case 'delete':
|
|
48
77
|
result = await repo.delete(req.args.id);
|
|
78
|
+
if (!result) {
|
|
79
|
+
return this.errorResponse(
|
|
80
|
+
ErrorCode.NOT_FOUND,
|
|
81
|
+
`Record with id '${req.args.id}' not found for delete`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
// Return standardized delete response on success
|
|
85
|
+
result = { id: req.args.id, deleted: true };
|
|
49
86
|
break;
|
|
50
87
|
case 'count':
|
|
51
88
|
result = await repo.count(req.args);
|
|
52
89
|
break;
|
|
53
90
|
case 'action':
|
|
54
|
-
//
|
|
55
|
-
// usually it's on the app level or via special method.
|
|
56
|
-
// For now, let's assume app.executeAction
|
|
91
|
+
// Map generic args to ActionContext
|
|
57
92
|
result = await app.executeAction(req.object, req.args.action, {
|
|
58
|
-
...ctx, // Pass context
|
|
59
|
-
|
|
93
|
+
...ctx, // Pass context (user, etc.)
|
|
94
|
+
id: req.args.id,
|
|
95
|
+
input: req.args.input || req.args.params // Support both for convenience
|
|
60
96
|
});
|
|
61
97
|
break;
|
|
62
98
|
default:
|
|
63
|
-
|
|
99
|
+
return this.errorResponse(
|
|
100
|
+
ErrorCode.INVALID_REQUEST,
|
|
101
|
+
`Unknown operation: ${req.op}`
|
|
102
|
+
);
|
|
64
103
|
}
|
|
65
104
|
|
|
66
105
|
return { data: result };
|
|
67
106
|
|
|
68
107
|
} catch (e: any) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
108
|
+
return this.handleError(e);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Handle errors and convert them to appropriate error responses
|
|
114
|
+
*/
|
|
115
|
+
private handleError(error: any): ObjectQLResponse {
|
|
116
|
+
console.error('[ObjectQL Server] Error:', error);
|
|
117
|
+
|
|
118
|
+
// Handle validation errors
|
|
119
|
+
if (error.name === 'ValidationError' || error.code === 'VALIDATION_ERROR') {
|
|
120
|
+
return this.errorResponse(
|
|
121
|
+
ErrorCode.VALIDATION_ERROR,
|
|
122
|
+
'Validation failed',
|
|
123
|
+
{ fields: error.fields || error.details }
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Handle permission errors
|
|
128
|
+
if (error.name === 'PermissionError' || error.code === 'FORBIDDEN') {
|
|
129
|
+
return this.errorResponse(
|
|
130
|
+
ErrorCode.FORBIDDEN,
|
|
131
|
+
error.message || 'You do not have permission to access this resource',
|
|
132
|
+
error.details
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Handle not found errors
|
|
137
|
+
if (error.name === 'NotFoundError' || error.code === 'NOT_FOUND') {
|
|
138
|
+
return this.errorResponse(
|
|
139
|
+
ErrorCode.NOT_FOUND,
|
|
140
|
+
error.message || 'Resource not found'
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Handle conflict errors (e.g., unique constraint violations)
|
|
145
|
+
if (error.name === 'ConflictError' || error.code === 'CONFLICT') {
|
|
146
|
+
return this.errorResponse(
|
|
147
|
+
ErrorCode.CONFLICT,
|
|
148
|
+
error.message || 'Resource conflict',
|
|
149
|
+
error.details
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Handle database errors
|
|
154
|
+
if (error.name === 'DatabaseError' || error.code?.startsWith('DB_')) {
|
|
155
|
+
return this.errorResponse(
|
|
156
|
+
ErrorCode.DATABASE_ERROR,
|
|
157
|
+
'Database operation failed',
|
|
158
|
+
{ originalError: error.message }
|
|
159
|
+
);
|
|
76
160
|
}
|
|
161
|
+
|
|
162
|
+
// Default to internal error
|
|
163
|
+
return this.errorResponse(
|
|
164
|
+
ErrorCode.INTERNAL_ERROR,
|
|
165
|
+
error.message || 'An error occurred'
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Create a standardized error response
|
|
171
|
+
*/
|
|
172
|
+
private errorResponse(code: ErrorCode, message: string, details?: any): ObjectQLResponse {
|
|
173
|
+
return {
|
|
174
|
+
error: {
|
|
175
|
+
code,
|
|
176
|
+
message,
|
|
177
|
+
details
|
|
178
|
+
}
|
|
179
|
+
};
|
|
77
180
|
}
|
|
78
181
|
}
|
|
@@ -3,27 +3,40 @@ import * as fs from 'fs';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Creates a handler to serve the
|
|
6
|
+
* Creates a handler to serve the Studio UI static files.
|
|
7
7
|
*/
|
|
8
|
-
export function
|
|
9
|
-
// Try to find the built console files
|
|
10
|
-
const possiblePaths = [
|
|
11
|
-
path.join(__dirname, '../../console/dist'),
|
|
12
|
-
path.join(process.cwd(), 'node_modules/@objectql/console/dist'),
|
|
13
|
-
path.join(process.cwd(), 'packages/console/dist'),
|
|
14
|
-
];
|
|
15
|
-
|
|
8
|
+
export function createStudioHandler() {
|
|
16
9
|
let distPath: string | null = null;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
|
|
11
|
+
// 1. Try to resolve from installed package (Standard way)
|
|
12
|
+
try {
|
|
13
|
+
const studioPkg = require.resolve('@objectql/studio/package.json');
|
|
14
|
+
const candidate = path.join(path.dirname(studioPkg), 'dist');
|
|
15
|
+
if (fs.existsSync(candidate)) {
|
|
16
|
+
distPath = candidate;
|
|
17
|
+
}
|
|
18
|
+
} catch (e) {
|
|
19
|
+
// @objectql/studio might not be installed
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 2. Fallback for local development (Monorepo)
|
|
23
|
+
if (!distPath) {
|
|
24
|
+
const possiblePaths = [
|
|
25
|
+
path.join(__dirname, '../../studio/dist'),
|
|
26
|
+
path.join(process.cwd(), 'packages/studio/dist'),
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
for (const p of possiblePaths) {
|
|
30
|
+
if (fs.existsSync(p)) {
|
|
31
|
+
distPath = p;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
21
34
|
}
|
|
22
35
|
}
|
|
23
36
|
|
|
24
37
|
return async (req: IncomingMessage, res: ServerResponse) => {
|
|
25
38
|
if (!distPath) {
|
|
26
|
-
// Return placeholder page if
|
|
39
|
+
// Return placeholder page if studio is not built
|
|
27
40
|
const html = getPlaceholderPage();
|
|
28
41
|
res.setHeader('Content-Type', 'text/html');
|
|
29
42
|
res.statusCode = 200;
|
|
@@ -31,8 +44,8 @@ export function createConsoleHandler() {
|
|
|
31
44
|
return;
|
|
32
45
|
}
|
|
33
46
|
|
|
34
|
-
// Parse the URL and remove /
|
|
35
|
-
let urlPath = (req.url || '').replace(/^\/
|
|
47
|
+
// Parse the URL and remove /studio prefix
|
|
48
|
+
let urlPath = (req.url || '').replace(/^\/studio/, '') || '/';
|
|
36
49
|
|
|
37
50
|
// Default to index.html for SPA routing
|
|
38
51
|
if (urlPath === '/' || !urlPath.includes('.')) {
|
|
@@ -97,7 +110,7 @@ function getPlaceholderPage(): string {
|
|
|
97
110
|
<head>
|
|
98
111
|
<meta charset="UTF-8">
|
|
99
112
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
100
|
-
<title>ObjectQL
|
|
113
|
+
<title>ObjectQL Studio</title>
|
|
101
114
|
<style>
|
|
102
115
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
103
116
|
body {
|
|
@@ -133,15 +146,15 @@ function getPlaceholderPage(): string {
|
|
|
133
146
|
</head>
|
|
134
147
|
<body>
|
|
135
148
|
<div class="container">
|
|
136
|
-
<h1>ObjectQL
|
|
137
|
-
<p>Web-based admin
|
|
149
|
+
<h1>ObjectQL Studio</h1>
|
|
150
|
+
<p>Web-based admin studio for database management</p>
|
|
138
151
|
<div class="info">
|
|
139
152
|
<p style="margin-bottom: 1rem;">
|
|
140
|
-
The
|
|
153
|
+
The studio is available but needs to be built separately.
|
|
141
154
|
</p>
|
|
142
155
|
<p style="font-size: 1rem;">
|
|
143
|
-
To use the full
|
|
144
|
-
<code>cd packages/
|
|
156
|
+
To use the full studio UI, run:<br>
|
|
157
|
+
<code>cd packages/studio && pnpm run build</code>
|
|
145
158
|
</p>
|
|
146
159
|
</div>
|
|
147
160
|
</div>
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,33 @@
|
|
|
1
1
|
// src/types.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Standardized error codes for ObjectQL API
|
|
5
|
+
*/
|
|
6
|
+
export enum ErrorCode {
|
|
7
|
+
INVALID_REQUEST = 'INVALID_REQUEST',
|
|
8
|
+
VALIDATION_ERROR = 'VALIDATION_ERROR',
|
|
9
|
+
UNAUTHORIZED = 'UNAUTHORIZED',
|
|
10
|
+
FORBIDDEN = 'FORBIDDEN',
|
|
11
|
+
NOT_FOUND = 'NOT_FOUND',
|
|
12
|
+
CONFLICT = 'CONFLICT',
|
|
13
|
+
INTERNAL_ERROR = 'INTERNAL_ERROR',
|
|
14
|
+
DATABASE_ERROR = 'DATABASE_ERROR',
|
|
15
|
+
RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* AI context for better logging, debugging, and AI processing
|
|
20
|
+
*/
|
|
21
|
+
export interface AIContext {
|
|
22
|
+
intent?: string;
|
|
23
|
+
natural_language?: string;
|
|
24
|
+
use_case?: string;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* ObjectQL JSON-RPC style request
|
|
30
|
+
*/
|
|
2
31
|
export interface ObjectQLRequest {
|
|
3
32
|
// Identity provided by the framework adapter (e.g. from session)
|
|
4
33
|
user?: {
|
|
@@ -13,12 +42,37 @@ export interface ObjectQLRequest {
|
|
|
13
42
|
|
|
14
43
|
// Arguments
|
|
15
44
|
args: any;
|
|
45
|
+
|
|
46
|
+
// Optional AI context for explainability
|
|
47
|
+
ai_context?: AIContext;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Error details structure
|
|
52
|
+
*/
|
|
53
|
+
export interface ErrorDetails {
|
|
54
|
+
field?: string;
|
|
55
|
+
reason?: string;
|
|
56
|
+
fields?: Record<string, string>;
|
|
57
|
+
required_permission?: string;
|
|
58
|
+
user_roles?: string[];
|
|
59
|
+
retry_after?: number;
|
|
60
|
+
[key: string]: unknown;
|
|
16
61
|
}
|
|
17
62
|
|
|
63
|
+
/**
|
|
64
|
+
* ObjectQL API response
|
|
65
|
+
*/
|
|
18
66
|
export interface ObjectQLResponse {
|
|
19
67
|
data?: any;
|
|
20
68
|
error?: {
|
|
21
|
-
code: string;
|
|
69
|
+
code: ErrorCode | string;
|
|
22
70
|
message: string;
|
|
23
|
-
|
|
71
|
+
details?: ErrorDetails;
|
|
72
|
+
};
|
|
73
|
+
meta?: {
|
|
74
|
+
total?: number;
|
|
75
|
+
page?: number;
|
|
76
|
+
per_page?: number;
|
|
77
|
+
};
|
|
24
78
|
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import request from 'supertest';
|
|
2
|
+
import { createServer } from 'http';
|
|
3
|
+
import { ObjectQL } from '@objectql/core';
|
|
4
|
+
import { createRESTHandler } from '../src/adapters/rest';
|
|
5
|
+
import { Driver } from '@objectql/types';
|
|
6
|
+
|
|
7
|
+
// Simple Mock Driver
|
|
8
|
+
class MockDriver implements Driver {
|
|
9
|
+
private data: Record<string, any[]> = {
|
|
10
|
+
user: [
|
|
11
|
+
{ _id: '1', name: 'Alice', email: 'alice@example.com' },
|
|
12
|
+
{ _id: '2', name: 'Bob', email: 'bob@example.com' }
|
|
13
|
+
]
|
|
14
|
+
};
|
|
15
|
+
private nextId = 3;
|
|
16
|
+
|
|
17
|
+
async init() {}
|
|
18
|
+
|
|
19
|
+
async find(objectName: string, query: any) {
|
|
20
|
+
return this.data[objectName] || [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async findOne(objectName: string, id: string | number, query?: any, options?: any) {
|
|
24
|
+
const items = this.data[objectName] || [];
|
|
25
|
+
if (id !== undefined && id !== null) {
|
|
26
|
+
const found = items.find(item => item._id === String(id));
|
|
27
|
+
return found || null;
|
|
28
|
+
}
|
|
29
|
+
return items[0] || null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async create(objectName: string, data: any) {
|
|
33
|
+
const newItem = { _id: String(this.nextId++), ...data };
|
|
34
|
+
if (!this.data[objectName]) {
|
|
35
|
+
this.data[objectName] = [];
|
|
36
|
+
}
|
|
37
|
+
this.data[objectName].push(newItem);
|
|
38
|
+
return newItem;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async update(objectName: string, id: string, data: any) {
|
|
42
|
+
const items = this.data[objectName] || [];
|
|
43
|
+
const index = items.findIndex(item => item._id === id);
|
|
44
|
+
if (index >= 0) {
|
|
45
|
+
this.data[objectName][index] = { ...items[index], ...data };
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async delete(objectName: string, id: string) {
|
|
52
|
+
const items = this.data[objectName] || [];
|
|
53
|
+
const index = items.findIndex(item => item._id === id);
|
|
54
|
+
if (index >= 0) {
|
|
55
|
+
this.data[objectName].splice(index, 1);
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async count(objectName: string, query: any) {
|
|
62
|
+
return (this.data[objectName] || []).length;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async execute(sql: string) {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('REST API Adapter', () => {
|
|
69
|
+
let app: ObjectQL;
|
|
70
|
+
let server: any;
|
|
71
|
+
let handler: any;
|
|
72
|
+
|
|
73
|
+
beforeAll(async () => {
|
|
74
|
+
app = new ObjectQL({
|
|
75
|
+
datasources: {
|
|
76
|
+
default: new MockDriver()
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Manual schema registration
|
|
81
|
+
app.metadata.register('object', {
|
|
82
|
+
type: 'object',
|
|
83
|
+
id: 'user',
|
|
84
|
+
content: {
|
|
85
|
+
name: 'user',
|
|
86
|
+
fields: {
|
|
87
|
+
name: { type: 'text' },
|
|
88
|
+
email: { type: 'email' }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Create handler and server once for all tests
|
|
94
|
+
handler = createRESTHandler(app);
|
|
95
|
+
server = createServer(handler);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should handle GET /api/data/:object - List records', async () => {
|
|
99
|
+
const response = await request(server)
|
|
100
|
+
.get('/api/data/user')
|
|
101
|
+
.set('Accept', 'application/json');
|
|
102
|
+
|
|
103
|
+
expect(response.status).toBe(200);
|
|
104
|
+
expect(response.body.data).toHaveLength(2);
|
|
105
|
+
expect(response.body.data[0].name).toBe('Alice');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should handle GET /api/data/:object/:id - Get single record', async () => {
|
|
109
|
+
const response = await request(server)
|
|
110
|
+
.get('/api/data/user/1')
|
|
111
|
+
.set('Accept', 'application/json');
|
|
112
|
+
|
|
113
|
+
expect(response.status).toBe(200);
|
|
114
|
+
expect(response.body.data.name).toBe('Alice');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should handle POST /api/data/:object - Create record', async () => {
|
|
118
|
+
const response = await request(server)
|
|
119
|
+
.post('/api/data/user')
|
|
120
|
+
.send({ name: 'Charlie', email: 'charlie@example.com' })
|
|
121
|
+
.set('Accept', 'application/json');
|
|
122
|
+
|
|
123
|
+
expect(response.status).toBe(201);
|
|
124
|
+
expect(response.body.data.name).toBe('Charlie');
|
|
125
|
+
expect(response.body.data._id).toBeDefined();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should handle PUT /api/data/:object/:id - Update record', async () => {
|
|
129
|
+
const response = await request(server)
|
|
130
|
+
.put('/api/data/user/1')
|
|
131
|
+
.send({ name: 'Alice Updated' })
|
|
132
|
+
.set('Accept', 'application/json');
|
|
133
|
+
|
|
134
|
+
expect(response.status).toBe(200);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should handle DELETE /api/data/:object/:id - Delete record', async () => {
|
|
138
|
+
const response = await request(server)
|
|
139
|
+
.delete('/api/data/user/1')
|
|
140
|
+
.set('Accept', 'application/json');
|
|
141
|
+
|
|
142
|
+
expect(response.status).toBe(200);
|
|
143
|
+
expect(response.body.data.deleted).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should return 404 for non-existent object', async () => {
|
|
147
|
+
const response = await request(server)
|
|
148
|
+
.get('/api/data/nonexistent')
|
|
149
|
+
.set('Accept', 'application/json');
|
|
150
|
+
|
|
151
|
+
expect(response.status).toBe(404);
|
|
152
|
+
expect(response.body.error.code).toBe('NOT_FOUND');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should return 400 for update without ID', async () => {
|
|
156
|
+
const response = await request(server)
|
|
157
|
+
.put('/api/data/user')
|
|
158
|
+
.send({ name: 'Test' })
|
|
159
|
+
.set('Accept', 'application/json');
|
|
160
|
+
|
|
161
|
+
expect(response.status).toBe(400);
|
|
162
|
+
expect(response.body.error.code).toBe('INVALID_REQUEST');
|
|
163
|
+
});
|
|
164
|
+
});
|