-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathphotos.ts
More file actions
162 lines (142 loc) · 4.56 KB
/
Copy pathphotos.ts
File metadata and controls
162 lines (142 loc) · 4.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
/**
* Example of a photo gallery which doesn't change much, and you want to display
* it with offset-based pagination. That is, each page has 10 items, and you can
* jump to any page in O(log(n)) time.
* The paginated list is sorted by _creationTime.
*
* Also demonstrates aggregates automatically updating when the underlying table changes.
*/
import { TableAggregate } from "@convex-dev/aggregate";
import {
internalMutation as rawInternalMutation,
mutation as rawMutation,
query,
} from "./_generated/server.js";
import { components } from "./_generated/api.js";
import type { DataModel } from "./_generated/dataModel.js";
import { v } from "convex/values";
import { resetStatusValidator } from "./utils/resetStatus.js";
import {
customCtx,
customMutation,
} from "convex-helpers/server/customFunctions";
import { Triggers } from "convex-helpers/server/triggers";
export const photos = new TableAggregate<{
Namespace: string;
Key: number;
DataModel: DataModel;
TableName: "photos";
}>(components.photos, {
namespace: (doc) => doc.album,
sortKey: (doc) => doc._creationTime,
});
const triggers = new Triggers<DataModel>();
triggers.register("photos", photos.trigger());
const mutation = customMutation(rawMutation, customCtx(triggers.wrapDB));
const internalMutation = customMutation(
rawInternalMutation,
customCtx(triggers.wrapDB),
);
export const addPhoto = mutation({
args: {
album: v.string(),
url: v.string(),
},
returns: v.id("photos"),
handler: async (ctx, args) => {
return await ctx.db.insert("photos", { album: args.album, url: args.url });
},
});
/**
* Get the total count of photos in an album - demonstrates O(log(n)) count operation
*/
export const photoCount = query({
args: { album: v.string() },
returns: v.number(),
handler: async (ctx, { album }) => {
return await photos.count(ctx, { namespace: album });
},
});
/**
* Call this with {offset:0, numItems:10} to get the first page of photos,
* then {offset:10, numItems:10} to get the second page, etc.
*
* This demonstrates the key feature: O(log(n)) offset-based pagination!
* Instead of scanning through all photos to find page N, we use the aggregate
* to jump directly to the right position.
*/
export const pageOfPhotos = query({
args: {
album: v.string(),
offset: v.number(),
numItems: v.number(),
},
returns: v.array(v.string()),
handler: async (ctx, { offset, numItems, album }) => {
// Check if the album has any photos first
const firstPhoto = await ctx.db
.query("photos")
.withIndex("by_album_creation_time", (q) => q.eq("album", album))
.first();
if (!firstPhoto) return [];
// This is the magic! photos.at() gives us O(log(n)) lookup to any position
const { key: firstPhotoCreationTime } = await photos.at(ctx, offset, {
namespace: album,
});
const photoDocs = await ctx.db
.query("photos")
.withIndex("by_album_creation_time", (q) =>
q.eq("album", album).gte("_creationTime", firstPhotoCreationTime),
)
.take(numItems);
return photoDocs.map((doc) => doc.url);
},
});
/**
* Get all available albums - useful for the UI
*/
export const availableAlbums = query({
args: {},
returns: v.array(v.object({ name: v.string(), count: v.number() })),
handler: async (ctx) => {
// Get unique albums from the photos table
const allPhotos = await ctx.db.query("photos").collect();
const albumNames = [...new Set(allPhotos.map((photo) => photo.album))];
// Get count for each album using the aggregate
const albumsWithCounts = await Promise.all(
albumNames.map(async (album) => ({
name: album,
count: await photos.count(ctx, { namespace: album }),
})),
);
return albumsWithCounts.sort((a, b) => a.name.localeCompare(b.name));
},
});
// ----- internal -----
export const resetAll = internalMutation({
args: {},
returns: resetStatusValidator,
handler: async (ctx) => {
const batchSize = 1000;
const docs = await ctx.db.query("photos").take(batchSize);
for (const doc of docs) await ctx.db.delete("photos", doc._id);
if (docs.length === batchSize) return "partial_reset";
await photos.clearAll(ctx, {
maxNodeSize: 4,
rootLazy: false,
});
return "all_reset";
},
});
export const addPhotos = internalMutation({
args: {
photos: v.array(v.object({ album: v.string(), url: v.string() })),
},
handler: async (ctx, { photos }) => {
await Promise.all(
photos.map((photo) =>
ctx.db.insert("photos", { album: photo.album, url: photo.url }),
),
);
},
});