@tspvivek/baasix-sdk 0.1.0-alpha.9 → 0.1.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/README.md CHANGED
@@ -558,6 +558,7 @@ await baasix.schemas.create({
558
558
 
559
559
  ```typescript
560
560
  // Many-to-One (BelongsTo)
561
+ // Auto-creates index on foreign key column
561
562
  await baasix.schemas.createRelationship('products', {
562
563
  type: 'M2O',
563
564
  target: 'categories',
@@ -566,14 +567,40 @@ await baasix.schemas.createRelationship('products', {
566
567
  });
567
568
 
568
569
  // Many-to-Many
570
+ // Auto-generates junction table: products_tags_tags_junction
569
571
  await baasix.schemas.createRelationship('products', {
570
572
  type: 'M2M',
571
573
  target: 'tags',
572
574
  name: 'tags',
573
575
  alias: 'products',
574
576
  });
577
+
578
+ // Many-to-Many with custom junction table name
579
+ // Useful when auto-generated name exceeds PostgreSQL's 63 char limit
580
+ await baasix.schemas.createRelationship('products', {
581
+ type: 'M2M',
582
+ target: 'tags',
583
+ name: 'tags',
584
+ alias: 'products',
585
+ through: 'product_tags', // Custom junction table name (max 63 chars)
586
+ });
587
+
588
+ // Many-to-Any (Polymorphic)
589
+ await baasix.schemas.createRelationship('comments', {
590
+ type: 'M2A',
591
+ name: 'commentable',
592
+ tables: ['posts', 'products'],
593
+ alias: 'comments',
594
+ through: 'comment_refs', // Optional custom junction table name
595
+ });
575
596
  ```
576
597
 
598
+ #### Junction Tables (M2M/M2A)
599
+ - **Auto-generated name**: `{source}_{target}_{name}_junction`
600
+ - **Custom name**: Use `through` property (max 63 characters for PostgreSQL)
601
+ - **Schema property**: Junction tables have `isJunction: true` in their schema
602
+ - **Auto-indexed**: Foreign key columns are automatically indexed
603
+
577
604
  ### Indexes
578
605
 
579
606
  ```typescript
@@ -374,6 +374,11 @@ interface SchemaDefinition {
374
374
  name: string;
375
375
  timestamps?: boolean;
376
376
  paranoid?: boolean;
377
+ /**
378
+ * True for M2M/M2A junction tables (system-generated)
379
+ * Junction tables are auto-created when defining M2M or M2A relationships
380
+ */
381
+ isJunction?: boolean;
377
382
  fields: Record<string, FieldDefinition>;
378
383
  indexes?: IndexDefinition[];
379
384
  }
@@ -390,8 +395,15 @@ interface RelationshipDefinition {
390
395
  target: string;
391
396
  name: string;
392
397
  alias?: string;
398
+ /**
399
+ * Custom junction table name for M2M/M2A relationships (max 63 chars for PostgreSQL)
400
+ * If not provided, auto-generated as: {source}_{target}_{name}_junction
401
+ * @example "product_tags" or "comment_refs"
402
+ */
403
+ through?: string;
393
404
  onDelete?: "CASCADE" | "RESTRICT" | "SET NULL";
394
405
  onUpdate?: "CASCADE" | "RESTRICT" | "SET NULL";
406
+ /** Target tables for M2A (polymorphic) relationships */
395
407
  tables?: string[];
396
408
  }
397
409
  interface SchemaInfo {
@@ -374,6 +374,11 @@ interface SchemaDefinition {
374
374
  name: string;
375
375
  timestamps?: boolean;
376
376
  paranoid?: boolean;
377
+ /**
378
+ * True for M2M/M2A junction tables (system-generated)
379
+ * Junction tables are auto-created when defining M2M or M2A relationships
380
+ */
381
+ isJunction?: boolean;
377
382
  fields: Record<string, FieldDefinition>;
378
383
  indexes?: IndexDefinition[];
379
384
  }
@@ -390,8 +395,15 @@ interface RelationshipDefinition {
390
395
  target: string;
391
396
  name: string;
392
397
  alias?: string;
398
+ /**
399
+ * Custom junction table name for M2M/M2A relationships (max 63 chars for PostgreSQL)
400
+ * If not provided, auto-generated as: {source}_{target}_{name}_junction
401
+ * @example "product_tags" or "comment_refs"
402
+ */
403
+ through?: string;
393
404
  onDelete?: "CASCADE" | "RESTRICT" | "SET NULL";
394
405
  onUpdate?: "CASCADE" | "RESTRICT" | "SET NULL";
406
+ /** Target tables for M2A (polymorphic) relationships */
395
407
  tables?: string[];
396
408
  }
397
409
  interface SchemaInfo {
package/dist/index.cjs CHANGED
@@ -3030,6 +3030,9 @@ var RealtimeModule = class {
3030
3030
  socketPath;
3031
3031
  subscriptions = /* @__PURE__ */ new Map();
3032
3032
  workflowCallbacks = /* @__PURE__ */ new Map();
3033
+ roomCallbacks = /* @__PURE__ */ new Map();
3034
+ // room -> event -> callbacks
3035
+ roomUserCallbacks = /* @__PURE__ */ new Map();
3033
3036
  connectionCallbacks = /* @__PURE__ */ new Set();
3034
3037
  reconnecting = false;
3035
3038
  connectionPromise = null;
@@ -3037,7 +3040,7 @@ var RealtimeModule = class {
3037
3040
  this.client = config.client;
3038
3041
  this.storage = config.storage;
3039
3042
  this.socketUrl = config.socketUrl || "";
3040
- this.socketPath = config.socketPath || "/socket";
3043
+ this.socketPath = config.socketPath || "/realtime";
3041
3044
  }
3042
3045
  /**
3043
3046
  * Set the socket.io client instance
@@ -3139,6 +3142,26 @@ var RealtimeModule = class {
3139
3142
  this.socket.on("workflow:execution:complete", (data) => {
3140
3143
  this.handleWorkflowUpdate({ ...data, status: "complete" });
3141
3144
  });
3145
+ this.socket.on("room:user:joined", (data) => {
3146
+ const callbacks = this.roomUserCallbacks.get(data.room);
3147
+ callbacks?.joined.forEach((cb) => {
3148
+ try {
3149
+ cb(data);
3150
+ } catch (e) {
3151
+ console.error("[Baasix Realtime] Error in room user joined callback:", e);
3152
+ }
3153
+ });
3154
+ });
3155
+ this.socket.on("room:user:left", (data) => {
3156
+ const callbacks = this.roomUserCallbacks.get(data.room);
3157
+ callbacks?.left.forEach((cb) => {
3158
+ try {
3159
+ cb(data);
3160
+ } catch (e) {
3161
+ console.error("[Baasix Realtime] Error in room user left callback:", e);
3162
+ }
3163
+ });
3164
+ });
3142
3165
  this.socket.connect();
3143
3166
  } catch (error) {
3144
3167
  this.connectionPromise = null;
@@ -3166,6 +3189,8 @@ var RealtimeModule = class {
3166
3189
  }
3167
3190
  this.subscriptions.clear();
3168
3191
  this.workflowCallbacks.clear();
3192
+ this.roomCallbacks.clear();
3193
+ this.roomUserCallbacks.clear();
3169
3194
  }
3170
3195
  /**
3171
3196
  * Check if connected to the realtime server
@@ -3382,6 +3407,214 @@ var RealtimeModule = class {
3382
3407
  }
3383
3408
  }
3384
3409
  // ===================
3410
+ // Custom Rooms API
3411
+ // ===================
3412
+ /**
3413
+ * Join a custom room for real-time communication
3414
+ *
3415
+ * @example
3416
+ * ```typescript
3417
+ * // Join a room
3418
+ * await baasix.realtime.joinRoom('game:lobby');
3419
+ *
3420
+ * // Listen for messages
3421
+ * baasix.realtime.onRoomMessage('game:lobby', 'chat', (data) => {
3422
+ * console.log(`${data.sender.userId}: ${data.payload.text}`);
3423
+ * });
3424
+ * ```
3425
+ */
3426
+ async joinRoom(roomName) {
3427
+ if (!this.socket?.connected) {
3428
+ throw new Error("Not connected. Call connect() first.");
3429
+ }
3430
+ return new Promise((resolve, reject) => {
3431
+ this.socket.emit("room:join", { room: roomName }, (response) => {
3432
+ if (response.status === "success") {
3433
+ this.setupRoomListeners(roomName);
3434
+ resolve();
3435
+ } else {
3436
+ reject(new Error(response.message || "Failed to join room"));
3437
+ }
3438
+ });
3439
+ });
3440
+ }
3441
+ /**
3442
+ * Leave a custom room
3443
+ *
3444
+ * @example
3445
+ * ```typescript
3446
+ * await baasix.realtime.leaveRoom('game:lobby');
3447
+ * ```
3448
+ */
3449
+ async leaveRoom(roomName) {
3450
+ if (!this.socket?.connected) {
3451
+ return;
3452
+ }
3453
+ return new Promise((resolve, reject) => {
3454
+ this.socket.emit("room:leave", { room: roomName }, (response) => {
3455
+ if (response.status === "success") {
3456
+ this.cleanupRoomListeners(roomName);
3457
+ resolve();
3458
+ } else {
3459
+ reject(new Error(response.message || "Failed to leave room"));
3460
+ }
3461
+ });
3462
+ });
3463
+ }
3464
+ /**
3465
+ * Send a message to a room
3466
+ *
3467
+ * @example
3468
+ * ```typescript
3469
+ * // Send a chat message
3470
+ * await baasix.realtime.sendToRoom('game:lobby', 'chat', { text: 'Hello!' });
3471
+ *
3472
+ * // Send a game event
3473
+ * await baasix.realtime.sendToRoom('game:123', 'move', { x: 10, y: 20 });
3474
+ * ```
3475
+ */
3476
+ async sendToRoom(roomName, event, payload) {
3477
+ if (!this.socket?.connected) {
3478
+ throw new Error("Not connected. Call connect() first.");
3479
+ }
3480
+ return new Promise((resolve, reject) => {
3481
+ this.socket.emit(
3482
+ "room:message",
3483
+ { room: roomName, event, payload },
3484
+ (response) => {
3485
+ if (response.status === "success") {
3486
+ resolve();
3487
+ } else {
3488
+ reject(new Error(response.message || "Failed to send message"));
3489
+ }
3490
+ }
3491
+ );
3492
+ });
3493
+ }
3494
+ /**
3495
+ * Listen for messages in a room with a specific event type
3496
+ *
3497
+ * @example
3498
+ * ```typescript
3499
+ * const unsubscribe = baasix.realtime.onRoomMessage('game:lobby', 'chat', (data) => {
3500
+ * console.log(`${data.sender.userId}: ${data.payload.text}`);
3501
+ * });
3502
+ *
3503
+ * // Later
3504
+ * unsubscribe();
3505
+ * ```
3506
+ */
3507
+ onRoomMessage(roomName, event, callback) {
3508
+ if (!this.roomCallbacks.has(roomName)) {
3509
+ this.roomCallbacks.set(roomName, /* @__PURE__ */ new Map());
3510
+ }
3511
+ const roomEvents = this.roomCallbacks.get(roomName);
3512
+ if (!roomEvents.has(event)) {
3513
+ roomEvents.set(event, /* @__PURE__ */ new Set());
3514
+ }
3515
+ roomEvents.get(event).add(callback);
3516
+ return () => {
3517
+ const events = this.roomCallbacks.get(roomName);
3518
+ if (events) {
3519
+ const callbacks = events.get(event);
3520
+ if (callbacks) {
3521
+ callbacks.delete(callback);
3522
+ if (callbacks.size === 0) {
3523
+ events.delete(event);
3524
+ }
3525
+ }
3526
+ if (events.size === 0) {
3527
+ this.roomCallbacks.delete(roomName);
3528
+ }
3529
+ }
3530
+ };
3531
+ }
3532
+ /**
3533
+ * Listen for users joining a room
3534
+ *
3535
+ * @example
3536
+ * ```typescript
3537
+ * const unsubscribe = baasix.realtime.onRoomUserJoined('game:lobby', (data) => {
3538
+ * console.log(`User ${data.userId} joined the room`);
3539
+ * });
3540
+ * ```
3541
+ */
3542
+ onRoomUserJoined(roomName, callback) {
3543
+ if (!this.roomUserCallbacks.has(roomName)) {
3544
+ this.roomUserCallbacks.set(roomName, { joined: /* @__PURE__ */ new Set(), left: /* @__PURE__ */ new Set() });
3545
+ }
3546
+ this.roomUserCallbacks.get(roomName).joined.add(callback);
3547
+ return () => {
3548
+ const callbacks = this.roomUserCallbacks.get(roomName);
3549
+ if (callbacks) {
3550
+ callbacks.joined.delete(callback);
3551
+ if (callbacks.joined.size === 0 && callbacks.left.size === 0) {
3552
+ this.roomUserCallbacks.delete(roomName);
3553
+ }
3554
+ }
3555
+ };
3556
+ }
3557
+ /**
3558
+ * Listen for users leaving a room
3559
+ *
3560
+ * @example
3561
+ * ```typescript
3562
+ * const unsubscribe = baasix.realtime.onRoomUserLeft('game:lobby', (data) => {
3563
+ * console.log(`User ${data.userId} left the room`);
3564
+ * });
3565
+ * ```
3566
+ */
3567
+ onRoomUserLeft(roomName, callback) {
3568
+ if (!this.roomUserCallbacks.has(roomName)) {
3569
+ this.roomUserCallbacks.set(roomName, { joined: /* @__PURE__ */ new Set(), left: /* @__PURE__ */ new Set() });
3570
+ }
3571
+ this.roomUserCallbacks.get(roomName).left.add(callback);
3572
+ return () => {
3573
+ const callbacks = this.roomUserCallbacks.get(roomName);
3574
+ if (callbacks) {
3575
+ callbacks.left.delete(callback);
3576
+ if (callbacks.joined.size === 0 && callbacks.left.size === 0) {
3577
+ this.roomUserCallbacks.delete(roomName);
3578
+ }
3579
+ }
3580
+ };
3581
+ }
3582
+ /**
3583
+ * Invoke a custom server-side handler
3584
+ *
3585
+ * @example
3586
+ * ```typescript
3587
+ * const result = await baasix.realtime.invoke('game:roll-dice', { sides: 6 });
3588
+ * console.log('Dice result:', result);
3589
+ * ```
3590
+ */
3591
+ async invoke(event, payload) {
3592
+ if (!this.socket?.connected) {
3593
+ throw new Error("Not connected. Call connect() first.");
3594
+ }
3595
+ return new Promise((resolve, reject) => {
3596
+ this.socket.emit("custom", { event, payload }, (response) => {
3597
+ if (response.status === "success") {
3598
+ resolve(response);
3599
+ } else {
3600
+ reject(new Error(response.message || "Custom event failed"));
3601
+ }
3602
+ });
3603
+ });
3604
+ }
3605
+ setupRoomListeners(roomName) {
3606
+ if (!this.roomCallbacks.has(roomName)) {
3607
+ this.roomCallbacks.set(roomName, /* @__PURE__ */ new Map());
3608
+ }
3609
+ if (!this.roomUserCallbacks.has(roomName)) {
3610
+ this.roomUserCallbacks.set(roomName, { joined: /* @__PURE__ */ new Set(), left: /* @__PURE__ */ new Set() });
3611
+ }
3612
+ }
3613
+ cleanupRoomListeners(roomName) {
3614
+ this.roomCallbacks.delete(roomName);
3615
+ this.roomUserCallbacks.delete(roomName);
3616
+ }
3617
+ // ===================
3385
3618
  // Channel (Room) API - Supabase-style
3386
3619
  // ===================
3387
3620
  /**