fauxbase 0.4.0 → 0.5.0

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/dist/index.d.cts CHANGED
@@ -143,6 +143,51 @@ interface Driver {
143
143
  clear(resource: string): void;
144
144
  }
145
145
 
146
+ type EventAction = 'created' | 'updated' | 'deleted' | 'bulkCreated' | 'bulkUpdated' | 'bulkDeleted';
147
+ interface FauxbaseEvent<T = any> {
148
+ action: EventAction;
149
+ resource: string;
150
+ data?: T;
151
+ id?: string;
152
+ ids?: string[];
153
+ timestamp: number;
154
+ source: 'local' | 'remote';
155
+ }
156
+ type EventHandler<T = any> = (event: FauxbaseEvent<T>) => void;
157
+ interface EventSourceAdapter {
158
+ connect(): void;
159
+ disconnect(): void;
160
+ }
161
+ interface SSEConfig {
162
+ type: 'sse';
163
+ url: string;
164
+ eventMap: Record<string, string>;
165
+ withCredentials?: boolean;
166
+ }
167
+ interface STOMPConfig {
168
+ type: 'stomp';
169
+ brokerUrl: string;
170
+ subscriptions: Record<string, string>;
171
+ connectHeaders?: Record<string, string>;
172
+ }
173
+ type EventSourceConfig = SSEConfig | STOMPConfig;
174
+ interface EventHandlersConfig {
175
+ [resource: string]: EventHandler;
176
+ }
177
+ type EventsConfig = true | {
178
+ source?: EventSourceConfig;
179
+ handlers?: EventHandlersConfig;
180
+ };
181
+
182
+ declare class EventBus {
183
+ private listeners;
184
+ private anyListeners;
185
+ on(resource: string, handler: EventHandler): () => void;
186
+ onAny(handler: EventHandler): () => void;
187
+ emit(event: FauxbaseEvent): void;
188
+ destroy(): void;
189
+ }
190
+
146
191
  declare function beforeCreate(): MethodDecorator;
147
192
  declare function beforeUpdate(): MethodDecorator;
148
193
  declare function afterCreate(): MethodDecorator;
@@ -153,6 +198,8 @@ declare abstract class Service<T extends Entity> {
153
198
  protected driver: Driver;
154
199
  protected resourceName: string;
155
200
  protected client: any;
201
+ /** @internal */
202
+ _eventBus?: EventBus;
156
203
  /** @internal — called by createClient to wire the service */
157
204
  _init(driver: Driver, resourceName: string): void;
158
205
  /** @internal — called by createClient to give services access to the client */
@@ -164,15 +211,16 @@ declare abstract class Service<T extends Entity> {
164
211
  delete(id: string): Promise<ApiResponse<T>>;
165
212
  count(filter?: Record<string, any>): Promise<number>;
166
213
  get bulk(): {
167
- create: (items: Array<Partial<T>>) => Promise<ApiResponse<T[]>>;
168
- update: (updates: Array<{
214
+ create(items: Array<Partial<T>>): Promise<ApiResponse<T[]>>;
215
+ update(updates: Array<{
169
216
  id: string;
170
217
  data: Partial<T>;
171
- }>) => Promise<ApiResponse<T[]>>;
172
- delete: (ids: string[]) => Promise<ApiResponse<{
218
+ }>): Promise<ApiResponse<T[]>>;
219
+ delete(ids: string[]): Promise<ApiResponse<{
173
220
  count: number;
174
221
  }>>;
175
222
  };
223
+ private emitEvent;
176
224
  private runHooks;
177
225
  }
178
226
 
@@ -270,6 +318,9 @@ type ClientResult<S extends Record<string, new (...args: any[]) => Service<any>>
270
318
  auth: InstanceType<A>;
271
319
  } : {}) & {
272
320
  readonly ready: Promise<void>;
321
+ } & {
322
+ _eventBus?: EventBus;
323
+ disconnect?: () => void;
273
324
  };
