@objectstack/nestjs 2.0.0 → 2.0.2

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @objectstack/nestjs@2.0.0 build /home/runner/work/spec/spec/packages/adapters/nestjs
2
+ > @objectstack/nestjs@2.0.2 build /home/runner/work/spec/spec/packages/adapters/nestjs
3
3
  > tsup --config ../../../tsup.config.ts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -12,13 +12,13 @@
12
12
  CJS Build start
13
13
  ESM You have emitDecoratorMetadata enabled but @swc/core was not installed, skipping swc plugin
14
14
  CJS You have emitDecoratorMetadata enabled but @swc/core was not installed, skipping swc plugin
15
- CJS dist/index.js 8.49 KB
16
- CJS dist/index.js.map 11.24 KB
17
- CJS ⚡️ Build success in 61ms
18
15
  ESM dist/index.mjs 6.87 KB
19
16
  ESM dist/index.mjs.map 11.21 KB
20
- ESM ⚡️ Build success in 61ms
17
+ ESM ⚡️ Build success in 50ms
18
+ CJS dist/index.js 8.49 KB
19
+ CJS dist/index.js.map 11.24 KB
20
+ CJS ⚡️ Build success in 50ms
21
21
  DTS Build start
22
- DTS ⚡️ Build success in 9472ms
23
- DTS dist/index.d.mts 2.44 KB
24
- DTS dist/index.d.ts 2.44 KB
22
+ DTS ⚡️ Build success in 6716ms
23
+ DTS dist/index.d.mts 8.75 KB
24
+ DTS dist/index.d.ts 8.75 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @objectstack/nestjs
2
2
 
3
+ ## 2.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - @objectstack/runtime@2.0.2
8
+
9
+ ## 2.0.1
10
+
11
+ ### Patch Changes
12
+
13
+ - Patch release for maintenance and stability improvements
14
+ - Updated dependencies
15
+ - @objectstack/runtime@2.0.1
16
+
3
17
  ## 2.0.0
4
18
 
5
19
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -23,25 +23,33 @@ declare class ObjectStackController {
23
23
  data: string;
24
24
  metadata: string;
25
25
  packages: string;
26
- auth: string;
27
- ui: string;
26
+ auth: string | undefined;
27
+ ui: string | undefined;
28
28
  graphql: string | undefined;
29
29
  storage: string | undefined;
30
30
  analytics: string | undefined;
31
- hub: string | undefined;
32
- automation: string;
31
+ automation: string | undefined;
32
+ workflow: string | undefined;
33
+ realtime: string | undefined;
34
+ notifications: string | undefined;
35
+ ai: string | undefined;
36
+ i18n: string | undefined;
33
37
  };
34
38
  endpoints: {
35
39
  data: string;
36
40
  metadata: string;
37
41
  packages: string;
38
- auth: string;
39
- ui: string;
42
+ auth: string | undefined;
43
+ ui: string | undefined;
40
44
  graphql: string | undefined;
41
45
  storage: string | undefined;
42
46
  analytics: string | undefined;
43
- hub: string | undefined;
44
- automation: string;
47
+ automation: string | undefined;
48
+ workflow: string | undefined;
49
+ realtime: string | undefined;
50
+ notifications: string | undefined;
51
+ ai: string | undefined;
52
+ i18n: string | undefined;
45
53
  };
