@objectstack/nextjs 4.0.3 → 4.0.5
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/README.md +72 -7
- package/package.json +35 -9
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -371
- package/src/__mocks__/runtime.ts +0 -4
- package/src/index.ts +0 -295
- package/src/metadata-api.test.ts +0 -724
- package/src/nextjs.test.ts +0 -589
- package/tsconfig.json +0 -26
- package/vitest.config.ts +0 -16
package/src/metadata-api.test.ts
DELETED
|
@@ -1,724 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @objectstack/nextjs — Comprehensive Metadata API Integration Tests
|
|
5
|
-
*
|
|
6
|
-
* Validates that the Next.js adapter correctly routes ALL metadata API operations
|
|
7
|
-
* defined by the @objectstack/metadata package through the HttpDispatcher.
|
|
8
|
-
*
|
|
9
|
-
* Covers: CRUD, Query, Bulk, Overlay, Import/Export, Validation, Type Registry, Dependencies
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
13
|
-
|
|
14
|
-
// Mock dispatcher instance
|
|
15
|
-
const mockDispatcher = {
|
|
16
|
-
getDiscoveryInfo: vi.fn().mockReturnValue({ version: '1.0', endpoints: [] }),
|
|
17
|
-
handleAuth: vi.fn().mockResolvedValue({ handled: true, response: { body: { ok: true }, status: 200 } }),
|
|
18
|
-
handleGraphQL: vi.fn().mockResolvedValue({ data: {} }),
|
|
19
|
-
handleMetadata: vi.fn().mockResolvedValue({ handled: true, response: { body: { success: true }, status: 200 } }),
|
|
20
|
-
handleData: vi.fn().mockResolvedValue({ handled: true, response: { body: { records: [] }, status: 200 } }),
|
|
21
|
-
handleStorage: vi.fn().mockResolvedValue({ handled: true, response: { body: {}, status: 200 } }),
|
|
22
|
-
dispatch: vi.fn().mockResolvedValue({ handled: true, response: { body: { success: true }, status: 200 } }),
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
vi.mock('@objectstack/runtime', () => {
|
|
26
|
-
return {
|
|
27
|
-
HttpDispatcher: function HttpDispatcher() {
|
|
28
|
-
return mockDispatcher;
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
vi.mock('next/server', () => {
|
|
34
|
-
class MockNextRequest {
|
|
35
|
-
url: string;
|
|
36
|
-
method: string;
|
|
37
|
-
private _body: any;
|
|
38
|
-
|
|
39
|
-
constructor(url: string, init?: any) {
|
|
40
|
-
this.url = url;
|
|
41
|
-
this.method = init?.method || 'GET';
|
|
42
|
-
this._body = init?.body;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async json() {
|
|
46
|
-
return this._body ? JSON.parse(this._body) : {};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async formData() {
|
|
50
|
-
const map = new Map();
|
|
51
|
-
map.set('file', { name: 'test.txt', type: 'text/plain' });
|
|
52
|
-
return { get: (key: string) => map.get(key) };
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
class MockNextResponse {
|
|
57
|
-
body: any;
|
|
58
|
-
status: number;
|
|
59
|
-
headers: Record<string, string>;
|
|
60
|
-
|
|
61
|
-
constructor(body?: any, init?: any) {
|
|
62
|
-
this.body = body;
|
|
63
|
-
this.status = init?.status || 200;
|
|
64
|
-
this.headers = init?.headers || {};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async json() {
|
|
68
|
-
return typeof this.body === 'string' ? JSON.parse(this.body) : this.body;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
static json(body: any, init?: any) {
|
|
72
|
-
return new MockNextResponse(body, init);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
static redirect(url: string | URL) {
|
|
76
|
-
const res = new MockNextResponse(null, { status: 307 });
|
|
77
|
-
(res as any).redirectUrl = typeof url === 'string' ? url : url.toString();
|
|
78
|
-
return res;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return { NextRequest: MockNextRequest, NextResponse: MockNextResponse };
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
import { NextRequest } from 'next/server';
|
|
86
|
-
import { createRouteHandler } from './index';
|
|
87
|
-
|
|
88
|
-
const mockKernel = { name: 'test-kernel' } as any;
|
|
89
|
-
|
|
90
|
-
function makeReq(url: string, method = 'GET', body?: any) {
|
|
91
|
-
const init: any = { method };
|
|
92
|
-
if (body) init.body = JSON.stringify(body);
|
|
93
|
-
return new (NextRequest as any)(url, init);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
describe('Next.js Metadata API Integration Tests', () => {
|
|
97
|
-
let handler: ReturnType<typeof createRouteHandler>;
|
|
98
|
-
|
|
99
|
-
beforeEach(() => {
|
|
100
|
-
vi.clearAllMocks();
|
|
101
|
-
handler = createRouteHandler({ kernel: mockKernel });
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// ==========================================
|
|
105
|
-
// CRUD Operations
|
|
106
|
-
// ==========================================
|
|
107
|
-
|
|
108
|
-
describe('CRUD Operations', () => {
|
|
109
|
-
describe('GET meta/objects — List all objects', () => {
|
|
110
|
-
it('dispatches to dispatch with correct path', async () => {
|
|
111
|
-
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
112
|
-
handled: true,
|
|
113
|
-
response: {
|
|
114
|
-
body: {
|
|
115
|
-
success: true,
|
|
116
|
-
data: [
|
|
117
|
-
{ name: 'account', label: 'Account' },
|
|
118
|
-
{ name: 'contact', label: 'Contact' },
|
|
119
|
-
],
|
|
120
|
-
},
|
|
121
|
-
status: 200,
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
const req = makeReq('http://localhost/api/meta/objects');
|
|
126
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'objects'] } });
|
|
127
|
-
expect(res.status).toBe(200);
|
|
128
|
-
expect(res.body.data).toHaveLength(2);
|
|
129
|
-
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
130
|
-
'GET',
|
|
131
|
-
'/meta/objects',
|
|
132
|
-
undefined,
|
|
133
|
-
{},
|
|
134
|
-
expect.objectContaining({ request: expect.anything() }),
|
|
135
|
-
'/api',
|
|
136
|
-
);
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
describe('GET meta/objects/account — Get single object', () => {
|
|
141
|
-
it('dispatches to dispatch with item-level path', async () => {
|
|
142
|
-
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
143
|
-
handled: true,
|
|
144
|
-
response: {
|
|
145
|
-
body: {
|
|
146
|
-
success: true,
|
|
147
|
-
data: { type: 'object', name: 'account', definition: { label: 'Account' } },
|
|
148
|
-
},
|
|
149
|
-
status: 200,
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
const req = makeReq('http://localhost/api/meta/objects/account');
|
|
154
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'objects', 'account'] } });
|
|
155
|
-
expect(res.status).toBe(200);
|
|
156
|
-
expect(res.body.data.name).toBe('account');
|
|
157
|
-
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
158
|
-
'GET',
|
|
159
|
-
'/meta/objects/account',
|
|
160
|
-
undefined,
|
|
161
|
-
{},
|
|
162
|
-
expect.objectContaining({ request: expect.anything() }),
|
|
163
|
-
'/api',
|
|
164
|
-
);
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
describe('POST meta/objects — Register metadata', () => {
|
|
169
|
-
it('dispatches POST with JSON body', async () => {
|
|
170
|
-
const body = {
|
|
171
|
-
type: 'object',
|
|
172
|
-
name: 'project_task',
|
|
173
|
-
data: { label: 'Project Task', fields: {} },
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
177
|
-
handled: true,
|
|
178
|
-
response: { body: { success: true }, status: 201 },
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
const req = makeReq('http://localhost/api/meta/objects', 'POST', body);
|
|
182
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'objects'] } });
|
|
183
|
-
expect(res.status).toBe(201);
|
|
184
|
-
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
185
|
-
'POST',
|
|
186
|
-
'/meta/objects',
|
|
187
|
-
body,
|
|
188
|
-
{},
|
|
189
|
-
expect.objectContaining({ request: expect.anything() }),
|
|
190
|
-
'/api',
|
|
191
|
-
);
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
describe('PUT meta/objects/account — Update metadata', () => {
|
|
196
|
-
it('dispatches PUT with JSON body', async () => {
|
|
197
|
-
const body = { label: 'Updated Account' };
|
|
198
|
-
|
|
199
|
-
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
200
|
-
handled: true,
|
|
201
|
-
response: { body: { success: true }, status: 200 },
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
const req = makeReq('http://localhost/api/meta/objects/account', 'PUT', body);
|
|
205
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'objects', 'account'] } });
|
|
206
|
-
expect(res.status).toBe(200);
|
|
207
|
-
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
208
|
-
'PUT',
|
|
209
|
-
'/meta/objects/account',
|
|
210
|
-
body,
|
|
211
|
-
{},
|
|
212
|
-
expect.objectContaining({ request: expect.anything() }),
|
|
213
|
-
'/api',
|
|
214
|
-
);
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
describe('DELETE meta/objects/old_entity — Delete metadata', () => {
|
|
219
|
-
it('dispatches DELETE to dispatch', async () => {
|
|
220
|
-
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
221
|
-
handled: true,
|
|
222
|
-
response: {
|
|
223
|
-
body: { success: true, data: { type: 'object', name: 'old_entity' } },
|
|
224
|
-
status: 200,
|
|
225
|
-
},
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
const req = makeReq('http://localhost/api/meta/objects/old_entity', 'DELETE');
|
|
229
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'objects', 'old_entity'] } });
|
|
230
|
-
expect(res.status).toBe(200);
|
|
231
|
-
expect(res.body.data.name).toBe('old_entity');
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
describe('Multiple metadata types', () => {
|
|
236
|
-
it('dispatches for views', async () => {
|
|
237
|
-
const req = makeReq('http://localhost/api/meta/views');
|
|
238
|
-
await handler(req, { params: { objectstack: ['meta', 'views'] } });
|
|
239
|
-
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
240
|
-
'GET',
|
|
241
|
-
'/meta/views',
|
|
242
|
-
undefined,
|
|
243
|
-
{},
|
|
244
|
-
expect.objectContaining({ request: expect.anything() }),
|
|
245
|
-
'/api',
|
|
246
|
-
);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it('dispatches for flows', async () => {
|
|
250
|
-
const req = makeReq('http://localhost/api/meta/flows');
|
|
251
|
-
await handler(req, { params: { objectstack: ['meta', 'flows'] } });
|
|
252
|
-
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
253
|
-
'GET',
|
|
254
|
-
'/meta/flows',
|
|
255
|
-
undefined,
|
|
256
|
-
{},
|
|
257
|
-
expect.objectContaining({ request: expect.anything() }),
|
|
258
|
-
'/api',
|
|
259
|
-
);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it('dispatches for agents', async () => {
|
|
263
|
-
const req = makeReq('http://localhost/api/meta/agents');
|
|
264
|
-
await handler(req, { params: { objectstack: ['meta', 'agents'] } });
|
|
265
|
-
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
266
|
-
'GET',
|
|
267
|
-
'/meta/agents',
|
|
268
|
-
undefined,
|
|
269
|
-
{},
|
|
270
|
-
expect.objectContaining({ request: expect.anything() }),
|
|
271
|
-
'/api',
|
|
272
|
-
);
|
|
273
|
-
});
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
// ==========================================
|
|
278
|
-
// Query / Search
|
|
279
|
-
// ==========================================
|
|
280
|
-
|
|
281
|
-
describe('Query / Search', () => {
|
|
282
|
-
describe('POST meta/query — Advanced search', () => {
|
|
283
|
-
it('dispatches query with full filter payload', async () => {
|
|
284
|
-
const queryBody = {
|
|
285
|
-
types: ['object', 'view'],
|
|
286
|
-
search: 'account',
|
|
287
|
-
page: 1,
|
|
288
|
-
pageSize: 25,
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
292
|
-
handled: true,
|
|
293
|
-
response: {
|
|
294
|
-
body: {
|
|
295
|
-
success: true,
|
|
296
|
-
data: {
|
|
297
|
-
items: [{ type: 'object', name: 'account' }],
|
|
298
|
-
total: 1,
|
|
299
|
-
page: 1,
|
|
300
|
-
pageSize: 25,
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
status: 200,
|
|
304
|
-
},
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
const req = makeReq('http://localhost/api/meta/query', 'POST', queryBody);
|
|
308
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'query'] } });
|
|
309
|
-
|
|
310
|
-
expect(res.status).toBe(200);
|
|
311
|
-
expect(res.body.data.items).toHaveLength(1);
|
|
312
|
-
});
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
// ==========================================
|
|
317
|
-
// Bulk Operations
|
|
318
|
-
// ==========================================
|
|
319
|
-
|
|
320
|
-
describe('Bulk Operations', () => {
|
|
321
|
-
describe('POST meta/bulk/register — Bulk register', () => {
|
|
322
|
-
it('dispatches bulk register', async () => {
|
|
323
|
-
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
324
|
-
handled: true,
|
|
325
|
-
response: {
|
|
326
|
-
body: { success: true, data: { total: 2, succeeded: 2, failed: 0 } },
|
|
327
|
-
status: 200,
|
|
328
|
-
},
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
const req = makeReq('http://localhost/api/meta/bulk/register', 'POST', {
|
|
332
|
-
items: [
|
|
333
|
-
{ type: 'object', name: 'customer', data: {} },
|
|
334
|
-
{ type: 'view', name: 'customer_list', data: {} },
|
|
335
|
-
],
|
|
336
|
-
});
|
|
337
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'bulk', 'register'] } });
|
|
338
|
-
|
|
339
|
-
expect(res.status).toBe(200);
|
|
340
|
-
expect(res.body.data.succeeded).toBe(2);
|
|
341
|
-
});
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
describe('POST meta/bulk/unregister — Bulk unregister', () => {
|
|
345
|
-
it('dispatches bulk unregister', async () => {
|
|
346
|
-
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
347
|
-
handled: true,
|
|
348
|
-
response: {
|
|
349
|
-
body: { success: true, data: { total: 2, succeeded: 2, failed: 0 } },
|
|
350
|
-
status: 200,
|
|
351
|
-
},
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
const req = makeReq('http://localhost/api/meta/bulk/unregister', 'POST', {
|
|
355
|
-
items: [{ type: 'object', name: 'old' }, { type: 'view', name: 'old_view' }],
|
|
356
|
-
});
|
|
357
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'bulk', 'unregister'] } });
|
|
358
|
-
|
|
359
|
-
expect(res.status).toBe(200);
|
|
360
|
-
expect(res.body.data.succeeded).toBe(2);
|
|
361
|
-
});
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
describe('Bulk operation with partial failures', () => {
|
|
365
|
-
it('returns error details', async () => {
|
|
366
|
-
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
367
|
-
handled: true,
|
|
368
|
-
response: {
|
|
369
|
-
body: {
|
|
370
|
-
success: true,
|
|
371
|
-
data: {
|
|
372
|
-
total: 3,
|
|
373
|
-
succeeded: 2,
|
|
374
|
-
failed: 1,
|
|
375
|
-
errors: [{ type: 'object', name: 'bad', error: 'Validation failed' }],
|
|
376
|
-
},
|
|
377
|
-
},
|
|
378
|
-
status: 200,
|
|
379
|
-
},
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
const req = makeReq('http://localhost/api/meta/bulk/register', 'POST', {
|
|
383
|
-
items: [
|
|
384
|
-
{ type: 'object', name: 'good', data: {} },
|
|
385
|
-
{ type: 'object', name: 'good2', data: {} },
|
|
386
|
-
{ type: 'object', name: 'bad', data: {} },
|
|
387
|
-
],
|
|
388
|
-
continueOnError: true,
|
|
389
|
-
});
|
|
390
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'bulk', 'register'] } });
|
|
391
|
-
|
|
392
|
-
expect(res.body.data.failed).toBe(1);
|
|
393
|
-
expect(res.body.data.errors[0].name).toBe('bad');
|
|
394
|
-
});
|
|
395
|
-
});
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
// ==========================================
|
|
399
|
-
// Overlay / Customization
|
|
400
|
-
// ==========================================
|
|
401
|
-
|
|
402
|
-
describe('Overlay / Customization', () => {
|
|
403
|
-
describe('GET meta/objects/account/overlay — Get overlay', () => {
|
|
404
|
-
it('dispatches overlay retrieval', async () => {
|
|
405
|
-
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
406
|
-
handled: true,
|
|
407
|
-
response: {
|
|
408
|
-
body: {
|
|
409
|
-
success: true,
|
|
410
|
-
data: {
|
|
411
|
-
id: 'overlay-001',
|
|
412
|
-
baseType: 'object',
|
|
413
|
-
baseName: 'account',
|
|
414
|
-
scope: 'platform',
|
|
415
|
-
patch: {},
|
|
416
|
-
},
|
|
417
|
-
},
|
|
418
|
-
status: 200,
|
|
419
|
-
},
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
const req = makeReq('http://localhost/api/meta/objects/account/overlay');
|
|
423
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'objects', 'account', 'overlay'] } });
|
|
424
|
-
expect(res.status).toBe(200);
|
|
425
|
-
expect(res.body.data.scope).toBe('platform');
|
|
426
|
-
});
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
describe('PUT meta/objects/account/overlay — Save overlay', () => {
|
|
430
|
-
it('dispatches overlay save', async () => {
|
|
431
|
-
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
432
|
-
handled: true,
|
|
433
|
-
response: { body: { success: true }, status: 200 },
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
const req = makeReq('http://localhost/api/meta/objects/account/overlay', 'PUT', {
|
|
437
|
-
id: 'overlay-002',
|
|
438
|
-
baseType: 'object',
|
|
439
|
-
baseName: 'account',
|
|
440
|
-
patch: { fields: { status: { label: 'Custom' } } },
|
|
441
|
-
});
|
|
442
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'objects', 'account', 'overlay'] } });
|
|
443
|
-
expect(res.status).toBe(200);
|
|
444
|
-
});
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
describe('GET meta/objects/account/effective — Get effective metadata', () => {
|
|
448
|
-
it('dispatches effective metadata retrieval', async () => {
|
|
449
|
-
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
450
|
-
handled: true,
|
|
451
|
-
response: {
|
|
452
|
-
body: {
|
|
453
|
-
success: true,
|
|
454
|
-
data: { name: 'account', fields: { status: { label: 'Custom Status' } } },
|
|
455
|
-
},
|
|
456
|
-
status: 200,
|
|
457
|
-
},
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
const req = makeReq('http://localhost/api/meta/objects/account/effective');
|
|
461
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'objects', 'account', 'effective'] } });
|
|
462
|
-
expect(res.status).toBe(200);
|
|
463
|
-
expect(res.body.data.fields.status.label).toBe('Custom Status');
|
|
464
|
-
});
|
|
465
|
-
});
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
// ==========================================
|
|
469
|
-
// Import / Export
|
|
470
|
-
// ==========================================
|
|
471
|
-
|
|
472
|
-
describe('Import / Export', () => {
|
|
473
|
-
describe('POST meta/export — Export metadata', () => {
|
|
474
|
-
it('dispatches export request', async () => {
|
|
475
|
-
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
476
|
-
handled: true,
|
|
477
|
-
response: {
|
|
478
|
-
body: { success: true, data: { version: '1.0', objects: {} } },
|
|
479
|
-
status: 200,
|
|
480
|
-
},
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
const req = makeReq('http://localhost/api/meta/export', 'POST', { types: ['object'], format: 'json' });
|
|
484
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'export'] } });
|
|
485
|
-
expect(res.status).toBe(200);
|
|
486
|
-
expect(res.body.data.version).toBe('1.0');
|
|
487
|
-
});
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
describe('POST meta/import — Import metadata', () => {
|
|
491
|
-
it('dispatches import request', async () => {
|
|
492
|
-
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
493
|
-
handled: true,
|
|
494
|
-
response: {
|
|
495
|
-
body: { success: true, data: { total: 3, imported: 3, skipped: 0, failed: 0 } },
|
|
496
|
-
status: 200,
|
|
497
|
-
},
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
const req = makeReq('http://localhost/api/meta/import', 'POST', {
|
|
501
|
-
data: { objects: { a: {} } },
|
|
502
|
-
conflictResolution: 'merge',
|
|
503
|
-
});
|
|
504
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'import'] } });
|
|
505
|
-
expect(res.status).toBe(200);
|
|
506
|
-
expect(res.body.data.imported).toBe(3);
|
|
507
|
-
});
|
|
508
|
-
});
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
// ==========================================
|
|
512
|
-
// Validation
|
|
513
|
-
// ==========================================
|
|
514
|
-
|
|
515
|
-
describe('Validation', () => {
|
|
516
|
-
describe('POST meta/validate — Validate metadata', () => {
|
|
517
|
-
it('dispatches validation', async () => {
|
|
518
|
-
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
519
|
-
handled: true,
|
|
520
|
-
response: {
|
|
521
|
-
body: { success: true, data: { valid: true } },
|
|
522
|
-
status: 200,
|
|
523
|
-
},
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
const req = makeReq('http://localhost/api/meta/validate', 'POST', { type: 'object', data: {} });
|
|
527
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'validate'] } });
|
|
528
|
-
expect(res.status).toBe(200);
|
|
529
|
-
expect(res.body.data.valid).toBe(true);
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
it('returns errors for invalid metadata', async () => {
|
|
533
|
-
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
534
|
-
handled: true,
|
|
535
|
-
response: {
|
|
536
|
-
body: {
|
|
537
|
-
success: true,
|
|
538
|
-
data: {
|
|
539
|
-
valid: false,
|
|
540
|
-
errors: [{ path: 'name', message: 'Required', code: 'required' }],
|
|
541
|
-
},
|
|
542
|
-
},
|
|
543
|
-
status: 200,
|
|
544
|
-
},
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
const req = makeReq('http://localhost/api/meta/validate', 'POST', { type: 'object', data: {} });
|
|
548
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'validate'] } });
|
|
549
|
-
expect(res.body.data.valid).toBe(false);
|
|
550
|
-
expect(res.body.data.errors).toHaveLength(1);
|
|
551
|
-
});
|
|
552
|
-
});
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
// ==========================================
|
|
556
|
-
// Type Registry
|
|
557
|
-
// ==========================================
|
|
558
|
-
|
|
559
|
-
describe('Type Registry', () => {
|
|
560
|
-
describe('GET meta/types — List types', () => {
|
|
561
|
-
it('returns all registered types', async () => {
|
|
562
|
-
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
563
|
-
handled: true,
|
|
564
|
-
response: {
|
|
565
|
-
body: { success: true, data: ['object', 'view', 'flow', 'agent'] },
|
|
566
|
-
status: 200,
|
|
567
|
-
},
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
const req = makeReq('http://localhost/api/meta/types');
|
|
571
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'types'] } });
|
|
572
|
-
expect(res.status).toBe(200);
|
|
573
|
-
expect(res.body.data).toContain('object');
|
|
574
|
-
});
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
describe('GET meta/types/object — Get type info', () => {
|
|
578
|
-
it('returns type metadata', async () => {
|
|
579
|
-
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
580
|
-
handled: true,
|
|
581
|
-
response: {
|
|
582
|
-
body: {
|
|
583
|
-
success: true,
|
|
584
|
-
data: {
|
|
585
|
-
type: 'object',
|
|
586
|
-
label: 'Object',
|
|
587
|
-
filePatterns: ['**/*.object.ts'],
|
|
588
|
-
supportsOverlay: true,
|
|
589
|
-
domain: 'data',
|
|
590
|
-
},
|
|
591
|
-
},
|
|
592
|
-
status: 200,
|
|
593
|
-
},
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
const req = makeReq('http://localhost/api/meta/types/object');
|
|
597
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'types', 'object'] } });
|
|
598
|
-
expect(res.status).toBe(200);
|
|
599
|
-
expect(res.body.data.domain).toBe('data');
|
|
600
|
-
});
|
|
601
|
-
});
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
// ==========================================
|
|
605
|
-
// Dependency Tracking
|
|
606
|
-
// ==========================================
|
|
607
|
-
|
|
608
|
-
describe('Dependency Tracking', () => {
|
|
609
|
-
describe('GET meta/objects/account/dependencies — Get dependencies', () => {
|
|
610
|
-
it('returns dependencies', async () => {
|
|
611
|
-
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
612
|
-
handled: true,
|
|
613
|
-
response: {
|
|
614
|
-
body: {
|
|
615
|
-
success: true,
|
|
616
|
-
data: [{
|
|
617
|
-
sourceType: 'object',
|
|
618
|
-
sourceName: 'account',
|
|
619
|
-
targetType: 'object',
|
|
620
|
-
targetName: 'organization',
|
|
621
|
-
kind: 'reference',
|
|
622
|
-
}],
|
|
623
|
-
},
|
|
624
|
-
status: 200,
|
|
625
|
-
},
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
const req = makeReq('http://localhost/api/meta/objects/account/dependencies');
|
|
629
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'objects', 'account', 'dependencies'] } });
|
|
630
|
-
expect(res.status).toBe(200);
|
|
631
|
-
expect(res.body.data).toHaveLength(1);
|
|
632
|
-
});
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
describe('GET meta/objects/account/dependents — Get dependents', () => {
|
|
636
|
-
it('returns dependents', async () => {
|
|
637
|
-
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
638
|
-
handled: true,
|
|
639
|
-
response: {
|
|
640
|
-
body: {
|
|
641
|
-
success: true,
|
|
642
|
-
data: [
|
|
643
|
-
{ sourceType: 'view', sourceName: 'account_list', targetType: 'object', targetName: 'account', kind: 'reference' },
|
|
644
|
-
{ sourceType: 'flow', sourceName: 'new_account', targetType: 'object', targetName: 'account', kind: 'triggers' },
|
|
645
|
-
],
|
|
646
|
-
},
|
|
647
|
-
status: 200,
|
|
648
|
-
},
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
const req = makeReq('http://localhost/api/meta/objects/account/dependents');
|
|
652
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'objects', 'account', 'dependents'] } });
|
|
653
|
-
expect(res.status).toBe(200);
|
|
654
|
-
expect(res.body.data).toHaveLength(2);
|
|
655
|
-
});
|
|
656
|
-
});
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
// ==========================================
|
|
660
|
-
// Error Handling
|
|
661
|
-
// ==========================================
|
|
662
|
-
|
|
663
|
-
describe('Error Handling', () => {
|
|
664
|
-
it('returns 404 when metadata not found', async () => {
|
|
665
|
-
mockDispatcher.dispatch.mockResolvedValueOnce({ handled: false });
|
|
666
|
-
|
|
667
|
-
const req = makeReq('http://localhost/api/meta/objects/nonexistent');
|
|
668
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'objects', 'nonexistent'] } });
|
|
669
|
-
expect(res.status).toBe(404);
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
it('returns 500 on dispatcher exception', async () => {
|
|
673
|
-
mockDispatcher.dispatch.mockRejectedValueOnce(new Error('Internal error'));
|
|
674
|
-
|
|
675
|
-
const req = makeReq('http://localhost/api/meta/objects');
|
|
676
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'objects'] } });
|
|
677
|
-
expect(res.status).toBe(500);
|
|
678
|
-
expect(res.body.error.message).toBe('Internal error');
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
it('returns custom status code from error', async () => {
|
|
682
|
-
mockDispatcher.dispatch.mockRejectedValueOnce(
|
|
683
|
-
Object.assign(new Error('Forbidden'), { statusCode: 403 }),
|
|
684
|
-
);
|
|
685
|
-
|
|
686
|
-
const req = makeReq('http://localhost/api/meta/objects');
|
|
687
|
-
const res = await handler(req, { params: { objectstack: ['meta', 'objects'] } });
|
|
688
|
-
expect(res.status).toBe(403);
|
|
689
|
-
});
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
// ==========================================
|
|
693
|
-
// Path Parsing
|
|
694
|
-
// ==========================================
|
|
695
|
-
|
|
696
|
-
describe('Path Parsing', () => {
|
|
697
|
-
it('correctly joins nested segments', async () => {
|
|
698
|
-
const req = makeReq('http://localhost/api/meta/objects/account/fields/name');
|
|
699
|
-
await handler(req, { params: { objectstack: ['meta', 'objects', 'account', 'fields', 'name'] } });
|
|
700
|
-
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
701
|
-
'GET',
|
|
702
|
-
'/meta/objects/account/fields/name',
|
|
703
|
-
undefined,
|
|
704
|
-
{},
|
|
705
|
-
expect.objectContaining({ request: expect.anything() }),
|
|
706
|
-
'/api',
|
|
707
|
-
);
|
|
708
|
-
});
|
|
709
|
-
|
|
710
|
-
it('handles single segment meta path', async () => {
|
|
711
|
-
const req = makeReq('http://localhost/api/meta');
|
|
712
|
-
// With just ['meta'], subPath becomes empty after slice(1)
|
|
713
|
-
await handler(req, { params: { objectstack: ['meta'] } });
|
|
714
|
-
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
715
|
-
'GET',
|
|
716
|
-
'/meta',
|
|
717
|
-
undefined,
|
|
718
|
-
{},
|
|
719
|
-
expect.objectContaining({ request: expect.anything() }),
|
|
720
|
-
'/api',
|
|
721
|
-
);
|
|
722
|
-
});
|
|
723
|
-
});
|
|
724
|
-
});
|