@objectstack/plugin-msw 4.0.4 → 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/src/msw-plugin.ts DELETED
@@ -1,417 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { http, HttpResponse, passthrough } from 'msw';
4
- import { setupWorker } from 'msw/browser';
5
- import {
6
- Plugin,
7
- PluginContext,
8
- ObjectKernel,
9
- HttpDispatcher,
10
- HttpDispatcherResult
11
- } from '@objectstack/runtime';
12
- // import { ObjectStackProtocolImplementation } from '@objectstack/objectql';
13
- import { ObjectStackProtocol } from '@objectstack/spec/api';
14
- import { IDataEngine } from '@objectstack/core';
15
-
16
- // Helper for parsing query parameters
17
- function parseQueryParams(url: URL): Record<string, any> {
18
- const params: Record<string, any> = {};
19
- const keys = Array.from(new Set(url.searchParams.keys()));
20
-
21
- for (const key of keys) {
22
- const values = url.searchParams.getAll(key);
23
- // If single value, use it directly. If multiple, keep as array.
24
- const rawValue = values.length === 1 ? values[0] : values;
25
-
26
- // Helper to parse individual value
27
- const parseValue = (val: string) => {
28
- if (val === 'true') return true;
29
- if (val === 'false') return false;
30
- if (val === 'null') return null;
31
- if (val === 'undefined') return undefined;
32
-
33
- // Try number (integers only or floats)
34
- // Safety check: Don't convert if it loses information (like leading zeros)
35
- const num = Number(val);
36
- if (!isNaN(num) && val.trim() !== '' && String(num) === val) {
37
- return num;
38
- }
39
-
40
- // Try JSON
41
- if ((val.startsWith('{') && val.endsWith('}')) || (val.startsWith('[') && val.endsWith(']'))) {
42
- try {
43
- return JSON.parse(val);
44
- } catch {}
45
- }
46
-
47
- return val;
48
- };
49
-
50
- if (Array.isArray(rawValue)) {
51
- params[key] = rawValue.map(parseValue);
52
- } else {
53
- params[key] = parseValue(rawValue as string);
54
- }
55
- }
56
-
57
- return params;
58
- }
59
-
60
- export interface MSWPluginOptions {
61
- /**
62
- * Enable MSW in the browser environment
63
- */
64
- enableBrowser?: boolean;
65
-
66
- /**
67
- * Custom handlers to add to MSW
68
- */
69
- customHandlers?: Array<any>;
70
-
71
- /**
72
- * Base URL for API endpoints
73
- */
74
- baseUrl?: string;
75
-
76
- /**
77
- * Whether to log requests
78
- */
79
- logRequests?: boolean;
80
- }
81
-
82
- /**
83
- * MSW Plugin for ObjectStack
84
-
85
- *
86
- * This plugin enables Mock Service Worker integration for testing and development.
87
- * It automatically mocks API endpoints using the ObjectStack runtime protocol.
88
- *
89
- * @example
90
- * ```typescript
91
- * import { MSWPlugin } from '@objectstack/plugin-msw';
92
- *
93
- * // With ObjectKernel
94
- * const kernel = new ObjectKernel();
95
- * kernel.use(new MSWPlugin({
96
- * enableBrowser: true,
97
- * baseUrl: '/api/v1'
98
- * }));
99
- * ```
100
- */
101
- export class MSWPlugin implements Plugin {
102
- name = 'com.objectstack.plugin.msw';
103
- type = 'server';
104
- version = '0.9.0';
105
-
106
- private options: MSWPluginOptions;
107
- private worker: any;
108
- private handlers: Array<any> = [];
109
- private protocol?: ObjectStackProtocol;
110
- private dispatcher?: HttpDispatcher;
111
-
112
- constructor(options: MSWPluginOptions = {}) {
113
- this.options = {
114
- enableBrowser: true,
115
- baseUrl: '/api/v1',
116
- logRequests: true,
117
- ...options
118
- };
119
- }
120
-
121
- /**
122
- * Init phase
123
- */
124
- init = async (ctx: PluginContext) => {
125
- ctx.logger.debug('Initializing MSW plugin', {
126
- enableBrowser: this.options.enableBrowser,
127
- baseUrl: this.options.baseUrl,
128
- logRequests: this.options.logRequests
129
- });
130
- // Protocol will be created in start phase
131
- ctx.logger.info('MSW plugin initialized');
132
- }
133
-
134
- /**
135
- * Start phase
136
- */
137
- start = async (ctx: PluginContext) => {
138
- ctx.logger.debug('Starting MSW plugin');
139
-
140
- try {
141
- // 1. Try to get existing protocol service
142
- try {
143
- this.protocol = ctx.getService<ObjectStackProtocol>('protocol');
144
- ctx.logger.debug('Protocol service found from context');
145
- } catch (e) {
146
- // Ignore, will try to create default implementation
147
- }
148
-
149
- // 2. If not found, try to instantiate default implementation dynamically
150
- if (!this.protocol) {
151
- try {
152
- const dataEngine = ctx.getService<IDataEngine>('objectql');
153
- // Dynamically import ObjectStackProtocolImplementation to avoid hard dependency
154
- const { ObjectStackProtocolImplementation } = await import('@objectstack/objectql');
155
- this.protocol = new ObjectStackProtocolImplementation(dataEngine);
156
- ctx.logger.debug('Protocol implementation created dynamically');
157
- } catch (e: any) {
158
- if (e.code === 'ERR_MODULE_NOT_FOUND') {
159
- ctx.logger.warn('Module @objectstack/objectql not found. Protocol not initialized.');
160
- } else {
161
- throw e;
162
- }
163
- }
164
- }
165
-
166
- if (!this.protocol) {
167
- // Without a protocol, MSW can't serve data APIs
168
- ctx.logger.warn('No ObjectStackProtocol service available. MSW will only serve static/custom handlers if configured.');
169
- }
170
-
171
- } catch (e) {
172
- ctx.logger.error('Failed to initialize protocol', e as Error);
173
- throw new Error('[MSWPlugin] Failed to initialize protocol');
174
- }
175
-
176
- this.setupHandlers(ctx);
177
- await this.startWorker(ctx);
178
- }
179
-
180
- /**
181
- * Destroy phase
182
- */
183
- async destroy() {
184
- await this.stopWorker();
185
- }
186
-
187
- /**
188
- * Setup MSW handlers
189
- */
190
- private setupHandlers(ctx: PluginContext) {
191
- // Initialize HttpDispatcher
192
- try {
193
- this.dispatcher = new HttpDispatcher(ctx.getKernel());
194
- } catch (e) {
195
- ctx.logger.warn('[MSWPlugin] Could not initialize HttpDispatcher via Kernel. Falling back to simple handlers.');
196
- }
197
-
198
- const baseUrl = this.options.baseUrl || '/api/v1';
199
-
200
- // Custom handlers have priority
201
- this.handlers = [
202
- ...(this.options.customHandlers || [])
203
- ];
204
-
205
- // Discovery Endpoint
206
- this.handlers.push(
207
- http.get('*/.well-known/objectstack', async () => {
208
- if (this.dispatcher) {
209
- return HttpResponse.json({
210
- data: await this.dispatcher.getDiscoveryInfo(baseUrl)
211
- });
212
- }
213
- return HttpResponse.json({
214
- data: {
215
- version: 'v1',
216
- apiName: 'ObjectStack API',
217
- url: baseUrl,
218
- capabilities: {
219
- graphql: false,
220
- search: false,
221
- websockets: false,
222
- files: false,
223
- analytics: false,
224
- hub: false
225
- }
226
- }
227
- });
228
- })
229
- );
230
-
231
- // Explicit /discovery endpoint — must be registered before catch-all
232
- // so dispatch() is not called with an empty prefix.
233
- this.handlers.push(
234
- http.get(`*${baseUrl}`, async () => {
235
- if (this.dispatcher) {
236
- return HttpResponse.json({ data: await this.dispatcher.getDiscoveryInfo(baseUrl) });
237
- }
238
- return HttpResponse.json({ data: { version: 'v1', url: baseUrl } });
239
- }),
240
- http.get(`*${baseUrl}/discovery`, async () => {
241
- if (this.dispatcher) {
242
- return HttpResponse.json({ data: await this.dispatcher.getDiscoveryInfo(baseUrl) });
243
- }
244
- return HttpResponse.json({ data: { version: 'v1', url: baseUrl } });
245
- })
246
- );
247
-
248
- if (this.dispatcher) {
249
- const dispatcher = this.dispatcher;
250
-
251
- // Catch-all handler for ObjectStack Runtime
252
- // We use a wildcard to capture all methods and paths under baseUrl
253
- const catchAll = async ({ request, params }: any) => {
254
- const url = new URL(request.url);
255
- // Calculate path relative to API prefix
256
- // e.g. /api/v1/data/contacts -> /data/contacts
257
- let path = url.pathname;
258
- if (path.startsWith(baseUrl)) {
259
- path = path.slice(baseUrl.length);
260
- }
261
-
262
- if (this.options.logRequests) {
263
- // eslint-disable-next-line no-console
264
- console.log(`[MSW] Intercepted: ${request.method} ${url.pathname}`, { path });
265
- }
266
-
267
- // Parse Body if present
268
- let body: any = undefined;
269
- if (request.method !== 'GET' && request.method !== 'HEAD') {
270
- try {
271
- body = await request.clone().json();
272
- } catch (e) {
273
- try {
274
- // Try form data if json fails?
275
- // Dispatcher expects objects usually.
276
- // For file upload, body might be FormData logic needed?
277
- // For now assume JSON or text
278
- } catch (e2) {}
279
- }
280
- }
281
-
282
- // Parse Query
283
- const query = parseQueryParams(url);
284
-
285
- // Dispatch
286
- const result = await dispatcher.dispatch(
287
- request.method,
288
- path,
289
- body,
290
- query,
291
- { request },
292
- baseUrl
293
- );
294
-
295
- if (result.handled) {
296
- if (result.response) {
297
- return HttpResponse.json(result.response.body, {
298
- status: result.response.status,
299
- headers: result.response.headers as any
300
- });
301
- }
302
- if (result.result) {
303
- // Handle special results (streams/redirects - unlikely in MSW but possible)
304
- if (result.result.type === 'redirect') {
305
- return HttpResponse.redirect(result.result.url);
306
- }
307
- // Fallback for others
308
- return HttpResponse.json(result.result);
309
- }
310
- }
311
-
312
- // Not handled by dispatcher (404 for this route subset)
313
- return undefined; // Let MSW pass through or handle next
314
- };
315
-
316
- this.handlers.push(
317
- http.all(`*${baseUrl}/*`, catchAll),
318
- http.all(`*${baseUrl}`, catchAll) // Handle root if needed
319
- );
320
-
321
- ctx.logger.info('MSW handlers set up using HttpDispatcher', { baseUrl });
322
- } else {
323
- ctx.logger.warn('[MSWPlugin] No dispatcher available. No API routes registered.');
324
- }
325
- }
326
-
327
-
328
- /**
329
- * Start the MSW worker
330
- */
331
- private async startWorker(ctx: PluginContext) {
332
- if (this.options.enableBrowser && typeof window !== 'undefined') {
333
- // Browser environment
334
- ctx.logger.debug('Starting MSW in browser mode');
335
- this.worker = setupWorker(...this.handlers);
336
- await this.worker.start({
337
- onUnhandledRequest: 'bypass',
338
- });
339
- ctx.logger.info('MSW started in browser mode');
340
- } else {
341
- ctx.logger.debug('MSW browser mode disabled or not in browser environment');
342
- }
343
- }
344
-
345
- /**
346
- * Stop the MSW worker
347
- */
348
- private async stopWorker() {
349
- if (this.worker) {
350
- this.worker.stop();
351
- console.log('[MSWPlugin] Stopped MSW worker');
352
- }
353
- }
354
-
355
- /**
356
- * Get the MSW worker instance for advanced use cases
357
- */
358
- getWorker() {
359
- return this.worker;
360
- }
361
-
362
- /**
363
- * Get registered handlers
364
- */
365
- getHandlers() {
366
- return this.handlers;
367
- }
368
- }
369
-
370
- /**
371
- * Static helper for interacting with ObjectStack protocol in MSW handlers
372
- */
373
- export class ObjectStackServer {
374
- private static protocol: ObjectStackProtocol;
375
-
376
- static init(protocol: ObjectStackProtocol) {
377
- this.protocol = protocol;
378
- }
379
-
380
- private static getProtocol(): ObjectStackProtocol {
381
- if (!this.protocol) {
382
- throw new Error('ObjectStackServer not initialized. Call ObjectStackServer.init(protocol) first.');
383
- }
384
- return this.protocol;
385
- }
386
-
387
- static async findData(objectName: string, query?: any) {
388
- const body = await this.getProtocol().findData({ object: objectName, query });
389
- return { data: body, status: 200 };
390
- }
391
-
392
- static async getData(objectName: string, id: string, options?: { expand?: string | string[]; select?: string | string[] }) {
393
- const normalize = (v?: string | string[]) => v == null ? undefined : Array.isArray(v) ? v : [v];
394
- const body = await this.getProtocol().getData({
395
- object: objectName,
396
- id,
397
- select: normalize(options?.select),
398
- expand: normalize(options?.expand),
399
- });
400
- return { data: body, status: 200 };
401
- }
402
-
403
- static async createData(objectName: string, data: any) {
404
- const body = await this.getProtocol().createData({ object: objectName, data });
405
- return { data: body, status: 201 };
406
- }
407
-
408
- static async updateData(objectName: string, id: string, data: any) {
409
- const body = await this.getProtocol().updateData({ object: objectName, id, data });
410
- return { data: body, status: 200 };
411
- }
412
-
413
- static async deleteData(objectName: string, id: string) {
414
- const body = await this.getProtocol().deleteData({ object: objectName, id });
415
- return { data: body, status: 200 };
416
- }
417
- }
package/tsconfig.json DELETED
@@ -1,26 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "ES2020",
5
- "moduleResolution": "bundler",
6
- "declaration": true,
7
- "outDir": "./dist",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true,
12
- "types": [
13
- "node"
14
- ],
15
- "ignoreDeprecations": "6.0",
16
- "rootDir": "./src"
17
- },
18
- "include": [
19
- "src/**/*"
20
- ],
21
- "exclude": [
22
- "node_modules",
23
- "dist",
24
- "**/*.test.ts"
25
- ]
26
- }