274
325
  declare function createClient<S extends Record<string, new (...args: any[]) => Service<any>>, A extends (new (...args: any[]) => AuthService<any>) | undefined = undefined>(config: {
275
326
  driver?: DriverConfig;
@@ -279,6 +330,7 @@ declare function createClient<S extends Record<string, new (...args: any[]) => S
279
330
  overrides?: Record<string, {
280
331
  driver: DriverConfig;
281
332
  }>;
333
+ events?: EventsConfig;
282
334
  }): ClientResult<S, A>;
283
335
 
284
336
  declare function seed<T>(entityClass: new (...args: any[]) => T, data: Array<Partial<T>>): SeedDefinition<T>;
@@ -384,4 +436,4 @@ declare const expressPreset: Preset;
384
436
 
385
437
  declare function getPreset(name: string): Preset;
386
438
 
387
- export { type ApiResponse, type AuthContext, AuthService, type AuthState, type BaseFields, ConflictError, type Driver, type DriverConfig, Entity, FauxbaseError, type FauxbaseErrorPayload, type FieldOptions, type FilterOperator, type FilterStyle, ForbiddenError, type HookType, HttpDriver, type HttpDriverConfig, HttpError, LocalDriver, type LocalDriverConfig, type LoginCredentials, NetworkError, NotFoundError, type PageMeta, type PagedResponse, type Preset, type QueryParams, type SeedDefinition, Service, type SortParams, TimeoutError, ValidationError, afterCreate, afterUpdate, beforeCreate, beforeUpdate, computed, createClient, defaultPreset, definePreset, djangoPreset, expressPreset, field, getPreset, laravelPreset, nestjsPreset, relation, seed, springBootPreset };
439
+ export { type ApiResponse, type AuthContext, AuthService, type AuthState, type BaseFields, ConflictError, type Driver, type DriverConfig, Entity, type EventAction, EventBus, type EventHandler, type EventHandlersConfig, type EventSourceAdapter, type EventSourceConfig, type EventsConfig, FauxbaseError, type FauxbaseErrorPayload, type FauxbaseEvent, type FieldOptions, type FilterOperator, type FilterStyle, ForbiddenError, type HookType, HttpDriver, type HttpDriverConfig, HttpError, LocalDriver, type LocalDriverConfig, type LoginCredentials, NetworkError, NotFoundError, type PageMeta, type PagedResponse, type Preset, type QueryParams, type SSEConfig, type STOMPConfig, type SeedDefinition, Service, type SortParams, TimeoutError, ValidationError, afterCreate, afterUpdate, beforeCreate, beforeUpdate, computed, createClient, defaultPreset, definePreset, djangoPreset, expressPreset, field, getPreset, laravelPreset, nestjsPreset, relation, seed, springBootPreset };
package/dist/index.d.ts CHANGED
@@ -143,6 +143,51 @@ interface Driver {
143
143
  clear(resource: string): void;
144
144
  }
145
145
 
146
+ type EventAction = 'created' | 'updated' | 'deleted' | 'bulkCreated' | 'bulkUpdated' | 'bulkDeleted';
147
+ interface FauxbaseEvent<T = any> {
148
+ action: EventAction;
149
+ resource: string;
150
+ data?: T;
151
+ id?: string;
152
+ ids?: string[];
153
+ timestamp: number;
154
+ source: 'local' | 'remote';
155
+ }
156
+ type EventHandler<T = any> = (event: FauxbaseEvent<T>) => void;
157
+ interface EventSourceAdapter {
158
+ connect(): void;
159
+ disconnect(): void;
160
+ }
161
+ interface SSEConfig {
162
+ type: 'sse';
163
+ url: string;
164
+ eventMap: Record<string, string>;
165
+ withCredentials?: boolean;
166
+ }
167
+ interface STOMPConfig {
168
+ type: 'stomp';
169
+ brokerUrl: string;
170
+ subscriptions: Record<string, string>;
171
+ connectHeaders?: Record<string, string>;
172
+ }
173
+ type EventSourceConfig = SSEConfig | STOMPConfig;
174
+ interface EventHandlersConfig {
175
+ [resource: string]: EventHandler;
176
+ }
177
+ type EventsConfig = true | {
178
+ source?: EventSourceConfig;
179
+ handlers?: EventHandlersConfig;
180
+ };
181
+
182
+ declare class EventBus {
183
+ private listeners;
184
+ private anyListeners;
185
+ on(resource: string, handler: EventHandler): () => void;
186
+ onAny(handler: EventHandler): () => void;
187
+ emit(event: FauxbaseEvent): void;
188
+ destroy(): void;
189
+ }
190
+
146
191
  declare function beforeCreate(): MethodDecorator;
