@protobase/server 0.28.1 → 0.28.3
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.
- package/dist/aggregate-groups.js +17 -9
- package/dist/aggregate-service.js +14 -6
- package/dist/check-views.js +3 -2
- package/dist/create-admin.js +3 -1
- package/dist/functions/routes.js +6 -4
- package/dist/openapi/resource-paths.js +15 -2
- package/dist/view-access.js +13 -1
- package/dist/view-record-chart.js +42 -0
- package/package.json +4 -4
package/dist/aggregate-groups.js
CHANGED
|
@@ -13,9 +13,12 @@ const aggregateParams = z.object({
|
|
|
13
13
|
filter: z.string().optional(),
|
|
14
14
|
sum: name.optional(),
|
|
15
15
|
average: name.optional(),
|
|
16
|
+
maximum: name.optional(),
|
|
16
17
|
limit: z.coerce.number().int().optional(),
|
|
17
18
|
label: name.optional(),
|
|
18
19
|
range: z.enum([
|
|
20
|
+
"1h",
|
|
21
|
+
"24h",
|
|
19
22
|
"7d",
|
|
20
23
|
"30d",
|
|
21
24
|
"90d",
|
|
@@ -25,6 +28,9 @@ const aggregateParams = z.object({
|
|
|
25
28
|
from: z.string().optional(),
|
|
26
29
|
to: z.string().optional(),
|
|
27
30
|
granularity: z.enum([
|
|
31
|
+
"minute",
|
|
32
|
+
"5_minutes",
|
|
33
|
+
"15_minutes",
|
|
28
34
|
"hour",
|
|
29
35
|
"day",
|
|
30
36
|
"week",
|
|
@@ -41,14 +47,16 @@ const invalid = (parameter, message) => badRequest("invalid-parameter", `Invalid
|
|
|
41
47
|
message
|
|
42
48
|
}] });
|
|
43
49
|
const measureOf = (params) => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const given = [
|
|
51
|
+
"sum",
|
|
52
|
+
"average",
|
|
53
|
+
"maximum"
|
|
54
|
+
].filter((kind) => params[kind] !== void 0);
|
|
55
|
+
if (given.length > 1) throw invalid(given[1], "Give one of sum, average and maximum");
|
|
56
|
+
const [kind] = given;
|
|
57
|
+
return kind === void 0 ? void 0 : {
|
|
58
|
+
kind,
|
|
59
|
+
field: params[kind]
|
|
52
60
|
};
|
|
53
61
|
};
|
|
54
62
|
const optionErrors = (params, dated) => {
|
|
@@ -75,7 +83,7 @@ const isDated = (model, field) => {
|
|
|
75
83
|
};
|
|
76
84
|
/**
|
|
77
85
|
* Records per value of a field, or per hour, day, week, month or year of a date field with the empty ones included,
|
|
78
|
-
* each with how many there are and, with `sum` or `
|
|
86
|
+
* each with how many there are and, with `sum`, `average` or `maximum`, that of a numeric field. Read as a list is: the caller's
|
|
79
87
|
* filter on the model as they see it, inside their tenant and row filter; the scan guard judges the grouped field.
|
|
80
88
|
*/
|
|
81
89
|
const aggregate = async (deps, entry, session, input) => {
|
|
@@ -22,6 +22,8 @@ const seriesParams = z.object({
|
|
|
22
22
|
field,
|
|
23
23
|
filter,
|
|
24
24
|
range: z.enum([
|
|
25
|
+
"1h",
|
|
26
|
+
"24h",
|
|
25
27
|
"7d",
|
|
26
28
|
"30d",
|
|
27
29
|
"90d",
|
|
@@ -31,6 +33,9 @@ const seriesParams = z.object({
|
|
|
31
33
|
from: z.string().optional(),
|
|
32
34
|
to: z.string().optional(),
|
|
33
35
|
granularity: z.enum([
|
|
36
|
+
"minute",
|
|
37
|
+
"5_minutes",
|
|
38
|
+
"15_minutes",
|
|
34
39
|
"hour",
|
|
35
40
|
"day",
|
|
36
41
|
"week",
|
|
@@ -43,11 +48,14 @@ const histogramParams = z.object({
|
|
|
43
48
|
filter,
|
|
44
49
|
buckets: z.coerce.number().int().default(20)
|
|
45
50
|
});
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
+
const hourMs = 36e5;
|
|
52
|
+
const rangeMs = {
|
|
53
|
+
"1h": hourMs,
|
|
54
|
+
"24h": 24 * hourMs,
|
|
55
|
+
"7d": 168 * hourMs,
|
|
56
|
+
"30d": 720 * hourMs,
|
|
57
|
+
"90d": 2160 * hourMs,
|
|
58
|
+
"1y": 8760 * hourMs
|
|
51
59
|
};
|
|
52
60
|
const scoped = async (deps, entry, session, source) => {
|
|
53
61
|
const access = await requestAccess(deps, entry, session);
|
|
@@ -119,7 +127,7 @@ const seriesRange = async (deps, access, field, params, timeZone, scope) => {
|
|
|
119
127
|
};
|
|
120
128
|
const range = params.range ?? "30d";
|
|
121
129
|
if (range !== "all") return {
|
|
122
|
-
from: rangeStart(type, now -
|
|
130
|
+
from: rangeStart(type, now - rangeMs[range], timeZone),
|
|
123
131
|
to: bound(model, field, to)
|
|
124
132
|
};
|
|
125
133
|
const lowest = await earliest(deps, access, field, scope);
|
package/dist/check-views.js
CHANGED
|
@@ -4,6 +4,7 @@ import { cardFavoriteErrors, favoriteOwnerErrors } from "./view-card-favorite.js
|
|
|
4
4
|
import { saveSearchErrors, searchesOfErrors } from "./view-saved-search.js";
|
|
5
5
|
import { chartErrors } from "./view-chart.js";
|
|
6
6
|
import { mapDisplayErrors, mapErrors } from "./view-map.js";
|
|
7
|
+
import { recordChartErrors } from "./view-record-chart.js";
|
|
7
8
|
import { relatedColumnErrors } from "./view-related-column.js";
|
|
8
9
|
import { editableErrors } from "./view-editable.js";
|
|
9
10
|
import { actionWhenErrors, actionWhenFields } from "./view-action-when.js";
|
|
@@ -147,7 +148,7 @@ const listOfErrors = (model, name, models) => {
|
|
|
147
148
|
* edits a list column in its cell that is not a field with an editor of one line the API may write,
|
|
148
149
|
* has a section, related section or sidebar whose `when` condition names what the app does not have, a related section or related list column, a cards display's or gallery's photos or a cards display's favorite button, a `listOf`, a "Save search" button or a
|
|
149
150
|
* `searchesOf` that do not fit the resources, cards
|
|
150
|
-
* displays of one list that name different photos, a chart per value that cannot group or add up its fields, a map that cannot show the
|
|
151
|
+
* displays of one list that name different photos, a chart per value that cannot group or add up its fields, a chart of related records that does not fit them, a map that cannot show the
|
|
151
152
|
* record's position or a map display that cannot find the records in its view, a board whose columns are no filterable enum, a calendar or timeline whose dates are no filterable date or timestamp fields, a link to an address the app does not have, an action whose `when` reads what the record does not have, an AI action that cannot write its field, action inputs that do not fit, or a create page that is not one of `pages`.
|
|
152
153
|
*/
|
|
153
154
|
const checkViews = (registry, views, pages = [], fieldTypes = {}) => {
|
|
@@ -167,7 +168,7 @@ const checkViews = (registry, views, pages = [], fieldTypes = {}) => {
|
|
|
167
168
|
const maps = [...layoutItems(view.layout).flatMap((item) => item.kind === "map" ? mapErrors(item, model).map((error) => `map "${item.title}": ${error}`) : []), ...(view.list?.displays ?? []).flatMap((display) => display.kind === "map" ? mapDisplayErrors(display, model).map((error) => `display "${display.name}": ${error}`) : [])];
|
|
168
169
|
const boards = (view.list?.displays ?? []).flatMap((display) => display.kind === "board" ? boardErrors(display, model).map((error) => `display "${display.name}": ${error}`) : []);
|
|
169
170
|
const calendars = (view.list?.displays ?? []).flatMap((display) => display.kind === "calendar" || display.kind === "timeline" ? datesErrors(display, model).map((error) => `display "${display.name}": ${error}`) : []);
|
|
170
|
-
const chart = view.chart ? chartErrors(view.chart, model).map((error) => `chart: ${error}`) : [];
|
|
171
|
+
const chart = [...view.chart ? chartErrors(view.chart, model).map((error) => `chart: ${error}`) : [], ...layoutItems(view.layout).flatMap((item) => item.kind === "chart" ? recordChartErrors(item, view.resource, models).map((error) => `chart "${item.title}": ${error}`) : [])];
|
|
171
172
|
const history = model.history ? [] : layoutItems(view.layout).flatMap((item) => item.kind === "history" ? [`history "${item.title}" needs .history() on resource "${view.resource}"`] : []);
|
|
172
173
|
const comments = model.comments ? [] : layoutItems(view.layout).flatMap((item) => item.kind === "comments" ? [`comments "${item.title}" needs .comments() on resource "${view.resource}"`] : []);
|
|
173
174
|
return [
|
package/dist/create-admin.js
CHANGED
|
@@ -4,6 +4,7 @@ import { unauthorized } from "./problem.js";
|
|
|
4
4
|
import { fillHook } from "./ai/fill-hook.js";
|
|
5
5
|
import { createErrorHandler } from "./error-handler.js";
|
|
6
6
|
import { isCaptured } from "./changes/capture-sql.js";
|
|
7
|
+
import { wakeDispatch } from "./changes/actor.js";
|
|
7
8
|
import { assistantRecords } from "./assistant/records.js";
|
|
8
9
|
import { fillJob } from "./ai/fill-job.js";
|
|
9
10
|
import { assistantSettings } from "./assistant/assistant-settings.js";
|
|
@@ -271,7 +272,8 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
271
272
|
authenticate,
|
|
272
273
|
db,
|
|
273
274
|
records: (session) => callerRecords(deps, session),
|
|
274
|
-
onError
|
|
275
|
+
onError,
|
|
276
|
+
wake: () => wakeDispatch(deps)
|
|
275
277
|
}));
|
|
276
278
|
app.route("/", batchRoute(deps, authenticate, meta));
|
|
277
279
|
app.route("/", fileRoutes(deps, authenticate));
|
package/dist/functions/routes.js
CHANGED
|
@@ -16,9 +16,10 @@ const requireRoles = (name, session, roles) => {
|
|
|
16
16
|
/**
|
|
17
17
|
* Every function at `<basePath>/<name>` and below, any method. A protected function authenticates the request first, so
|
|
18
18
|
* a call without a valid token is the authenticator's 401 problem; a public one gets the caller only when it asks. Each
|
|
19
|
-
* call runs as background work, so calls that hold the database for long leave a connection to page requests.
|
|
19
|
+
* call runs as background work, so calls that hold the database for long leave a connection to page requests. Once a
|
|
20
|
+
* call ends, failed or not, `wake` runs, so what it wrote reaches open pages without waiting for the dispatcher's poll.
|
|
20
21
|
*/
|
|
21
|
-
const functionRoutes = ({ basePath, functions, authenticate, db, records, onError }) => {
|
|
22
|
+
const functionRoutes = ({ basePath, functions, authenticate, db, records, onError, wake }) => {
|
|
22
23
|
const app = new Hono().basePath(basePath);
|
|
23
24
|
app.onError(onError);
|
|
24
25
|
const callerOf = async (request) => {
|
|
@@ -28,11 +29,12 @@ const functionRoutes = ({ basePath, functions, authenticate, db, records, onErro
|
|
|
28
29
|
records: records(session)
|
|
29
30
|
};
|
|
30
31
|
};
|
|
32
|
+
const call = (work) => work().finally(wake);
|
|
31
33
|
for (const [name, fn] of Object.entries(functions)) {
|
|
32
34
|
const prefix = `${basePath}/${name}`;
|
|
33
35
|
const { options } = fn;
|
|
34
36
|
if (options.cors) app.use(`/${name}/*`, cors(options.cors));
|
|
35
|
-
app.all(`/${name}/*`, (c) => runInBackground(async () => {
|
|
37
|
+
app.all(`/${name}/*`, (c) => runInBackground(() => call(async () => {
|
|
36
38
|
const request = relativeRequest(c.req.raw, prefix);
|
|
37
39
|
if (options.public) {
|
|
38
40
|
let caller;
|
|
@@ -51,7 +53,7 @@ const functionRoutes = ({ basePath, functions, authenticate, db, records, onErro
|
|
|
51
53
|
caller: async () => caller
|
|
52
54
|
};
|
|
53
55
|
return invokeFunction(name, fn, request, context);
|
|
54
|
-
}));
|
|
56
|
+
})));
|
|
55
57
|
}
|
|
56
58
|
app.all("/*", (c) => {
|
|
57
59
|
throw notFound(`There is no function at ${c.req.path}`);
|
|
@@ -590,17 +590,21 @@ const resourcePaths = (access) => {
|
|
|
590
590
|
operationId: `aggregate_${entry.name}`,
|
|
591
591
|
tags: tag,
|
|
592
592
|
summary: `Totals per group of ${entry.name} records`,
|
|
593
|
-
description: "Groups the records the filter finds, its own terms on `group_by` included, by the value of a field, or by the hour, day, week, month or year of a date or timestamp field with the empty ones included. Each group has its count and, with `sum` or `
|
|
593
|
+
description: "Groups the records the filter finds, its own terms on `group_by` included, by the value of a field, or by the minute, 5 or 15 minutes, hour, day, week, month or year of a date or timestamp field with the empty ones included. Each group has its count and, with `sum`, `average` or `maximum`, that of a numeric field, `null` when no record of the group has a value. Value groups come largest first. An index must serve the grouped field. A date group can be split: see `split_by`.",
|
|
594
594
|
parameters: [
|
|
595
595
|
query("group_by", "A filterable field", { type: "string" }, true),
|
|
596
596
|
param("filter"),
|
|
597
597
|
query("sum", "A numeric field to add up per group"),
|
|
598
598
|
query("average", "A numeric field to average per group, instead of `sum`"),
|
|
599
|
+
query("maximum", "A numeric field whose largest value per group it returns, instead of `sum`"),
|
|
599
600
|
query("limit", "Most value groups to return, default 100, at most 1000", { type: "integer" }),
|
|
600
601
|
query("label", "For a relation to a single-key resource: the field of that resource whose value each group carries as `label`"),
|
|
601
602
|
query("granularity", "For a date group_by: the bucket, default day", {
|
|
602
603
|
type: "string",
|
|
603
604
|
enum: [
|
|
605
|
+
"minute",
|
|
606
|
+
"5_minutes",
|
|
607
|
+
"15_minutes",
|
|
604
608
|
"hour",
|
|
605
609
|
"day",
|
|
606
610
|
"week",
|
|
@@ -611,6 +615,8 @@ const resourcePaths = (access) => {
|
|
|
611
615
|
query("range", "For a date group_by: the relative range ending now (default 30d); `all` starts at the earliest row", {
|
|
612
616
|
type: "string",
|
|
613
617
|
enum: [
|
|
618
|
+
"1h",
|
|
619
|
+
"24h",
|
|
614
620
|
"7d",
|
|
615
621
|
"30d",
|
|
616
622
|
"90d",
|
|
@@ -659,6 +665,7 @@ const resourcePaths = (access) => {
|
|
|
659
665
|
count: { type: "integer" },
|
|
660
666
|
sum: { type: ["number", "null"] },
|
|
661
667
|
average: { type: ["number", "null"] },
|
|
668
|
+
maximum: { type: ["number", "null"] },
|
|
662
669
|
splits: {
|
|
663
670
|
type: "array",
|
|
664
671
|
description: "Only with `split_by`: the kept values that have rows in the group, in rank order",
|
|
@@ -668,7 +675,8 @@ const resourcePaths = (access) => {
|
|
|
668
675
|
value: {},
|
|
669
676
|
count: { type: "integer" },
|
|
670
677
|
sum: { type: ["number", "null"] },
|
|
671
|
-
average: { type: ["number", "null"] }
|
|
678
|
+
average: { type: ["number", "null"] },
|
|
679
|
+
maximum: { type: ["number", "null"] }
|
|
672
680
|
}
|
|
673
681
|
}
|
|
674
682
|
}
|
|
@@ -692,6 +700,8 @@ const resourcePaths = (access) => {
|
|
|
692
700
|
query("range", "Relative range ending now (default 30d); `all` starts at the earliest row", {
|
|
693
701
|
type: "string",
|
|
694
702
|
enum: [
|
|
703
|
+
"1h",
|
|
704
|
+
"24h",
|
|
695
705
|
"7d",
|
|
696
706
|
"30d",
|
|
697
707
|
"90d",
|
|
@@ -704,6 +714,9 @@ const resourcePaths = (access) => {
|
|
|
704
714
|
query("granularity", "Default day", {
|
|
705
715
|
type: "string",
|
|
706
716
|
enum: [
|
|
717
|
+
"minute",
|
|
718
|
+
"5_minutes",
|
|
719
|
+
"15_minutes",
|
|
707
720
|
"hour",
|
|
708
721
|
"day",
|
|
709
722
|
"week",
|
package/dist/view-access.js
CHANGED
|
@@ -2,6 +2,7 @@ import { openErrors, relatedErrors } from "./view-related.js";
|
|
|
2
2
|
import { cardPhotosErrors } from "./view-card-photos.js";
|
|
3
3
|
import { cardFavoriteErrors } from "./view-card-favorite.js";
|
|
4
4
|
import { searchesOfFor } from "./view-saved-search.js";
|
|
5
|
+
import { recordChartErrors } from "./view-record-chart.js";
|
|
5
6
|
import { relatedColumnErrors } from "./view-related-column.js";
|
|
6
7
|
import { actionWhenFields } from "./view-action-when.js";
|
|
7
8
|
import { conditionReadable, resolveFieldPath } from "@protobase/layout";
|
|
@@ -25,6 +26,16 @@ const pruneRelated = (item, resource, models) => {
|
|
|
25
26
|
} : narrowed;
|
|
26
27
|
return relatedErrors(opened, resource, models).length === 0 ? opened : void 0;
|
|
27
28
|
};
|
|
29
|
+
const pruneRecordChart = (item, resource, models) => {
|
|
30
|
+
const target = models[item.resource];
|
|
31
|
+
const series = target ? item.series.filter((name) => Object.hasOwn(target.fields, name)) : [];
|
|
32
|
+
if (series.length === 0) return void 0;
|
|
33
|
+
const narrowed = {
|
|
34
|
+
...item,
|
|
35
|
+
series
|
|
36
|
+
};
|
|
37
|
+
return recordChartErrors(narrowed, resource, models).length === 0 ? narrowed : void 0;
|
|
38
|
+
};
|
|
28
39
|
const recordWhenShape = (resource, models) => shapes.map((when) => isRecordCondition(when) && !conditionReadable(when, resource, models) ? void 0 : when);
|
|
29
40
|
const pruneGallery = (item, resource, model, models) => {
|
|
30
41
|
const image = item.image !== void 0 && Object.hasOwn(model.fields, item.image) ? item.image : void 0;
|
|
@@ -218,6 +229,7 @@ const layoutShape = (resource, model, models) => {
|
|
|
218
229
|
sidebar: shapes.data,
|
|
219
230
|
when: shapes.data
|
|
220
231
|
}, { requires: ["latitude", "longitude"] }),
|
|
232
|
+
chart: shapes.map((item) => pruneRecordChart(item, resource, models)),
|
|
221
233
|
gallery: shapes.map((item) => pruneGallery(item, resource, model, models)),
|
|
222
234
|
history: shapes.object({
|
|
223
235
|
kind: shapes.data,
|
|
@@ -336,7 +348,7 @@ const viewShape = (resource, model, models, prompted, createPages) => {
|
|
|
336
348
|
/**
|
|
337
349
|
* The view with every reference to a field the caller cannot read removed: list columns, sort, search and displays,
|
|
338
350
|
* field hints, filter widgets, record sections, maps, galleries and custom components' props, the chart, title and search result, the
|
|
339
|
-
* actions that set or read one, and the sidebar's `recent` group. Nothing in it may name a hidden field. Related sections, related list columns
|
|
351
|
+
* actions that set or read one, and the sidebar's `recent` group. Nothing in it may name a hidden field. Related sections, charts of related records, related list columns
|
|
340
352
|
* and links are checked against `models`, the caller's view of every resource, and so are `searchesOf`, cards' favorite
|
|
341
353
|
* buttons and the "Save search" button; the buttons stay for a visitor when their resource is one of `prompted`, those
|
|
342
354
|
* that ask the visitor to sign in. A create link may give values of any name to a resource of `createPages`, whose view
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { matchErrors } from "./view-related.js";
|
|
2
|
+
//#region src/view-record-chart.ts
|
|
3
|
+
const numericTypes = [
|
|
4
|
+
"integer",
|
|
5
|
+
"bigint",
|
|
6
|
+
"decimal"
|
|
7
|
+
];
|
|
8
|
+
const ranges = [
|
|
9
|
+
"1h",
|
|
10
|
+
"24h",
|
|
11
|
+
"7d"
|
|
12
|
+
];
|
|
13
|
+
const atErrors = (model, name) => {
|
|
14
|
+
const field = Object.hasOwn(model.fields, name) ? model.fields[name] : void 0;
|
|
15
|
+
if (!field) return [`at: unknown field "${name}" on ${model.name}`];
|
|
16
|
+
if (field.type !== "timestamp") return [`at: "${name}" on ${model.name} is ${field.type}, not a timestamp`];
|
|
17
|
+
return field.filterable ? [] : [`at: "${name}" on ${model.name} is not filterable`];
|
|
18
|
+
};
|
|
19
|
+
const seriesErrors = (model, name) => {
|
|
20
|
+
const field = Object.hasOwn(model.fields, name) ? model.fields[name] : void 0;
|
|
21
|
+
if (!field) return [`series: unknown field "${name}" on ${model.name}`];
|
|
22
|
+
if (!numericTypes.includes(field.type)) return [`series: "${name}" on ${model.name} is ${field.type}, not a number`];
|
|
23
|
+
return field.sensitive ? [`series: "${name}" on ${model.name} is sensitive`] : [];
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* What is wrong with a chart of related records on the record page of `resource`, against these models; empty when it
|
|
27
|
+
* can be shown. Against the full models it checks the config; against a caller's models, whether that caller can use it.
|
|
28
|
+
*/
|
|
29
|
+
const recordChartErrors = (item, resource, models) => {
|
|
30
|
+
const owner = models[resource];
|
|
31
|
+
const target = models[item.resource];
|
|
32
|
+
if (!owner || !target) return [`unknown resource "${owner ? item.resource : resource}"`];
|
|
33
|
+
if (owner.primaryKey.length !== 1) return [`${resource} has a composite key, so a chart cannot match its records`];
|
|
34
|
+
return [
|
|
35
|
+
...matchErrors(target, item.field, resource),
|
|
36
|
+
...atErrors(target, item.at),
|
|
37
|
+
...item.series.length === 0 ? ["series: name at least one field"] : item.series.flatMap((name) => seriesErrors(target, name)),
|
|
38
|
+
...item.range === void 0 || ranges.includes(item.range) ? [] : [`range must be one of ${ranges.join(", ")}, not "${String(item.range)}"`]
|
|
39
|
+
];
|
|
40
|
+
};
|
|
41
|
+
//#endregion
|
|
42
|
+
export { recordChartErrors };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.3",
|
|
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": {
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@better-auth/passkey": "^1.7.7",
|
|
34
34
|
"@better-auth/sso": "^1.7.7",
|
|
35
|
-
"@protobase/layout": "0.28.
|
|
36
|
-
"@protobase/query": "0.28.
|
|
37
|
-
"@protobase/schema": "0.28.
|
|
35
|
+
"@protobase/layout": "0.28.3",
|
|
36
|
+
"@protobase/query": "0.28.3",
|
|
37
|
+
"@protobase/schema": "0.28.3",
|
|
38
38
|
"@react-email/render": "^2.1.0",
|
|
39
39
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
40
40
|
"@sentry/core": "^11.7.0",
|