aggio
TypeScript icon, indicating that this package has built-in type declarations

0.3.4 • Public • Published

Aggio

Aggregation utility for objects like in MongoDB

Installation

npm install aggio --save    # Put latest version in your package.json
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: 'antonio@example.com',
        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: 'antonio@example.com',
            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('antonio@example.com');
    });

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

      expect(res).toEqual({
        'email#antonio@example.com': {
          createdAt: '2022-10-17T02:09:47.948Z',
          kind: 'email',
          updatedAt: '2022-10-17T02:09:47.948Z',
          value: 'antonio@example.com',
          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<UserWithAddress>(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<UserWithAddress>(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<UserWithAddress>(
        [
          {
            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);
    });
  });
});
export type AggregationOperatorKeys = typeof aggregationOperatorKeys.enum;

export type Aggregation<TSchema> = AggregationOperator<TSchema>[];

export type AggregationOperatorKey = AggregationOperator<any> 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<TSchema> = {
  $pick:
    | DotNotations<TSchema>
    | { $join: (DotNotations<TSchema> | `#${string | number}`)[]; $stringify?: StringifyDefinition }
    | { $joinEach: (DotNotations<TSchema> | `#${string | number}`)[]; $stringify?: StringifyDefinition }
    | { $each: DotNotations<TSchema> | DotNotations<TSchema>[]; $stringify?: StringifyDefinition };
};

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

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

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

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

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

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

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

export type DotNotations<T> = Join<NestedPaths<T>, '.'>;

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

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

export type Condition<T> = AlternativeType<T> | Query<AlternativeType<T>>;

export type AlternativeType<T> = T extends ReadonlyArray<infer U> ? T | RegExpOrString<U> : RegExpOrString<T>;

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

export type EnhancedOmit<TRecordOrUnion, KeyUnion> = string extends keyof TRecordOrUnion
  ? TRecordOrUnion
  : TRecordOrUnion extends any
  ? Pick<TRecordOrUnion, Exclude<keyof TRecordOrUnion, KeyUnion>>
  : never;

export type WithId<TSchema> = EnhancedOmit<TSchema, '_id'> & {
  _id: string;
};

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

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

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

export type AcceptedFields<TSchema, FieldType, AssignableType> = {
  readonly [key in KeysOfAType<TSchema, FieldType>]?: AssignableType;
};

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

export declare type NotAcceptedFields<TSchema, FieldType> = {
  readonly [key in KeysOfOtherType<TSchema, FieldType>]?: never;
};

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

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

export type Flatten<Type> = Type extends ReadonlyArray<infer Item> ? Item : Type;

export type FilterOperations<T> = T extends Record<string, any>
  ? {
      [key in keyof T]?: Query<T[key]>;
    }
  : Query<T>;

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

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

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

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

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

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

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

export type NumericType = number;

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

export type OptionalId<TSchema> = EnhancedOmit<TSchema, '_id'> & {
  _id?: InferIdType<TSchema>;
};

// @ts-ignore
export type InferIdType<TSchema> = string;

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

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

Package Sidebar

Install

npm i aggio

Weekly Downloads

759

Version

0.3.4

License

MIT

Unpacked Size

1.91 MB

Total Files

149

Last publish

Collaborators

  • antonio.presto