@protobase/server 0.22.13 → 0.23.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.
@@ -104,7 +104,7 @@ const aggregate = async (deps, entry, session, input) => {
104
104
  if (dated) {
105
105
  const granularity = params.granularity ?? "day";
106
106
  const timeZone = params.time_zone ?? "UTC";
107
- const range = await seriesRange(deps, access, params.group_by, params, scope);
107
+ const range = await seriesRange(deps, access, params.group_by, params, timeZone, scope);
108
108
  if (params.split_by !== void 0) {
109
109
  const split = {
110
110
  by: params.split_by,
@@ -5,6 +5,7 @@ import { columnId, filterCondition, scopeConditions, tableId, whereClause } from
5
5
  import { readTransaction } from "./transactions.js";
6
6
  import { parseParams } from "./params.js";
7
7
  import { labelRequests, relatedLabels } from "./related-labels.js";
8
+ import { rangeEnd, rangeStart } from "./range-ends.js";
8
9
  import { z } from "zod";
9
10
  import { sql } from "kysely";
10
11
  import { facetCounts, histogram, series, withoutField } from "@protobase/query";
@@ -103,22 +104,24 @@ const earliest = async (deps, access, fieldName, scope) => {
103
104
  return (await readTransaction(deps.db, deps.statementTimeoutMs, (trx) => sql`select min(${columnId(target)})::text as lowest from ${tableId(access.full)} as t${whereClause(conditions)}`.execute(trx))).rows[0]?.lowest ?? void 0;
104
105
  };
105
106
  const bound = (model, fieldName, value) => model.fields[fieldName]?.type === "date" ? value.slice(0, 10) : value;
106
- /** The span a series of `field` covers: `from` and `to`, else the `range` up to now, `all` from the earliest row. */
107
- const seriesRange = async (deps, access, field, params, scope) => {
108
- const to = params.to ?? (/* @__PURE__ */ new Date()).toISOString();
107
+ /**
108
+ * The span a series of `field` covers: `from` and `to`, else the `range` up to now, `all` from the earliest row. A
109
+ * relative range of a date field runs over the days of `timeZone`, the viewer's today included.
110
+ */
111
+ const seriesRange = async (deps, access, field, params, timeZone, scope) => {
109
112
  const { model } = access;
113
+ const type = model.fields[field]?.type;
114
+ const now = Date.now();
115
+ const to = params.to ?? rangeEnd(type, now, timeZone);
110
116
  if (params.from) return {
111
117
  from: bound(model, field, params.from),
112
118
  to: bound(model, field, to)
113
119
  };
114
120
  const range = params.range ?? "30d";
115
- if (range !== "all") {
116
- const from = (/* @__PURE__ */ new Date(Date.now() - rangeDays[range] * 864e5)).toISOString();
117
- return {
118
- from: bound(model, field, from),
119
- to: bound(model, field, to)
120
- };
121
- }
121
+ if (range !== "all") return {
122
+ from: rangeStart(type, now - rangeDays[range] * 864e5, timeZone),
123
+ to: bound(model, field, to)
124
+ };
122
125
  const lowest = await earliest(deps, access, field, scope);
123
126
  return lowest ? {
124
127
  from: bound(model, field, lowest),
@@ -133,8 +136,8 @@ const timeSeries = async (deps, entry, session, input) => {
133
136
  parameter: "field",
134
137
  message: "Unknown field"
135
138
  }] });
136
- const range = await seriesRange(deps, access, params.field, params, scope);
137
139
  const timeZone = params.time_zone ?? "UTC";
140
+ const range = await seriesRange(deps, access, params.field, params, timeZone, scope);
138
141
  const points = range ? await readTransaction(deps.db, deps.statementTimeoutMs, (trx) => series(trx, model, params.field, {
139
142
  range,
140
143
  granularity: params.granularity,
@@ -0,0 +1,29 @@
1
+ import { badRequest } from "./problem.js";
2
+ //#region src/range-ends.ts
3
+ const dayMs = 864e5;
4
+ /** The day an instant falls on in `zone`, as YYYY-MM-DD; an unknown zone is a bad request. */
5
+ const dayIn = (ms, zone) => {
6
+ try {
7
+ return new Intl.DateTimeFormat("en-CA", {
8
+ timeZone: zone,
9
+ year: "numeric",
10
+ month: "2-digit",
11
+ day: "2-digit"
12
+ }).format(ms);
13
+ } catch (error) {
14
+ if (error instanceof RangeError) throw badRequest("invalid-parameter", `Unknown time zone "${zone}"`, { errors: [{
15
+ parameter: "time_zone",
16
+ message: "Unknown time zone"
17
+ }] });
18
+ throw error;
19
+ }
20
+ };
21
+ /** Where a range starting at instant `ms` starts: that instant, or for a date the viewer's day of it. */
22
+ const rangeStart = (type, ms, zone) => type === "date" ? dayIn(ms, zone) : new Date(ms).toISOString();
23
+ /** Where a range up to `now` ends, exclusive: that instant, or for a date the day after the viewer's today. */
24
+ const rangeEnd = (type, now, zone) => {
25
+ if (type !== "date") return new Date(now).toISOString();
26
+ return new Date(Date.parse(dayIn(now, zone)) + dayMs).toISOString().slice(0, 10);
27
+ };
28
+ //#endregion
29
+ export { dayIn, rangeEnd, rangeStart };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.22.13",
3
+ "version": "0.23.1",
4
4
  "description": "The Protobase REST API as a Hono app, with Better Auth sign-in.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -31,9 +31,9 @@
31
31
  "dependencies": {
32
32
  "@better-auth/passkey": "^1.7.7",
33
33
  "@better-auth/sso": "^1.7.7",
34
- "@protobase/layout": "0.22.13",
35
- "@protobase/query": "0.22.13",
36
- "@protobase/schema": "0.22.13",
34
+ "@protobase/layout": "0.23.1",
35
+ "@protobase/query": "0.23.1",
36
+ "@protobase/schema": "0.23.1",
37
37
  "@react-email/render": "^2.1.0",
38
38
  "@scalar/hono-api-reference": "^0.12.10",
39
39
  "@sentry/core": "^11.7.0",