@objectql/server 1.8.3 → 1.8.4
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 +9 -0
- package/dist/adapters/node.d.ts +3 -1
- package/dist/adapters/node.js +35 -29
- package/dist/adapters/node.js.map +1 -1
- package/dist/adapters/rest.d.ts +20 -10
- package/dist/adapters/rest.js +39 -31
- package/dist/adapters/rest.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/metadata.d.ts +12 -2
- package/dist/metadata.js +31 -23
- package/dist/metadata.js.map +1 -1
- package/dist/openapi.d.ts +2 -2
- package/dist/openapi.js +6 -4
- package/dist/openapi.js.map +1 -1
- package/dist/utils.d.ts +15 -0
- package/dist/utils.js +24 -0
- package/dist/utils.js.map +1 -0
- package/package.json +3 -3
- package/src/adapters/node.ts +26 -19
- package/src/adapters/rest.ts +34 -19
- package/src/index.ts +1 -1
- package/src/metadata.ts +29 -14
- package/src/openapi.ts +6 -5
- package/src/utils.ts +21 -0
- package/test/custom-routes.test.ts +297 -0
- package/test/metadata.test.ts +259 -0
- package/test/openapi.test.ts +354 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/studio.d.ts +0 -5
- package/dist/studio.js +0 -186
- package/dist/studio.js.map +0 -1
- package/src/studio.ts +0 -164
package/src/metadata.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { IObjectQL } from '@objectql/types';
|
|
1
|
+
import { IObjectQL, ApiRouteConfig, resolveApiRoutes } from '@objectql/types';
|
|
2
2
|
import { IncomingMessage, ServerResponse } from 'http';
|
|
3
3
|
import { ErrorCode } from './types';
|
|
4
|
+
import { escapeRegexPath } from './utils';
|
|
4
5
|
|
|
5
6
|
function readBody(req: IncomingMessage): Promise<any> {
|
|
6
7
|
return new Promise((resolve, reject) => {
|
|
@@ -18,11 +19,24 @@ function readBody(req: IncomingMessage): Promise<any> {
|
|
|
18
19
|
});
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Options for createMetadataHandler
|
|
24
|
+
*/
|
|
25
|
+
export interface MetadataHandlerOptions {
|
|
26
|
+
/** Custom API route configuration */
|
|
27
|
+
routes?: ApiRouteConfig;
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
/**
|
|
22
31
|
* Creates a handler for metadata endpoints.
|
|
23
32
|
* These endpoints expose information about registered objects and other metadata.
|
|
33
|
+
*
|
|
34
|
+
* @param app - ObjectQL application instance
|
|
35
|
+
* @param options - Optional configuration including custom routes
|
|
24
36
|
*/
|
|
25
|
-
export function createMetadataHandler(app: IObjectQL) {
|
|
37
|
+
export function createMetadataHandler(app: IObjectQL, options?: MetadataHandlerOptions) {
|
|
38
|
+
const routes = resolveApiRoutes(options?.routes);
|
|
39
|
+
const metadataPath = routes.metadata;
|
|
26
40
|
return async (req: IncomingMessage, res: ServerResponse) => {
|
|
27
41
|
// Parse the URL
|
|
28
42
|
const url = req.url || '';
|
|
@@ -52,13 +66,14 @@ export function createMetadataHandler(app: IObjectQL) {
|
|
|
52
66
|
};
|
|
53
67
|
|
|
54
68
|
// ---------------------------------------------------------
|
|
55
|
-
// 1. List Entries (GET
|
|
69
|
+
// 1. List Entries (GET {metadataPath}/:type)
|
|
56
70
|
// ---------------------------------------------------------
|
|
57
71
|
|
|
58
|
-
// Generic List:
|
|
59
|
-
// Also handles legacy
|
|
60
|
-
const
|
|
61
|
-
const
|
|
72
|
+
// Generic List: {metadataPath}/:type
|
|
73
|
+
// Also handles legacy {metadataPath} (defaults to objects)
|
|
74
|
+
const escapedPath = escapeRegexPath(metadataPath);
|
|
75
|
+
const listMatch = url.match(new RegExp(`^${escapedPath}/([^/]+)$`));
|
|
76
|
+
const isRootMetadata = url === metadataPath;
|
|
62
77
|
|
|
63
78
|
if (method === 'GET' && (listMatch || isRootMetadata)) {
|
|
64
79
|
let type = isRootMetadata ? 'object' : listMatch![1];
|
|
@@ -85,10 +100,10 @@ export function createMetadataHandler(app: IObjectQL) {
|
|
|
85
100
|
}
|
|
86
101
|
|
|
87
102
|
// ---------------------------------------------------------
|
|
88
|
-
// 2. Get Single Entry (GET
|
|
103
|
+
// 2. Get Single Entry (GET {metadataPath}/:type/:id)
|
|
89
104
|
// ---------------------------------------------------------
|
|
90
105
|
|
|
91
|
-
const detailMatch = url.match(
|
|
106
|
+
const detailMatch = url.match(new RegExp(`^${escapedPath}/([^/]+)/([^/\\?]+)$`));
|
|
92
107
|
|
|
93
108
|
if (method === 'GET' && detailMatch) {
|
|
94
109
|
let [, type, id] = detailMatch;
|
|
@@ -127,7 +142,7 @@ export function createMetadataHandler(app: IObjectQL) {
|
|
|
127
142
|
}
|
|
128
143
|
|
|
129
144
|
// ---------------------------------------------------------
|
|
130
|
-
// 3. Update Entry (POST/PUT
|
|
145
|
+
// 3. Update Entry (POST/PUT {metadataPath}/:type/:id)
|
|
131
146
|
// ---------------------------------------------------------
|
|
132
147
|
if ((method === 'POST' || method === 'PUT') && detailMatch) {
|
|
133
148
|
let [, type, id] = detailMatch;
|
|
@@ -152,9 +167,9 @@ export function createMetadataHandler(app: IObjectQL) {
|
|
|
152
167
|
// 4. Object Sub-resources (Fields, Actions)
|
|
153
168
|
// ---------------------------------------------------------
|
|
154
169
|
|
|
155
|
-
// GET /
|
|
170
|
+
// GET {metadataPath}/object/:name/fields/:field
|
|
156
171
|
// Legacy path support.
|
|
157
|
-
const fieldMatch = url.match(
|
|
172
|
+
const fieldMatch = url.match(new RegExp(`^${escapedPath}/(?:objects|object)/([^/]+)/fields/([^/\\?]+)$`));
|
|
158
173
|
if (method === 'GET' && fieldMatch) {
|
|
159
174
|
const [, objectName, fieldName] = fieldMatch;
|
|
160
175
|
const metadata = app.getObject(objectName);
|
|
@@ -180,8 +195,8 @@ export function createMetadataHandler(app: IObjectQL) {
|
|
|
180
195
|
});
|
|
181
196
|
}
|
|
182
197
|
|
|
183
|
-
// GET /
|
|
184
|
-
const actionsMatch = url.match(
|
|
198
|
+
// GET {metadataPath}/object/:name/actions
|
|
199
|
+
const actionsMatch = url.match(new RegExp(`^${escapedPath}/(?:objects|object)/([^/]+)/actions$`));
|
|
185
200
|
if (method === 'GET' && actionsMatch) {
|
|
186
201
|
const [, objectName] = actionsMatch;
|
|
187
202
|
const metadata = app.getObject(objectName);
|
package/src/openapi.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IObjectQL, ObjectConfig,
|
|
1
|
+
import { IObjectQL, ObjectConfig, FieldConfig, ApiRouteConfig, resolveApiRoutes } from '@objectql/types';
|
|
2
2
|
|
|
3
3
|
interface OpenAPISchema {
|
|
4
4
|
openapi: string;
|
|
@@ -12,16 +12,17 @@ interface OpenAPISchema {
|
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function generateOpenAPI(app: IObjectQL): OpenAPISchema {
|
|
15
|
+
export function generateOpenAPI(app: IObjectQL, routeConfig?: ApiRouteConfig): OpenAPISchema {
|
|
16
16
|
const registry = (app as any).metadata; // Direct access or via interface
|
|
17
17
|
const objects = registry.list('object') as ObjectConfig[];
|
|
18
|
+
const routes = resolveApiRoutes(routeConfig);
|
|
18
19
|
|
|
19
20
|
const paths: Record<string, any> = {};
|
|
20
21
|
const schemas: Record<string, any> = {};
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
// 1. JSON-RPC Endpoint
|
|
24
|
-
paths[
|
|
25
|
+
paths[routes.rpc] = {
|
|
25
26
|
post: {
|
|
26
27
|
summary: 'JSON-RPC Entry Point',
|
|
27
28
|
description: 'Execute any ObjectQL operation via a JSON body.',
|
|
@@ -72,9 +73,9 @@ export function generateOpenAPI(app: IObjectQL): OpenAPISchema {
|
|
|
72
73
|
// 3. REST API Paths
|
|
73
74
|
for (const obj of objects) {
|
|
74
75
|
const name = obj.name;
|
|
75
|
-
const basePath =
|
|
76
|
+
const basePath = `${routes.data}/${name}`; // Standard REST Path
|
|
76
77
|
|
|
77
|
-
// GET
|
|
78
|
+
// GET {dataPath}/:name (List)
|
|
78
79
|
paths[basePath] = {
|
|
79
80
|
get: {
|
|
80
81
|
summary: `List ${name}`,
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for server operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Escapes special regex characters in a path string for use in RegExp
|
|
7
|
+
* @param path - The path string to escape
|
|
8
|
+
* @returns Escaped path string safe for use in RegExp
|
|
9
|
+
*/
|
|
10
|
+
export function escapeRegexPath(path: string): string {
|
|
11
|
+
return path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Normalizes a path to ensure it starts with a forward slash
|
|
16
|
+
* @param path - The path string to normalize
|
|
17
|
+
* @returns Normalized path string starting with '/'
|
|
18
|
+
*/
|
|
19
|
+
export function normalizePath(path: string): string {
|
|
20
|
+
return path.startsWith('/') ? path : `/${path}`;
|
|
21
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Custom API Route Configuration
|
|
3
|
+
*
|
|
4
|
+
* Validates that API routes can be configured at initialization time
|
|
5
|
+
* instead of being hardcoded.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import request from 'supertest';
|
|
9
|
+
import { createServer } from 'http';
|
|
10
|
+
import { ObjectQL } from '@objectql/core';
|
|
11
|
+
import { createNodeHandler, createRESTHandler, createMetadataHandler } from '../src';
|
|
12
|
+
import { Driver } from '@objectql/types';
|
|
13
|
+
|
|
14
|
+
// Simple Mock Driver for testing
|
|
15
|
+
class MockDriver implements Driver {
|
|
16
|
+
private data: Record<string, any[]> = {
|
|
17
|
+
user: [
|
|
18
|
+
{ _id: '1', name: 'Alice', email: 'alice@example.com' },
|
|
19
|
+
{ _id: '2', name: 'Bob', email: 'bob@example.com' }
|
|
20
|
+
]
|
|
21
|
+
};
|
|
22
|
+
private nextId = 3;
|
|
23
|
+
|
|
24
|
+
async init() {}
|
|
25
|
+
|
|
26
|
+
async find(objectName: string, query: any) {
|
|
27
|
+
return this.data[objectName] || [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async findOne(objectName: string, id: string | number) {
|
|
31
|
+
const items = this.data[objectName] || [];
|
|
32
|
+
return items.find(item => item._id === String(id)) || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async create(objectName: string, data: any) {
|
|
36
|
+
const newItem = { _id: String(this.nextId++), ...data };
|
|
37
|
+
if (!this.data[objectName]) {
|
|
38
|
+
this.data[objectName] = [];
|
|
39
|
+
}
|
|
40
|
+
this.data[objectName].push(newItem);
|
|
41
|
+
return newItem;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async update(objectName: string, id: string, data: any) {
|
|
45
|
+
const items = this.data[objectName] || [];
|
|
46
|
+
const index = items.findIndex(item => item._id === id);
|
|
47
|
+
if (index >= 0) {
|
|
48
|
+
this.data[objectName][index] = { ...items[index], ...data };
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async delete(objectName: string, id: string) {
|
|
55
|
+
const items = this.data[objectName] || [];
|
|
56
|
+
const index = items.findIndex(item => item._id === id);
|
|
57
|
+
if (index >= 0) {
|
|
58
|
+
this.data[objectName].splice(index, 1);
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async count(objectName: string) {
|
|
65
|
+
return (this.data[objectName] || []).length;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async createMany(objectName: string, data: any[]) {
|
|
69
|
+
const results = [];
|
|
70
|
+
for (const item of data) {
|
|
71
|
+
results.push(await this.create(objectName, item));
|
|
72
|
+
}
|
|
73
|
+
return results;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async updateMany(objectName: string, filters: any, data: any) {
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async deleteMany(objectName: string, filters: any) {
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe('Custom API Routes', () => {
|
|
86
|
+
let app: ObjectQL;
|
|
87
|
+
|
|
88
|
+
beforeEach(async () => {
|
|
89
|
+
app = new ObjectQL({
|
|
90
|
+
datasources: {
|
|
91
|
+
default: new MockDriver()
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
app.registerObject({
|
|
96
|
+
name: 'user',
|
|
97
|
+
label: 'User',
|
|
98
|
+
fields: {
|
|
99
|
+
name: { type: 'text', label: 'Name' },
|
|
100
|
+
email: { type: 'email', label: 'Email' }
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await app.init();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('REST Handler with Custom Routes', () => {
|
|
108
|
+
it('should work with custom data path /v1/resources', async () => {
|
|
109
|
+
const customRoutes = {
|
|
110
|
+
data: '/v1/resources'
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handler = createRESTHandler(app, { routes: customRoutes });
|
|
114
|
+
const server = createServer(handler);
|
|
115
|
+
|
|
116
|
+
const res = await request(server)
|
|
117
|
+
.get('/v1/resources/user')
|
|
118
|
+
.expect(200);
|
|
119
|
+
|
|
120
|
+
expect(res.body.items).toBeDefined();
|
|
121
|
+
expect(res.body.items.length).toBe(2);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should not respond to default path when custom path is set', async () => {
|
|
125
|
+
const customRoutes = {
|
|
126
|
+
data: '/v1/resources'
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const handler = createRESTHandler(app, { routes: customRoutes });
|
|
130
|
+
const server = createServer(handler);
|
|
131
|
+
|
|
132
|
+
await request(server)
|
|
133
|
+
.get('/api/data/user')
|
|
134
|
+
.expect(404);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should support multiple custom paths', async () => {
|
|
138
|
+
const customRoutes = {
|
|
139
|
+
data: '/api/v2/data',
|
|
140
|
+
metadata: '/api/v2/metadata'
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const restHandler = createRESTHandler(app, { routes: customRoutes });
|
|
144
|
+
const restServer = createServer(restHandler);
|
|
145
|
+
|
|
146
|
+
const metadataHandler = createMetadataHandler(app, { routes: customRoutes });
|
|
147
|
+
const metadataServer = createServer(metadataHandler);
|
|
148
|
+
|
|
149
|
+
// Test REST API with custom path
|
|
150
|
+
const restRes = await request(restServer)
|
|
151
|
+
.get('/api/v2/data/user')
|
|
152
|
+
.expect(200);
|
|
153
|
+
expect(restRes.body.items).toBeDefined();
|
|
154
|
+
|
|
155
|
+
// Test Metadata API with custom path
|
|
156
|
+
const metadataRes = await request(metadataServer)
|
|
157
|
+
.get('/api/v2/metadata/objects')
|
|
158
|
+
.expect(200);
|
|
159
|
+
expect(metadataRes.body.items).toBeDefined();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('Node Handler with Custom Routes', () => {
|
|
164
|
+
it('should work with custom RPC path /v1/rpc', async () => {
|
|
165
|
+
const customRoutes = {
|
|
166
|
+
rpc: '/v1/rpc',
|
|
167
|
+
data: '/v1/data'
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const handler = createNodeHandler(app, { routes: customRoutes });
|
|
171
|
+
const server = createServer(handler);
|
|
172
|
+
|
|
173
|
+
// Test custom RPC endpoint
|
|
174
|
+
const res = await request(server)
|
|
175
|
+
.post('/v1/rpc')
|
|
176
|
+
.send({
|
|
177
|
+
op: 'find',
|
|
178
|
+
object: 'user',
|
|
179
|
+
args: {}
|
|
180
|
+
})
|
|
181
|
+
.expect(200);
|
|
182
|
+
|
|
183
|
+
// NodeHandler returns the full ObjectQLResponse
|
|
184
|
+
expect(res.body.items || res.body.data).toBeDefined();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should work with custom data path for REST operations', async () => {
|
|
188
|
+
const customRoutes = {
|
|
189
|
+
rpc: '/v1/rpc',
|
|
190
|
+
data: '/v1/data'
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const handler = createNodeHandler(app, { routes: customRoutes });
|
|
194
|
+
const server = createServer(handler);
|
|
195
|
+
|
|
196
|
+
// Test custom REST endpoint
|
|
197
|
+
const res = await request(server)
|
|
198
|
+
.get('/v1/data/user')
|
|
199
|
+
.expect(200);
|
|
200
|
+
|
|
201
|
+
expect(res.body.items).toBeDefined();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should work with custom files path', async () => {
|
|
205
|
+
const customRoutes = {
|
|
206
|
+
rpc: '/v1/rpc',
|
|
207
|
+
data: '/v1/data',
|
|
208
|
+
files: '/v1/storage'
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const handler = createNodeHandler(app, { routes: customRoutes });
|
|
212
|
+
const server = createServer(handler);
|
|
213
|
+
|
|
214
|
+
// Test that file upload path is recognized (will fail without multipart body, but path is recognized)
|
|
215
|
+
const res = await request(server)
|
|
216
|
+
.post('/v1/storage/upload');
|
|
217
|
+
|
|
218
|
+
// Should not be 404 (not found), but may be 400 (bad request) or 500 (server error)
|
|
219
|
+
expect(res.status).not.toBe(404);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('Metadata Handler with Custom Routes', () => {
|
|
224
|
+
it('should work with custom metadata path /v1/schema', async () => {
|
|
225
|
+
const customRoutes = {
|
|
226
|
+
metadata: '/v1/schema'
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const handler = createMetadataHandler(app, { routes: customRoutes });
|
|
230
|
+
const server = createServer(handler);
|
|
231
|
+
|
|
232
|
+
const res = await request(server)
|
|
233
|
+
.get('/v1/schema/objects')
|
|
234
|
+
.expect(200);
|
|
235
|
+
|
|
236
|
+
expect(res.body.items).toBeDefined();
|
|
237
|
+
expect(res.body.items.length).toBeGreaterThan(0);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should support object detail endpoint with custom path', async () => {
|
|
241
|
+
const customRoutes = {
|
|
242
|
+
metadata: '/v1/schema'
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const handler = createMetadataHandler(app, { routes: customRoutes });
|
|
246
|
+
const server = createServer(handler);
|
|
247
|
+
|
|
248
|
+
const res = await request(server)
|
|
249
|
+
.get('/v1/schema/object/user')
|
|
250
|
+
.expect(200);
|
|
251
|
+
|
|
252
|
+
expect(res.body.name).toBe('user');
|
|
253
|
+
expect(res.body.fields).toBeDefined();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('Default Routes (Backward Compatibility)', () => {
|
|
258
|
+
it('should use default routes when no custom routes provided', async () => {
|
|
259
|
+
const handler = createRESTHandler(app);
|
|
260
|
+
const server = createServer(handler);
|
|
261
|
+
|
|
262
|
+
const res = await request(server)
|
|
263
|
+
.get('/api/data/user')
|
|
264
|
+
.expect(200);
|
|
265
|
+
|
|
266
|
+
expect(res.body.items).toBeDefined();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should use default RPC route when no custom routes provided', async () => {
|
|
270
|
+
const handler = createNodeHandler(app);
|
|
271
|
+
const server = createServer(handler);
|
|
272
|
+
|
|
273
|
+
const res = await request(server)
|
|
274
|
+
.post('/api/objectql')
|
|
275
|
+
.send({
|
|
276
|
+
op: 'find',
|
|
277
|
+
object: 'user',
|
|
278
|
+
args: {}
|
|
279
|
+
})
|
|
280
|
+
.expect(200);
|
|
281
|
+
|
|
282
|
+
// NodeHandler returns the full ObjectQLResponse
|
|
283
|
+
expect(res.body.items || res.body.data).toBeDefined();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should use default metadata route when no custom routes provided', async () => {
|
|
287
|
+
const handler = createMetadataHandler(app);
|
|
288
|
+
const server = createServer(handler);
|
|
289
|
+
|
|
290
|
+
const res = await request(server)
|
|
291
|
+
.get('/api/metadata/objects')
|
|
292
|
+
.expect(200);
|
|
293
|
+
|
|
294
|
+
expect(res.body.items).toBeDefined();
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
});
|