appflare 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -230,11 +230,14 @@ function renderRouteFactory(operation: HttpOperation): string {
230
230
  }
231
231
 
232
232
  if (!resolvedAuthToken) {
233
- onError?.(
234
- new Error(
235
- "authToken is required for realtime subscribe (pass options.authToken or Authorization Bearer header)",
236
- ),
233
+ const authError = new Error(
234
+ "authToken is required for realtime subscribe (pass options.authToken or Authorization Bearer header)",
237
235
  );
236
+ if (onError) {
237
+ onError(authError);
238
+ } else {
239
+ console.warn("[appflare:subscribe]", authError.message);
240
+ }
238
241
  return;
239
242
  }
240
243
 
@@ -295,7 +298,11 @@ function renderRouteFactory(operation: HttpOperation): string {
295
298
  onError?.(new Error("Realtime websocket error"));
296
299
  });
297
300
  } catch (error) {
298
- onError?.(error);
301
+ if (onError) {
302
+ onError(error);
303
+ } else {
304
+ console.warn("[appflare:subscribe] Subscription failed:", error);
305
+ }
299
306
  }
300
307
  })();
301
308
 
@@ -191,6 +191,94 @@ Execution contexts store these as `ctx.mutationEvents`, and mutation routes call
191
191
 
192
192
  ---
193
193
 
194
+ ## DB aggregate helpers
195
+
196
+ Generated `ctx.db.<table>` wrappers now include aggregate helpers:
197
+
198
+ - `count(args?)`
199
+ - `where?: WhereInput<TModel>`
200
+ - `field?: keyof TModel | string` (supports relation paths, e.g. `comments.id`)
201
+ - `distinct?: boolean`
202
+ - `with?: QueryWithInput<...>`
203
+ - returns `Promise<number>`
204
+ - `avg(args)`
205
+ - `where?: WhereInput<TModel>`
206
+ - `field: NumericFieldKey<TModel> | string` (supports relation paths, e.g. `comments.id`)
207
+ - `distinct?: boolean`
208
+ - `with?: QueryWithInput<...>`
209
+ - returns `Promise<number | null>`
210
+
211
+ Aggregate behavior with `with`:
212
+
213
+ - Relation `with.where` and nested `with` filters are treated as parent-row constraints for aggregates (EXISTS-style).
214
+ - For `count` with `with`, `distinct` defaults to `true` when `field` is provided.
215
+ - Nested relation paths are supported recursively for both `count` and `avg`.
216
+
217
+ Relation `with` aggregates on `findMany`/`findFirst`:
218
+
219
+ - You can request per-parent relation aggregates directly inside `with` using `_count` and `_avg`.
220
+ - Result rows include a sibling `<relationName>Aggregate` object.
221
+ - `_avg` returns `0` for parents with no related rows.
222
+
223
+ Example:
224
+
225
+ ```ts
226
+ const total = await ctx.db.posts.count({
227
+ where: { ownerId: user.id },
228
+ });
229
+
230
+ const uniqueOwners = await ctx.db.posts.count({
231
+ field: "ownerId",
232
+ distinct: true,
233
+ });
234
+
235
+ const averageId = await ctx.db.posts.avg({
236
+ field: "id",
237
+ where: { ownerId: user.id },
238
+ });
239
+
240
+ const postsWithMatchingComments = await ctx.db.posts.count({
241
+ with: {
242
+ comments: {
243
+ where: {
244
+ id: { gte: 10000 },
245
+ },
246
+ },
247
+ },
248
+ });
249
+
250
+ const averageCommentId = await ctx.db.posts.avg({
251
+ field: "comments.id",
252
+ with: {
253
+ comments: {
254
+ where: {
255
+ id: { gte: 10000 },
256
+ },
257
+ },
258
+ },
259
+ });
260
+
261
+ const postsWithCommentStats = await ctx.db.posts.findMany({
262
+ with: {
263
+ comments: {
264
+ _count: true,
265
+ _avg: {
266
+ id: true,
267
+ },
268
+ },
269
+ },
270
+ });
271
+
272
+ const firstPostCommentCount =
273
+ postsWithCommentStats[0]?.commentsAggregate.count ?? 0;
274
+ const firstPostAverageCommentId =
275
+ postsWithCommentStats[0]?.commentsAggregate.avg.id ?? 0;
276
+ ```
277
+
278
+ `geoWithin.latitudeField` and `geoWithin.longitudeField` are now typed to table keys (instead of free-form strings). Invalid field names still no-op the geo filter at runtime, but now emit a warning.
279
+
280
+ ---
281
+
194
282
  ## Durable Object responsibilities
195
283
 
196
284
  `AppflareRealtimeDurableObject` keeps in-memory maps for:
@@ -1,6 +1,7 @@
1
1
  export function generateAuth(): string {
2
2
  return `
3
3
 
4
+ import { getHeaders } from "./server";
4
5
  export async function resolveSession(
5
6
  \trequest: Request,
6
7
  \tdatabase: D1Database,
@@ -17,7 +18,7 @@ export async function resolveSession(
17
18
 
18
19
  \ttry {
19
20
  \t\tconst session = await auth.api.getSession({
20
- \t\t\theaders: request.headers,
21
+ \t\t\theaders: getHeaders(request.headers),
21
22
  \t\t});
22
23
 
23
24
  \t\treturn {
@@ -0,0 +1,30 @@
1
+ export const cronModule = `
2
+ export async function executeCronTriggers(
3
+ controller: { cron: string },
4
+ env: Record<string, unknown>,
5
+ options: RegisterHandlersOptions,
6
+ ): Promise<void> {
7
+ const cronValue = controller?.cron;
8
+ if (!cronValue) {
9
+ return;
10
+ }
11
+
12
+ if (cronHandlers.length === 0) {
13
+ return;
14
+ }
15
+
16
+ const ctx = createSchedulerExecutionContext(env, options);
17
+
18
+ for (const cronEntry of cronHandlers) {
19
+ if (!cronEntry.cronTriggers.includes(cronValue)) {
20
+ continue;
21
+ }
22
+
23
+ try {
24
+ await cronEntry.definition.handler(ctx);
25
+ } catch (error) {
26
+ console.error("Cron task failed", cronEntry.taskName, error);
27
+ }
28
+ }
29
+ }
30
+ `;