Skip to content

Commit

Permalink
feat: provide a way to define typed JS map/reduce
Browse files Browse the repository at this point in the history
  • Loading branch information
yj7o5 committed Oct 13, 2020
1 parent a2c683d commit 9db45fe
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 64 deletions.
72 changes: 72 additions & 0 deletions src/Documents/Indexes/AbstractJavaScriptIndexCreationTask.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,41 @@
import { IndexDefinition } from "./IndexDefinition";
import { AbstractIndexCreationTaskBase } from "./AbstractIndexCreationTaskBase";

type IndexMapDefinition<T> = (document: T) => object | object[];

type AggregateResult<T> = (result: T) => object;

type KeySelector<T> = (document: T) => string;

interface Group<TValue>
{
key: string;
values: TValue[];
}

class ReduceResults<TSource> {
private _group: KeySelector<TSource>;

constructor(selector: KeySelector<TSource>) {
this._group = selector;
}

public aggregate(selector: AggregateResult<Group<TSource>>): string {
let reduce = `
groupBy(${this._group.toString()})
.aggregate(${selector.toString()})
`
return reduce;
}
}

type LoadDocment<T> = (id: string, collection: string) => T

interface MapUtils<T>
{
load: LoadDocment<T>
}

export class AbstractJavaScriptIndexCreationTask extends AbstractIndexCreationTaskBase {

private readonly _definition: IndexDefinition = new IndexDefinition();
Expand Down Expand Up @@ -81,6 +116,43 @@ export class AbstractJavaScriptIndexCreationTask extends AbstractIndexCreationTa
this._definition.patternForOutputReduceToCollectionReferences = value;
}

/**
* @param collection Collection name to index over
* @param definition Index definition that maps to the indexed properties
*/
protected map<T>(collection: String, definition: IndexMapDefinition<T>) : void {
this.maps.add(`map(${collection}, ${definition.toString()}`);
}

/**
*
* @param key
* @param source Additional source script to provide to the index maps
*/
protected addSource(key: String, source: Function) : void {
this.additionalSources = this.additionalSources ?? {};

this.additionalSources[key.toString()] = source.toString();
}

/**
*
* @param selector Group key selector for the reduce results
* @returns Group reducer that exposes the `aggregate` function over the grouped results
*/
protected groupBy<TSource>(selector: KeySelector<TSource>) : ReduceResults<TSource> {
return new ReduceResults(selector);
}

/**
* No implementation is required; The interface is only what's exposed to allow usage of helper map methods
* such as `load(id, collection)` etc.
* @returns null
*/
protected getMapUtils<T>() : MapUtils<T> {
return null;
}

