s3db.js 13.3.1 → 13.4.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
@@ -137,6 +137,8 @@ console.log("🎉 Connected to S3 database!");
137
137
 
138
138
  ### 3. Create your first resource
139
139
 
140
+ Schema validation powered by **[fastest-validator](https://github.com/icebob/fastest-validator)** ⚡
141
+
140
142
  ```javascript
141
143
  const users = await s3db.createResource({
142
144
  name: "users",
@@ -718,7 +720,7 @@ await users.insert({ email: 'john@example.com', password: 'secret123', age: 25 }
718
720
 
719
721
  ### Schema & Field Types
720
722
 
721
- Define your data structure with powerful validation:
723
+ Define your data structure with powerful validation using **[fastest-validator](https://github.com/icebob/fastest-validator)** - a blazing-fast validation library with comprehensive type support:
722
724
 
723
725
  #### Basic Types
724
726
 
@@ -749,6 +751,9 @@ Define your data structure with powerful validation:
749
751
 
750
752
  #### Schema Examples
751
753
 
754
+ > **📖 Validation powered by [fastest-validator](https://github.com/icebob/fastest-validator)**
755
+ > All schemas use fastest-validator's syntax with full support for shorthand notation.
756
+
752
757
  ```javascript
753
758
  // Simple schema
754
759
  {
@@ -757,21 +762,40 @@ Define your data structure with powerful validation:
757
762
  age: 'number|integer|min:0|max:150'
758
763
  }
759
764
 
760
- // Nested objects
765
+ // Nested objects - MAGIC AUTO-DETECT! ✨ (recommended)
766
+ // Just write your object structure - s3db detects it automatically!
767
+ {
768
+ name: 'string|required',
769
+ profile: { // ← No $$type needed! Auto-detected as optional object
770
+ bio: 'string|max:500',
771
+ avatar: 'url|optional',
772
+ social: { // ← Deeply nested also works!
773
+ twitter: 'string|optional',
774
+ github: 'string|optional'
775
+ }
776
+ }
777
+ }
778
+
779
+ // Need validation control? Use $$type (when you need required/optional)
780
+ {
781
+ name: 'string|required',
782
+ profile: {
783
+ $$type: 'object|required', // ← Add required validation
784
+ bio: 'string|max:500',
785
+ avatar: 'url|optional'
786
+ }
787
+ }
788
+
789
+ // Advanced: Full control (rare cases - strict mode, etc)
761
790
  {
762
791
  name: 'string|required',
763
792
  profile: {
764
793
  type: 'object',
794
+ optional: false,
795
+ strict: true, // ← Enable strict validation
765
796
  props: {
766
797
  bio: 'string|max:500',
767
- avatar: 'url|optional',
768
- social: {
769
- type: 'object',
770
- props: {
771
- twitter: 'string|optional',
772
- github: 'string|optional'
773
- }
774
- }
798
+ avatar: 'url|optional'
775
799
  }
776
800
  }
777
801
  }
package/dist/s3db.cjs.js CHANGED
@@ -26182,7 +26182,7 @@ class Database extends EventEmitter {
26182
26182
  })();
26183
26183
  this.version = "1";
26184
26184
  this.s3dbVersion = (() => {
26185
- const [ok, err, version] = tryFn(() => true ? "13.3.1" : "latest");
26185
+ const [ok, err, version] = tryFn(() => true ? "13.4.0" : "latest");
26186
26186
  return ok ? version : "latest";
26187
26187
  })();
26188
26188
  this._resourcesMap = {};
@@ -31511,14 +31511,36 @@ class StateMachinePlugin extends Plugin {
31511
31511
  }
31512
31512
  /**
31513
31513
  * Setup an event-based trigger
31514
+ * Supports both old API (trigger.event) and new API (trigger.eventName + eventSource)
31514
31515
  * @private
31515
31516
  */
