@objectstack/plugin-msw 0.6.1 → 0.7.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.
package/src/msw-plugin.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { http, HttpResponse } from 'msw';
1
+ import { http, HttpResponse, passthrough } from 'msw';
2
2
  import { setupWorker } from 'msw/browser';
3
3
  import {
4
4
  Plugin,
@@ -7,7 +7,7 @@ import {
7
7
  IDataEngine
8
8
  } from '@objectstack/runtime';
9
9
  import { ObjectStackProtocolImplementation } from '@objectstack/objectql';
10
- import { IObjectStackProtocol } from '@objectstack/spec/api';
10
+ import { ObjectStackProtocol } from '@objectstack/spec/api';
11
11
  // import { IDataEngine } from '@objectstack/core';
12
12
 
13
13
  export interface MSWPluginOptions {
@@ -36,12 +36,17 @@ export interface MSWPluginOptions {
36
36
  * ObjectStack Server Mock - Provides mock database functionality
37
37
  */
38
38
  export class ObjectStackServer {
39
- private static protocol: IObjectStackProtocol | null = null;
40
- private static logger: ((message: string, ...meta: any[]) => void) | null = null;
39
+ private static protocol: ObjectStackProtocol | null = null;
40
+ private static logger: any | null = null;
41
41
 
42
- static init(protocol: IObjectStackProtocol, logger?: (message: string, ...meta: any[]) => void) {
42
+ static init(protocol: ObjectStackProtocol, logger?: any) {
43
43
  this.protocol = protocol;
44
- this.logger = logger || console.log;
44
+ this.logger = logger || {
45
+ info: console.log,
46
+ debug: console.debug,
47
+ warn: console.warn,
48
+ error: console.error
49
+ };
45
50
  }
46
51
 
47
52
  static async findData(object: string, params?: any) {
@@ -49,8 +54,9 @@ export class ObjectStackServer {
49
54
  throw new Error('ObjectStackServer not initialized. Call ObjectStackServer.init() first.');
50
55
  }
51
56
 
52
- this.logger?.(`[MSW] Finding ${object} records`, params);
53
- const result = await this.protocol.findData(object, params || {});
57
+ this.logger?.debug?.('MSW: Finding records', { object, params });
58
+ const result = await this.protocol.findData({ object, query: params || {} });
59
+ this.logger?.debug?.('MSW: Find completed', { object, count: result?.records?.length ?? 0 });
54
60
  return {
55
61
  status: 200,
56
62
  data: result
@@ -62,14 +68,16 @@ export class ObjectStackServer {
62
68
  throw new Error('ObjectStackServer not initialized. Call ObjectStackServer.init() first.');
63
69
  }
64
70
 
65
- this.logger?.(`[MSW] Getting ${object} record:`, id);
71
+ this.logger?.debug?.('MSW: Getting record', { object, id });
66
72
  try {
67
- const result = await this.protocol.getData(object, id);
73
+ const result = await this.protocol.getData({ object, id });
74
+ this.logger?.debug?.('MSW: Get completed', { object, id });
68
75
  return {
69
76
  status: 200,
70
77
  data: result
71
78
  };
72
79
  } catch (error) {
80
+ this.logger?.warn?.('MSW: Get failed - not found', { object, id });
73
81
  const message = error instanceof Error ? error.message : 'Unknown error';
74
82
  return {
75
83
  status: 404,
@@ -83,14 +91,16 @@ export class ObjectStackServer {
83
91
  throw new Error('ObjectStackServer not initialized. Call ObjectStackServer.init() first.');
84
92
  }
85
93
 
86
- this.logger?.(`[MSW] Creating ${object} record:`, data);
94
+ this.logger?.debug?.('MSW: Creating record', { object });
87
95
  try {
88
- const result = await this.protocol.createData(object, data);
96
+ const result = await this.protocol.createData({ object, data });
97
+ this.logger?.info?.('MSW: Record created', { object, id: result?.id });
89
98
  return {
90
99
  status: 201,
91
100
  data: result
92
101
  };
93
102
  } catch (error) {
103
+ this.logger?.error?.('MSW: Create failed', error, { object });
94
104
  const message = error instanceof Error ? error.message : 'Unknown error';
95
105
  return {
96
106
  status: 400,
@@ -104,14 +114,16 @@ export class ObjectStackServer {
104
114
  throw new Error('ObjectStackServer not initialized. Call ObjectStackServer.init() first.');
105
115
  }
106
116
 
107
- this.logger?.(`[MSW] Updating ${object} record ${id}:`, data);
117
+ this.logger?.debug?.('MSW: Updating record', { object, id });
108
118
  try {
109
- const result = await this.protocol.updateData(object, id, data);
119
+ const result = await this.protocol.updateData({ object, id, data });
120
+ this.logger?.info?.('MSW: Record updated', { object, id });
110
121
  return {
111
122
  status: 200,
112
123
  data: result
113
124
  };
114
125
  } catch (error) {
126
+ this.logger?.error?.('MSW: Update failed', error, { object, id });
115
127
  const message = error instanceof Error ? error.message : 'Unknown error';
116
128
  return {
117
129
  status: 400,
@@ -125,14 +137,16 @@ export class ObjectStackServer {
125
137
  throw new Error('ObjectStackServer not initialized. Call ObjectStackServer.init() first.');
126
138
  }
127
139
 
128
- this.logger?.(`[MSW] Deleting ${object} record:`, id);
140
+ this.logger?.debug?.('MSW: Deleting record', { object, id });
129
141
  try {
130
- const result = await this.protocol.deleteData(object, id);
142
+ const result = await this.protocol.deleteData({ object, id });
143
+ this.logger?.info?.('MSW: Record deleted', { object, id, success: result?.success });
131
144
  return {
132
145
  status: 200,
133
146
  data: result
134
147
  };
135
148
  } catch (error) {
149
+ this.logger?.error?.('MSW: Delete failed', error, { object, id });
136
150
  const message = error instanceof Error ? error.message : 'Unknown error';
137
151
  return {
138
152
  status: 400,
@@ -176,7 +190,7 @@ export class MSWPlugin implements Plugin {
176
190
  private options: MSWPluginOptions;
177
191
  private worker: any;
178
192
  private handlers: Array<any> = [];
179
- private protocol?: IObjectStackProtocol;
193
+ private protocol?: ObjectStackProtocol;
180
194
 
181
195
  constructor(options: MSWPluginOptions = {}) {
182
196
  this.options = {
@@ -191,24 +205,32 @@ export class MSWPlugin implements Plugin {
191
205
  * Init phase
192
206
  */
193
207
  async init(ctx: PluginContext) {
208
+ ctx.logger.debug('Initializing MSW plugin', {
209
+ enableBrowser: this.options.enableBrowser,
210
+ baseUrl: this.options.baseUrl,
211
+ logRequests: this.options.logRequests
212
+ });
194
213
  // Protocol will be created in start phase
195
- ctx.logger.log('[MSWPlugin] Initialized');
214
+ ctx.logger.info('MSW plugin initialized');
196
215
  }
197
216
 
198
217
  /**
199
218
  * Start phase
200
219
  */
201
220
  async start(ctx: PluginContext) {
221
+ ctx.logger.debug('Starting MSW plugin');
222
+
202
223
  try {
203
224
  const dataEngine = ctx.getService<IDataEngine>('objectql');
204
225
  this.protocol = new ObjectStackProtocolImplementation(dataEngine);
226
+ ctx.logger.debug('Protocol implementation created');
205
227
  } catch (e) {
206
- console.error('[MSWPlugin] Failed to initialize protocol', e);
228
+ ctx.logger.error('Failed to initialize protocol', e as Error);
207
229
  throw new Error('[MSWPlugin] Failed to initialize protocol (missing objectql service?)');
208
230
  }
209
231
 
210
- this.setupHandlers();
211
- await this.startWorker();
232
+ this.setupHandlers(ctx);
233
+ await this.startWorker(ctx);
212
234
  }
213
235
 
214
236
  /**
@@ -221,41 +243,56 @@ export class MSWPlugin implements Plugin {
221
243
  /**
222
244
  * Setup MSW handlers
223
245
  */
224
- private setupHandlers() {
246
+ private setupHandlers(ctx: PluginContext) {
225
247
  if (!this.protocol) {
226
248
  throw new Error('[MSWPlugin] Protocol not initialized');
227
249
  }
228
250
 
229
251
  const protocol = this.protocol;
230
252
 
231
- // Initialize ObjectStackServer
253
+ // Initialize ObjectStackServer with structured logger
232
254
  ObjectStackServer.init(
233
255
  protocol,
234
- this.options.logRequests ? console.log : undefined
256
+ this.options.logRequests ? ctx.logger : undefined
235
257
  );
236
258
 
259
+ ctx.logger.debug('Initialized ObjectStackServer', { logRequests: this.options.logRequests });
260
+
237
261
  const baseUrl = this.options.baseUrl || '/api/v1';
238
262
 
239
263
  // Define standard ObjectStack API handlers
240
264
  this.handlers = [
265
+ // Passthrough for external resources
266
+ http.get('https://fonts.googleapis.com/*', () => passthrough()),
267
+ http.get('https://fonts.gstatic.com/*', () => passthrough()),
268
+
241
269
  // Discovery endpoint
242
- http.get(`${baseUrl}`, () => {
243
- return HttpResponse.json(protocol.getDiscovery());
270
+ http.get(`${baseUrl}`, async () => {
271
+ const discovery = await protocol.getDiscovery({});
272
+ return HttpResponse.json({
273
+ ...discovery,
274
+ routes: {
275
+ data: `${baseUrl}/data`,
276
+ metadata: `${baseUrl}/meta`,
277
+ ui: `${baseUrl}/ui`,
278
+ auth: `${baseUrl}/auth`
279
+ }
280
+ });
244
281
  }),
245
282
 
246
283
  // Meta endpoints
247
- http.get(`${baseUrl}/meta`, () => {
248
- return HttpResponse.json(protocol.getMetaTypes());
284
+ http.get(`${baseUrl}/meta`, async () => {
285
+ return HttpResponse.json(await protocol.getMetaTypes({}));
249
286
  }),
250
287
 
251
- http.get(`${baseUrl}/meta/:type`, ({ params }) => {
252
- return HttpResponse.json(protocol.getMetaItems(params.type as string));
288
+ http.get(`${baseUrl}/meta/:type`, async ({ params }) => {
289
+ return HttpResponse.json(await protocol.getMetaItems({ type: params.type as string }));
253
290
  }),
254
291
 
255
- http.get(`${baseUrl}/meta/:type/:name`, ({ params }) => {
292
+ http.get(`${baseUrl}/meta/:type/:name`, async ({ params }) => {
256
293
  try {
257
294
  return HttpResponse.json(
258
- protocol.getMetaItem(params.type as string, params.name as string)
295
+ await protocol.getMetaItem({ type: params.type as string, name: params.name as string })
259
296
  );
260
297
  } catch (error) {
261
298
  const message = error instanceof Error ? error.message : 'Unknown error';
@@ -276,7 +313,10 @@ export class MSWPlugin implements Plugin {
276
313
  params.object as string,
277
314
  queryParams
278
315
  );
279
- return HttpResponse.json(result.data, { status: result.status });
316
+ return HttpResponse.json(result.data, {
317
+ status: result.status,
318
+ headers: { 'Cache-Control': 'no-store' }
319
+ });
280
320
  } catch (error) {
281
321
  const message = error instanceof Error ? error.message : 'Unknown error';
282
322
  return HttpResponse.json({ error: message }, { status: 404 });
@@ -289,7 +329,10 @@ export class MSWPlugin implements Plugin {
289
329
  params.object as string,
290
330
  params.id as string
291
331
  );
292
- return HttpResponse.json(result.data, { status: result.status });
332
+ return HttpResponse.json(result.data, {
333
+ status: result.status,
334
+ headers: { 'Cache-Control': 'no-store' }
335
+ });
293
336
  } catch (error) {
294
337
  const message = error instanceof Error ? error.message : 'Unknown error';
295
338
  return HttpResponse.json({ error: message }, { status: 404 });
@@ -338,12 +381,106 @@ export class MSWPlugin implements Plugin {
338
381
  }
339
382
  }),
340
383
 
384
+ // Batch Operations
385
+ http.post(`${baseUrl}/data/:object/batch`, async ({ params, request }) => {
386
+ try {
387
+ const body = await request.json();
388
+ const result = await protocol.batchData({ object: params.object as string, request: body as any });
389
+ return HttpResponse.json(result);
390
+ } catch (error) {
391
+ const message = error instanceof Error ? error.message : 'Unknown error';
392
+ return HttpResponse.json({ error: message }, { status: 400 });
393
+ }
394
+ }),
395
+
396
+ http.post(`${baseUrl}/data/:object/createMany`, async ({ params, request }) => {
397
+ try {
398
+ const body = await request.json();
399
+ const records = Array.isArray(body) ? body : [];
400
+ const result = await protocol.createManyData({ object: params.object as string, records });
401
+ return HttpResponse.json(result, { status: 201 });
402
+ } catch (error) {
403
+ const message = error instanceof Error ? error.message : 'Unknown error';
404
+ return HttpResponse.json({ error: message }, { status: 400 });
405
+ }
406
+ }),
407
+
408
+ http.post(`${baseUrl}/data/:object/updateMany`, async ({ params, request }) => {
409
+ try {
410
+ const body = await request.json() as any;
411
+ const result = await protocol.updateManyData({
412
+ object: params.object as string,
413
+ records: body?.records || [],
414
+ options: body?.options
415
+ });
416
+ return HttpResponse.json(result);
417
+ } catch (error) {
418
+ const message = error instanceof Error ? error.message : 'Unknown error';
419
+ return HttpResponse.json({ error: message }, { status: 400 });
420
+ }
421
+ }),
422
+
423
+ http.post(`${baseUrl}/data/:object/deleteMany`, async ({ params, request }) => {
424
+ try {
425
+ const body = await request.json() as any;
426
+ const result = await protocol.deleteManyData({
427
+ object: params.object as string,
428
+ ids: body?.ids || [],
429
+ options: body?.options
430
+ });
431
+ return HttpResponse.json(result);
432
+ } catch (error) {
433
+ const message = error instanceof Error ? error.message : 'Unknown error';
434
+ return HttpResponse.json({ error: message }, { status: 400 });
435
+ }
436
+ }),
437
+
438
+ // Enhanced Metadata with Cache Support
439
+ http.get(`${baseUrl}/meta/:type/:name`, async ({ params, request }) => {
440
+ try {
441
+ const cacheRequest = {
442
+ ifNoneMatch: request.headers.get('if-none-match') || undefined,
443
+ ifModifiedSince: request.headers.get('if-modified-since') || undefined,
444
+ };
445
+
446
+ const result = await protocol.getMetaItemCached({
447
+ type: params.type as string,
448
+ name: params.name as string,
449
+ cacheRequest
450
+ });
451
+
452
+ if (result.notModified) {
453
+ return new HttpResponse(null, { status: 304 });
454
+ }
455
+
456
+ // Build response headers
457
+ const headers: Record<string, string> = {};
458
+ if (result.etag) {
459
+ const etagValue = result.etag.weak ? `W/"${result.etag.value}"` : `"${result.etag.value}"`;
460
+ headers['ETag'] = etagValue;
461
+ }
462
+ if (result.lastModified) {
463
+ headers['Last-Modified'] = new Date(result.lastModified).toUTCString();
464
+ }
465
+ if (result.cacheControl) {
466
+ const directives = result.cacheControl.directives.join(', ');
467
+ const maxAge = result.cacheControl.maxAge ? `, max-age=${result.cacheControl.maxAge}` : '';
468
+ headers['Cache-Control'] = directives + maxAge;
469
+ }
470
+
471
+ return HttpResponse.json(result.data, { headers });
472
+ } catch (error) {
473
+ const message = error instanceof Error ? error.message : 'Unknown error';
474
+ return HttpResponse.json({ error: message }, { status: 404 });
475
+ }
476
+ }),
477
+
341
478
  // UI Protocol endpoint
342
- http.get(`${baseUrl}/ui/view/:object`, ({ params, request }) => {
479
+ http.get(`${baseUrl}/ui/view/:object`, async ({ params, request }) => {
343
480
  try {
344
481
  const url = new URL(request.url);
345
482
  const viewType = url.searchParams.get('type') || 'list';
346
- const view = protocol.getUiView(params.object as string, viewType as 'list' | 'form');
483
+ const view = await protocol.getUiView({ object: params.object as string, type: viewType as 'list' | 'form' });
347
484
  return HttpResponse.json(view);
348
485
  } catch (error) {
349
486
  const message = error instanceof Error ? error.message : 'Unknown error';
@@ -351,26 +488,122 @@ export class MSWPlugin implements Plugin {
351
488
  }
352
489
  }),
353
490
 
491
+ // View Storage Operations
492
+ http.post(`${baseUrl}/ui/views`, async ({ request }) => {
493
+ try {
494
+ const body = await request.json();
495
+ const result = await protocol.createView(body as any);
496
+ if (result.success) {
497
+ return HttpResponse.json(result, { status: 201 });
498
+ } else {
499
+ return HttpResponse.json(result, { status: 400 });
500
+ }
501
+ } catch (error) {
502
+ const message = error instanceof Error ? error.message : 'Unknown error';
503
+ return HttpResponse.json({ success: false, error: { code: 'internal_error', message } }, { status: 500 });
504
+ }
505
+ }),
506
+
507
+ http.get(`${baseUrl}/ui/views/:id`, async ({ params }) => {
508
+ try {
509
+ const result = await protocol.getView({ id: params.id as string });
510
+ if (result.success) {
511
+ return HttpResponse.json(result);
512
+ } else {
513
+ return HttpResponse.json(result, { status: 404 });
514
+ }
515
+ } catch (error) {
516
+ const message = error instanceof Error ? error.message : 'Unknown error';
517
+ return HttpResponse.json({ success: false, error: { code: 'internal_error', message } }, { status: 500 });
518
+ }
519
+ }),
520
+
521
+ http.get(`${baseUrl}/ui/views`, async ({ request }) => {
522
+ try {
523
+ const url = new URL(request.url);
524
+ const queryRequest: any = {};
525
+ if (url.searchParams.get('object')) queryRequest.object = url.searchParams.get('object');
526
+ if (url.searchParams.get('type')) queryRequest.type = url.searchParams.get('type');
527
+ if (url.searchParams.get('visibility')) queryRequest.visibility = url.searchParams.get('visibility');
528
+ if (url.searchParams.get('createdBy')) queryRequest.createdBy = url.searchParams.get('createdBy');
529
+ if (url.searchParams.get('isDefault')) queryRequest.isDefault = url.searchParams.get('isDefault') === 'true';
530
+
531
+ // Parse numeric parameters with validation
532
+ const limitParam = url.searchParams.get('limit');
533
+ const offsetParam = url.searchParams.get('offset');
534
+ if (limitParam) {
535
+ const limit = parseInt(limitParam, 10);
536
+ if (!isNaN(limit) && limit > 0) queryRequest.limit = limit;
537
+ }
538
+ if (offsetParam) {
539
+ const offset = parseInt(offsetParam, 10);
540
+ if (!isNaN(offset) && offset >= 0) queryRequest.offset = offset;
541
+ }
542
+
543
+ const result = await protocol.listViews(queryRequest);
544
+ return HttpResponse.json(result);
545
+ } catch (error) {
546
+ const message = error instanceof Error ? error.message : 'Unknown error';
547
+ return HttpResponse.json({ success: false, error: { code: 'internal_error', message } }, { status: 500 });
548
+ }
549
+ }),
550
+
551
+ http.patch(`${baseUrl}/ui/views/:id`, async ({ params, request }) => {
552
+ try {
553
+ const body = await request.json() as any;
554
+ // Merge body with id parameter, ensuring body is an object
555
+ const updateData = (typeof body === 'object' && body !== null)
556
+ ? { ...body, id: params.id as string }
557
+ : { id: params.id as string };
558
+
559
+ const result = await protocol.updateView(updateData as any);
560
+ if (result.success) {
561
+ return HttpResponse.json(result);
562
+ } else {
563
+ const statusCode = result.error?.code === 'resource_not_found' ? 404 : 400;
564
+ return HttpResponse.json(result, { status: statusCode });
565
+ }
566
+ } catch (error) {
567
+ const message = error instanceof Error ? error.message : 'Unknown error';
568
+ return HttpResponse.json({ success: false, error: { code: 'internal_error', message } }, { status: 500 });
569
+ }
570
+ }),
571
+
572
+ http.delete(`${baseUrl}/ui/views/:id`, async ({ params }) => {
573
+ try {
574
+ const result = await protocol.deleteView({ id: params.id as string });
575
+ if (result.success) {
576
+ return HttpResponse.json(result);
577
+ } else {
578
+ return HttpResponse.json(result, { status: 404 });
579
+ }
580
+ } catch (error) {
581
+ const message = error instanceof Error ? error.message : 'Unknown error';
582
+ return HttpResponse.json({ success: false, error: { code: 'internal_error', message } }, { status: 500 });
583
+ }
584
+ }),
585
+
354
586
  // Add custom handlers
355
587
  ...(this.options.customHandlers || [])
356
588
  ];
357
589
 
358
- console.log(`[MSWPlugin] Installed ${this.handlers.length} request handlers.`);
590
+ ctx.logger.info('MSW request handlers installed', { count: this.handlers.length, baseUrl });
359
591
  }
360
592
 
361
593
  /**
362
594
  * Start the MSW worker
363
595
  */
364
- private async startWorker() {
596
+ private async startWorker(ctx: PluginContext) {
365
597
  if (this.options.enableBrowser && typeof window !== 'undefined') {
366
598
  // Browser environment
599
+ ctx.logger.debug('Starting MSW in browser mode');
367
600
  this.worker = setupWorker(...this.handlers);
368
601
  await this.worker.start({
369
602
  onUnhandledRequest: 'bypass',
370
603
  });
371
- console.log(`[MSWPlugin] Started MSW in browser mode.`);
604
+ ctx.logger.info('MSW started in browser mode');
372
605
  } else {
373
- console.log(`[MSWPlugin] Browser mode disabled or not in browser environment.`);
606
+ ctx.logger.debug('MSW browser mode disabled or not in browser environment');
374
607
  }
375
608
  }
376
609
 
@@ -380,7 +613,7 @@ export class MSWPlugin implements Plugin {
380
613
  private async stopWorker() {
381
614
  if (this.worker) {
382
615
  this.worker.stop();
383
- console.log(`[MSWPlugin] Stopped MSW worker.`);
616
+ console.log('[MSWPlugin] Stopped MSW worker');
384
617
  }
385
618
  }
386
619