An open API service indexing awesome lists of open source software.

https://github.com/antoniopresto/aggio


https://github.com/antoniopresto/aggio

Last synced: about 2 months ago
JSON representation

Awesome Lists containing this project

README

        

# Aggio

> Aggregation utility for objects like in MongoDB

## Installation

```
npm install aggio --save # Put latest version in your package.json
```

```ts
import { aggio, createDB, DB } from 'aggio';

type UserWithAddress = { name: string; address?: { street: string } };

describe('DB', () => {
let db: DB<{ name: string }>;

beforeEach(async () => (db = createDB()));

const Antonio = { name: 'Antonio' };
const Rafaela = { name: 'Rafaela' };
const users = [Antonio, Rafaela];

const usersWithAddress: UserWithAddress[] = [
{
name: 'Antonio',
address: {
street: 'Rua',
},
},
{
name: 'Rafaela',
address: {
street: 'Avenida',
},
},
{
name: 'Goat',
},
];

const account = {
username: 'antonio',
firstName: 'antonio',
lastName: 'Silva',
access: [
{
kind: 'email',
value: '[email protected]',
updatedAt: '2022-10-17T02:09:47.948Z',
createdAt: '2022-10-17T02:09:47.948Z',
verified: false,
},
{
kind: 'phone',
value: '+5511999988888',
updatedAt: '2022-10-17T02:09:47.948Z',
createdAt: '2022-10-17T02:09:47.948Z',
verified: false,
},
],
};

describe('aggio', () => {
test('$groupBy accessKind', () => {
const res = aggio(
[account],
[
//
{ $pick: 'access' },
{ $groupBy: 'kind' },
]
);
expect(res).toEqual({
email: [
{
createdAt: '2022-10-17T02:09:47.948Z',
kind: 'email',
updatedAt: '2022-10-17T02:09:47.948Z',
value: '[email protected]',
verified: false,
},
],
phone: [
{
createdAt: '2022-10-17T02:09:47.948Z',
kind: 'phone',
updatedAt: '2022-10-17T02:09:47.948Z',
value: '+5511999988888',
verified: false,
},
],
});
});

test('$pick email', () => {
const res = aggio(
[account],
[
//
{ $pick: 'access' },
{ $matchOne: { kind: 'email' } },
{ $pick: 'value' },
]
);
expect(res).toEqual('[email protected]');
});

test('$keyBy accessKind', () => {
const res = aggio(
[account],
[
//
{ $pick: 'access' },
{ $keyBy: { $template: '{kind}#{value}' } },
]
);

expect(res).toEqual({
'email#[email protected]': {
createdAt: '2022-10-17T02:09:47.948Z',
kind: 'email',
updatedAt: '2022-10-17T02:09:47.948Z',
value: '[email protected]',
verified: false,
},
'phone#+5511999988888': {
createdAt: '2022-10-17T02:09:47.948Z',
kind: 'phone',
updatedAt: '2022-10-17T02:09:47.948Z',
value: '+5511999988888',
verified: false,
},
});
});

test('$matchOne', () => {
const sut = aggio(users, [{ $matchOne: { name: 'Antonio' } }]);
expect(sut).toMatchObject(Antonio);
});

test('$template', () => {
const sut = aggio<{ name: string; address?: { street: string } }>(
[
{
name: 'Antonio',
address: {
street: 'Rua',
},
},
{
name: 'Rafaela',
address: {
street: 'Avenida',
},
},
],
[
{ $sort: { name: -1 } }, //
{ $template: '{name}#{lowercase(address.street)}' },
{ $first: true },
{ $limit: 10 },
]
);

expect(sut).toEqual('Rafaela#av');
});

test('$keyBy: field.subField', () => {
const sut = aggio(usersWithAddress, [
{ $keyBy: 'address.street' },
{ $sort: { name: -1 } }, //
{ $matchOne: {} },
]);

expect(sut).toEqual({
Avenida: {
address: {
street: 'Avenida',
},
name: 'Rafaela',
},
Rua: {
address: {
street: 'Rua',
},
name: 'Antonio',
},
});
});

test('$groupBy: field.subField', () => {
const sut = aggio(usersWithAddress, [
{ $groupBy: 'address.street' },
{ $sort: { name: -1 } }, //
{ $matchOne: {} },
]);

expect(sut).toEqual({
Avenida: [
{
address: {
street: 'Avenida',
},
name: 'Rafaela',
},
],
Rua: [
{
address: {
street: 'Rua',
},
name: 'Antonio',
},
],
});
});

test('$keyBy:{ $pick }', () => {
const sut = aggio<{ name: string }>(users, [
{ $keyBy: { $pick: 'name' } },
{ $sort: { name: -1 } }, //
{ $matchOne: {} },
]);

expect(sut).toMatchObject({
Antonio,
Rafaela,
});
});

test('$keyBy:{ $pick: `field.subField` }', () => {
const sut = aggio(
[
{
name: 'Antonio',
address: {
street: 'Rua',
},
},
{
name: 'Rafaela',
address: {
street: 'Avenida',
},
},
{
name: 'Goat',
},
],
[
{ $keyBy: { $pick: { $join: ['name', '##', 'address.street'], $stringify: 'snakeCase' } } },
{ $sort: { name: -1 } }, //
{ $matchOne: {} },
]
);

expect(sut).toEqual({
'rafaela#avenida': {
address: {
street: 'Avenida',
},
name: 'Rafaela',
},
'antonio#rua': {
address: {
street: 'Rua',
},
name: 'Antonio',
},
});
});

test('$keyBy:{ $pick: $template }', () => {
const sut = aggio<{ name: string; address?: { street: string } }>(
[
{
name: 'Antonio',
address: {
street: 'Rua',
},
},
{
name: 'Rafaela',
address: {
street: 'Avenida',
},
},
{
name: 'Goat',
},
],
[
{ $match: { 'address.street': { $exists: true } } },
{
$keyBy: {
$pick: { $join: ['address'], $stringify: { $template: `{uppercase(name)}#{lowercase(street)}` } },
},
},
{ $sort: { name: -1 } }, //
{ $matchOne: {} },
]
);

