Skip to content

Commit

Permalink
feat: add parse()/serialize() and change .where()
Browse files Browse the repository at this point in the history
  • Loading branch information
xantiagoma committed Sep 9, 2023
1 parent e0788a4 commit 144a174
Show file tree
Hide file tree
Showing 18 changed files with 1,932 additions and 1,778 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"nuxt.isNuxtApp": false
}
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# drizzle-cursor

## 0.1.0

### Minor Changes

- - Add `parse()`/`serialize()` function and `config.parse()`/`config.serialize()` methods to handle base64 tokens

- Refactor `config.where` so intead of being an object is a clousure function which enables to use the same instance of `generateCursor()` accross multiple calls calling `.where()` without and with arguments.
- Remove second argument from `generateCursor()` which is now used as the argument of `.where()`

## 0.0.3

### Patch Changes
Expand Down
54 changes: 53 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,56 @@

Utils to generate cursor based pagination on Drizzle ORM

Check example at: [test/example.ts](./test/example.ts)
Check example at: [test/example.ts](./test/example.ts)

Use like:

```ts
const cursorConfig: CursorConfig = {
cursors: [
{ order: "ASC", key: "lastName", schema: schema.users.lastName },
{ order: "ASC", key: "firstName", schema: schema.users.firstName },
{ order: "ASC", key: "middleName", schema: schema.users.middleName },
],
primaryCursor: { order: "ASC", key: "id", schema: schema.users.id },
};

const cursor = generateCursor(cursorConfig);

const page1 = await db
.select({
lastName: schema.users.lastName,
firstName: schema.users.firstName,
middleName: schema.users.middleName,
id: schema.users.id,
})
.from(schema.users)
.orderBy(...cursor.orderBy) // Always include the order
.where(cursor.where()) // .where() is called empty the first time, meaning "there's not previous records"
.limit(page_size);

const page2 = await db
.select() // .select() can vary while includes the needed data to create next curso (the same as the tables listed in primaryCursor and cursors)
.from(schema.users)
.orderBy(...cursor.orderBy)
.where(cursor.where(page1.at(-1))) // last record of previous query (or any record "before: the one you want to start with)
.limit(page_size);

const lastToken = cursor.serialize(page2.at(-1)); // use serialize method/function to send tokens to your FE
const lastItem = cursor.parse(lastToken); // use parse method/function to transform back in an object

const page3 = await db.query.users.findMany({
// It also works with Relational Queries
columns: {
// Be sure to include the data needed to create the cursor if using columns
lastName: true,
firstName: true,
middleName: true,
id: true,
},
orderBy: cursor.orderBy, // no need to destructure here
where: cursor.where(lastToken), // .where() also accepts the string token directly, no need to pre-parse it (at least you want to run extra validations)
limit: page_size,
});

```
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "drizzle-cursor",
"version": "0.0.3",
"version": "0.1.0",
"description": "Utils for Drizzle ORM cursor based pagination",
"main": "dist/index.js",
"module": "dist/index.mjs",
Expand Down Expand Up @@ -37,6 +37,7 @@
},
"devDependencies": {
"@changesets/cli": "^2.26.2",
"@total-typescript/ts-reset": "^0.5.1",
"@types/better-sqlite3": "^7.6.4",
"@types/node": "^20.5.9",
"better-sqlite3": "^8.6.0",
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 62 additions & 0 deletions src/generateCursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { SQL, and, asc, desc, eq, gt, lt, or } from "drizzle-orm";

import type { CursorConfig } from "./types";
import { generateSubArrays } from "./utils";
import { parse } from "./parse";
import { serialize } from "./serialize";

export const generateCursor = (config: CursorConfig) => {
const { cursors = [], primaryCursor } = config;
const orderBy: Array<SQL> = [];
for (const { order = "ASC", schema } of [...cursors, primaryCursor]) {
const fn = order === "ASC" ? asc : desc;
const sql = fn(schema);
orderBy.push(sql);
}
return {
orderBy,
where: (lastPreviousItemData?: Record<string, unknown> | string | null) => {
if (!lastPreviousItemData) {
return undefined;
}

const data =
typeof lastPreviousItemData === "string"
? parse(config, lastPreviousItemData)
: lastPreviousItemData;

if (!data) {
return undefined;
}

const matrix = generateSubArrays([...cursors, primaryCursor]);

const ors: Array<SQL> = [];
for (const posibilities of matrix) {
const ands: Array<SQL> = [];
for (const cursor of posibilities) {
const lastValue = cursor === posibilities?.at(-1);
const { order = "ASC", schema, key } = cursor;
const fn = order === "ASC" ? gt : lt;
const sql = !lastValue
? eq(schema, data[key])
: fn(schema, data[key]);
ands.push(sql);
}
const _and = and(...ands);
if (!_and) {
continue;
}
ors.push(_and);
}
const where = or(...ors);

return where;
},
parse: (cursor: string) => parse(config, cursor),
serialize: (data?: Record<string, unknown> | null) =>
serialize(config, data),
};
};

export default generateCursor;
60 changes: 5 additions & 55 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,5 @@
import { AnyColumn, SQL, and, asc, desc, eq, gt, lt, or } from "drizzle-orm";

import { generateSubArrays } from "./utils";

export type Cursor = { order?: "ASC" | "DESC"; key: string; schema: AnyColumn };

export type CursorConfig = {
primaryCursor: Cursor;
cursors?: Array<Cursor>;
};

export const generateCursor = (
{ cursors = [], primaryCursor }: CursorConfig,
lastPreviousItemData?: Record<string, any>
) => {
const orderBy: Array<SQL> = [];
for (const { order = "ASC", schema } of [...cursors, primaryCursor]) {
const fn = order === "ASC" ? asc : desc;
const sql = fn(schema);
orderBy.push(sql);
}

if (!lastPreviousItemData) {
return {
orderBy,
where: undefined,
};
}

const matrix = generateSubArrays([...cursors, primaryCursor]);

const ors: Array<SQL> = [];
for (const posibilities of matrix) {
const ands: Array<SQL> = [];
for (const cursor of posibilities) {
const lastValue = cursor === posibilities?.at(-1);
const { order = "ASC", schema, key } = cursor;
const fn = order === "ASC" ? gt : lt;
const sql = !lastValue
? eq(schema, lastPreviousItemData[key])
: fn(schema, lastPreviousItemData[key]);
ands.push(sql);
}
const _and = and(...ands);
if (!_and) {
continue;
}
ors.push(_and);
}
const where = or(...ors);
return {
orderBy,
where,
};
};
export * from "./types";
export * from "./generateCursor";
export { generateCursor as default } from "./generateCursor";
export { parse } from "./parse";
export { serialize } from "./serialize";
26 changes: 26 additions & 0 deletions src/parse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import "@total-typescript/ts-reset";

import type { CursorConfig } from "./types";

export function parse<
T extends Record<string, unknown> = Record<string, unknown>
>(
{ primaryCursor, cursors = [] }: CursorConfig,
cursor?: string | null
): T | null {
if (!cursor) {
return null;
}

const keys = [primaryCursor, ...cursors].map((cursor) => cursor.key);
const data = JSON.parse(atob(cursor)) as T;

const item = keys.reduce((acc, key) => {
const value = data[key];
acc[key] = value;
return acc;
}, {} as Record<string, unknown>);
return item as T;
}

export default parse;
25 changes: 25 additions & 0 deletions src/serialize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import "@total-typescript/ts-reset";

import type { CursorConfig } from "./types";

export function serialize<
T extends Record<string, unknown> = Record<string, unknown>
>(
{ primaryCursor, cursors = [] }: CursorConfig,
data?: T | null
): string | null {
if (!data) {
return null;
}

const keys = [primaryCursor, ...cursors].map((cursor) => cursor.key);
const item = keys.reduce((acc, key) => {
const value = data[key];
acc[key] = value;
return acc;
}, {} as Record<string, unknown>);

return btoa(JSON.stringify(item));
}

export default serialize;
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { AnyColumn } from "drizzle-orm";

export type Cursor = { order?: "ASC" | "DESC"; key: string; schema: AnyColumn };

export type CursorConfig = {
primaryCursor: Cursor;
cursors?: Array<Cursor>;
};
2 changes: 2 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import "@total-typescript/ts-reset";

export function generateSubArrays<T>(arr: ReadonlyArray<T>): T[][] {
const subArrays: T[][] = [];
for (let i = 0; i < arr.length; i++) {
Expand Down
3 changes: 1 addition & 2 deletions test/db/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import * as schema from "./schema";

import { BetterSQLite3Database, drizzle } from "drizzle-orm/better-sqlite3";

import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";

const sqlite = new Database("db.db");
export const db = drizzle(sqlite, { schema, logger: true });
Expand Down
10 changes: 1 addition & 9 deletions test/db/schema.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
import {
AnySQLiteColumn,
integer,
numeric,
sqliteTable,
text,
} from "drizzle-orm/sqlite-core";

import { sql } from "drizzle-orm";
import { integer, numeric, sqliteTable, text } from "drizzle-orm/sqlite-core";

export const users = sqliteTable("users", {
id: integer("id").primaryKey({ autoIncrement: true }),
Expand Down
Loading

0 comments on commit 144a174

Please sign in to comment.