/ test / fact.spec.js
fact.spec.js
  1  import { fact, same, Text, Memory, $, Math, Link, Task } from './lib.js'
  2  import proofsDB from './proofs.db.js'
  3  import moviesDB from './movie.db.js'
  4  import employeeDB from './microshaft.db.js'
  5  
  6  /**
  7   * @type {import('entail').Suite}
  8   */
  9  export const testDB = {
 10    'test claim and assert derive equal this': async (assert) => {
 11      const Person = fact({
 12        name: String,
 13        address: String,
 14      })
 15  
 16      const Employee = fact({ name: String }).where(({ name }) => [
 17        Person.match({ name }),
 18        Employee.claim({ name }),
 19      ])
 20  
 21      const marije = Person.assert({
 22        name: 'Marije',
 23        address: 'Amsterdam, Netherlands',
 24      })
 25  
 26      const db = Memory.create([])
 27      await Task.perform(db.transact(marije))
 28      const [employee, ...none] = await Employee().query({ from: db })
 29  
 30      assert.deepEqual([employee, ...none], [Employee.assert({ name: 'Marije' })])
 31      assert.deepEqual(employee.this, Employee.assert({ name: 'Marije' }).this)
 32      assert.notDeepEqual(employee.this, marije.this)
 33    },
 34    'can provide explicit this': async (assert) => {
 35      const Person = fact({
 36        name: String,
 37        address: String,
 38      })
 39  
 40      const Employee = fact({ name: String }).where(({ this: person, name }) => [
 41        Person.match({ this: person, name }),
 42        Employee.claim({ this: person, name }),
 43      ])
 44  
 45      const marije = Person.assert({
 46        name: 'Marije',
 47        address: 'Amsterdam, Netherlands',
 48      })
 49  
 50      const db = Memory.create([])
 51      await Task.perform(db.transact(marije))
 52      const [employee, ...none] = await Employee().query({ from: db })
 53  
 54      assert.deepEqual(
 55        [employee, ...none],
 56        [Employee.assert({ this: marije.this, name: 'Marije' })]
 57      )
 58      assert.notDeepEqual(employee.this, Employee.assert({ name: 'Marije' }).this)
 59      assert.deepEqual(employee.this, marije.this)
 60    },
 61    'test fact assert': async (assert) => {
 62      const db = Memory.create([])
 63  
 64      const Person = fact({
 65        name: String,
 66        address: String,
 67      })
 68  
 69      assert.deepEqual(await Person().query({ from: db }), [], 'db is empty')
 70  
 71      const marije = Person.assert({
 72        name: 'Marije',
 73        address: 'Amsterdam, Netherlands',
 74      })
 75  
 76      await Task.perform(db.transact(marije))
 77  
 78      assert.deepEqual(
 79        await Person().query({ from: db }),
 80        [marije],
 81        'fact was asserted'
 82      )
 83  
 84      assert.throws(
 85        () =>
 86          // @ts-expect-error
 87          Person.assert({ name: 'Bob' }),
 88        /Required attribute "address" is missing/
 89      )
 90  
 91      assert.throws(
 92        () =>
 93          // @ts-expect-error
 94          Person.assert({ address: 'Paris, France' }),
 95        /Required attribute "name" is missing/
 96      )
 97  
 98      assert.throws(
 99        () =>
100          // @ts-expect-error
101          Person.assert({ address: 'Paris, France' }),
102        /Required attribute .* is missing/
103      )
104  
105      const paul = Person.assert({
106        this: Link.of({ whatever: {} }),
107        name: 'Paul',
108        address: 'Paris, France',
109      })
110  
111      assert.deepEqual(
112        paul.this,
113        Link.of({ whatever: {} }),
114        'uses provided "this"'
115      )
116  
117      assert.deepEqual(
118        Person.assert({
119          name: 'Rome',
120          address: 'Florence, Italy',
121        }).this,
122        Person.assert({
123          name: 'Rome',
124          address: 'Florence, Italy',
125        }).this,
126        'entity generation is deterministic'
127      )
128  
129      assert.notDeepEqual(
130        Person.assert({
131          this: Link.of({ other: 'other' }),
132          name: 'Rome',
133          address: 'Florence, Italy',
134        }).this,
135        Person.assert({
136          name: 'Rome',
137          address: 'Florence, Italy',
138        }).this
139      )
140    },
141    'test fact derives namespace': async (assert) => {
142      const Person = fact({
143        name: String,
144        address: String,
145      })
146  
147      const marije = {
148        [`${Person.the}/name`]: 'Marije',
149        [`${Person.the}/address`]: 'Amsterdam, Netherlands',
150      }
151  
152      const people = await Person().query({
153        from: Memory.create({ marije }),
154      })
155  
156      assert.deepEqual(people, [
157        Person.assert({
158          this: Link.of(marije),
159          name: 'Marije',
160          address: 'Amsterdam, Netherlands',
161        }),
162      ])
163    },
164    'test partial query': async (assert) => {
165      const Person = fact({
166        name: String,
167        address: String,
168      })
169  
170      const marije = {
171        [`${Person.the}/name`]: 'Marije',
172        [`${Person.the}/address`]: 'Amsterdam, Netherlands',
173      }
174      const bjorn = {
175        [`${Person.the}/name`]: 'Bjorn',
176        [`${Person.the}/address`]: 'Amsterdam, Netherlands',
177      }
178      const jack = {
179        [`${Person.the}/name`]: 'Jack',
180      }
181  
182      const db = Memory.create({ marije, bjorn })
183      const out = await Person.match({ name: Person.attributes.name }).query({
184        from: db,
185      })
186  
187      assert.deepEqual(
188        await Person.match({ name: 'Marije' }).query({
189          from: db,
190        }),
191        [
192          Person.assert({
193            this: Link.of(marije),
194            name: 'Marije',
195            address: 'Amsterdam, Netherlands',
196          }),
197        ],
198        'finds by name'
199      )
200  
201      assert.deepEqual(
202        await Person.match({ address: 'Amsterdam, Netherlands' }).query({
203          from: db,
204        }),
205        [
206          Person.assert({
207            this: Link.of(marije),
208            name: 'Marije',
209            address: 'Amsterdam, Netherlands',
210          }),
211          Person.assert({
212            this: Link.of(bjorn),
213            name: 'Bjorn',
214            address: 'Amsterdam, Netherlands',
215          }),
216        ],
217        'finds by address'
218      )
219  
220      assert.deepEqual(
221        await Person.match({ name: $.address }).query({
222          from: db,
223        }),
224        [
225          Person.assert({
226            this: Link.of(marije),
227            name: 'Marije',
228            address: 'Amsterdam, Netherlands',
229          }),
230          Person.assert({
231            this: Link.of(bjorn),
232            name: 'Bjorn',
233            address: 'Amsterdam, Netherlands',
234          }),
235        ],
236        'does not conflade variables'
237      )
238    },
239    'test can provide namespace': async (assert) => {
240      const Person = fact({
241        the: 'person',
242        name: String,
243        address: String,
244      })
245  
246      const marije = {
247        'person/name': 'Marije',
248        'person/address': 'Amsterdam, Netherlands',
249      }
250  
251      const people = await Person().query({
252        from: Memory.create({ marije }),
253      })
254  
255      assert.deepEqual(people, [
256        Person.assert({
257          this: Link.of(marije),
258          name: 'Marije',
259          address: 'Amsterdam, Netherlands',
260        }),
261      ])
262    },
263    'test use in nested context': async (assert) => {
264      const Person = fact({
265        the: 'person',
266        name: String,
267        address: String,
268      })
269  
270      const marije = {
271        'person/name': 'Marije',
272        'person/address': 'Amsterdam, Netherlands',
273      }
274  
275      const bob = {
276        'person/name': 'Bob',
277        'person/address': 'San Francisco, CA, USA',
278      }
279  
280      const Remote = Person.where(($) => [
281        Person($),
282        Text.match({ this: $.address, pattern: '*Netherlands' }),
283      ])
284  
285      const db = Memory.create({ marije, bob })
286  
287      assert.deepEqual(await Person().query({ from: db }), [
288        Person.assert({
289          this: Link.of(marije),
290          name: 'Marije',
291          address: 'Amsterdam, Netherlands',
292        }),
293        Person.assert({
294          this: Link.of(bob),
295          name: 'Bob',
296          address: 'San Francisco, CA, USA',
297        }),
298      ])
299  
300      assert.deepEqual(await Remote().query({ from: db }), [
301        Person.assert({
302          this: Link.of(marije),
303          name: 'Marije',
304          address: 'Amsterdam, Netherlands',
305        }),
306      ])
307    },
308    'test use assert in deriviation': async (assert) => {
309      const Model = fact({
310        name: String,
311        address: String,
312      })
313  
314      const View = fact({
315        the: 'person',
316        name: String,
317        address: String,
318      })
319        .with({ model: Object })
320        .where(($) => [
321          Model({ this: $.model, name: $.name, address: $.address }),
322          View.claim({ this: $.model, name: $.name, address: $.address }),
323        ])
324  
325      const marije = {
326        'person/name': 'Marije',
327        'person/address': 'Amsterdam, Netherlands',
328      }
329  
330      const bob = {
331        'person/name': 'Bob',
332        'person/address': 'San Francisco, CA, USA',
333      }
334  
335      const alice = {
336        [`${Model.the}/name`]: 'Alice',
337        [`${Model.the}/address`]: 'Paris, France',
338      }
339  
340      const db = Memory.create({ marije, bob, alice })
341      assert.deepEqual(await View().query({ from: db }), [
342        View.assert({
343          this: Link.of(alice),
344          name: 'Alice',
345          address: 'Paris, France',
346        }),
347      ])
348    },
349  
350    'test behavior modeling': async (assert) => {
351      const Counter = fact({
352        the: 'io.gozala.counter',
353        count: Number,
354        title: String,
355      })
356  
357      const Increment = fact({
358        the: 'io.gozala.increment',
359        command: Object,
360      })
361  
362      const Behavior = Counter.with({ lastCount: Number }).when((counter) => ({
363        // If we have no counter we derive a new one.
364        new: [
365          Counter.not({ this: counter.this }),
366          Behavior.claim({
367            this: Link.of({ counter: { v: 1 } }),
368            count: 0,
369            title: 'basic counter',
370          }),
371        ],
372        // If we have a counter but it's note benig incremented it continues
373        // as is.
374        continue: [Increment.not({ this: counter.this }), Counter(counter)],
375        // If there is a counter with `lastCount` for the a count and
376        // there is an `Increment` fact for it this counter count is
377        // incremented by one.
378        increment: [
379          Counter({ ...counter, count: counter.lastCount }),
380          Increment({ this: counter.this, command: counter._ }),
381          Math.Sum({ of: counter.lastCount, with: 1, is: counter.count }),
382        ],
383      }))
384  
385      const db = Memory.create([])
386  
387      const init = await Behavior().query({ from: db })
388      assert.deepEqual(
389        init,
390        [
391          Behavior.assert({
392            this: Link.of({ counter: { v: 1 } }),
393            count: 0,
394            title: 'basic counter',
395          }),
396        ],
397        'starts with empty counter'
398      )
399  
400      await Task.perform(
401        db.transact(
402          Counter.assert({
403            this: Link.of({ counter: { v: 1 } }),
404            count: 0,
405            title: 'persisted counter',
406          })
407        )
408      )
409  
410      const idle = await Behavior().query({ from: db })
411      assert.deepEqual(
412        idle,
413        [
414          Behavior.assert({
415            this: Link.of({ counter: { v: 1 } }),
416            count: 0,
417            title: 'persisted counter',
418          }),
419        ],
420        'remains idle'
421      )
422  
423      // Assert increment action
424      await Task.perform(
425        db.transact(
426          Increment.assert({
427            this: Link.of({ counter: { v: 1 } }),
428            command: Link.of({}),
429          })
430        )
431      )
432  
433      const increment = await Behavior().query({ from: db })
434      assert.deepEqual(
435        increment,
436        [
437          Behavior.assert({
438            this: Link.of({ counter: { v: 1 } }),
439            count: 1,
440            title: 'persisted counter',
441          }),
442        ],
443        'incrementns counter'
444      )
445    },
446    'skip test ui idea': async (assert) => {
447      const UI = fact({
448        the: 'io.gozala.view',
449        this: Object,
450        ui: Object,
451      })
452  
453      const Counter = fact({
454        the: 'io.gozala.counter',
455        count: Number,
456        title: String,
457      })
458  
459      const Increment = fact({
460        the: 'io.gozala.increment',
461        command: Object,
462      })
463  
464      Increment.assert({ command: Link.of({}) })
465  
466      const View = UI.with({ count: Number }).where(($) => [
467        Counter.match({ this: $.this, count: $.count }),
468        View.claim({
469          this: $.this,
470          // @ts-expect-error
471          ui: html`<div>${$.count}<button onclick=${Increment}>+</button></div>`,
472        }),
473      ])
474    },
475  
476    'test basic fact': async (assert) => {
477      assert.throws(() => fact({}), /schema must contain at least one property/i)
478      assert.throws(
479        () => fact({ this: Object }),
480        /schema must contain at least one property/i
481      )
482  
483      const tag = fact({ the: 'action' })
484      assert.deepEqual(tag.assert({}).the, 'action')
485  
486      assert.throws(
487        // @ts-expect-error
488        () => fact({ this: Number, that: Number }),
489        /Schema may not have \"this\" property that is not an entity/i
490      )
491  
492      assert.throws(
493        // @ts-expect-error
494        () => fact({ _: Object }),
495        /Schema may no have reserved \"_\" property/i
496      )
497    },
498  
499    'test map method': async (assert) => {
500      const Counter = fact({
501        the: 'io.gozala.counter',
502        count: Number,
503        title: String,
504      })
505  
506      class CounterView {
507        /**
508         * @param {{count:number, title:string}} model
509         */
510        constructor(model) {
511          this.model = model
512        }
513      }
514  
515      const counter = {
516        'io.gozala.counter/count': 0,
517        'io.gozala.counter/title': 'test',
518      }
519      const db = Memory.create({ import: { counter } })
520  
521      const View = Counter.map((fact) => new CounterView(fact))
522  
523      const views = await View().query({ from: db })
524  
525      assert.deepEqual(
526        views,
527        [
528          new CounterView(
529            Counter.assert({ this: Link.of(counter), count: 0, title: 'test' })
530          ),
531        ],
532        'results got mapped'
533      )
534    },
535  }