expect(sut).toEqual({
'ANTONIO#rua': {
address: {
street: 'Rua',
},
name: 'Antonio',
},
'RAFAELA#avenida': {
address: {
street: 'Avenida',
},
name: 'Rafaela',
},
});
});

test('$groupBy with $sort and $update', () => {
const sut = aggio<{ name: string; age?: number }>(
[
...users,
{
name: 'Antonio',
age: 55,
},
],
[
{
$update: {
$match: { age: { $exists: false } },
$inc: { age: 20 },
},
},
{ $sort: { name: -1, age: -1 } },
{
$groupBy: { name: { $exists: true } },
},
{ $matchOne: {} },
]
);

expect(sut).toEqual({
Antonio: [
{
age: 55,
name: 'Antonio',
},
{
age: 20,
name: 'Antonio',
},
],
Rafaela: [
{
age: 20,
name: 'Rafaela',
},
],
});
});

test('$pick with $sort and $update', () => {
const sut = aggio<{ name: string; age?: number }>(
[
...users,
{
name: 'Antonio',
age: 55,
},
],
[
{
$update: {
$match: { age: { $exists: false } },
$inc: { age: 20 },
},
},
{ $sort: { name: -1, age: -1 } },
{ $pick: 'name' },
]
);

expect(sut).toEqual('Rafaela');
});

test('$pick $join', () => {
const sut = aggio<{ name: string; age?: number; address?: { street?: string } }>(
[
{
name: 'Antonio',
address: {
street: 'Rua',
},
},
{
name: 'Rafaela',
address: {
street: 'Avenida',
},
},
],
[
{ $match: { 'address.street': { $exists: true } } }, //
{ $sort: { name: -1, age: -1 } }, //
{ $pick: { $join: ['name', '##', 'address.street'] } },
]
);

expect(sut).toEqual('Rafaela#Avenida');
});

test('$pick $joinEach', () => {
const sut = aggio<{ name: string; age?: number; address?: { street?: string } }>(
[
{
name: 'Antonio',
address: {
street: 'Rua',
},
},
{
name: 'Rafaela',
address: {
street: 'Avenida',
},
},
],
[
{ $match: { 'address.street': { $exists: true } } }, //
{ $sort: { name: -1, age: -1 } }, //
{ $pick: { $joinEach: ['name', '##', 'address.street'] } },
]
);

expect(sut).toEqual(['Rafaela#Avenida', 'Antonio#Rua']);
});

test('$pick $each', () => {
const sut = aggio<{ name: string; age?: number; address?: { street?: string } }>(
[
...users,
{
name: 'Antonio',
age: 55,
address: {
street: 'Rua',
},
},
],
[
{
$update: {
$match: { age: { $exists: false } },
$inc: { age: 20 },
},
},
{ $sort: { name: -1, age: -1 } },
{ $pick: { $each: 'name' } },
]
);

expect(sut).toEqual(['Rafaela', 'Antonio', 'Antonio']);
});

