@toa.io/core 1.0.0-alpha.27 → 1.0.0-alpha.270

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 (63) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/package.json +7 -9
  3. package/src/call.js +21 -1
  4. package/src/cascade.js +7 -5
  5. package/src/component.js +59 -15
  6. package/src/composition.js +1 -1
  7. package/src/connector.js +28 -17
  8. package/src/context.js +6 -0
  9. package/src/contract/contract.js +22 -0
  10. package/src/contract/reply.js +26 -9
  11. package/src/contract/request.js +19 -5
  12. package/src/contract/schemas/index.js +1 -0
  13. package/src/contract/schemas/query.yaml +14 -1
  14. package/src/contract/schemas/source.yaml +23 -0
  15. package/src/discovery.js +13 -7
  16. package/src/effect.js +19 -0
  17. package/src/entities/changeset.js +5 -8
  18. package/src/entities/entity.js +76 -24
  19. package/src/entities/factory.js +17 -6
  20. package/src/entities/newid.js +11 -0
  21. package/src/entities/set.js +13 -0
  22. package/src/event.js +19 -1
  23. package/src/exceptions.js +26 -19
  24. package/src/exposition.js +3 -2
  25. package/src/guard.js +17 -0
  26. package/src/index.js +6 -0
  27. package/src/observation.js +1 -9
  28. package/src/operation.js +38 -8
  29. package/src/query/options.js +3 -2
  30. package/src/query.js +32 -2
  31. package/src/receiver.js +73 -11
  32. package/src/remote.js +8 -7
  33. package/src/state.js +74 -35
  34. package/src/transition.js +11 -19
  35. package/src/transmission.js +12 -3
  36. package/src/unmanaged.js +11 -0
  37. package/test/component.test.js +4 -3
  38. package/test/connector.fixtures.js +8 -0
  39. package/test/connector.test.js +20 -0
  40. package/test/contract/conditions.test.js +5 -5
  41. package/test/contract/request.test.js +46 -7
  42. package/test/discovery.test.js +56 -0
  43. package/test/entities/entity.test.js +54 -40
  44. package/test/entities/factory.test.js +5 -3
  45. package/test/event.test.js +4 -4
  46. package/test/query.test.js +17 -0
  47. package/test/receiver.fixtures.js +1 -0
  48. package/test/state.test.js +13 -12
  49. package/types/bindings.d.ts +7 -5
  50. package/types/bridges.ts +3 -0
  51. package/types/component.d.ts +4 -1
  52. package/types/context.d.ts +3 -0
  53. package/types/entity.d.ts +2 -2
  54. package/types/extensions.d.ts +7 -4
  55. package/types/index.ts +2 -1
  56. package/types/message.d.ts +1 -0
  57. package/types/operations.d.ts +6 -0
  58. package/types/query.d.ts +2 -0
  59. package/types/remote.d.ts +18 -0
  60. package/types/request.d.ts +15 -0
  61. package/types/state.d.ts +2 -2
  62. package/types/storages.d.ts +11 -9
  63. package/src/contract/conditions.js +0 -21
@@ -7,40 +7,71 @@ beforeEach(() => {
7
7
  jest.clearAllMocks()
8
8
  })
9
9
 
