interaqt 2.0.3 → 3.0.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.
Files changed (75) hide show
  1. package/README.md +1 -1
  2. package/agent/agentspace/knowledge/generator/api-reference.md +2 -9
  3. package/agent/agentspace/knowledge/generator/permission-test-implementation.md +18 -0
  4. package/agent/agentspace/knowledge/generator/test-implementation.md +6 -5
  5. package/agent/agentspace/knowledge/usage/02-define-entities-properties.md +11 -6
  6. package/agent/agentspace/knowledge/usage/04-reactive-computations.md +6 -1
  7. package/agent/agentspace/knowledge/usage/05-interactions.md +51 -60
  8. package/agent/agentspace/knowledge/usage/06-attributive-permissions.md +54 -7
  9. package/agent/agentspace/knowledge/usage/07-payload-parameters.md +95 -72
  10. package/agent/agentspace/knowledge/usage/11-global-dictionaries.md +13 -14
  11. package/agent/agentspace/knowledge/usage/12-data-querying.md +13 -0
  12. package/agent/agentspace/knowledge/usage/13-testing.md +2 -2
  13. package/agent/agentspace/knowledge/usage/14-api-reference.md +24 -66
  14. package/agent/agentspace/knowledge/usage/16-frontend-page-design-guide.md +3 -3
  15. package/agent/agentspace/knowledge/usage/18-api-exports-reference.md +2 -5
  16. package/agent/agentspace/knowledge/usage/README.md +2 -2
  17. package/agent/skill/interaqt-reference.md +6 -18
  18. package/dist/builtins/interaction/Interaction.d.ts +1 -22
  19. package/dist/builtins/interaction/Interaction.d.ts.map +1 -1
  20. package/dist/builtins/interaction/PayloadItem.d.ts +5 -14
  21. package/dist/builtins/interaction/PayloadItem.d.ts.map +1 -1
  22. package/dist/builtins/interaction/activity/ActivityCall.d.ts +2 -5
  23. package/dist/builtins/interaction/activity/ActivityCall.d.ts.map +1 -1
  24. package/dist/builtins/interaction/activity/ActivityManager.d.ts.map +1 -1
  25. package/dist/builtins/interaction/index.d.ts +0 -2
  26. package/dist/builtins/interaction/index.d.ts.map +1 -1
  27. package/dist/core/Entity.d.ts.map +1 -1
  28. package/dist/core/Property.d.ts.map +1 -1
  29. package/dist/core/Relation.d.ts.map +1 -1
  30. package/dist/core/interfaces.d.ts +0 -12
  31. package/dist/core/interfaces.d.ts.map +1 -1
  32. package/dist/drivers/Mysql.d.ts +2 -0
  33. package/dist/drivers/Mysql.d.ts.map +1 -1
  34. package/dist/drivers/PGLite.d.ts +2 -0
  35. package/dist/drivers/PGLite.d.ts.map +1 -1
  36. package/dist/drivers/PostgreSQL.d.ts +2 -0
  37. package/dist/drivers/PostgreSQL.d.ts.map +1 -1
  38. package/dist/drivers/SQLite.d.ts +2 -1
  39. package/dist/drivers/SQLite.d.ts.map +1 -1
  40. package/dist/drivers.js +129 -112
  41. package/dist/drivers.js.map +1 -1
  42. package/dist/index.js +2166 -2311
  43. package/dist/index.js.map +1 -1
  44. package/dist/runtime/Controller.d.ts +11 -0
  45. package/dist/runtime/Controller.d.ts.map +1 -1
  46. package/dist/runtime/MonoSystem.d.ts.map +1 -1
  47. package/dist/runtime/Scheduler.d.ts +7 -0
  48. package/dist/runtime/Scheduler.d.ts.map +1 -1
  49. package/dist/runtime/System.d.ts +2 -0
  50. package/dist/runtime/System.d.ts.map +1 -1
  51. package/dist/runtime/computations/Custom.d.ts.map +1 -1
  52. package/dist/runtime/computations/Transform.d.ts.map +1 -1
  53. package/dist/runtime/computations/TransitionFinder.d.ts.map +1 -1
  54. package/dist/runtime/computations/aggregationTemplate.d.ts.map +1 -1
  55. package/dist/runtime/errors/ConditionErrors.d.ts +2 -5
  56. package/dist/runtime/errors/ConditionErrors.d.ts.map +1 -1
  57. package/dist/runtime/transaction.d.ts.map +1 -1
  58. package/dist/storage/erstorage/EntityToTableMap.d.ts.map +1 -1
  59. package/dist/storage/erstorage/MatchExp.d.ts.map +1 -1
  60. package/dist/storage/erstorage/Modifier.d.ts.map +1 -1
  61. package/dist/storage/erstorage/QueryExecutor.d.ts.map +1 -1
  62. package/dist/storage/erstorage/RecordQuery.d.ts.map +1 -1
  63. package/dist/storage/erstorage/SQLBuilder.d.ts +8 -2
  64. package/dist/storage/erstorage/SQLBuilder.d.ts.map +1 -1
  65. package/dist/storage/erstorage/SchemaDialect.d.ts +1 -0
  66. package/dist/storage/erstorage/SchemaDialect.d.ts.map +1 -1
  67. package/dist/storage/erstorage/Setup.d.ts.map +1 -1
  68. package/dist/storage/erstorage/UpdateExecutor.d.ts.map +1 -1
  69. package/dist/storage/utils.d.ts +8 -0
  70. package/dist/storage/utils.d.ts.map +1 -1
  71. package/package.json +1 -1
  72. package/dist/builtins/interaction/Attributive.d.ts +0 -91
  73. package/dist/builtins/interaction/Attributive.d.ts.map +0 -1
  74. package/dist/builtins/interaction/User.d.ts +0 -10
  75. package/dist/builtins/interaction/User.d.ts.map +0 -1