147
192
  declare function beforeUpdate(): MethodDecorator;
148
193
  declare function afterCreate(): MethodDecorator;
@@ -153,6 +198,8 @@ declare abstract class Service<T extends Entity> {
153
198
  protected driver: Driver;
154
199
  protected resourceName: string;
155
200
  protected client: any;
201
+ /** @internal */
202
+ _eventBus?: EventBus;
156
203
  /** @internal — called by createClient to wire the service */
157
204
  _init(driver: Driver, resourceName: string): void;
158
205
  /** @internal — called by createClient to give services access to the client */
@@ -164,15 +211,16 @@ declare abstract class Service<T extends Entity> {
164
211
  delete(id: string): Promise<ApiResponse<T>>;
165
212
  count(filter?: Record<string, any>): Promise<number>;
166
213
  get bulk(): {
167
- create: (items: Array<Partial<T>>) => Promise<ApiResponse<T[]>>;
168
- update: (updates: Array<{
214
+ create(items: Array<Partial<T>>): Promise<ApiResponse<T[]>>;
215
+ update(updates: Array<{
169
216
  id: string;
170
217
  data: Partial<T>;
171
- }>) => Promise<ApiResponse<T[]>>;
172
- delete: (ids: string[]) => Promise<ApiResponse<{
218
+ }>): Promise<ApiResponse<T[]>>;
219
+ delete(ids: string[]): Promise<ApiResponse<{
173
220
  count: number;
174
221
  }>>;
175
222
  };
223
+ private emitEvent;
176
224
  private runHooks;
177
225
  }
178
226
 
@@ -270,6 +318,9 @@ type ClientResult<S extends Record<string, new (...args: any[]) => Service<any>>
270
318
  auth: InstanceType<A>;
271
319
  } : {}) & {
272
320
  readonly ready: Promise<void>;
321
+ } & {
322
+ _eventBus?: EventBus;
323
+ disconnect?: () => void;
273
324
  };
274
325
  declare function createClient<S extends Record<string, new (...args: any[]) => Service<any>>, A extends (new (...args: any[]) => AuthService<any>) | undefined = undefined>(config: {
275
326
  driver?: DriverConfig;
@@ -279,6 +330,7 @@ declare function createClient<S extends Record<string, new (...args: any[]) => S
279
330
  overrides?: Record<string, {
280
331
  driver: DriverConfig;
281
332
  }>;
333
+ events?: EventsConfig;
282
334
  }): ClientResult<S, A>;
283
335
 
284
336
  declare function seed<T>(entityClass: new (...args: any[]) => T, data: Array<Partial<T>>): SeedDefinition<T>;
@@ -384,4 +436,4 @@ declare const expressPreset: Preset;
384
436
 
385
437
  declare function getPreset(name: string): Preset;
386
438
 