10
- describe('new', () => {
11
- it('should throw on schema error', () => {
12
- const entity = new Entity(fixtures.schema)
10
+ describe('argument', () => {
11
+ it('should set state', () => {
12
+ const state = fixtures.state()
13
+ const entity = new Entity(fixtures.schema, state)
13
14
 
14
- expect(() => entity.set(fixtures.failed())).toThrow()
15
+ expect(entity.get()).toEqual(state)
15
16
  })
16
17
 
17
- it('should provide state', () => {
18
- const entity = new Entity(fixtures.schema)
19
- const state = fixtures.state()
18
+ it('should snapshot the record it may commit', () => {
19
+ const record = fixtures.state()
20
+ const entity = new Entity(fixtures.schema, record)
20
21
 
21
- entity.set(state)
22
+ expect(entity.get()).not.toBe(record)
23
+ expect(entity.event().origin).toBe(record)
24
+ })
25
+ })
22
26
 
23
- expect(entity.get()).toEqual(state)
27
+ describe('read-only', () => {
28
+ it('should take the record as it came', () => {
29
+ const record = fixtures.state()
30
+ const entity = new Entity(fixtures.schema, record, undefined, false)
31
+
32
+ // no pre-image to diff against, hence no copy of it
33
+ expect(entity.get()).toBe(record)
34
+ })
35
+
36
+ it('should still report a tombstone', () => {
37
+ const record = { ...fixtures.state(), _deleted: Date.now() }
38
+ const entity = new Entity(fixtures.schema, record, undefined, false)
39
+
40
+ expect(entity.deleted).toBe(true)
41
+ })
42
+
43
+ it('should refuse to be modified', () => {
44
+ const entity = new Entity(fixtures.schema, fixtures.state(), undefined, false)
45
+
46
+ expect(() => entity.set(entity.get())).toThrow('read-only')
24
47
  })
25
48
  })
26
49
 
27
- describe('argument', () => {
28
- it('should provide initial state if no argument passed', () => {
29
- const entity = new Entity(fixtures.schema)
30
- const defaults = fixtures.schema.defaults.mock.results[0].value
31
- const expected = {
32
- ...defaults,
33
- _version: 0
34
- }
35
-
36
- expect(entity.get()).toStrictEqual(expect.objectContaining(expected))
50
+ describe('tombstone', () => {
51
+ it('should lift tombstone when transition leaves _deleted untouched', () => {
52
+ const origin = fixtures.state()
53
+ const entity = new Entity(fixtures.schema, origin)
54
+ const state = entity.get()
55
+
56
+ state.foo = 'revived'
57
+ entity.set(state)
58
+
59
+ expect(entity.get()._deleted).toBeNull()
60
+ expect(entity.deleted).toBe(false)
61
+ expect(entity.event().changeset._deleted).toBeNull()
37
62
  })
38
63
 
39
- it('should set state', () => {
40
- const state = fixtures.state()
41
- const entity = new Entity(fixtures.schema, state)
64
+ it('should keep tombstone written by transition', () => {
65
+ const origin = { ...fixtures.state(), _deleted: null }
66
+ const entity = new Entity(fixtures.schema, origin)
67
+ const state = entity.get()
68
+ const timestamp = Date.now()
42
69
 
43
- expect(entity.get()).toEqual(state)
70
+ state._deleted = timestamp
71
+ entity.set(state)
72
+
73
+ expect(entity.get()._deleted).toBe(timestamp)
74
+ expect(entity.deleted).toBe(true)
44
75
  })
45
76
  })
46
77
 
@@ -63,20 +94,3 @@ it('should provide event', () => {
63
94
  })
64
95
  }))
65
96
  })
66
-
67
- it('should define `id` as readonly', async () => {
68
- const origin = fixtures.state()
69
- const entity = new Entity(fixtures.schema, origin)
70
- const state = entity.get()
71
-
72
- expect(() => (state.id = 1)).toThrow('assign to read only property')
73
- })
74
-
75
- it('should seal id', async () => {
76
- const origin = fixtures.state()
77
- const entity = new Entity(fixtures.schema, origin)
78
- const state = entity.get()
79
- const redefine = () => Object.defineProperty(state, 'id', { writable: true })
80
-
81
- expect(redefine).toThrow('redefine property')
82
- })
@@ -23,14 +23,15 @@ it('should create initial', () => {
23
23
  const initial = factory.init(id)
24
24
 
25
25
  expect(initial).toBeInstanceOf(mock.Entity)
26
- expect(initial.constructor).toHaveBeenCalledWith(fixtures.schema, id)
26
+ expect(initial.constructor).toHaveBeenCalledWith(fixtures.schema, id, expect.any(Function))
27
27
  })
28
28
 
29
29
  it('should create instance', () => {
30
30
  const object = factory.object(fixtures.entity)
31
31
 
32
32
  expect(object).toBeInstanceOf(mock.Entity)
33
- expect(object.constructor).toHaveBeenCalledWith(fixtures.schema, fixtures.entity)
33
+ expect(object.constructor)
34
+ .toHaveBeenCalledWith(fixtures.schema, fixtures.entity, expect.any(Function), true)
34
35
  })
35
36
 
36
37
  it('should create set', () => {
@@ -39,7 +40,8 @@ it('should create set', () => {
39
40
  expect(objects).toBeInstanceOf(mock.EntitySet)
40
41
 
41
42
  const instances = fixtures.set.map((entity, index) => {
42
- expect(mock.Entity).toHaveBeenNthCalledWith(index + 1, fixtures.schema, entity)
43
+ expect(mock.Entity)
44
+ .toHaveBeenNthCalledWith(index + 1, fixtures.schema, entity, expect.any(Function), true)
43
45
 
44
46
  return mock.Entity.mock.instances[index]
45
47
  })
@@ -41,7 +41,7 @@ describe('condition', () => {
41
41
  await emit()
42
42
 
43
43
  const payload = await fixtures.bridge.payload.mock.results[0].value
44
- const message = { payload }
44
+ const message = { payload, telemetry: expect.any(String) }
45
45
  expect(fixtures.binding.emit).toHaveBeenCalledWith(message)
46
46
  })
47
47
 
@@ -71,7 +71,7 @@ describe('condition', () => {
71
71
  expect(fixtures.bridge.condition).not.toHaveBeenCalledWith()
72
72
 
73
73
  const payload = await fixtures.bridge.payload.mock.results[0].value
74
- const message = { payload }
74
+ const message = { payload, telemetry: expect.any(String) }
75
75
 
76
76
  expect(fixtures.binding.emit).toHaveBeenCalledWith(message)
77
77
  })
@@ -84,7 +84,7 @@ describe('payload', () => {
84
84
  await emit()
85
85
 
86
86
  const payload = await fixtures.bridge.payload.mock.results[0].value
87
- const message = { payload }
87
+ const message = { payload, telemetry: expect.any(String) }
88
88
 
89
89
  expect(fixtures.binding.emit).toHaveBeenCalledWith(message)
90
90
  })
@@ -110,7 +110,7 @@ describe('payload', () => {
110
110
  await emit()
111
111
 
112
112
  const payload = fixtures.event.state
113
- const message = { payload }
113
+ const message = { payload, telemetry: expect.any(String) }
114
114
 
115
115
  expect(fixtures.binding.emit).toHaveBeenCalledWith(message)
116
116
  })
@@ -22,6 +22,23 @@ describe('criteria', () => {
22
22
  expect(query.criteria).toEqual(fixtures.samples.simple.parsed.criteria)
23
23
  })
24
24
 
25
+ it('should keep a parsed criteria', () => {
26
+ const instance = new Query(fixtures.samples.simple.properties)
27
+
28
+ const first = instance.parse(fixtures.samples.simple.query).criteria
29
+ const second = instance.parse(fixtures.samples.simple.query).criteria
30
+
31
+ expect(second).toBe(first)
32
+ })
33
+
34
+ it('should not keep an invalid criteria', () => {
35
+ const instance = new Query(fixtures.samples.simple.properties)
36
+ const query = { criteria: 'nonexistent==1' }
37
+
38
+ expect(() => instance.parse(query)).toThrow()
39
+ expect(() => instance.parse(query)).toThrow()
40
+ })
41
+
25
42
  it('should parse criteria with type coercion', () => {
26
43
  const instance = new Query(fixtures.samples.extended.properties)
27
44
  const query = instance.parse(fixtures.samples.extended.query)
@@ -9,6 +9,7 @@ const definition = /** @type {toa.norm.component.Receiver} */ {
9
9
  }
10
10
 
11
11
  const local = /** @type {toa.core.Component} */ {
12
+ locator: { id: 'default.dummy' },
12
13
  invoke: jest.fn()
13
14
  }
14
15
 
@@ -16,15 +16,22 @@ it('should provide object', async () => {
16
16
 
17
17
  expect(fixtures.storage.get).toHaveBeenCalledWith(fixtures.query)
18
18
  expect(entity).toStrictEqual(fixtures.factory.object.mock.results[0].value)
19
- expect(fixtures.factory.object).toHaveBeenCalledWith(fixtures.storage.get.mock.results[0].value)
19
+ expect(fixtures.factory.object)
20
+ .toHaveBeenCalledWith(fixtures.storage.get.mock.results[0].value, true)
20
21
  })
21
22
 
22
- it('should provide objects', async () => {
23
- const set = await state.objects(fixtures.query)
23
+ it('should provide read-only object', async () => {
24
+ await state.object(fixtures.query, false)
24
25
 
25
- expect(fixtures.storage.find).toHaveBeenCalledWith(fixtures.query)
26
- expect(set).toStrictEqual(fixtures.factory.objects.mock.results[0].value)
27
- expect(fixtures.factory.objects).toHaveBeenCalledWith(fixtures.storage.find.mock.results[0].value)
26
+ expect(fixtures.factory.object)
27
+ .toHaveBeenCalledWith(fixtures.storage.get.mock.results[0].value, false)
28
+ })
29
+
30
+ it('should provide read-only objects', async () => {
31
+ await state.objects(fixtures.query, false)
32
+
33
+ expect(fixtures.factory.objects)
34
+ .toHaveBeenCalledWith(fixtures.storage.find.mock.results[0].value, undefined, false)
28
35
  })
29
36
 
30
37
  it('should store entity', async () => {
@@ -38,9 +45,3 @@ it('should emit', async () => {
38
45
 
39
46
  expect(fixtures.emitter.emit).toHaveBeenCalledWith(fixtures.entity.event.mock.results[0].value)
40
47
  })
41
-
42
- it('should not emit if state has not been changed', async () => {
43
- await state.commit(fixtures.unchanged)
44
-
45
- expect(fixtures.emitter.emit).not.toHaveBeenCalled()
46
- })
@@ -1,26 +1,28 @@
1
1
  import * as _core from './index'
2
2
 
3
- declare namespace toa.core.bindings{
3
+ declare namespace toa.core.bindings {
4
4
 
5
5
  type Properties = {
6
6
  async?: boolean
7
7
  }
8
8
 
9
- interface Consumer extends _core.Connector{
9
+ interface Consumer extends _core.Connector {
10
10
  request (request: Request): Promise<_core.Reply>
11
+
12
+ task (request: Request): Promise<void>
11
13
  }
12
14
 
13
- interface Emitter extends _core.Connector{
15
+ interface Emitter extends _core.Connector {
14
16
  emit (message: _core.Message): Promise<void>
15
17
  }
16
18
 
17
- interface Broadcast<L> extends _core.Connector{
19
+ interface Broadcast<L> extends _core.Connector {
18
20
  transmit<T> (label: L, payload: T): Promise<void>
19
21
 
20
22
  receive<T> (label: L, callback: (payload: T) => void | Promise<void>): Promise<void>
21
23
  }
22
24
 
23
- interface Factory{
25
+ interface Factory {
24
26
  producer? (locator: _core.Locator, endpoints: Array<string>, producer: _core.Component): _core.Connector
25
27
 
26
28
  consumer? (locator: _core.Locator, endpoint: string): Consumer
package/types/bridges.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Request, Reply } from './request'
2
2
  import type { Context } from './context'
3
+ import type { Connector } from './connector'
3
4
 
4
5
  export interface Algorithm {
5
6
  mount: (context?: Context) => Promise<void>
@@ -27,4 +28,6 @@ export interface Factory {
27
28
  event?: (path: string, label: string) => Event
28
29
 
29
30
  receiver?: (path: string, label: string) => Receiver
31
+
32
+ rc?: (path: string, context: Context) => Promise<{ preflight?: Connector, settle?: Connector, dispose?: Connector } | undefined>
30
33
  }
@@ -1,9 +1,12 @@
1
1
  import { Connector } from './connector'
2
2
  import { Locator } from './locator'
3
3
  import { Request } from './request'
4
+ import { Operation } from './operations'
4
5
 
5
- export interface Component extends Connector{
6
+ export class Component extends Connector {
6
7
  locator: Locator
7
8
 
9
+ constructor (locator: Locator, operations: Record<string, Operation>)
10
+
8
11
  invoke<T = any> (endpoint: string, request: Request): Promise<T>
9
12
  }
@@ -1,4 +1,5 @@
1
1
  import * as _request from './request'
2
+ import { Locator } from './locator'
2
3
  import * as _reply from './reply'
3
4
  import * as _extensions from './extensions'
4
5
  import * as _connector from './connector'
@@ -6,6 +7,8 @@ import * as _connector from './connector'
6
7
  export interface Context extends _connector.Connector{
7
8
  aspects: _extensions.Aspect[]
8
9
 
10
+ locator: Locator
11
+
9
12
  /**
10
13
  * Calls local endpoint
11
14
  */
package/types/entity.d.ts CHANGED
@@ -8,9 +8,9 @@ declare namespace toa.core {
8
8
  interface Factory {
9
9
  init(id: string): Entity
10
10
 
11
- object(record: Object): Entity
11
+ object(record: Object, mutable?: boolean): Entity
12
12
 
13
- objects(recordset: Object[]): Entity[]
13
+ objects(recordset: Object[], init?: string[], mutable?: boolean): Entity[]
14
14
 
15
15
  changeset(query: _storages.Query): Changeset
16
16
  }
@@ -3,9 +3,10 @@ import * as _component from './component'
3
3
  import * as _context from './context'
4
4
  import * as _storages from './storages'
5
5
  import * as _bindings from './bindings'
6
+ import { Manifest } from '@toa.io/norm'
6
7
 
7
- export interface Factory{
8
- tenant? (locator: _core.Locator, manifest: object): _core.Connector
8
+ export interface Factory {
9
+ tenant? (locator: _core.Locator, manifest: object, component: Manifest): _core.Connector
9
10
 
10
11
  aspect? (locator: _core.Locator, manifest: object | null): Aspect | Aspect[]
11
12
 
@@ -15,14 +16,16 @@ export interface Factory{
15
16
 
16
17
  context? (context: _context.Context): _context.Context
17
18
 
19
+ manage? (composition: _core.Connector): _core.Connector
20
+
18
21
  storage? (storage: _storages.Storage): _storages.Storage
19
22
 
20
- emitter? (emitter: _bindings.Emitter, label: string): _bindings.Emitter
23
+ emitter? (emitter: _bindings.Emitter, label: string, locator: _core.Locator): _bindings.Emitter
21
24
 
22
25
  receiver? (receiver: _core.Receiver, locator: _core.Locator): _core.Receiver
23
26
  }
24
27
 
25
- export interface Aspect extends _core.Connector{
28
+ export interface Aspect extends _core.Connector {
26
29
  name: string
27
30
 
28
31
  invoke (...args: any[]): any
package/types/index.ts CHANGED
@@ -5,10 +5,11 @@ export * as bridges from './bridges'
5
5
  export * as operations from './operations'
6
6
 
7
7
  export type { Component } from './component'
8
+ export type { Remote } from './remote'
8
9
  export { Connector } from './connector'
9
10
  export type { Context } from './context'
10
11
  export type { Exception } from './exception'
11
12
  export { Locator } from './locator'
12
13
  export type { Receiver } from './receiver'
13
14
  export type { Message } from './message'
14
- export type { Request, Query, Reply } from './request'
15
+ export type { Request, Query, Reply, Source } from './request'
@@ -1,3 +1,4 @@
1
1
  export interface Message<T = any> {
2
2
  payload: T
3
+ telemetry?: string // W3C traceparent
3
4
  }
@@ -1,2 +1,8 @@
1
+ import { Request } from './request'
2
+
1
3
  export type type = 'transition' | 'observation' | 'assignment' | 'computation' | 'effect'
2
4
  export type scope = 'object' | 'objects' | 'changeset'
5
+
6
+ export class Operation {
7
+ invoke<T = any> (request: Request): Promise<T>
8
+ }
package/types/query.d.ts CHANGED
@@ -4,6 +4,8 @@ declare namespace toa.core {
4
4
  id?: string
5
5
  version?: number
6
6
  criteria?: Object
7
+ search?: string
8
+ sample?: number
7
9
  omit?: number
8
10
  limit?: number
9
11
  sort?: string[]
@@ -0,0 +1,18 @@
1
+ import { Component } from './component'
2
+
3
+ export class Remote extends Component {
4
+ explain (endpoint: string): Promise<Explanation>
5
+ }
6
+
7
+ interface Explanation {
8
+ input: Schema | null
9
+ output: Schema | null
10
+ errors?: string[]
11
+ }
12
+
13
+ interface Schema {
14
+ type: string
15
+ properties: {
16
+ [key: string]: Schema
17
+ }
18
+ }
@@ -2,18 +2,33 @@ import { Exception } from './exception'
2
2
 
3
3
  export interface Query {
4
4
  id?: string
5
+ ids?: Array<string>
5
6
  criteria?: string
7
+ search?: string
8
+ sample?: number
6
9
  omit?: number
7
10
  limit?: number
8
11
  sort?: Array<string>
9
12
  projection?: Array<string>
10
13
  version?: number
14
+ deleted?: boolean
11
15
  }
12
16
 
17
+ /**
18
+ * Origin of a call. Stamped by the caller, sanitized by the request contract.
19
+ */
20
+ export type Source =
21
+ | { namespace: string, component: string, operation: string }
22
+ | { namespace: string, component: string, event: string }
23
+ | { service: string }
24
+
13
25
  export interface Request {
14
26
  input?: any
15
27
  query?: Query
16
28
  authentic?: boolean
29
+ task?: boolean
30
+ telemetry?: string // W3C traceparent
31
+ source?: Source
17
32
  }
18
33
 
19
34
  export interface Reply {
package/types/state.d.ts CHANGED
@@ -16,9 +16,9 @@ declare namespace toa.core {
16
16
  interface State {
17
17
  init(id: string): _entity.Entity
18
18
 
19
- object(query: _storages.Query): Promise<_entity.Entity>
19
+ object(query: _storages.Query, mutable?: boolean): Promise<_entity.Entity>
20
20
 
21
- objects(query: _storages.Query): Promise<_entity.Entity[]>
21
+ objects(query: _storages.Query, mutable?: boolean): Promise<_entity.Entity[]>
22
22
 
23
23
  changeset(query: _storages.Query): _entity.Changeset
24
24
 
@@ -30,36 +30,38 @@ declare namespace toa.core {
30
30
  id?: string
31
31
  version?: number
32
32
  criteria?: ast.Node
33
+ search?: string
34
+ sample?: number
33
35
  options?: Object
34
36
  }
35
37
 
36
38
  interface Migration {
37
- disconnect(): Promise<void>
39
+ disconnect (): Promise<void>
38
40
 
39
- database(name: string): Promise<void>
41
+ database (name: string): Promise<void>
40
42
 
41
- table(database: string, locator: Locator, schema: Object, reset?: boolean): Promise<string>
43
+ table (database: string, locator: Locator, schema: Object, reset?: boolean): Promise<string>
42
44
  }
43
45
 
44
46
  interface Factory {
45
- storage(locator: Locator, properties?: object): Storage
47
+ storage (locator: Locator, properties?: object): Storage
46
48
 
47
- migration?(driver?: string): Migration
49
+ migration? (driver?: string): Migration
48
50
  }
49
51
  }
50
52
 
51
53
  interface Storage extends Connector {
52
54
  // object observation
53
- get?(query: storages.Query): Promise<storages.Record | null>
55
+ get? (query: storages.Query): Promise<storages.Record | null>
54
56
 
55
57
  // objects observation
56
- find?(query: storages.Query): Promise<storages.Record[]>
58
+ find? (query: storages.Query): Promise<storages.Record[]>
57
59
 
58
60
  // commit
59
- store?(record: storages.Record): Promise<boolean>
61
+ store? (record: storages.Record): Promise<boolean>
60
62
 
61
63
  // assignment
62
- upsert?(query: storages.Query, changeset: Object, insert: storages.Record): Promise<storages.Record>
64
+ upsert? (query: storages.Query, changeset: Object, insert: storages.Record): Promise<storages.Record>
63
65
  }
64
66
 
65
67
  }
@@ -1,21 +0,0 @@
1
- 'use strict'
2
-
3
- const { SystemException } = require('../exceptions')
4
-
5
- class Conditions {
6
- #schema
7
-
8
- constructor (schema) {
9
- this.#schema = schema
10
- }
11
-
12
- fit (value) {
13
- const error = this.#schema.fit(value)
14
-
15
- if (error !== null) throw new this.constructor.Exception(error)
16
- }
17
-
18
- static Exception = SystemException
19
- }
20
-
21
- exports.Conditions = Conditions