package/README.md CHANGED
@@ -354,7 +354,7 @@ npm install @electric-sql/pglite
354
354
  - **StateMachine** — Model entity lifecycles with explicit state transitions triggered by events
355
355
  - **Filtered Entities** — Virtual views over entities, like reactive database views
356
356
  - **Activities** — Compose multi-step business workflows from ordered Interactions
357
- - **Attributive Permissions** — Declarative, entity-aware access control
357
+ - **Condition Guards** — Declarative access control with full event context
358
358
  - **Dictionary** — Global reactive key-value state
359
359
  - **Schema Constraints** — Persistent unique constraints with structured duplicate errors
360
360
  - **Hard Deletion** — Built-in support for both soft and hard delete patterns
@@ -2656,7 +2656,7 @@ const GetPostsPaginated = Interaction.create({
2656
2656
  > **⚠️ SECURITY: always declare `dataPolicy.match` for row-level access control.**
2657
2657
  > The caller-supplied `query.match` is merged with `dataPolicy.match` using AND — it can only *narrow* the
2658
2658
  > policy-defined scope. But if an interaction has **no** `dataPolicy.match`, the caller fully controls the filter
2659
- > and can read **any** rows of the entity (userAttributives only decide *whether* the interaction may run,
2659
+ > and can read **any** rows of the entity (conditions only decide *whether* the interaction may run,
2660
2660
  > not *which rows* are visible). Every `GetAction` interaction on non-public data MUST declare `dataPolicy.match`.
2661
2661
 
2662
2662
  **Usage Examples for Get Data Interactions**
@@ -2971,12 +2971,11 @@ PayloadItem.create(config: PayloadItemConfig): PayloadItemInstance
2971
2971
  - `config.name` (string, required): Parameter name
2972
2972
  - `config.required` (boolean, optional): Whether it's required, defaults to false
2973
2973
  - `config.isCollection` (boolean, optional): Whether it's a collection type, defaults to false
2974
- - `config.itemRef` (Attributive|Entity, optional): Used to reference entities defined in other interactions within Activity
2975
2974
 
2976
2975
  **Examples**
2977
2976
  ```typescript
2978
2977
  // Reference existing user
2979
- const userRef = PayloadItem.create({
2978
+ const userItem = PayloadItem.create({
2980
2979
  name: 'user',
2981
2980
  required: true
2982
2981
  })
@@ -2992,12 +2991,6 @@ const reviewersItem = PayloadItem.create({
2992
2991
  name: 'reviewers',
2993
2992
  isCollection: true
2994
2993
  })
2995
-
2996
- // Activity item reference
2997
- const activityItem = PayloadItem.create({
2998
- name: 'to',
2999
- itemRef: userRefB
3000
- })
3001
2994
  ```
3002
2995
 
3003
2996
  ## 13.5 Activity-Related APIs
@@ -42,6 +42,24 @@ expect((result.error as ConditionError).error.data.name).toBe('AdminRole')
42
42
  - `'no permission'` → Never used (legacy)
43
43
  - `'condition check failed'` → What you'll actually see
44
44
 
45
+ ### Guard Callback Return Contract (strict boolean)
46
+
47
+ Condition callbacks must return an actual `boolean` (`true` or `false`).
48
+ Any non-boolean return value — including truthy values like `'admin'` and falsy values
49
+ like `null` / `0` / `''` — is treated as a definition error and the check fails
50
+ (fail-closed), with an error message telling you what was returned.
51
+
52
+ Why falsy non-booleans matter: under a `not(...)` combination, a sloppy falsy value
53
+ would otherwise be inverted into a pass. Always coerce explicitly:
54
+
55
+ ```typescript
56
+ // ❌ WRONG: short-circuit expressions can produce null/undefined/0
57
+ content: async (event) => event.user.profile && event.user.profile.isAdmin
58
+
59
+ // ✅ CORRECT: coerce to boolean
60
+ content: async (event) => !!(event.user.profile && event.user.profile.isAdmin)
61
+ ```
62
+
45
63
  ## Testing Permission Patterns
46
64
 
47
65
  ### 1. Role-Based Permission Test
@@ -301,15 +301,16 @@ The interaqt framework wraps all exceptions in the return value, so you NEVER ne
301
301
  ### Error Types
302
302
 
303
303
  ```typescript
304
- // 1. Permission errors
304
+ // 1. Permission errors (condition checks)
305
305
  const result = await controller.callInteraction('DeleteStyle', {
306
306
  user: viewerUser, // viewer role cannot delete
307
307
  payload: { id: style.id }
308
308
  })
309
309
  expect(result.error).toBeDefined()
310
- expect((result.error as any).type).toBe('check user failed')
310
+ expect((result.error as any).type).toBe('condition check failed')
311
+ expect((result.error as any).error.data.name).toBe('AdminOnly') // which condition failed
311
312
 
312
- // 2. Validation errors (payload attributive checks)
313
+ // 2. Payload content validation errors (also condition checks)
313
314
  const result = await controller.callInteraction('PublishStyle', {
314
315
  user: adminUser,
315
316
  payload: {
@@ -317,7 +318,7 @@ const result = await controller.callInteraction('PublishStyle', {
317
318
  }
318
319
  })
319
320
  expect(result.error).toBeDefined()
320
- expect((result.error as any).type).toBe('id not match attributive')
321
+ expect((result.error as any).type).toBe('condition check failed')
321
322
 
322
323
  // 3. Missing required fields
323
324
  const result = await controller.callInteraction('CreateStyle', {
@@ -328,7 +329,7 @@ const result = await controller.callInteraction('CreateStyle', {
328
329
  }
329
330
  })
330
331
  expect(result.error).toBeDefined()
331
- expect((result.error as any).type).toBe('payload label missing')
332
+ expect((result.error as any).type).toBe('label missing')
332
333
 
333
334
  // 4. Business rule violations (condition checks)
334
335
  const result = await controller.callInteraction('CreateStyle', {
@@ -320,18 +320,23 @@ const User = Entity.create({
320
320
  });
321
321
  ```
322
322
 
323
- If you need to perform uniqueness checks at the application level, you can implement them through the Attributive system:
323
+ If you need to perform uniqueness checks at the application level, you can implement them as a Condition on the relevant Interaction:
324
324
 
325
325
  ```javascript
326
- const UniqueEmailAttributive = Attributive.create({
326
+ const UniqueEmail = Condition.create({
327
327
  name: 'UniqueEmail',
328
- content: async function(user, { system }) {
329
- const existingUser = await system.storage.findOne('User',
330
- MatchExp.atom({ key: 'email', value: ['=', user.email] })
328
+ content: async function(event) {
329
+ const existingUser = await this.system.storage.findOne('User',
330
+ MatchExp.atom({ key: 'email', value: ['=', event.payload.user.email] }),
331
+ undefined,
332
+ ['id']
331
333
  );
332
- return !existingUser || existingUser.id === user.id;
334
+ return !existingUser || existingUser.id === event.payload.user.id;
333
335
  }
334
336
  });
337
+
338
+ // Attach to the interaction that creates/updates users
339
+ // Interaction.create({ name: 'UpdateUser', ..., conditions: UniqueEmail })
335
340
  ```
336
341
 
337
342
  ## Complete Example
@@ -728,6 +728,11 @@ const User = Entity.create({
728
728
 
729
729
  ```javascript
730
730
  // ✅ Correct: Create a derived entity based on another entity
731
+ // ⚠️ IMPORTANT: the callback must NOT return a top-level `id` field.
732
+ // Derived-record identity is managed by the framework; spreading the source
733
+ // record (`(product) => ({...product})`) carries its id along and fails fast.
734
+ // Strip it first: `({id: _, ...rest}) => ({...rest})`.
735
+ // (Nested references like `{author: {id}}` are fine — that's how relations attach.)
731
736
  const Product = Entity.create({
732
737
  name: 'Product',
733
738
  properties: [
@@ -1237,7 +1242,7 @@ Note: if several transfers share the same `current` state and their triggers mat
1237
1242
 
1238
1243
  ### Guarding Transitions
1239
1244
 
1240
- There is no `condition` field on `StateTransfer`. Preconditions belong to the interaction layer (use `conditions` / `userAttributives` on the Interaction so unauthorized or invalid calls never produce an event), and fine-grained per-record filtering is expressed in `computeTarget` — return `null`/`undefined` (or an empty array) to skip the transition for records that should not transition:
1245
+ There is no `condition` field on `StateTransfer`. Preconditions belong to the interaction layer (use `conditions` on the Interaction so unauthorized or invalid calls never produce an event), and fine-grained per-record filtering is expressed in `computeTarget` — return `null`/`undefined` (or an empty array) to skip the transition for records that should not transition:
1241
1246
 
1242
1247
  ```javascript
1243
1248
  StateTransfer.create({
@@ -387,9 +387,23 @@ const CreateProduct = Interaction.create({
387
387
 
388
388
  ### Conditional Parameters
389
389
 
390
- The framework itself doesn't support dynamic required conditions and complex validation functions. These logics should be implemented in interaction handling:
390
+ The framework itself doesn't support dynamic required conditions and complex validation functions. These logics should be implemented as conditions on the Interaction:
391
391
 
392
392
  ```javascript
393
+ // Validation logic should be implemented through Condition
394
+ const OrderIsValid = Condition.create({
395
+ name: 'OrderIsValid',
396
+ content: async function(event) {
397
+ const { payload } = event;
398
+ // Shipping address is required for orders under $100
399
+ if (payload.totalAmount < 100 && !payload.shippingAddress) {
400
+ return false;
401
+ }
402
+ // Coupon validation etc.
403
+ return true;
404
+ }
405
+ });
406
+
393
407
  const CreateOrder = Interaction.create({
394
408
  name: 'CreateOrder',
395
409
  action: Action.create({ name: 'createOrder' }),
@@ -402,29 +416,15 @@ const CreateOrder = Interaction.create({
402
416
  }),
403
417
  PayloadItem.create({
404
418
  name: 'shippingAddress'
405
- // Conditional required logic should be checked in interaction handling
419
+ // Conditional required logic is checked by OrderIsValid
406
420
  }),
407
421
  PayloadItem.create({
408
422
  name: 'couponCode'
409
- // Coupon validation should be implemented in business logic
423
+ // Coupon validation is checked by OrderIsValid
410
424
  })
411
425
  ]
412
- })
413
- });
414
-
415
- // Validation logic should be implemented in Transform or Attributive
416
- const orderValidation = Transform.create({
417
- record: InteractionEventEntity,
418
- callback: function(event) {
419
- if (event.interactionName === 'CreateOrder') {
420
- // Implement complex validation logic here
421
- const { payload } = event;
422
- if (payload.totalAmount < 100 && !payload.shippingAddress) {
423
- throw new Error('Shipping address is required for orders under $100');
424
- }
425
- // Coupon validation etc.
426
- }
427
- }
426
+ }),
427
+ conditions: OrderIsValid
428
428
  });
429
429
  ```
430
430
 
@@ -986,6 +986,25 @@ const Review = Entity.create({
986
986
  ### Basic Permission Checks
987
987
 
988
988
  ```javascript
989
+ // Use Condition for permission control
990
+ const CanDeletePost = Condition.create({
991
+ name: 'canDeletePost',
992
+ content: async function(event) {
993
+ // Admin can delete any post
994
+ if (event.user.role === 'admin') {
995
+ return true;
996
+ }
997
+
998
+ // Author can delete their own post
999
+ const post = await this.system.storage.findOne('Post',
1000
+ MatchExp.atom({ key: 'id', value: ['=', event.payload.postId.id] }),
1001
+ undefined,
1002
+ [['author', { attributeQuery: ['id'] }]]
1003
+ );
1004
+ return !!post && post.author?.id === event.user.id;
1005
+ }
1006
+ });
1007
+
989
1008
  // Interaction with permission requirements
990
1009
  const DeletePost = Interaction.create({
991
1010
  name: 'DeletePost',
@@ -995,36 +1014,23 @@ const DeletePost = Interaction.create({
995
1014
  PayloadItem.create({ name: 'postId', base: Post, isRef: true })
996
1015
  ]
997
1016
  }),
998
- // Permission logic should be implemented through Attributive
999
- });
1000
-
1001
- // Use Attributive for permission control
1002
- const DeletePostPermission = Attributive.create({
1003
- name: 'canDeletePost',
1004
- type: 'boolean',
1005
- record: InteractionEventEntity,
1006
- computation: function(interactionEvent) {
1007
- if (interactionEvent.interactionName === 'DeletePost') {
1008
- const user = interactionEvent.user;
1009
- const postId = interactionEvent.payload.postId;
1010
-
1011
- // Admin can delete any post
1012
- if (user.role === 'admin') {
1013
- return true;
1014
- }
1015
-
1016
- // Author can delete their own post
1017
- // This would need to be checked against the actual post data
1018
- return false; // Simplified for example
1019
- }
1020
- return true;
1021
- }
1017
+ // Permission logic is implemented through conditions
1018
+ conditions: CanDeletePost
1022
1019
  });
1023
1020
  ```
1024
1021
 
1025
1022
  ### Role-Based Permission Control
1026
1023
 
1027
1024
  ```javascript
1025
+ // Permission check through Condition
1026
+ const CanModerateContent = Condition.create({
1027
+ name: 'canModerateContent',
1028
+ content: async function(event) {
1029
+ // Only moderators and admins can moderate content
1030
+ return ['moderator', 'admin'].includes(event.user.role);
1031
+ }
1032
+ });
1033
+
1028
1034
  // Content moderation interaction
1029
1035
  const ModerateContent = Interaction.create({
1030
1036
  name: 'ModerateContent',
@@ -1035,23 +1041,8 @@ const ModerateContent = Interaction.create({
1035
1041
  PayloadItem.create({ name: 'action', required: true }), // approve, reject, flag
1036
1042
  PayloadItem.create({ name: 'reason' })
1037
1043
  ]
1038
- })
1039
- });
1040
-
1041
- // Permission check through Attributive
1042
- const ModerationPermission = Attributive.create({
1043
- name: 'canModerateContent',
1044
- type: 'boolean',
1045
- record: InteractionEventEntity,
1046
- computation: function(interactionEvent) {
1047
- if (interactionEvent.interactionName === 'ModerateContent') {
1048
- const user = interactionEvent.user;
1049
-
1050
- // Only moderators and admins can moderate content
1051
- return ['moderator', 'admin'].includes(user.role);
1052
- }
1053
- return true;
1054
- }
1044
+ }),
1045
+ conditions: CanModerateContent
1055
1046
  });
1056
1047
  ```
1057
1048
 
@@ -1,10 +1,57 @@
1
- # Permission Control (Deprecated)
1
+ # Permission Control (Attributive — Removed)
2
2
 
3
- The `userAttributives` and `dataAttributives` permission control features have been removed from the interaqt framework.
3
+ The `Attributive` / `Attributives` concept has been **removed** from the interaqt framework, along with all of its attachment points:
4
4
 
5
- For controlling access to interactions and data, please use:
6
- - `conditions` parameter in Interaction.create() for execution conditions
7
- - Application-level authentication and authorization (e.g., JWT, OAuth)
8
- - Custom business logic in your interactions
5
+ - `Interaction.userAttributives` / `Interaction.userRef`
6
+ - `PayloadItem.attributives` / `PayloadItem.itemRef`
7
+ - `createUserRoleAttributive` / `boolExpToAttributives`
8
+ - activity `refs` and `isRef` user binding
9
9
 
10
- The framework now focuses on reactive business logic rather than built-in permission control.
10
+ Declaring any of these now fails fast at declaration time with an error pointing here.
11
+
12
+ ## Use Conditions instead
13
+
14
+ `Condition` is the single guard concept. A condition callback receives the **full event args** (`user`, `payload`, `query`, `activityId`) with the Controller as `this`, so every form previously expressed with attributives is expressible as a condition:
15
+
16
+ ```typescript
17
+ // Role check (was: createUserRoleAttributive)
18
+ const AdminOnly = Condition.create({
19
+ name: 'AdminOnly',
20
+ content: async function(event) {
21
+ return !!(event.user.roles && event.user.roles.includes('admin'))
22
+ }
23
+ })
24
+
25
+ // Payload-content check (was: PayloadItem.attributives)
26
+ const PublishedPostOnly = Condition.create({
27
+ name: 'PublishedPostOnly',
28
+ content: async function(this: Controller, event) {
29
+ const post = await this.system.storage.findOne('Post',
30
+ MatchExp.atom({ key: 'id', value: ['=', event.payload.post.id] }),
31
+ undefined, ['status'])
32
+ return post?.status === 'published'
33
+ }
34
+ })
35
+
36
+ // Activity user binding (was: userRef/itemRef + isRef attributive):
37
+ // query this activity's own interaction events to locate the bound user.
38
+ const MustBeRequestReceiver = Condition.create({
39
+ name: 'MustBeRequestReceiver',
40
+ content: async function(this: Controller, event) {
41
+ if (!event.activityId) return false
42
+ const sendEvent = await this.system.storage.findOne(InteractionEventEntity.name,
43
+ MatchExp.atom({ key: 'interactionName', value: ['=', 'sendRequest'] })
44
+ .and({ key: 'activity.id', value: ['=', event.activityId] }),
45
+ undefined, ['*'])
46
+ return !!sendEvent && sendEvent.payload?.to?.id === event.user.id
47
+ }
48
+ })
49
+ ```
50
+
51
+ Combine conditions with `Conditions.create({ content: BoolExp.atom(a).and(b).or(c) })`.
52
+
53
+ ## Contract
54
+
55
+ - Guard callbacks must return an actual `boolean`; any non-boolean result fails the check (fail-closed).
56
+ - Conditions run inside the dispatch transaction, before the event record is persisted.
57
+ - For row-level data access control on queries, use `dataPolicy` (match/modifier/attributeQuery) on GetAction interactions.