https://github.com/antoniopresto/aggio
https://github.com/antoniopresto/aggio
Last synced: about 2 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/antoniopresto/aggio
- Owner: antoniopresto
- License: mit
- Created: 2022-10-16T14:33:03.000Z (over 2 years ago)
- Default Branch: master
- Last Pushed: 2022-12-24T01:11:18.000Z (over 2 years ago)
- Last Synced: 2025-03-23T18:52:18.893Z (2 months ago)
- Language: TypeScript
- Size: 679 KB
- Stars: 2
- Watchers: 1
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
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)