/ test / aggregate.spec.js
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  }