Skip to content

Commit

Permalink
feat(criteria-to-firestore): +criteria to firestore converter
Browse files Browse the repository at this point in the history
  • Loading branch information
jbuendia1y committed Oct 11, 2024
1 parent 1fe168f commit 0b78ef1
Show file tree
Hide file tree
Showing 7 changed files with 430 additions and 0 deletions.
23 changes: 23 additions & 0 deletions packages/criteria-to-firestore/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<p align="center">
<a href="https://codely.com">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://codely.com/logo/codely_logo-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://codely.com/logo/codely_logo-light.svg">
<img alt="Codely logo" src="https://codely.com/logo/codely_logo.svg">
</picture>
</a>
</p>

<h1 align="center">
🎼 Criteria to Firestore converter
</h1>

<p align="center">
<a href="https://github.com/CodelyTV"><img src="https://img.shields.io/badge/Codely-OS-green.svg?style=flat-square" alt="codely.com"/></a>
</p>

## 📥 Installation

```sh
npm i @codelytv/criteria-to-firestore
```
21 changes: 21 additions & 0 deletions packages/criteria-to-firestore/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@codelytv/criteria-to-firestore",
"version": "1.0.0",
"description": "",
"keywords": [],
"author": "Codely (https://codely.com)",
"license": "MIT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"test": "node --import tsx --test test/*.test.ts",
"build": "tsc --build --verbose tsconfig.json"
},
"dependencies": {
"@codelytv/criteria": "workspace:^",
"@firebase/firestore": "^4.7.3"
},
"devDependencies": {
"@codelytv/criteria-test-mother": "workspace:^"
}
}
67 changes: 67 additions & 0 deletions packages/criteria-to-firestore/src/CriteriaToFirestore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Criteria, Filter, OrderTypes } from "@codelytv/criteria";
import { limit, orderBy, QueryConstraint, startAfter, where } from "@firebase/firestore";

type CriteriaMapper = Record<string, (v: string) => string | number>;

export class CriteriaToFirestoreConverter {
convert(criteria: Criteria, mapper: CriteriaMapper = {}): QueryConstraint[] {
const constraints: QueryConstraint[] = [];
if (criteria.hasFilters()) {
criteria.filters.value.forEach((filter) => {
const constraint = this.getFilterFromCriteria(filter, mapper);
if (constraint) {
constraints.push(constraint);
}
});
}
if (criteria.hasOrder() && !criteria.order.isNone()) {
constraints.push(this.getOrderFromCriteria(criteria));
}
if (criteria.pageSize !== null) {
constraints.push(limit(criteria.pageSize));
}
if (criteria.pageNumber !== null && criteria.pageSize !== null) {
constraints.push(startAfter(criteria.pageSize * (criteria.pageNumber - 1)));
}

return constraints;
}

private getOrderFromCriteria(criteria: Criteria) {
const by = criteria.order.orderBy.value;
const orderType = criteria.order.orderType.value === OrderTypes.DESC ? "desc" : "asc";

return orderBy(by, orderType);
}

private getFilterFromCriteria(filter: Filter, mapper: CriteriaMapper) {
const field = filter.field.value;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const value = mapper[field] ? mapper[field](filter.value.value) : filter.value.value;

if (filter.operator.isContains()) {
return where(field, "in", value);
}
if (filter.operator.isNotContains()) {
return where(field, "not-in", value);
}
if (filter.operator.value.valueOf() === "=") {
return where(field, "==", value);
}
if (filter.operator.isNotEquals()) {
return where(field, "!=", value);
}
if (filter.operator.isGreaterThan()) {
return where(field, ">", value);
}
if (filter.operator.isGreaterThanOrEqual()) {
return where(field, ">=", value);
}
if (filter.operator.isLowerThan()) {
return where(field, "<", value);
}
if (filter.operator.isLowerThanOrEqual()) {
return where(field, "<=", value);
}
}
}
1 change: 1 addition & 0 deletions packages/criteria-to-firestore/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./CriteriaToFirestore";
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import assert from "node:assert";
import { describe, it } from "node:test";

import { CriteriaMother } from "@codelytv/criteria-test-mother";
import { limit, orderBy, startAfter, where } from "@firebase/firestore";

import { CriteriaToFirestoreConverter } from "../src";

describe("CriteriaToFirestoreConverter should", () => {
const converter = new CriteriaToFirestoreConverter();

it("Generate simple query with an empty criteria", () => {
const actualQuery = converter.convert(CriteriaMother.empty());

assert.deepEqual(actualQuery, []);
});

it("Generate query with order", () => {
const actualQuery = converter.convert(CriteriaMother.emptySorted("id", "DESC"));

assert.deepEqual(actualQuery, [orderBy("id", "desc")]);
});

it("Generate query with one filter", () => {
const actualQuery = converter.convert(CriteriaMother.withOneFilter("name", "EQUAL", "Javier"));

assert.deepEqual(actualQuery, [where("name", "==", "Javier")]);
});

it("Generate query with one filter sorted", () => {
const actualQuery = converter.convert(
CriteriaMother.withOneFilterSorted("name", "EQUAL", "Javier", "id", "DESC"),
);

assert.deepEqual(actualQuery, [where("name", "==", "Javier"), orderBy("id", "desc")]);
});

it("Generate query with multiples filters", () => {
const actualQuery = converter.convert(
CriteriaMother.create({
filters: [
{
field: "name",
operator: "EQUAL",
value: "Javier",
},
{
field: "email",
operator: "EQUAL",
value: "[email protected]",
},
],
orderBy: null,
orderType: null,
pageSize: null,
pageNumber: null,
}),
);

assert.deepEqual(actualQuery, [
where("name", "==", "Javier"),
where("email", "==", "[email protected]"),
]);
});

it("Generate query with multiples filters and sort", () => {
const actualQuery = converter.convert(
CriteriaMother.create({
filters: [
{
field: "name",
operator: "EQUAL",
value: "Javier",
},
{
field: "email",
operator: "EQUAL",
value: "[email protected]",
},
],
orderBy: "id",
orderType: "DESC",
pageSize: null,
pageNumber: null,
}),
);

assert.deepEqual(actualQuery, [
where("name", "==", "Javier"),
where("email", "==", "[email protected]"),
orderBy("id", "desc"),
]);
});

it("Generate query with one contains filter", () => {
const actualQuery = converter.convert(
CriteriaMother.withOneFilter("name", "CONTAINS", "Javier"),
);

assert.deepEqual(actualQuery, [where("name", "in", "Javier")]);
});

it("Generate query with one greater filter and value converter", () => {
const actualQuery = converter.convert(
CriteriaMother.withOneFilter("age", "GREATER_THAN", "20"),
{ age: (v) => parseInt(v, 10) },
);

assert.deepEqual(actualQuery, [where("age", ">", 20)]);
});

it("Generate query with one not contains filter", () => {
const actualQuery = converter.convert(
CriteriaMother.withOneFilter("name", "NOT_CONTAINS", "Javier"),
);

assert.deepEqual(actualQuery, [where("name", "not-in", "Javier")]);
});

it("Generate simple query paginated", () => {
const pageSize = 10;
const pageNumber = 3;
const expectedSkip = pageSize * (pageNumber - 1);
const actualQuery = converter.convert(CriteriaMother.emptyPaginated(pageSize, pageNumber));

assert.deepEqual(actualQuery, [limit(pageSize), startAfter(expectedSkip)]);
});
});
7 changes: 7 additions & 0 deletions packages/criteria-to-firestore/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*"],
}
Loading

0 comments on commit 0b78ef1

Please sign in to comment.