public createIndexDefinition(): IndexDefinition {
this._definition.type = this.isMapReduce ? "JavaScriptMapReduce" : "JavaScriptMap";

Expand Down
156 changes: 92 additions & 64 deletions test/Ported/Indexing/JavaScriptIndexTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,56 +39,68 @@ class Product {
class UsersByNameWithAdditionalSources extends AbstractJavaScriptIndexCreationTask {
public constructor() {
super();
this.maps = new Set(["map('Users', function(u) { return { name: mr(u.name)}; })"]);
this.additionalSources = { "The Script": "function mr(x) { return 'Mr. ' + x; }" };

function mr(name: String) : String {
return 'Mr ' + name;
}

this.map<Product>('Products', p => {
return ({
name: mr(p.name)
});
});

this.addSource("The Script", mr);
}
}

interface FanoutByNumbersWithReduceResult {
foo: string;
sum: number;
}

class FanoutByNumbersWithReduce extends AbstractJavaScriptIndexCreationTask {
public constructor() {
super();
this.maps = new Set([
`map('Fanouts', function (f) {
var result = [];
for(var i = 0; i < f.numbers.length; i++)
{
result.push({
foo: f.foo,
sum: f.numbers[i]
});
}
return result;
})`]);
this.reduce =
`groupBy(f => f.foo)
.aggregate(g => ({
foo: g.key,
sum: g.values.reduce((total, val) =>
val.sum + total,0)
}))`;
}
constructor() {
super();

this.map<Fanout>('Fanouts', f => {
var results = [];

for (var i = 0; i < f.numbers.length; i++) {
results.push({
foo: f.foo,
sum: f.numbers[i]
})
}

return results;
});

this.reduce = this.groupBy<FanoutByNumbersWithReduceResult>(f => f.foo)
.aggregate(g => ({
foo: g.key,
sum: g.values.reduce((total, val) =>
val.sum + total, 0)
}))
}
}

class UsersByNameAndAnalyzedNameResult {
public analyzedName: string;
}

class UsersByNameAndAnalyzedName extends AbstractJavaScriptIndexCreationTask {
public constructor() {
super();
this.maps = new Set([`map('Users', function (u){
return {
name: u.name,
_: {
$value: u.name,
$name:'analyzedName',
$options:{ indexing: 'Search', storage: true }
}
};
})`]);

this.map<User>('Users', u => ({
name: u.name,
_: {
$value: u.name,
$name: 'analyzedName',
$options: { indexing: 'Search', storage: true }
}
}));

const fieldOptions = this.fields = {};
const indexFieldOptions = new IndexFieldOptions();
Expand All @@ -101,27 +113,44 @@ class UsersByNameAndAnalyzedName extends AbstractJavaScriptIndexCreationTask {
class UsersAndProductsByName extends AbstractJavaScriptIndexCreationTask {
public constructor() {
super();
this.maps = new Set([
"map('Users', function (u) { return { name: u.name, count: 1}; })",
"map('Products', function (p) { return { name: p.name, count: 1}; })"
]);

this.map<User>('Users', u => ({
name: u.name,
count: 1
}));

this.map<Product>('Products', p => ({
name: p.name,
count: 1
}));
}
}

interface Entity
{
name: string;
count: number;
}

class UsersAndProductsByNameAndCount extends AbstractJavaScriptIndexCreationTask {
public constructor() {
super();
this.maps = new Set([
"map('Users', function (u){ return { name: u.name, count: 1};})",
"map('Products', function (p){ return { name: p.name, count: 1};})"
]);
this.reduce = `groupBy(x => x.name)
.aggregate(g => {
return {
name: g.key,
count: g.values.reduce((total, val) => val.count + total,0)
};
})`;

this.map<User>('Users', u => ({
name: u.name,
count: 1
}));

this.map<Product>('Products', p => ({
name: p.name,
count: 1
}));

this.reduce = this.groupBy<Entity>(x => x.name)
.aggregate(g => ({
name: g.key,
count: g.values.reduce((total, val) => val.count + total, 0)
}));
}
}

Expand All @@ -133,23 +162,22 @@ interface ProductsByCategoryResult {
class ProductsByCategory extends AbstractJavaScriptIndexCreationTask {
public constructor() {
super();
this.maps = new Set(["map('product2s', function(p){\n" +
" return {\n" +
" category:\n" +
" load(p.category, 'categories').name,\n" +
" count:\n" +
" 1\n" +
" }\n" +
" })"]);
this.reduce = "groupBy( x => x.category )\n" +
" .aggregate(g => {\n" +
" return {\n" +
" category: g.key,\n" +
" count: g.values.reduce((count, val) => val.count + count, 0)\n" +
" };})";
this.outputReduceToCollection = "CategoryCounts";

var { load } = this.getMapUtils<Product2>();

this.map<Product2>('Product2s', p => ({
category: load(p.category, 'categories').name,
count: 1
}));

this.reduce = this.groupBy<ProductsByCategoryResult>(x => x.category)
.aggregate(g => ({
category: g.key,
count: g.values.reduce((count, val) => val.count + count, 0)
}));
}
}

class Fanout {
public foo: string;
public numbers: number[];
Expand Down

0 comments on commit 9db45fe

Please sign in to comment.