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 }