Skip to content

Commit

Permalink
.
Browse files Browse the repository at this point in the history
  • Loading branch information
ldanilek committed Nov 4, 2024
2 parents 0ed7ff5 + e646c69 commit a81fcb6
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 79 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ that the Aggregate component makes easy and efficient:
2. Count the number of scores greater than 65: `aggregate.count(ctx, { lower: { key: 65, inclusive: false } })`
3. Find the p95 score: `aggregate.at(ctx, Math.floor(aggregate.count(ctx) * 0.95))`
4. Find the overall average score: `aggregate.sum(ctx) / aggregate.count(ctx)`
5. Find the ranking for a score of 65 in the leaderboard: `aggregate.offsetOf(ctx, 65)`
5. Find the ranking for a score of 65 in the leaderboard: `aggregate.indexOf(ctx, 65)`
6. Find the average score for an individual user. You can define another aggregate
partitioned by user and aggregate within each:

Expand Down Expand Up @@ -400,7 +400,9 @@ ways to perform migrations, but here's an overview of one way:
aggregate component.
3. Use a paginated background
[migration](https://www.npmjs.com/package/@convex-dev/migrations)
to walk all existing data and call `insertIfDoesNotExist`.
to walk all existing data and call `insertIfDoesNotExist`. In the example,
you would run `runAggregateBackfill` in
[leaderboard.ts](example/convex/leaderboard.ts).
4. Now all of the data is represented in the `Aggregate`, you can start calling
read methods like `aggregate.count(ctx)` and you can change the write methods
back (`insertIfDoesNotExist` -> `insert` etc.).
Expand Down
85 changes: 85 additions & 0 deletions example/convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -759,4 +759,89 @@ export declare const components: {
>;
};
};
migrations: {
lib: {
cancel: FunctionReference<
"mutation",
"internal",
{ name: string },
{
batchSize?: number;
cursor?: string | null;
error?: string;
isDone: boolean;
latestEnd?: number;
latestStart: number;
name: string;
next?: Array<string>;
processed: number;
state: "inProgress" | "success" | "failed" | "canceled" | "unknown";
}
>;
cancelAll: FunctionReference<
"mutation",
"internal",
{ sinceTs?: number },
Array<{
batchSize?: number;
cursor?: string | null;
error?: string;
isDone: boolean;
latestEnd?: number;
latestStart: number;
name: string;
next?: Array<string>;
processed: number;
state: "inProgress" | "success" | "failed" | "canceled" | "unknown";
}>
>;
clearAll: FunctionReference<
"mutation",
"internal",
{ before?: number },
any
>;
getStatus: FunctionReference<
"query",
"internal",
{ limit?: number; names?: Array<string> },
Array<{
batchSize?: number;
cursor?: string | null;
error?: string;
isDone: boolean;
latestEnd?: number;
latestStart: number;
name: string;
next?: Array<string>;
processed: number;
state: "inProgress" | "success" | "failed" | "canceled" | "unknown";
}>
>;
migrate: FunctionReference<
"mutation",
"internal",
{
batchSize?: number;
cursor?: string | null;
dryRun: boolean;
fnHandle: string;
name: string;
next?: Array<{ fnHandle: string; name: string }>;
},
{
batchSize?: number;
cursor?: string | null;
error?: string;
isDone: boolean;
latestEnd?: number;
latestStart: number;
name: string;
next?: Array<string>;
processed: number;
state: "inProgress" | "success" | "failed" | "canceled" | "unknown";
}
>;
};
};
};
9 changes: 6 additions & 3 deletions example/convex/aggregate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ import { convexTest } from "convex-test";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"
import schema from "./schema";
import componentSchema from "../../src/component/schema";
import migrationsSchema from "../node_modules/@convex-dev/migrations/src/component/schema";
import { api, components, internal } from "./_generated/api";

const modules = import.meta.glob("./**/*.ts");
const componentModules = import.meta.glob("../../src/component/**/*.ts");
const migrationsModules = import.meta.glob("../node_modules/@convex-dev/migrations/src/component/**/*.ts");