31516
31517
  async _setupEventTrigger(machineId, stateName, trigger, triggerName) {
31517
- const eventName = trigger.event;
31518
+ const baseEventName = trigger.eventName || trigger.event;
31519
+ const eventSource = trigger.eventSource;
31520
+ if (!baseEventName) {
31521
+ throw new StateMachineError(`Event trigger '${triggerName}' must have either 'event' or 'eventName' property`, {
31522
+ operation: "_setupEventTrigger",
31523
+ machineId,
31524
+ stateName,
31525
+ triggerName
31526
+ });
31527
+ }
31518
31528
  const eventHandler = async (eventData) => {
31519
31529
  const entities = await this._getEntitiesInState(machineId, stateName);
31520
31530
  for (const entity of entities) {
31521
31531
  try {
31532
+ let resolvedEventName;
31533
+ if (typeof baseEventName === "function") {
31534
+ resolvedEventName = baseEventName(entity.context);
31535
+ } else {
31536
+ resolvedEventName = baseEventName;
31537
+ }
31538
+ if (eventSource && typeof baseEventName === "function") {
31539
+ const eventIdMatch = eventData?.id || eventData?.entityId;
31540
+ if (eventIdMatch && entity.entityId !== eventIdMatch) {
31541
+ continue;
31542
+ }
31543
+ }
31522
31544
  if (trigger.condition) {
31523
31545
  const shouldTrigger = await trigger.condition(entity.context, entity.entityId, eventData);
31524
31546
  if (!shouldTrigger) continue;
@@ -31532,28 +31554,76 @@ class StateMachinePlugin extends Plugin {
31532
31554
  continue;
31533
31555
  }
31534
31556
  }
31535
- const result = await this._executeAction(
31536
- trigger.action,
31537
- { ...entity.context, eventData },
31538
- "TRIGGER",
31539
- machineId,
31540
- entity.entityId
31541
- );
31542
- await this._incrementTriggerCount(machineId, entity.entityId, triggerName);
31543
- if (trigger.sendEvent) {
31544
- await this.send(machineId, entity.entityId, trigger.sendEvent, {
31545
- ...entity.context,
31546
- triggerResult: result,
31547
- eventData
31557
+ if (trigger.targetState) {
31558
+ await this._transition(
31559
+ machineId,
31560
+ entity.entityId,
31561
+ stateName,
31562
+ trigger.targetState,
31563
+ "TRIGGER",
31564
+ { ...entity.context, eventData, triggerName }
31565
+ );
31566
+ const machine = this.machines.get(machineId);
31567
+ const resourceConfig = machine.config;
31568
+ if (resourceConfig.resource && resourceConfig.stateField) {
31569
+ let resource;
31570
+ if (typeof resourceConfig.resource === "string") {
31571
+ resource = await this.database.getResource(resourceConfig.resource);
31572
+ } else {
31573
+ resource = resourceConfig.resource;
31574
+ }
31575
+ if (resource) {
31576
+ const [ok] = await tryFn(
31577
+ () => resource.patch(entity.entityId, { [resourceConfig.stateField]: trigger.targetState })
31578
+ );
31579
+ if (!ok && this.config.verbose) {
31580
+ console.warn(`[StateMachinePlugin] Failed to update resource stateField for entity ${entity.entityId}`);
31581
+ }
31582
+ }
31583
+ }
31584
+ const targetStateConfig = machine.config.states[trigger.targetState];
31585
+ if (targetStateConfig?.entry) {
31586
+ await this._executeAction(
31587
+ targetStateConfig.entry,
31588
+ { ...entity.context, eventData },
31589
+ "TRIGGER",
31590
+ machineId,
31591
+ entity.entityId
31592
+ );
31593
+ }
31594
+ this.emit("plg:state-machine:transition", {
31595
+ machineId,
31596
+ entityId: entity.entityId,
31597
+ from: stateName,
31598
+ to: trigger.targetState,
31599
+ event: "TRIGGER",
31600
+ context: { ...entity.context, eventData, triggerName }
31548
31601
  });
31602
+ } else if (trigger.action) {
31603
+ const result = await this._executeAction(
31604
+ trigger.action,
31605
+ { ...entity.context, eventData },
31606
+ "TRIGGER",
31607
+ machineId,
31608
+ entity.entityId
31609
+ );
31610
+ if (trigger.sendEvent) {
31611
+ await this.send(machineId, entity.entityId, trigger.sendEvent, {
31612
+ ...entity.context,
31613
+ triggerResult: result,
31614
+ eventData
31615
+ });
31616
+ }
31549
31617
  }
31618
+ await this._incrementTriggerCount(machineId, entity.entityId, triggerName);
31550
31619
  this.emit("plg:state-machine:trigger-executed", {
31551
31620
  machineId,
31552
31621
  entityId: entity.entityId,
31553
31622
  state: stateName,
31554
31623
  trigger: triggerName,
31555
31624
  type: "event",
31556
- eventName
31625
+ eventName: resolvedEventName,
31626
+ targetState: trigger.targetState
31557
31627
  });
31558
31628
  } catch (error) {
31559
31629
  if (this.config.verbose) {
@@ -31562,16 +31632,25 @@ class StateMachinePlugin extends Plugin {
31562
31632
  }
31563
31633
  }
31564
31634
  };
31565
- if (eventName.startsWith("db:")) {
31566
- const dbEventName = eventName.substring(3);
31567
- this.database.on(dbEventName, eventHandler);
31635
+ if (eventSource) {
31636
+ const baseEvent = typeof baseEventName === "function" ? "updated" : baseEventName;
31637
+ eventSource.on(baseEvent, eventHandler);
31568
31638
  if (this.config.verbose) {
31569
- console.log(`[StateMachinePlugin] Listening to database event '${dbEventName}' for trigger '${triggerName}'`);
31639
+ console.log(`[StateMachinePlugin] Listening to resource event '${baseEvent}' from '${eventSource.name}' for trigger '${triggerName}'`);
31570
31640
  }
31571
31641
  } else {
31572
- this.on(eventName, eventHandler);
31573
- if (this.config.verbose) {
31574
- console.log(`[StateMachinePlugin] Listening to plugin event '${eventName}' for trigger '${triggerName}'`);
31642
+ const staticEventName = typeof baseEventName === "function" ? "updated" : baseEventName;
31643
+ if (staticEventName.startsWith("db:")) {
31644
+ const dbEventName = staticEventName.substring(3);
31645
+ this.database.on(dbEventName, eventHandler);
31646
+ if (this.config.verbose) {
31647
+ console.log(`[StateMachinePlugin] Listening to database event '${dbEventName}' for trigger '${triggerName}'`);
31648
+ }
31649
+ } else {
31650
+ this.on(staticEventName, eventHandler);
31651
+ if (this.config.verbose) {
31652
+ console.log(`[StateMachinePlugin] Listening to plugin event '${staticEventName}' for trigger '${triggerName}'`);
31653
+ }
31575
31654
  }
31576
31655
  }
31577
31656
  }