387
- export { type ApiResponse, type AuthContext, AuthService, type AuthState, type BaseFields, ConflictError, type Driver, type DriverConfig, Entity, FauxbaseError, type FauxbaseErrorPayload, type FieldOptions, type FilterOperator, type FilterStyle, ForbiddenError, type HookType, HttpDriver, type HttpDriverConfig, HttpError, LocalDriver, type LocalDriverConfig, type LoginCredentials, NetworkError, NotFoundError, type PageMeta, type PagedResponse, type Preset, type QueryParams, type SeedDefinition, Service, type SortParams, TimeoutError, ValidationError, afterCreate, afterUpdate, beforeCreate, beforeUpdate, computed, createClient, defaultPreset, definePreset, djangoPreset, expressPreset, field, getPreset, laravelPreset, nestjsPreset, relation, seed, springBootPreset };
439
+ export { type ApiResponse, type AuthContext, AuthService, type AuthState, type BaseFields, ConflictError, type Driver, type DriverConfig, Entity, type EventAction, EventBus, type EventHandler, type EventHandlersConfig, type EventSourceAdapter, type EventSourceConfig, type EventsConfig, FauxbaseError, type FauxbaseErrorPayload, type FauxbaseEvent, type FieldOptions, type FilterOperator, type FilterStyle, ForbiddenError, type HookType, HttpDriver, type HttpDriverConfig, HttpError, LocalDriver, type LocalDriverConfig, type LoginCredentials, NetworkError, NotFoundError, type PageMeta, type PagedResponse, type Preset, type QueryParams, type SSEConfig, type STOMPConfig, type SeedDefinition, Service, type SortParams, TimeoutError, ValidationError, afterCreate, afterUpdate, beforeCreate, beforeUpdate, computed, createClient, defaultPreset, definePreset, djangoPreset, expressPreset, field, getPreset, laravelPreset, nestjsPreset, relation, seed, springBootPreset };
package/dist/index.js CHANGED
@@ -181,6 +181,8 @@ var Service = class {
181
181
  driver;
182
182
  resourceName;
183
183
  client;
184
+ /** @internal */
185
+ _eventBus;
184
186
  /** @internal — called by createClient to wire the service */
185
187
  _init(driver, resourceName) {
186
188
  this.driver = driver;
@@ -201,27 +203,56 @@ var Service = class {
201
203
  await this.runHooks("beforeCreate", data, allItems);
202
204
  const result = await this.driver.create(this.resourceName, data);
203
205
  await this.runHooks("afterCreate", result.data);
206
+ this.emitEvent("created", { data: result.data, id: result.data.id });
204
207
  return result;
205
208
  }
206
209
  async update(id, data) {
207
210
  await this.runHooks("beforeUpdate", id, data);
208
211
  const result = await this.driver.update(this.resourceName, id, data);
209
212
  await this.runHooks("afterUpdate", result.data);
213
+ this.emitEvent("updated", { data: result.data, id });
210
214
  return result;
211
215
  }
212
216
  async delete(id) {
213
- return this.driver.delete(this.resourceName, id);
217
+ const result = await this.driver.delete(this.resourceName, id);
218
+ this.emitEvent("deleted", { data: result.data, id });
219
+ return result;
214
220
  }
215
221
  async count(filter) {
216
222
  return this.driver.count(this.resourceName, filter);
217
223
  }
218
224
  get bulk() {
225
+ const self = this;
219
226
  return {
220
- create: (items) => this.driver.bulkCreate(this.resourceName, items),
221
- update: (updates) => this.driver.bulkUpdate(this.resourceName, updates),
222
- delete: (ids) => this.driver.bulkDelete(this.resourceName, ids)
227
+ async create(items) {
228
+ const result = await self.driver.bulkCreate(self.resourceName, items);
229
+ self.emitEvent("bulkCreated", { data: result.data });
230
+ return result;
231
+ },
232
+ async update(updates) {
233
+ const result = await self.driver.bulkUpdate(self.resourceName, updates);
234
+ self.emitEvent("bulkUpdated", { data: result.data, ids: updates.map((u) => u.id) });
235
+ return result;
236
+ },
237
+ async delete(ids) {
238
+ const result = await self.driver.bulkDelete(self.resourceName, ids);
239
+ self.emitEvent("bulkDeleted", { ids });
240
+ return result;
241
+ }
223
242
  };
224
243
  }
244
+ emitEvent(action, extra) {
245
+ if (!this._eventBus) return;
246
+ this._eventBus.emit({
247
+ action,
248
+ resource: this.resourceName,
249
+ data: extra.data,
250
+ id: extra.id,
251
+ ids: extra.ids,
252
+ timestamp: Date.now(),
253
+ source: "local"
254
+ });
255
+ }
225
256
  async runHooks(hookType, ...args) {
226
257
  const methods = getHooks(this.constructor, hookType);
227
258
  for (const methodName of methods) {
@@ -1358,6 +1389,146 @@ var HttpDriver = class {
1358
1389
  }
1359
1390
  };
1360
1391
 
1392
+ // src/events/event-bus.ts
1393
+ var EventBus = class {
1394
+ listeners = /* @__PURE__ */ new Map();
1395
+ anyListeners = /* @__PURE__ */ new Set();
1396
+ on(resource, handler) {
1397
+ if (!this.listeners.has(resource)) {
1398
+ this.listeners.set(resource, /* @__PURE__ */ new Set());
1399
+ }
1400
+ this.listeners.get(resource).add(handler);
1401
+ return () => {
1402
+ this.listeners.get(resource)?.delete(handler);
1403
+ };
1404
+ }
1405
+ onAny(handler) {
1406
+ this.anyListeners.add(handler);
1407
+ return () => {
1408
+ this.anyListeners.delete(handler);
1409
+ };
1410
+ }
1411
+ emit(event) {
1412
+ const resourceListeners = this.listeners.get(event.resource);
1413
+ if (resourceListeners) {
1414
+ for (const handler of resourceListeners) {
1415
+ handler(event);
1416
+ }
1417
+ }
1418
+ for (const handler of this.anyListeners) {
1419
+ handler(event);
1420
+ }
1421
+ }
1422
+ destroy() {
1423
+ this.listeners.clear();
1424
+ this.anyListeners.clear();
1425
+ }
1426
+ };
1427
+
1428
+ // src/events/sse-source.ts
1429
+ var SSESource = class {
1430
+ constructor(config, eventBus) {
1431
+ this.config = config;
1432
+ this.eventBus = eventBus;
1433
+ }
1434
+ eventSource = null;
1435
+ connect() {
1436
+ this.eventSource = new EventSource(this.config.url, {
1437
+ withCredentials: this.config.withCredentials
1438
+ });
1439
+ for (const [eventType, resource] of Object.entries(this.config.eventMap)) {
1440
+ this.eventSource.addEventListener(eventType, (e) => {
1441
+ const parsed = this.parseEvent(e, resource);
1442
+ if (parsed) {
1443
+ this.eventBus.emit(parsed);
1444
+ }
1445
+ });
1446
+ }
1447
+ }
1448
+ disconnect() {
1449
+ if (this.eventSource) {
1450
+ this.eventSource.close();
1451
+ this.eventSource = null;
1452
+ }
1453
+ }
1454
+ parseEvent(e, resource) {
1455
+ try {
1456
+ const raw = JSON.parse(e.data);
1457
+ return {
1458
+ action: raw.action,
1459
+ resource,
1460
+ data: raw.data,
1461
+ id: raw.id,
1462
+ ids: raw.ids,
1463
+ timestamp: raw.timestamp ?? Date.now(),
1464
+ source: "remote"
1465
+ };
1466
+ } catch {
1467
+ return null;
1468
+ }
1469
+ }
1470
+ };
1471
+
1472
+ // src/events/stomp-source.ts
1473
+ var STOMPSource = class {
1474
+ constructor(config, eventBus) {
1475
+ this.config = config;
1476
+ this.eventBus = eventBus;
1477
+ }
1478
+ client = null;
1479
+ connect() {
1480
+ this.connectAsync();
1481
+ }
1482
+ async connectAsync() {
1483
+ let StompJs;
1484
+ try {
1485
+ const moduleName = "@stomp/stompjs";
1486
+ StompJs = await Function("m", "return import(m)")(moduleName);
1487
+ } catch {
1488
+ throw new Error(
1489
+ "STOMP source requires @stomp/stompjs. Install it: npm install @stomp/stompjs"
1490
+ );
1491
+ }
1492
+ this.client = new StompJs.Client({
1493
+ brokerURL: this.config.brokerUrl,
1494
+ connectHeaders: this.config.connectHeaders,
1495
+ onConnect: () => {
1496
+ for (const [destination, resource] of Object.entries(this.config.subscriptions)) {
1497
+ this.client.subscribe(destination, (message) => {
1498
+ const parsed = this.parseMessage(message, resource);
1499
+ if (parsed) {
1500
+ this.eventBus.emit(parsed);
1501
+ }
1502
+ });
1503
+ }
1504
+ }
1505
+ });
1506
+ this.client.activate();
1507
+ }
1508
+ disconnect() {
1509
+ if (this.client) {
1510
+ this.client.deactivate();
1511
+ this.client = null;
1512
+ }
1513
+ }
1514
+ parseMessage(message, resource) {
1515
+ try {
1516
+ const raw = JSON.parse(message.body);
1517
+ return {
1518
+ action: raw.action,
1519
+ resource,
1520
+ data: raw.data,
1521
+ id: raw.id,
1522
+ ids: raw.ids,
1523
+ timestamp: raw.timestamp ?? Date.now(),
1524
+ source: "remote"
1525
+ };
1526
+ } catch {
1527
+ return null;
1528
+ }
1529
+ }
1530
+ };
1531
+
1361
1532
  // src/seed.ts
1362
1533
  function seed(entityClass, data) {
1363
1534
  const entityName = entityClass.name.toLowerCase();
@@ -1456,6 +1627,33 @@ function createClient(config) {
1456
1627
  svc._setClient(client);
1457
1628
  }
1458
1629
  }
1630
+ let eventSource = null;
1631
+ if (config.events) {
1632
+ const eventBus = new EventBus();
1633
+ client._eventBus = eventBus;
1634
+ for (const [name, ServiceClass] of Object.entries(config.services)) {
1635
+ const svc = client[name];
1636
+ svc._eventBus = eventBus;
1637
+ }
1638
+ const eventsConfig = config.events === true ? {} : config.events;
1639
+ if (eventsConfig.handlers) {
1640
+ for (const [resource, handler] of Object.entries(eventsConfig.handlers)) {
1641
+ eventBus.on(resource, handler);
1642
+ }
1643
+ }
1644
+ if (eventsConfig.source) {
1645
+ if (eventsConfig.source.type === "sse") {
1646
+ eventSource = new SSESource(eventsConfig.source, eventBus);
1647
+ } else if (eventsConfig.source.type === "stomp") {
1648
+ eventSource = new STOMPSource(eventsConfig.source, eventBus);
1649
+ }
1650
+ eventSource?.connect();
1651
+ }
1652
+ client.disconnect = () => {
1653
+ eventSource?.disconnect();
1654
+ eventBus.destroy();
1655
+ };
1656
+ }
1459
1657
  let readyPromise;
1460
1658
  if (defaultDriver instanceof LocalDriver) {
1461
1659
  if (defaultDriver.isReady) {
@@ -1501,6 +1699,6 @@ function applySeedsIfNeeded(driver, seeds) {
1501
1699
  driver.setSeedVersion(newVersion);
1502
1700
  }
1503
1701
 
1504
- export { AuthService, ConflictError, Entity, FauxbaseError, ForbiddenError, HttpDriver, HttpError, LocalDriver, NetworkError, NotFoundError, Service, TimeoutError, ValidationError, afterCreate, afterUpdate, beforeCreate, beforeUpdate, computed, createClient, defaultPreset, definePreset, djangoPreset, expressPreset, field, getPreset, laravelPreset, nestjsPreset, relation, seed, springBootPreset };
1702
+ export { AuthService, ConflictError, Entity, EventBus, FauxbaseError, ForbiddenError, HttpDriver, HttpError, LocalDriver, NetworkError, NotFoundError, Service, TimeoutError, ValidationError, afterCreate, afterUpdate, beforeCreate, beforeUpdate, computed, createClient, defaultPreset, definePreset, djangoPreset, expressPreset, field, getPreset, laravelPreset, nestjsPreset, relation, seed, springBootPreset };
1505
1703
  //# sourceMappingURL=index.js.map
1506
1704
  //# sourceMappingURL=index.js.map