describe("leaderboard", () => {
async function setupTest() {
const t = convexTest(schema, modules);
t.registerComponent("aggregateByScore", componentSchema, componentModules);
t.registerComponent("aggregateScoreByUser", componentSchema, componentModules);
t.registerComponent("migrations", migrationsSchema, migrationsModules);
// Reduce maxNodeSize so we can test complex trees with fewer items.
await t.mutation(components.aggregateByScore.public.clear, { maxNodeSize: 4 });
await t.mutation(components.aggregateScoreByUser.public.clear, { maxNodeSize: 4 });
Expand Down Expand Up @@ -80,10 +83,10 @@ describe("leaderboard", () => {
await t.mutation(api.leaderboard.addScore, { name: "Lee", score: 15 });
await t.mutation(api.leaderboard.addScore, { name: "Lee", score: 25 });
await t.mutation(api.leaderboard.addScore, { name: "Lee", score: 30 });
await t.mutation(components.aggregateByScore.public.clear, {});
await t.mutation(components.aggregateScoreByUser.public.clear, {});
await t.mutation(internal.leaderboard.clearAggregates, {});
expect(await t.query(api.leaderboard.countScores)).toStrictEqual(0);
await t.mutation(internal.leaderboard.backfillAggregates);
await t.mutation(internal.leaderboard.runAggregateBackfill, {});
await t.finishAllScheduledFunctions(vi.runAllTimers);
expect(await t.query(api.leaderboard.countScores)).toStrictEqual(5);
});

Expand Down
3 changes: 3 additions & 0 deletions example/convex/convex.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defineApp } from "convex/server";
import aggregate from "@convex-dev/aggregate/convex.config";
import migrations from "@convex-dev/migrations/convex.config";

const app = defineApp();
app.use(aggregate, { name: "aggregateByScore" });
Expand All @@ -8,4 +9,6 @@ app.use(aggregate, { name: "music" });
app.use(aggregate, { name: "photos" });
app.use(aggregate, { name: "stats" });

app.use(migrations);

export default app;
32 changes: 23 additions & 9 deletions example/convex/leaderboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@

import { TableAggregate } from "@convex-dev/aggregate";
import { mutation, query, internalMutation } from "./_generated/server";
import { components } from "./_generated/api";
import { components, internal } from "./_generated/api";
import { DataModel } from "./_generated/dataModel";
import { ConvexError, v } from "convex/values";
import { Migrations } from "@convex-dev/migrations";

export const migrations = new Migrations<DataModel>(components.migrations);
export const run = migrations.runner();

const aggregateByScore = new TableAggregate<number, DataModel, "leaderboard">(
components.aggregateByScore,
Expand All @@ -21,20 +25,30 @@ const aggregateScoreByUser = new TableAggregate<
sumValue: (doc) => doc.score,
});

export const backfillAggregates = internalMutation({
export const backfillAggregatesMigration = migrations.define({
table: "leaderboard",
migrateOne: async (ctx, doc) => {
await aggregateByScore.insertIfDoesNotExist(ctx, doc);
await aggregateScoreByUser.insertIfDoesNotExist(ctx, doc);
console.log("backfilled", doc.name, doc.score);
},
});

export const clearAggregates = internalMutation({
args: {},
handler: async (ctx) => {
await aggregateByScore.clear(ctx);
await aggregateScoreByUser.clear(ctx);

for await (const doc of ctx.db.query("leaderboard")) {
await aggregateByScore.insert(ctx, doc);
await aggregateScoreByUser.insert(ctx, doc);
console.log("backfilled", doc.name, doc.score);
}
},
});

// This is what you can run, from the Convex dashboard or with `npx convex run`,
// to backfill aggregates for existing leaderboard entries, if you created the
// leaderboard before adding the aggregate components.
export const runAggregateBackfill = migrations.runner(
internal.leaderboard.backfillAggregatesMigration
);

export const addScore = mutation({
args: {
name: v.string(),
Expand Down Expand Up @@ -110,7 +124,7 @@ export const rankOfScore = query({
score: v.number(),
},
handler: async (ctx, args) => {
return await aggregateByScore.offsetUntil(ctx, args.score);
return await aggregateByScore.indexOf(ctx, args.score, { order: "desc" });
},
});

Expand Down
17 changes: 16 additions & 1 deletion example/package-lock.json

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

1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
},
"dependencies": {
"@convex-dev/aggregate": "file:..",
"@convex-dev/migrations": "^0.2.1",
"convex": "^1.17.0",
"convex-helpers": "^0.1.61",
"rand-seed": "^2.1.7"
Expand Down
51 changes: 2 additions & 49 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@convex-dev/aggregate",
"description": "Convex component to calculate counts and sums of values for efficient aggregation.",
"version": "0.1.15",
"version": "0.1.16",
"keywords": [
"convex",
"component",
Expand Down
Loading

0 comments on commit a81fcb6

Please sign in to comment.