46
54
  features: {
47
55
  graphql: boolean;
@@ -49,7 +57,175 @@ declare class ObjectStackController {
49
57
  websockets: boolean;
50
58
  files: boolean;
51
59
  analytics: boolean;
52
- hub: boolean;
60
+ ai: boolean;
61
+ workflow: boolean;
62
+ notifications: boolean;
63
+ i18n: boolean;
64
+ };
65
+ services: {
66
+ metadata: {
67
+ enabled: boolean;
68
+ status: "degraded";
69
+ route: string;
70
+ provider: string;
71
+ message: string;
72
+ };
73
+ data: {
74
+ enabled: boolean;
75
+ status: "available";
76
+ route: string | undefined;
77
+ provider: string | undefined;
78
+ };
79
+ auth: {
80
+ enabled: boolean;
81
+ status: "available";
82
+ route: string | undefined;
83
+ provider: string | undefined;
84
+ } | {
85
+ enabled: boolean;
86
+ status: "unavailable";
87
+ message: string;
88
+ };
89
+ automation: {
90
+ enabled: boolean;
91
+ status: "available";
92
+ route: string | undefined;
93
+ provider: string | undefined;
94
+ } | {
95
+ enabled: boolean;
96
+ status: "unavailable";
97
+ message: string;
98
+ };
99
+ analytics: {
100
+ enabled: boolean;
101
+ status: "available";
102
+ route: string | undefined;
103
+ provider: string | undefined;
104
+ } | {
105
+ enabled: boolean;
106
+ status: "unavailable";
107
+ message: string;
108
+ };
109
+ cache: {
110
+ enabled: boolean;
111
+ status: "available";
112
+ route: string | undefined;
113
+ provider: string | undefined;
114
+ } | {
115
+ enabled: boolean;
116
+ status: "unavailable";
117
+ message: string;
118
+ };
119
+ queue: {
120
+ enabled: boolean;
121
+ status: "available";
122
+ route: string | undefined;
123
+ provider: string | undefined;
124
+ } | {
125
+ enabled: boolean;
126
+ status: "unavailable";
127
+ message: string;
128
+ };
129
+ job: {
130
+ enabled: boolean;
131
+ status: "available";
132
+ route: string | undefined;
133
+ provider: string | undefined;
134
+ } | {
135
+ enabled: boolean;
136
+ status: "unavailable";
137
+ message: string;
138
+ };
139
+ ui: {
140
+ enabled: boolean;
141
+ status: "available";
142
+ route: string | undefined;
143
+ provider: string | undefined;
144
+ } | {
145
+ enabled: boolean;
146
+ status: "unavailable";
147
+ message: string;
148
+ };
149
+ workflow: {
150
+ enabled: boolean;
151
+ status: "available";
152
+ route: string | undefined;
153
+ provider: string | undefined;
154
+ } | {
155
+ enabled: boolean;
156
+ status: "unavailable";
157
+ message: string;
158
+ };
159
+ realtime: {
160
+ enabled: boolean;
161
+ status: "available";
162
+ route: string | undefined;
163
+ provider: string | undefined;
164
+ } | {
165
+ enabled: boolean;
166
+ status: "unavailable";
167
+ message: string;
168
+ };
169
+ notification: {
170
+ enabled: boolean;
171
+ status: "available";
172
+ route: string | undefined;
173
+ provider: string | undefined;
174
+ } | {
175
+ enabled: boolean;
176
+ status: "unavailable";
177
+ message: string;
178
+ };
179
+ ai: {
180
+ enabled: boolean;
181
+ status: "available";
182
+ route: string | undefined;
183
+ provider: string | undefined;
184
+ } | {
185
+ enabled: boolean;
186
+ status: "unavailable";
187
+ message: string;
188
+ };
189
+ i18n: {
190
+ enabled: boolean;
191
+ status: "available";
192
+ route: string | undefined;
193
+ provider: string | undefined;
194
+ } | {
195
+ enabled: boolean;
196
+ status: "unavailable";
197
+ message: string;
198
+ };
199
+ graphql: {
200
+ enabled: boolean;
201
+ status: "available";
202
+ route: string | undefined;
203
+ provider: string | undefined;
204
+ } | {
205
+ enabled: boolean;
206
+ status: "unavailable";
207
+ message: string;
208
+ };
209
+ "file-storage": {
210
+ enabled: boolean;
211
+ status: "available";
212
+ route: string | undefined;
213
+ provider: string | undefined;
214
+ } | {
215
+ enabled: boolean;
216
+ status: "unavailable";
217
+ message: string;
218
+ };
219
+ search: {
220
+ enabled: boolean;
221
+ status: "available";
222
+ route: string | undefined;
223
+ provider: string | undefined;
224
+ } | {
225
+ enabled: boolean;
226
+ status: "unavailable";
227
+ message: string;
228
+ };
53
229
  };
54
230
  locale: {
55
231
  default: string;
package/dist/index.d.ts CHANGED
@@ -23,25 +23,33 @@ declare class ObjectStackController {
23
23
  data: string;
24
24
  metadata: string;
25
25
  packages: string;
26
- auth: string;
27
- ui: string;
26
+ auth: string | undefined;
27
+ ui: string | undefined;
28
28
  graphql: string | undefined;
29
29
  storage: string | undefined;
30
30
  analytics: string | undefined;
31
- hub: string | undefined;
32
- automation: string;
31
+ automation: string | undefined;
32
+ workflow: string | undefined;
33
+ realtime: string | undefined;
34
+ notifications: string | undefined;
35
+ ai: string | undefined;
36
+ i18n: string | undefined;
33
37
  };
34
38
  endpoints: {
35
39
  data: string;
36
40
  metadata: string;
37
41
  packages: string;
38
- auth: string;
39
- ui: string;
42
+ auth: string | undefined;
43
+ ui: string | undefined;
40
44
  graphql: string | undefined;
41
45
  storage: string | undefined;
42
46
  analytics: string | undefined;
43
- hub: string | undefined;
44
- automation: string;
47
+ automation: string | undefined;
48
+ workflow: string | undefined;
49
+ realtime: string | undefined;
50
+ notifications: string | undefined;
51
+ ai: string | undefined;
52
+ i18n: string | undefined;
45
53
  };
46
54
  features: {
47
55
  graphql: boolean;
@@ -49,7 +57,175 @@ declare class ObjectStackController {
49
57
  websockets: boolean;
50
58
  files: boolean;
51
59
  analytics: boolean;
52
- hub: boolean;
60
+ ai: boolean;
61
+ workflow: boolean;
62
+ notifications: boolean;
63
+ i18n: boolean;
64
+ };
65
+ services: {
66
+ metadata: {
67
+ enabled: boolean;
68
+ status: "degraded";
69
+ route: string;
70
+ provider: string;
71
+ message: string;
72
+ };
73
+ data: {
74
+ enabled: boolean;
75
+ status: "available";
76
+ route: string | undefined;
77
+ provider: string | undefined;
78
+ };
79
+ auth: {
80
+ enabled: boolean;
81
+ status: "available";
82
+ route: string | undefined;
83
+ provider: string | undefined;
84
+ } | {
85
+ enabled: boolean;
86
+ status: "unavailable";
87
+ message: string;
88
+ };
89
+ automation: {
90
+ enabled: boolean;
91
+ status: "available";
92
+ route: string | undefined;
93
+ provider: string | undefined;
94
+ } | {
95
+ enabled: boolean;
96
+ status: "unavailable";
97
+ message: string;
98
+ };
99
+ analytics: {
100
+ enabled: boolean;
101
+ status: "available";
102
+ route: string | undefined;
103
+ provider: string | undefined;
104
+ } | {
105
+ enabled: boolean;
106
+ status: "unavailable";
107
+ message: string;
108
+ };
109
+ cache: {
110
+ enabled: boolean;
111
+ status: "available";
112
+ route: string | undefined;
113
+ provider: string | undefined;
114
+ } | {
115
+ enabled: boolean;
116
+ status: "unavailable";
117
+ message: string;
118
+ };
119
+ queue: {
120
+ enabled: boolean;
121
+ status: "available";
122
+ route: string | undefined;
123
+ provider: string | undefined;
124
+ } | {
125
+ enabled: boolean;
126
+ status: "unavailable";
127
+ message: string;
128
+ };
129
+ job: {
130
+ enabled: boolean;
131
+ status: "available";
132
+ route: string | undefined;
133
+ provider: string | undefined;
134
+ } | {
135
+ enabled: boolean;
136
+ status: "unavailable";
137
+ message: string;
138
+ };
139
+ ui: {
140
+ enabled: boolean;
141
+ status: "available";
142
+ route: string | undefined;
143
+ provider: string | undefined;
144
+ } | {
145
+ enabled: boolean;
146
+ status: "unavailable";
147
+ message: string;
148
+ };
149
+ workflow: {
150
+ enabled: boolean;
151
+ status: "available";
152
+ route: string | undefined;
153
+ provider: string | undefined;
154
+ } | {
155
+ enabled: boolean;
156
+ status: "unavailable";
157
+ message: string;
158
+ };
159
+ realtime: {
160
+ enabled: boolean;
161
+ status: "available";
162
+ route: string | undefined;
163
+ provider: string | undefined;
164
+ } | {
165
+ enabled: boolean;
166
+ status: "unavailable";
167
+ message: string;
168
+ };
169
+ notification: {
170
+ enabled: boolean;
171
+ status: "available";
172
+ route: string | undefined;
173
+ provider: string | undefined;
174
+ } | {
175
+ enabled: boolean;
176
+ status: "unavailable";
177
+ message: string;
178
+ };
179
+ ai: {
180
+ enabled: boolean;
181
+ status: "available";
182
+ route: string | undefined;
183
+ provider: string | undefined;
184
+ } | {
185
+ enabled: boolean;
186
+ status: "unavailable";
187
+ message: string;
188
+ };
189
+ i18n: {
190
+ enabled: boolean;
191
+ status: "available";
192
+ route: string | undefined;
193
+ provider: string | undefined;
194
+ } | {
195
+ enabled: boolean;
196
+ status: "unavailable";
197
+ message: string;
198
+ };
199
+ graphql: {
200
+ enabled: boolean;
201
+ status: "available";
202
+ route: string | undefined;
203
+ provider: string | undefined;
204
+ } | {
205
+ enabled: boolean;
206
+ status: "unavailable";
207
+ message: string;
208
+ };
209
+ "file-storage": {
210
+ enabled: boolean;
211
+ status: "available";
212
+ route: string | undefined;
213
+ provider: string | undefined;
214
+ } | {
215
+ enabled: boolean;
216
+ status: "unavailable";
217
+ message: string;
218
+ };
219
+ search: {
220
+ enabled: boolean;
221
+ status: "available";
222
+ route: string | undefined;
223
+ provider: string | undefined;
224
+ } | {
225
+ enabled: boolean;
226
+ status: "unavailable";
227
+ message: string;
228
+ };
53
229
  };
54
230
  locale: {
55
231
  default: string;
package/package.json CHANGED
@@ -1,21 +1,24 @@
1
1
  {
2
2
  "name": "@objectstack/nestjs",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "license": "Apache-2.0",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "peerDependencies": {
8
- "@nestjs/common": "^10.0.0",
9
- "@nestjs/core": "^10.0.0",
10
- "@objectstack/runtime": "2.0.0"
8
+ "@nestjs/common": "^11.1.13",
9
+ "@nestjs/core": "^11.1.13",
10
+ "@objectstack/runtime": "2.0.2"
11
11
  },
12
12
  "devDependencies": {
13
- "@nestjs/common": "^10.0.0",
14
- "@nestjs/core": "^10.0.0",
13
+ "@nestjs/common": "^11.1.13",
14
+ "@nestjs/core": "^11.1.13",
15
15
  "typescript": "^5.0.0",
16
- "@objectstack/runtime": "2.0.0"
16
+ "vitest": "^4.0.18",
17
+ "@objectstack/runtime": "2.0.2"
17
18
  },
18
19
  "scripts": {
19
- "build": "tsup --config ../../../tsup.config.ts"
20
+ "build": "tsup --config ../../../tsup.config.ts",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest"
20
23
  }
21
24
  }
@@ -0,0 +1,16 @@
1
+ // Stub for @objectstack/runtime - resolved via vitest alias
2
+ import { vi } from 'vitest';
3
+
4
+ export class HttpDispatcher {
5
+ getDiscoveryInfo = vi.fn().mockReturnValue({ version: '1.0' });
6
+ handleGraphQL = vi.fn().mockResolvedValue({ data: {} });
7
+ handleAuth = vi.fn().mockResolvedValue({ handled: true, response: { status: 200, body: { ok: true } } });
8
+ handleMetadata = vi.fn().mockResolvedValue({ handled: true, response: { status: 200, body: [] } });
9
+ handleData = vi.fn().mockResolvedValue({ handled: true, response: { status: 200, body: [] } });
10
+ handleStorage = vi.fn().mockResolvedValue({ handled: true, response: { status: 200, body: {} } });
11
+
12
+ constructor(_kernel: any) {}
13
+ }
14
+
15
+ export type ObjectKernel = any;
16
+ export type HttpDispatcherResult = any;
@@ -0,0 +1,350 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
4
+
5
+ // Mock NestJS decorators as no-ops
6
+ vi.mock('@nestjs/common', () => {
7
+ const classDecorator = () => (target: any) => target;
8
+ const methodDecorator = () => (_target: any, _key: string, descriptor: PropertyDescriptor) => descriptor;
9
+ const paramDecorator = () => () => (_target: any, _key: string, _index: number) => {};
10
+ return {
11
+ Module: classDecorator,
12
+ Global: () => (target: any) => target,
13
+ Controller: (_prefix?: string) => (target: any) => target,
14
+ Injectable: () => (target: any) => target,
15
+ Inject: (_token: any) => (_target: any, _key: string | undefined, _index: number) => {},
16
+ DynamicModule: class {},
17
+ Post: methodDecorator,
18
+ Get: methodDecorator,
19
+ All: methodDecorator,
20
+ Body: paramDecorator,
21
+ Query: paramDecorator,
22
+ Req: paramDecorator,
23
+ Res: paramDecorator,
24
+ createParamDecorator: (_fn: any) => () => (_target: any, _key: string, _index: number) => {},
25
+ ExecutionContext: class {},
26
+ Provider: class {},
27
+ };
28
+ });
29
+
30
+ import {
31
+ OBJECT_KERNEL,
32
+ ObjectStackModule,
33
+ ObjectStackService,
34
+ ObjectStackController,
35
+ DiscoveryController,
36
+ } from './index.js';
37
+
38
+ // --- Helpers ---
39
+
40
+ function createMockKernel() {
41
+ return { id: 'test-kernel' } as any;
42
+ }
43
+
44
+ function createMockRes() {
45
+ const res: any = {
46
+ _status: 200,
47
+ _body: null,
48
+ _headers: {} as Record<string, string>,
49
+ _redirectUrl: null as string | null,
50
+ status(code: number) { res._status = code; return res; },
51
+ json(body: any) { res._body = body; return res; },
52
+ send(body: any) { res._body = body; return res; },
53
+ setHeader(k: string, v: string) { res._headers[k] = v; return res; },
54
+ redirect(url: string) { res._redirectUrl = url; return res; },
55
+ };
56
+ return res;
57
+ }
58
+
59
+ // --- Tests ---
60
+
61
+ describe('OBJECT_KERNEL constant', () => {
62
+ it('is exported as a string token', () => {
63
+ expect(OBJECT_KERNEL).toBe('OBJECT_KERNEL');
64
+ });
65
+ });
66
+
67
+ describe('ObjectStackModule', () => {
68
+ it('forRoot returns a DynamicModule with correct shape', () => {
69
+ const kernel = createMockKernel();
70
+ const mod = ObjectStackModule.forRoot(kernel);
71
+
72
+ expect(mod).toBeDefined();
73
+ expect(mod.module).toBe(ObjectStackModule);
74
+ expect(mod.controllers).toContain(ObjectStackController);
75
+ expect(mod.controllers).toContain(DiscoveryController);
76
+ expect(mod.providers).toHaveLength(2);
77
+ expect(mod.exports).toHaveLength(2);
78
+ });
79
+
80
+ it('forRoot provides the kernel under OBJECT_KERNEL token', () => {
81
+ const kernel = createMockKernel();
82
+ const mod = ObjectStackModule.forRoot(kernel);
83
+
84
+ const kernelProvider = (mod.providers as any[])?.find(
85
+ (p: any) => p.provide === OBJECT_KERNEL,
86
+ );
87
+ expect(kernelProvider).toBeDefined();
88
+ expect(kernelProvider.useValue).toBe(kernel);
89
+ });
90
+
91
+ it('forRoot exports ObjectStackService', () => {
92
+ const kernel = createMockKernel();
93
+ const mod = ObjectStackModule.forRoot(kernel);
94
+
95
+ expect(mod.exports).toContain(ObjectStackService);
96
+ });
97
+ });
98
+
99
+ describe('ObjectStackService', () => {
100
+ let service: ObjectStackService;
101
+ let kernel: any;
102
+
103
+ beforeEach(() => {
104
+ kernel = createMockKernel();
105
+ service = new ObjectStackService(kernel);
106
+ });
107
+
108
+ it('creates an HttpDispatcher on construction', () => {
109
+ expect(service.dispatcher).toBeDefined();
110
+ });
111
+
112
+ it('getKernel returns the injected kernel', () => {
113
+ expect(service.getKernel()).toBe(kernel);
114
+ });
115
+ });
116
+
117
+ describe('ObjectStackController', () => {
118
+ let controller: ObjectStackController;
119
+ let service: ObjectStackService;
120
+ let res: ReturnType<typeof createMockRes>;
121
+
122
+ beforeEach(() => {
123
+ const kernel = createMockKernel();
124
+ service = new ObjectStackService(kernel);
125
+ controller = new ObjectStackController(service);
126
+ res = createMockRes();
127
+ });
128
+
129
+ it('has all expected route handler methods', () => {
130
+ expect(typeof controller.discovery).toBe('function');
131
+ expect(typeof controller.graphql).toBe('function');
132
+ expect(typeof controller.auth).toBe('function');
133
+ expect(typeof controller.metadata).toBe('function');
134
+ expect(typeof controller.data).toBe('function');
135
+ expect(typeof controller.storage).toBe('function');
136
+ });
137
+
138
+ describe('discovery()', () => {
139
+ it('returns discovery info from the dispatcher', () => {
140
+ const result = controller.discovery();
141
+ expect(result).toEqual({ data: { version: '1.0' } });
142
+ expect(service.dispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api');
143
+ });
144
+ });
145
+
146
+ describe('graphql()', () => {
147
+ it('dispatches to handleGraphQL and returns json', async () => {
148
+ const req = { headers: {} };
149
+ const body = { query: '{ objects { name } }' };
150
+
151
+ await controller.graphql(body, req, res);
152
+
153
+ expect(service.dispatcher.handleGraphQL).toHaveBeenCalledWith(body, { request: req });
154
+ expect(res._body).toEqual({ data: {} });
155
+ });
156
+
157
+ it('handles errors from handleGraphQL', async () => {
158
+ (service.dispatcher.handleGraphQL as any).mockRejectedValueOnce(
159
+ Object.assign(new Error('GQL Error'), { statusCode: 400 }),
160
+ );
161
+
162
+ await controller.graphql({}, {}, res);
163
+
164
+ expect(res._status).toBe(400);
165
+ expect(res._body.success).toBe(false);
166
+ expect(res._body.error.message).toBe('GQL Error');
167
+ });
168
+ });
169
+
170
+ describe('auth()', () => {
171
+ it('dispatches to handleAuth with extracted path', async () => {
172
+ const req = { params: { 0: 'login' }, url: '/api/auth/login', method: 'POST' };
173
+ const body = { username: 'admin' };
174
+
175
+ await controller.auth(req, res, body);
176
+
177
+ expect(service.dispatcher.handleAuth).toHaveBeenCalledWith(
178
+ 'login', 'POST', body, { request: req, response: res },
179
+ );
180
+ });
181
+
182
+ it('falls back to URL parsing for path extraction', async () => {
183
+ const req = { params: {}, url: '/api/auth/callback?code=abc', method: 'GET' };
184
+
185
+ await controller.auth(req, res, {});
186
+
187
+ expect(service.dispatcher.handleAuth).toHaveBeenCalledWith(
188
+ 'callback', 'GET', {}, { request: req, response: res },
189
+ );
190
+ });
191
+ });
192
+
193
+ describe('metadata()', () => {
194
+ it('dispatches to handleMetadata with extracted path', async () => {
195
+ const req = { params: { 0: '' }, url: '/api/meta/objects', method: 'GET' };
196
+
197
+ await controller.metadata(req, res, undefined);
198
+
199
+ expect(service.dispatcher.handleMetadata).toHaveBeenCalledWith(
200
+ '/objects', { request: req }, 'GET', undefined,
201
+ );
202
+ });
203
+ });
204
+
205
+ describe('data()', () => {
206
+ it('dispatches to handleData with extracted path', async () => {
207
+ const req = { params: { 0: '' }, url: '/api/data/account', method: 'GET' };
208
+ const query = { limit: '10' };
209
+
210
+ await controller.data(req, res, {}, query);
211
+
212
+ expect(service.dispatcher.handleData).toHaveBeenCalledWith(
213
+ '/account', 'GET', {}, query, { request: req },
214
+ );
215
+ });
216
+ });
217
+
218
+ describe('storage()', () => {
219
+ it('dispatches to handleStorage with extracted path', async () => {
220
+ const req = { params: { 0: '' }, url: '/api/storage/files/test.png', method: 'GET', file: null, files: {} };
221
+
222
+ await controller.storage(req, res);
223
+
224
+ expect(service.dispatcher.handleStorage).toHaveBeenCalledWith(
225
+ '/files/test.png', 'GET', undefined, { request: req },
226
+ );
227
+ });
228
+ });
229
+
230
+ describe('normalizeResponse (via handlers)', () => {
231
+ it('returns 404 when result is not handled', async () => {
232
+ (service.dispatcher.handleAuth as any).mockResolvedValueOnce({ handled: false });
233
+ const req = { params: { 0: 'noop' }, url: '/api/auth/noop', method: 'GET' };
234
+
235
+ await controller.auth(req, res, {});
236
+
237
+ expect(res._status).toBe(404);
238
+ expect(res._body.error.code).toBe(404);
239
+ });
240
+
241
+ it('sets custom headers from response', async () => {
242
+ (service.dispatcher.handleData as any).mockResolvedValueOnce({
243
+ handled: true,
244
+ response: { status: 201, body: { id: 1 }, headers: { 'X-Custom': 'yes' } },
245
+ });
246
+ const req = { params: {}, url: '/api/data/account', method: 'POST' };
247
+
248
+ await controller.data(req, res, {}, {});
249
+
250
+ expect(res._status).toBe(201);
251
+ expect(res._headers['X-Custom']).toBe('yes');
252
+ expect(res._body).toEqual({ id: 1 });
253
+ });
254
+
255
+ it('handles redirect results', async () => {
256
+ (service.dispatcher.handleAuth as any).mockResolvedValueOnce({
257
+ handled: true,
258
+ result: { type: 'redirect', url: 'https://example.com/callback' },
259
+ });
260
+ const req = { params: { 0: 'oauth' }, url: '/api/auth/oauth', method: 'GET' };
261
+
262
+ await controller.auth(req, res, {});
263
+
264
+ expect(res._redirectUrl).toBe('https://example.com/callback');
265
+ });
266
+
267
+ it('handles stream results', async () => {
268
+ const pipeFn = vi.fn();
269
+ (service.dispatcher.handleStorage as any).mockResolvedValueOnce({
270
+ handled: true,
271
+ result: {
272
+ type: 'stream',
273
+ stream: { pipe: pipeFn },
274
+ headers: { 'Content-Type': 'application/octet-stream' },
275
+ },
276
+ });
277
+ const req = { params: {}, url: '/api/storage/download', method: 'GET', file: null, files: {} };
278
+
279
+ await controller.storage(req, res);
280
+
281
+ expect(pipeFn).toHaveBeenCalledWith(res);
282
+ expect(res._headers['Content-Type']).toBe('application/octet-stream');
283
+ });
284
+
285
+ it('handles generic result objects with 200 status', async () => {
286
+ (service.dispatcher.handleData as any).mockResolvedValueOnce({
287
+ handled: true,
288
+ result: { foo: 'bar' },
289
+ });
290
+ const req = { params: {}, url: '/api/data/x', method: 'GET' };
291
+
292
+ await controller.data(req, res, {}, {});
293
+
294
+ expect(res._status).toBe(200);
295
+ expect(res._body).toEqual({ foo: 'bar' });
296
+ });
297
+
298
+ it('handles Response-like objects', async () => {
299
+ const mockHeaders = new Map([['content-type', 'text/plain']]);
300
+ (service.dispatcher.handleData as any).mockResolvedValueOnce({
301
+ handled: true,
302
+ result: {
303
+ status: 203,
304
+ headers: mockHeaders,
305
+ text: vi.fn().mockResolvedValue('hello world'),
306
+ },
307
+ });
308
+ const req = { params: {}, url: '/api/data/x', method: 'GET' };
309
+
310
+ await controller.data(req, res, {}, {});
311
+
312
+ expect(res._status).toBe(203);
313
+ expect(res._body).toBe('hello world');
314
+ });
315
+ });
316
+
317
+ describe('handleError', () => {
318
+ it('uses statusCode from error if available', async () => {
319
+ (service.dispatcher.handleGraphQL as any).mockRejectedValueOnce(
320
+ Object.assign(new Error('Forbidden'), { statusCode: 403, details: { reason: 'no access' } }),
321
+ );
322
+
323
+ await controller.graphql({}, {}, res);
324
+
325
+ expect(res._status).toBe(403);
326
+ expect(res._body.error.message).toBe('Forbidden');
327
+ expect(res._body.error.details).toEqual({ reason: 'no access' });
328
+ });
329
+
330
+ it('defaults to 500 when no statusCode', async () => {
331
+ (service.dispatcher.handleGraphQL as any).mockRejectedValueOnce(new Error('Unexpected'));
332
+
333
+ await controller.graphql({}, {}, res);
334
+
335
+ expect(res._status).toBe(500);
336
+ expect(res._body.error.code).toBe(500);
337
+ });
338
+ });
339
+ });
340
+
341
+ describe('DiscoveryController', () => {
342
+ it('redirects to /api', () => {
343
+ const controller = new DiscoveryController();
344
+ const res = createMockRes();
345
+
346
+ controller.discover(res);
347
+
348
+ expect(res._redirectUrl).toBe('/api');
349
+ });
350
+ });
@@ -0,0 +1,16 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import { defineConfig } from 'vitest/config';
4
+ import path from 'node:path';
5
+
6
+ export default defineConfig({
7
+ test: {
8
+ globals: true,
9
+ environment: 'node',
10
+ },
11
+ resolve: {
12
+ alias: {
13
+ '@objectstack/runtime': path.resolve(__dirname, 'src/__mocks__/runtime.ts'),
14
+ },
15
+ },
16
+ });