test('$match with $sort', () => {
const sut = aggio(users, [{ $match: { name: { $exists: true } } }, { $sort: { name: 1 } }]);
expect(sut).toMatchObject([{ name: 'Antonio' }, { name: 'Rafaela' }]);
});

test('$keyBy with $sort', () => {
const sut = aggio<{ name: string }>(users, [
{ $keyBy: { name: { $exists: true } } },
{ $sort: { name: -1 } }, //
{ $matchOne: {} },
]);

expect(sut).toMatchObject({
Antonio,
Rafaela,
});
});
});

describe('DB methods', () => {
test('db.insert', async () => {
const sut = db.insert(users);

expect(sut).toEqual([
{
_id: expect.any(String),
name: 'Antonio',
},
{
_id: expect.any(String),
name: 'Rafaela',
},
]);
});

test('db.update', async () => {
db.insert(users);
const sut = db.update({ name: /ant/i }, { $inc: { age: 1 } });

expect(sut).toEqual({
numAffected: 1,
updated: expect.objectContaining({
...Antonio,
age: 1,
}),
upsert: false,
});
});

test('db.count', async () => {
db.insert(users);
const sut = db.count({ name: /ant/i });
expect(sut).toEqual(1);
});

test('db.find', async () => {
db.insert(users);
const sut = db.find({ name: /ant/i }).exec();
expect(sut).toEqual([expect.objectContaining(Antonio)]);
});

test('db.findOne', async () => {
db.insert(users);
const sut = db.findOne({ name: /ant/i }).exec();
expect(sut).toMatchObject(Antonio);
});

test('db.remove', async () => {
db.insert(users);
const sut = db.remove({ name: /ant/i });
expect(sut).toEqual(1);
});
});
});
```

```typescript
export type AggregationOperatorKeys = typeof aggregationOperatorKeys.enum;

export type Aggregation = AggregationOperator[];

export type AggregationOperatorKey = AggregationOperator extends infer R
? R extends unknown
? keyof R
: never
: never;

export type TemplateDefinition = { $template: string; options?: TemplateOptions };
export type StringifyDefinition = keyof typeof stringCase | TemplateDefinition;

export type PickDefinition = {
$pick:
| DotNotations
| { $join: (DotNotations | `#${string | number}`)[]; $stringify?: StringifyDefinition }
| { $joinEach: (DotNotations | `#${string | number}`)[]; $stringify?: StringifyDefinition }
| { $each: DotNotations | DotNotations[]; $stringify?: StringifyDefinition };
};

export type AggregationOperator =
| { $first: true | 1 }
| { $last: true | 1 }
| { $update: UpdateDefinition & { $match?: Query; $multi?: boolean; $upsert?: boolean } }
| { $matchOne: Query }
| { $limit: number }
| { $sort: Sort }
| { $match: Query }
| { $project: TDocument }
| { $groupBy: GroupByDefinition }
| { $keyBy: KeyByDefinition }
| PickDefinition
| TemplateDefinition;

export type GroupByDefinition =
| {
[Property in Join>, '.'> as PropertyType extends number | string
? Property
: never]?: PropertyType, Property> | Condition, Property>>;
}
| Join>, '.'>;

export type KeyByDefinition =
| ((
| {
[Property in Join>, '.'> as PropertyType extends
| number
| string
? Property
: never]?: PropertyType, Property> | Condition, Property>>;
}
| PickDefinition
) & {
$onMany?: 'first' | 'last' | 'error' | 'warn' | 'list';
})
| Join>, '.'>;

// Some Types from The official MongoDB driver for Node.js
export type Query =
| Partial
| ({
[Property in Join>, '.'>]?: Condition, Property>>;
} & RootFilterOperators>);

export type Join = T extends []
? ''
: T extends [string | number]
? `${T[0]}`
: T extends [string | number, ...infer R]
? `${T[0]}${D}${Join}`
: string;

export interface TDocument {
[key: string]: any;
}

export declare type NestedPaths = Type extends string | number | boolean | Date | RegExp
? []
: Type extends ReadonlyArray
? [] | [number, ...NestedPaths]
: Type extends object
? {
[Key in Extract]: Type[Key] extends Type
? [Key]
: Type extends Type[Key]
? [Key]
: Type[Key] extends ReadonlyArray
? Type extends ArrayType
? [Key]
: ArrayType extends Type
? [Key]
: [Key, ...NestedPaths] // child is not structured the same as the parent
: [Key, ...NestedPaths] | [Key];
}[Extract]
: [];

export type DotNotations = Join, '.'>;

