aggregate.spec.js
1 import { Memory, $, fact, Collection, Task, Link, API } from './lib.js' 2 3 /** 4 * @type {import('entail').Suite} 5 */ 6 export const testAggregate = { 7 'aggregate items': (assert) => 8 Task.spawn(function* () { 9 const groceries = Link.of({ name: 'Groceries' }) 10 const milk = Link.of({ title: 'Buy Milk' }) 11 const eggs = Link.of({ title: 'Buy Eggs' }) 12 const bread = Link.of({ title: 'Buy Bread' }) 13 14 const chores = Link.of({ name: 'Chores' }) 15 const laundry = Link.of({ title: 'Do Laundry' }) 16 const dishes = Link.of({ title: 'Do Dishes' }) 17 18 const db = Memory.create([ 19 { the: 'list/name', of: groceries, is: 'Groceries' }, 20 { the: 'todo/title', of: milk, is: 'Buy Milk' }, 21 { the: 'todo/title', of: eggs, is: 'Buy Eggs' }, 22 { the: 'todo/title', of: bread, is: 'Buy Bread' }, 23 { the: 'list/item', of: groceries, is: milk }, 24 { the: 'list/item', of: groceries, is: eggs }, 25 { the: 'list/item', of: groceries, is: bread }, 26 { the: 'list/name', of: chores, is: 'Chores' }, 27 { the: 'todo/title', of: laundry, is: 'Do Laundry' }, 28 { the: 'todo/title', of: dishes, is: 'Do Dishes' }, 29 { the: 'list/item', of: chores, is: laundry }, 30 { the: 'list/item', of: chores, is: dishes }, 31 ]) 32 33 const TodoItem = fact({ 34 the: 'todo', 35 title: String, 36 }) 37 38 const TodoList = fact({ 39 the: 'list', 40 name: String, 41 item: Object, 42 }) 43 44 const Todo = fact({ 45 name: String, 46 task: Object, 47 title: String, 48 }) 49 .where(({ this: list, name, task, title }) => [ 50 TodoList({ this: list, name, item: task }), 51 TodoItem({ this: task, title }), 52 ]) 53 .select(({ this: list, name, task, title }) => ({ 54 name, 55 item: [{ todo: task, title }], 56 })) 57 58 assert.deepEqual(yield* Todo().query({ from: db }), [ 59 { 60 name: 'Groceries', 61 item: [ 62 { todo: milk, title: 'Buy Milk' }, 63 { todo: eggs, title: 'Buy Eggs' }, 64 { todo: bread, title: 'Buy Bread' }, 65 ], 66 }, 67 { 68 name: 'Chores', 69 item: [ 70 { todo: laundry, title: 'Do Laundry' }, 71 { todo: dishes, title: 'Do Dishes' }, 72 ], 73 }, 74 ]) 75 }), 76 77 'double aggregate': (assert) => 78 Task.spawn(function* () { 79 const lib = Link.of({ name: 'datalogia' }) 80 const tags = Link.of({ tags: {} }) 81 const files = Link.of({ files: {} }) 82 83 const source = { 84 'package/name': 'synopsys', 85 'package/keywords': ['datalog', 'db', 'datomic', 'graph'], 86 'package/null': null, 87 'package/dev': true, 88 'package/score': 1024n, 89 'package/dependencies': { 90 '@canvas-js/okra': '0.4.5', 91 '@canvas-js/okra-lmdb': '0.2.0', 92 '@canvas-js/okra-memory': '0.4.5', 93 '@ipld/dag-cbor': '^9.2.1', 94 '@ipld/dag-json': '10.2.2', 95 '@noble/hashes': '1.3.3', 96 '@types/node': '22.5.5', 97 datalogia: '^0.9.0', 98 multiformats: '^13.3.0', 99 'merkle-reference': '^0.0.3', 100 }, 101 'package/types': [{ './src/lib.js': './dist/lib.d.ts' }], 102 } 103 104 const db = Memory.create({ Import: source }) 105 106 const Manifest = fact({ 107 the: 'package', 108 name: String, 109 keywords: Object, 110 null: null, 111 dev: Boolean, 112 score: BigInt, 113 dependencies: Object, 114 types: Object, 115 }) 116 117 const Package = fact({ 118 the: 'package', 119 name: String, 120 keywords: Object, 121 null: null, 122 dev: Boolean, 123 score: BigInt, 124 dependencies: Object, 125 types: Object, 126 127 dependencyName: String, 128 dependencyVersion: String, 129 keywordPosition: String, 130 keyword: String, 131 }) 132 .where((manifest) => [ 133 Manifest(manifest), 134 Collection({ 135 this: manifest.dependencies, 136 at: manifest.dependencyName, 137 of: manifest.dependencyVersion, 138 }), 139 Collection({ 140 this: manifest.keywords, 141 at: manifest.keywordPosition, 142 of: manifest.keyword, 143 }), 144 ]) 145 .select( 146 ({ 147 name, 148 keyword, 149 keywordPosition: at, 150 dependencyName: dependency, 151 dependencyVersion: version, 152 null: nil, 153 score, 154 dev, 155 }) => ({ 156 name, 157 keywords: [{ at, keyword }], 158 dependencies: [{ name: dependency, version }], 159 null: nil, 160 score, 161 dev, 162 }) 163 ) 164 165 const selection = yield* Package().query({ from: db }) 166 167 assert.deepEqual(selection, [ 168 { 169 name: 'synopsys', 170 null: null, 171 score: 1024n, 172 dev: true, 173 keywords: [ 174 { at: '[0]', keyword: 'datalog' }, 175 { at: '[1]', keyword: 'db' }, 176 { at: '[2]', keyword: 'datomic' }, 177 { at: '[3]', keyword: 'graph' }, 178 ], 179 dependencies: [ 180 { name: '@canvas-js/okra', version: '0.4.5' }, 181 { name: '@canvas-js/okra-lmdb', version: '0.2.0' }, 182 { name: '@canvas-js/okra-memory', version: '0.4.5' }, 183 { name: '@ipld/dag-cbor', version: '^9.2.1' }, 184 { name: '@ipld/dag-json', version: '10.2.2' }, 185 { name: '@noble/hashes', version: '1.3.3' }, 186 { name: '@types/node', version: '22.5.5' }, 187 { name: 'datalogia', version: '^0.9.0' }, 188 { name: 'multiformats', version: '^13.3.0' }, 189 { name: 'merkle-reference', version: '^0.0.3' }, 190 ], 191 }, 192 ]) 193 }), 194 'real test case': (assert) => 195 Task.spawn(function* () { 196 const source = [ 197 { 198 'post/title': 'The Art of Programming', 199 'post/author': 'John Smith', 200 'post/tags': ['coding', 'software', 'computer science'], 201 }, 202 { 203 'post/title': 'Digital Dreams', 204 'post/author': 'Sarah Johnson', 205 'post/tags': ['technology', 'future', 'innovation'], 206 }, 207 { 208 'post/title': 'Cloud Atlas', 209 'post/author': 'Michael Chen', 210 'post/tags': ['cloud computing', 'infrastructure', 'devops'], 211 }, 212 { 213 'post/title': 'Web Development Mastery', 214 'post/author': 'Emma Davis', 215 'post/tags': ['javascript', 'html', 'css', 'web'], 216 }, 217 { 218 'post/title': 'AI Revolution', 219 'post/author': 'Robert Zhang', 220 'post/tags': [ 221 'artificial intelligence', 222 'machine learning', 223 'future', 224 ], 225 }, 226 { 227 'post/title': 'Clean Code Principles', 228 'post/author': 'David Miller', 229 'post/tags': [ 230 'programming', 231 'best practices', 232 'software engineering', 233 ], 234 }, 235 { 236 'post/title': 'Database Design', 237 'post/author': 'Lisa Wang', 238 'post/tags': ['sql', 'data modeling', 'databases'], 239 }, 240 { 241 'post/title': 'Mobile First', 242 'post/author': 'James Wilson', 243 'post/tags': ['mobile development', 'responsive design', 'UX'], 244 }, 245 { 246 'post/title': 'Security Essentials', 247 'post/author': 'Alex Thompson', 248 'post/tags': ['cybersecurity', 'networking', 'privacy'], 249 }, 250 { 251 'post/title': 'DevOps Handbook', 252 'post/author': 'Maria Garcia', 253 'post/tags': ['devops', 'automation', 'continuous integration'], 254 }, 255 ] 256 const db = Memory.create({ Import: { source } }) 257 258 const Post = fact({ 259 the: 'post', 260 title: String, 261 author: String, 262 tags: Object, 263 }) 264 265 const TaggedPost = fact({ 266 the: 'post', 267 title: String, 268 author: String, 269 tags: Object, 270 tag: String, 271 }) 272 .where((post) => [ 273 Post(post), 274 Collection({ this: post.tags, of: post.tag }), 275 ]) 276 .select(({ this: post, title, author, tags, tag }) => ({ 277 '/': post, 278 title, 279 author, 280 tags: [tag], 281 'tags/': tags, 282 })) 283 284 const selection = yield* TaggedPost().query({ from: db }) 285 286 assert.deepEqual( 287 selection, 288 source.map((member) => ({ 289 '/': Link.of(member), 290 title: member['post/title'], 291 author: member['post/author'], 292 'tags/': Link.of(member['post/tags']), 293 tags: member['post/tags'], 294 })) 295 ) 296 }), 297 298 'test reduce method': (assert) => 299 Task.spawn(function* () { 300 const groceries = Link.of({ name: 'Groceries' }) 301 const milk = Link.of({ title: 'Buy Milk' }) 302 const eggs = Link.of({ title: 'Buy Eggs' }) 303 const bread = Link.of({ title: 'Buy Bread' }) 304 305 const chores = Link.of({ name: 'Chores' }) 306 const laundry = Link.of({ title: 'Do Laundry' }) 307 const dishes = Link.of({ title: 'Do Dishes' }) 308 309 const db = Memory.create([ 310 { the: 'list/name', of: groceries, is: 'Groceries' }, 311 { the: 'todo/title', of: milk, is: 'Buy Milk' }, 312 { the: 'todo/title', of: eggs, is: 'Buy Eggs' }, 313 { the: 'todo/title', of: bread, is: 'Buy Bread' }, 314 { the: 'list/item', of: groceries, is: milk }, 315 { the: 'list/item', of: groceries, is: eggs }, 316 { the: 'list/item', of: groceries, is: bread }, 317 { the: 'list/name', of: chores, is: 'Chores' }, 318 { the: 'todo/title', of: laundry, is: 'Do Laundry' }, 319 { the: 'todo/title', of: dishes, is: 'Do Dishes' }, 320 { the: 'list/item', of: chores, is: laundry }, 321 { the: 'list/item', of: chores, is: dishes }, 322 ]) 323 324 const TodoItem = fact({ 325 the: 'todo', 326 title: String, 327 }) 328 329 const TodoList = fact({ 330 the: 'list', 331 name: String, 332 item: Object, 333 }) 334 335 const Todo = fact({ 336 name: String, 337 task: Object, 338 title: String, 339 }) 340 .where(({ this: list, name, task, title }) => [ 341 TodoList({ this: list, name, item: task }), 342 TodoItem({ this: task, title }), 343 ]) 344 .aggregate({ 345 /** 346 * @returns {Map<string, { this: API.Entity, name: string, todo: {this: API.Entity, title:string }[]}>} 347 */ 348 open() { 349 return new Map() 350 }, 351 merge(state, { this: todo, name, title, task }) { 352 const id = todo.toString() 353 let result = state.get(id) 354 if (result == null) { 355 state.set(id, { 356 this: todo, 357 name, 358 todo: [{ this: task, title }], 359 }) 360 } else { 361 result.todo.push({ 362 this: task, 363 title, 364 }) 365 } 366 367 return state 368 }, 369 close(state) { 370 return [...state.values()] 371 }, 372 }) 373 374 assert.deepEqual(yield* Todo().query({ from: db }), [ 375 { 376 this: groceries, 377 name: 'Groceries', 378 todo: [ 379 { this: milk, title: 'Buy Milk' }, 380 { this: eggs, title: 'Buy Eggs' }, 381 { this: bread, title: 'Buy Bread' }, 382 ], 383 }, 384 { 385 this: chores, 386 name: 'Chores', 387 todo: [ 388 { this: laundry, title: 'Do Laundry' }, 389 { this: dishes, title: 'Do Dishes' }, 390 ], 391 }, 392 ]) 393 }), 394 }