export type PropertyType = string extends Property
? unknown
: Property extends keyof Type
? Type[Property]
: Property extends `${number}`
? Type extends ReadonlyArray
? ArrayType
: unknown
: Property extends `${infer Key}.${infer Rest}`
? Key extends `${number}`
? Type extends ReadonlyArray
? PropertyType
: unknown
: Key extends keyof Type
? Type[Key] extends Map
? MapType
: PropertyType
: unknown
: unknown;

export interface RootFilterOperators extends TDocument {
$and?: Query[];
$or?: Query[];
$not?: Query;
}

export type Condition = AlternativeType | Query>;

export type AlternativeType = T extends ReadonlyArray ? T | RegExpOrString : RegExpOrString;

export type RegExpOrString = T extends string ? RegExp | T : T;

export type EnhancedOmit = string extends keyof TRecordOrUnion
? TRecordOrUnion
: TRecordOrUnion extends any
? Pick>
: never;

export type WithId = EnhancedOmit & {
_id: string;
};

export interface RootFilterOperators extends TDocument {
$and?: Query[];
$or?: Query[];
$not?: Query;
}

export declare type UpdateDefinition = {
$inc?: OnlyFieldsOfType;
$min?: MatchKeysAndValues;
$max?: MatchKeysAndValues;
$set?: MatchKeysAndValues;
$unset?: OnlyFieldsOfType;
$addToSet?: SetFields;
$pop?: OnlyFieldsOfType, 1 | -1>;
$pull?: PullOperator;
$push?: PushOperator;
} & TDocument;

export type OnlyFieldsOfType = IfAny<
TSchema[keyof TSchema],
Record,
AcceptedFields &
NotAcceptedFields &
Record
>;

export type AcceptedFields = {
readonly [key in KeysOfAType]?: AssignableType;
};

type KeysOfAType = {
[key in keyof TSchema]: NonNullable extends Type ? key : never;
}[keyof TSchema];

export declare type NotAcceptedFields = {
readonly [key in KeysOfOtherType]?: never;
};

export type IfAny = true extends false & Type ? ResultIfAny : ResultIfNotAny;

export type PullOperator = ({
readonly [key in KeysOfAType>]?:
| Partial>
| FilterOperations>;
} & NotAcceptedFields>) & {
readonly [key: string]: Query | any;
};

export type Flatten = Type extends ReadonlyArray ? Item : Type;

export type FilterOperations = T extends Record
? {
[key in keyof T]?: Query;
}
: Query;

export type MatchKeysAndValues = Readonly<
{
[Property in Join, '.'>]?: PropertyType;
} & {
[Property in `${NestedPathsOfType}.$${`[${string}]` | ''}`]?: ArrayElement<
PropertyType
>;
} & {
[Property in `${NestedPathsOfType[]>}.$${`[${string}]` | ''}.${string}`]?: any;
}
>;

export type ArrayElement = Type extends ReadonlyArray ? Item : never;

export type NestedPathsOfType = KeysOfAType<
{
[Property in Join, '.'>]: PropertyType;
},
Type
>;

// export type PullAllOperator = ({
// readonly [key in KeysOfAType>]?: TSchema[key];
// } & NotAcceptedFields>) & {
// readonly [key: string]: ReadonlyArray;
// };

export type PushOperator = ({
readonly [key in KeysOfAType>]?:
| Flatten
| ArrayOperator>>;
} & NotAcceptedFields>) & {
readonly [key: string]: ArrayOperator | any;
};

// @ts-ignore
export type ArrayOperator = {
// $each?: Array>;
// $slice?: number;
// $position?: number;
// $sort?: Sort; // TODO
};

export type KeysOfOtherType = {
[key in keyof TSchema]: NonNullable extends Type ? never : key;
}[keyof TSchema];

export type NumericType = number;

export type SetFields = ({
readonly [key in KeysOfAType | undefined>]?:
| OptionalId>
| AddToSetOperators>>>;
} & NotAcceptedFields | undefined>) & {
readonly [key: string]: AddToSetOperators | any;
};

export type OptionalId = EnhancedOmit & {
_id?: InferIdType;
};

// @ts-ignore
export type InferIdType = string;

// @ts-ignore
export type AddToSetOperators = {
// $each?: Array>;
};

export type Sort =
| string
| Exclude<
SortDirection,
{
$meta: string;
}
>
| string[]
| {
[key: string]: SortDirection;
}
| [string, SortDirection][]
| [string, SortDirection];

export type SortDirection = 1 | -1 | 'asc' | 'desc' | 'ascending' | 'descending';
```

## License

See [License](LICENSE)