@softr/sdk 0.0.25 → 0.0.26

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.
Files changed (40) hide show
  1. package/dist/action.d.ts +195 -0
  2. package/dist/action.js +21 -0
  3. package/dist/auth/isomorphic.d.ts +1 -2
  4. package/dist/auth/permissions.d.ts +7 -2
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +639 -0
  7. package/dist/codegen/datasource-types.js +471 -0
  8. package/dist/codegen/softr-env.js +103 -0
  9. package/dist/codegen/user-groups.js +11 -0
  10. package/dist/core/integration.js +1 -1
  11. package/dist/datasource/chart.d.ts +196 -0
  12. package/dist/datasource/chart.js +289 -0
  13. package/dist/datasource/datasource.d.ts +16 -0
  14. package/dist/datasource/datasource.js +17 -0
  15. package/dist/datasource/fields.d.ts +122 -0
  16. package/dist/datasource/fields.js +82 -0
  17. package/dist/datasource/filter.d.ts +30 -0
  18. package/dist/datasource/filter.js +146 -0
  19. package/dist/datasource/record.d.ts +138 -0
  20. package/dist/datasource/record.js +258 -0
  21. package/dist/index.d.ts +3 -5
  22. package/dist/index.js +2 -2
  23. package/dist/package.js +5 -0
  24. package/dist/routes/visibility.d.ts +1 -1
  25. package/dist/server.d.ts +1 -1
  26. package/dist/server.js +1 -1
  27. package/package.json +2 -1
  28. package/skills/softr-sdk/SKILL.md +6 -2
  29. package/skills/softr-sdk/auth/SKILL.md +1 -1
  30. package/skills/softr-sdk/current-user/SKILL.md +1 -1
  31. package/skills/softr-sdk/data-mutations/SKILL.md +1 -1
  32. package/skills/softr-sdk/integrations/SKILL.md +140 -0
  33. package/skills/softr-sdk/reading-records/SKILL.md +1 -1
  34. package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
  35. package/dist/core/datasource.d.ts +0 -31
  36. package/dist/core/datasource.js +0 -226
  37. package/dist/core/query.d.ts +0 -354
  38. package/dist/core/query.js +0 -528
  39. package/dist/core/utils.js +0 -14
  40. package/dist/types.d.ts +0 -128
@@ -0,0 +1,196 @@
1
+ import { Action, FieldTypeOf, ReadShape, SoftrDatasources } from "../action.js";
2
+ import { ConstructableOrderBy } from "./filter.js";
3
+ import { DateFieldType, NumberFieldType, SortSet } from "./fields.js";
4
+ import { WhereCallback } from "./record.js";
5
+
6
+ //#region src/datasource/chart.d.ts
7
+ declare const dimensionValue: unique symbol;
8
+ declare const measureValue: unique symbol;
9
+ /** A chart column that groups rows. The brand carries the value type the column reads back as. */
10
+ type Dimension<TValue$1> = {
11
+ readonly [dimensionValue]: TValue$1;
12
+ };
13
+ /** A chart column that aggregates rows. The brand carries the value type the column reads back as. */
14
+ type Measure<TValue$1> = {
15
+ readonly [measureValue]: TValue$1;
16
+ };
17
+ type ColumnValue<T> = T extends Dimension<infer TValue> ? TValue : T extends Measure<infer TValue> ? TValue : never;
18
+ type DateBuckets = {
19
+ /** Groups by calendar day. The value reads as `"2025-06-14"`. */
20
+ readonly byDay: Dimension<string | null>;
21
+ /**
22
+ * Groups by calendar week. The value is the ISO date of the week's Monday. Monday-based,
23
+ * while `byDayOfWeek` counts from Sunday: the server disagrees with itself, so the two
24
+ * cannot be lined up by arithmetic alone.
25
+ */
26
+ readonly byWeek: Dimension<string | null>;
27
+ /** Groups by calendar month. The value reads as `"2025-06"`. */
28
+ readonly byMonth: Dimension<string | null>;
29
+ /** Groups by calendar quarter. The value is a display string like `"Q2 2025"`, not a parseable date. */
30
+ readonly byQuarter: Dimension<string | null>;
31
+ /** Groups by calendar year. The value is the year as a number. */
32
+ readonly byYear: Dimension<number | null>;
33
+ /**
34
+ * Groups by day of week, 1 through 7 with 1 as Sunday. Sunday-based, while `byWeek` is
35
+ * Monday-based: the server disagrees with itself, so the two cannot be lined up by
36
+ * arithmetic alone.
37
+ */
38
+ readonly byDayOfWeek: Dimension<number | null>;
39
+ /** Groups by month of year, 1 through 12. */
40
+ readonly byMonthOfYear: Dimension<number | null>;
41
+ /** Groups by quarter of year, 1 through 4. */
42
+ readonly byQuarterOfYear: Dimension<number | null>;
43
+ };
44
+ type DatetimeBuckets = DateBuckets & {
45
+ /** Groups by hour of day, 0 through 23. */
46
+ readonly byHourOfDay: Dimension<number | null>;
47
+ };
48
+ type DateDimension = Dimension<string | null> & DateBuckets;
49
+ type DatetimeDimension = Dimension<string | null> & DatetimeBuckets;
50
+ type NumericMeasures = {
51
+ /** Adds the group's values. `null` when the group has none. */
52
+ readonly sum: Measure<number | null>;
53
+ /** Averages the group's values, rounded to two decimals. `null` when the group has none. */
54
+ readonly avg: Measure<number | null>;
55
+ /** The group's smallest value. `null` when the group has none. */
56
+ readonly min: Measure<number | null>;
57
+ /** The group's largest value. `null` when the group has none. */
58
+ readonly max: Measure<number | null>;
59
+ /** Counts the group's distinct values. */
60
+ readonly distinct: Measure<number>;
61
+ };
62
+ type DateMeasures = {
63
+ /** The group's earliest date, as `"2025-06-14"`. `null` when the group has none. */
64
+ readonly min: Measure<string | null>;
65
+ /** The group's latest date, as `"2025-06-14"`. `null` when the group has none. */
66
+ readonly max: Measure<string | null>;
67
+ /** Counts the group's distinct values. */
68
+ readonly distinct: Measure<number>;
69
+ };
70
+ type AnyDimension = Dimension<unknown> & DatetimeBuckets;
71
+ type AnyMeasures = {
72
+ readonly sum: Measure<number | null>;
73
+ readonly avg: Measure<number | null>;
74
+ readonly min: Measure<unknown>;
75
+ readonly max: Measure<unknown>;
76
+ readonly distinct: Measure<number>;
77
+ };
78
+ /** A field a chart cannot use for this: the missing leaves are the compile error saying so. */
79
+ type Unusable = {
80
+ readonly [dimensionValue]?: never;
81
+ };
82
+ /**
83
+ * The handles a `groupBy` callback is handed: one per alias. The bare handle groups by the
84
+ * field's value; a date field also carries bucket leaves. A multi-value field has no handle
85
+ * body, because exploding it would count each record once per value and inflate every metric.
86
+ */
87
+ type DimensionSet<TShape extends ReadShape> = keyof SoftrDatasources extends never ? Record<string, AnyDimension> : { [Alias in keyof TShape["fields"]]: DimensionFor<FieldTypeOf<TShape["datasource"], TShape["fields"][Alias]>> };
88
+ type DimensionFor<TFieldType> = [TFieldType] extends [never] ? AnyDimension : TFieldType extends `${string}[]` | "ATTACHMENT" | "DATE_RANGE" ? Unusable : TFieldType extends "DATE" ? DateDimension : TFieldType extends DateFieldType ? DatetimeDimension : TFieldType extends NumberFieldType ? Dimension<number | null> : TFieldType extends "CHECKBOX" ? Dimension<boolean | null> : Dimension<string | null>;
89
+ /**
90
+ * The handles a `metrics` callback is handed: aggregation leaves per alias, gated by the
91
+ * field's declared type, plus `$count`. A field whose aggregate would come back as a
92
+ * coerced non-number has no leaves at all.
93
+ */
94
+ type MeasureSet<TShape extends ReadShape> = (keyof SoftrDatasources extends never ? Record<string, AnyMeasures> : { [Alias in keyof TShape["fields"]]: MeasureFor<FieldTypeOf<TShape["datasource"], TShape["fields"][Alias]>> }) & {
95
+ /** Counts the rows in each group. Takes no field: every row counts once. */
96
+ readonly $count: Measure<number>;
97
+ };
98
+ type MeasureFor<TFieldType> = [TFieldType] extends [never] ? AnyMeasures : TFieldType extends `${string}[]` ? Unusable : TFieldType extends NumberFieldType ? NumericMeasures : TFieldType extends DateFieldType ? DateMeasures : Unusable;
99
+ type IsUnion<T, TAll = T> = [T] extends [never] ? false : T extends unknown ? [TAll] extends [T] ? false : true : never;
100
+ type MoreThanTwo<TKeys, TAll = TKeys> = TKeys extends unknown ? IsUnion<Exclude<TAll, TKeys>> : never;
101
+ type GroupByBounds<TGroups> = [keyof TGroups] extends [never] ? {
102
+ __error: "a chart needs at least one column to group by";
103
+ } : true extends MoreThanTwo<keyof TGroups> ? {
104
+ __error: "a chart may group by at most two columns";
105
+ } : unknown;
106
+ type MetricBounds<TMetrics, TGroups> = [Extract<keyof TMetrics, keyof TGroups>] extends [never] ? unknown : {
107
+ __error: "a metric key must not collide with a group key";
108
+ };
109
+ /** One row, keyed by the caller's own `groupBy` and `metrics` keys. */
110
+ type ChartRow<TGroups, TMetrics> = { [K in keyof TGroups | keyof TMetrics]: ColumnValue<(TGroups & TMetrics)[K]> };
111
+ interface ChartResult<TRow> {
112
+ data: Array<TRow>;
113
+ executionTimestamp: Date;
114
+ }
115
+ /** A chart before `groupBy`. It cannot be awaited: an un-grouped chart is not a request. */
116
+ type ChartSeed<TShape extends ReadShape> = {
117
+ /**
118
+ * Names the group columns: one or two entries, each a handle from the callback, keyed by
119
+ * the name the row will carry. A bare handle groups by the field's value; a bucket leaf
120
+ * such as `q.closedAt.byMonth` groups a date field by the bucket.
121
+ *
122
+ * @example
123
+ * ds.chart(sales).groupBy((q) => ({ month: q.closedAt.byMonth, region: q.region }))
124
+ */
125
+ groupBy<TGroups extends Record<string, Dimension<unknown>>>(build: (q: DimensionSet<TShape>) => TGroups & GroupByBounds<TGroups>): ChartBuilder<TShape, TGroups, {}>;
126
+ /**
127
+ * Narrows which records the chart aggregates. Same filters as `ds.list`.
128
+ */
129
+ where(build: WhereCallback<TShape>): ChartSeed<TShape>;
130
+ };
131
+ type ChartBuilder<TShape extends ReadShape, TGroups, TMetrics> = {
132
+ /**
133
+ * Names the aggregated columns, keyed by the name the row will carry. Omit the call for a
134
+ * distinct-values query: the rows then hold the group columns alone.
135
+ *
136
+ * @example
137
+ * ds.chart(sales)
138
+ * .groupBy((q) => ({ region: q.region }))
139
+ * .metrics((q) => ({ revenue: q.amount.sum, orders: q.$count }))
140
+ */
141
+ metrics<TNext extends Record<string, Measure<unknown>>>(build: (q: MeasureSet<TShape>) => TNext & MetricBounds<TNext, TGroups>): ChartBuilder<TShape, TGroups, TNext>;
142
+ /**
143
+ * Narrows which records the chart aggregates. Same filters as `ds.list`.
144
+ */
145
+ where(build: WhereCallback<TShape>): ChartBuilder<TShape, TGroups, TMetrics>;
146
+ /**
147
+ * Sorts the rows by the chart's own keys. Call it last: it sees the keys declared so far,
148
+ * so placed before `metrics` a metric key is not yet a column. Without a sort the rows come
149
+ * back by the first bucket ascending, else by the first metric descending.
150
+ *
151
+ * @example
152
+ * ds.chart(sales)
153
+ * .groupBy((q) => ({ month: q.closedAt.byMonth }))
154
+ * .metrics((q) => ({ revenue: q.amount.sum }))
155
+ * .orderBy((q) => [q.month.asc, q.revenue.desc])
156
+ */
157
+ orderBy(build: (q: SortSet<ChartRow<TGroups, TMetrics>>) => ConstructableOrderBy | Array<ConstructableOrderBy>): ChartBuilder<TShape, TGroups, TMetrics>;
158
+ } & Promise<ChartResult<ChartRow<TGroups, TMetrics>>>;
159
+ interface MetricResult<TValue$1> {
160
+ value: TValue$1;
161
+ executionTimestamp: Date;
162
+ }
163
+ type MetricBuilder<TShape extends ReadShape, TValue$1> = {
164
+ /**
165
+ * Narrows which records the metric aggregates. Same filters as `ds.list`.
166
+ */
167
+ where(build: WhereCallback<TShape>): MetricBuilder<TShape, TValue$1>;
168
+ } & Promise<MetricResult<TValue$1>>;
169
+ /**
170
+ * Aggregates records into chart rows: one row per group, keyed by the caller's own names.
171
+ *
172
+ * `groupBy` is required and takes one or two columns. `metrics` is optional; without it the
173
+ * rows hold the distinct group values. `orderBy` comes last, over the chart's own keys.
174
+ *
175
+ * @example
176
+ * const { data } = await ds
177
+ * .chart(sales)
178
+ * .groupBy((q) => ({ month: q.closedAt.byMonth }))
179
+ * .metrics((q) => ({ revenue: q.amount.sum, orders: q.$count }))
180
+ * .where((q) => q.status.is("paid"))
181
+ * .orderBy((q) => q.month.asc)
182
+ * // data: Array<{ month: string | null; revenue: number | null; orders: number }>
183
+ */
184
+ declare function chart<TShape extends ReadShape>(query: Action<TShape>): ChartSeed<TShape>;
185
+ /**
186
+ * Aggregates records into one value, with no grouping.
187
+ *
188
+ * @example
189
+ * const { value } = await ds
190
+ * .metric(sales, (q) => q.amount.sum)
191
+ * .where((q) => q.status.is("paid"))
192
+ * // value: number | null
193
+ */
194
+ declare function metric<TShape extends ReadShape, TMeasure extends Measure<unknown>>(query: Action<TShape>, pick: (q: MeasureSet<TShape>) => TMeasure): MetricBuilder<TShape, ColumnValue<TMeasure>>;
195
+ //#endregion
196
+ export { chart, metric };
@@ -0,0 +1,289 @@
1
+ import { assert, panic } from "../assertions.js";
2
+ import { readUserTimezone } from "../cookies.js";
3
+ import { readSessionToken } from "../auth/session.js";
4
+ import { INTERNAL_KIND, buildFilterCriteria, buildOrderBy, dealiasFilterCriteria } from "./filter.js";
5
+ import { andAll, makeFieldSet, makeSortSet } from "./fields.js";
6
+ import { dsUrl, resolveTarget } from "./record.js";
7
+
8
+ //#region src/datasource/chart.ts
9
+ const COUNT_CNT = "COUNT_CNT";
10
+ function makeDimensionSet(select) {
11
+ const set = {};
12
+ for (const [alias, fieldId] of Object.entries(select)) set[alias] = Object.assign(dimension(fieldId), {
13
+ byDay: dimension(fieldId, {
14
+ timeInterval: "DAY",
15
+ displayFormat: "YYYY-MM-DD"
16
+ }),
17
+ byWeek: dimension(fieldId, { timeInterval: "WEEK" }),
18
+ byMonth: dimension(fieldId, {
19
+ timeInterval: "MONTH",
20
+ displayFormat: "YYYY-MM"
21
+ }),
22
+ byQuarter: dimension(fieldId, { timeInterval: "QUARTER" }),
23
+ byYear: dimension(fieldId, { timeInterval: "YEAR" }),
24
+ byHourOfDay: dimension(fieldId, { timeInterval: "HOUR_OF_DAY" }),
25
+ byDayOfWeek: dimension(fieldId, { timeInterval: "DAY_OF_WEEK" }),
26
+ byMonthOfYear: dimension(fieldId, { timeInterval: "MONTH_OF_YEAR" }),
27
+ byQuarterOfYear: dimension(fieldId, { timeInterval: "QUARTER_OF_YEAR" })
28
+ });
29
+ return set;
30
+ }
31
+ function dimension(fieldId, options) {
32
+ return options ? {
33
+ fieldId,
34
+ options,
35
+ [INTERNAL_KIND]: "DIMENSION"
36
+ } : {
37
+ fieldId,
38
+ [INTERNAL_KIND]: "DIMENSION"
39
+ };
40
+ }
41
+ function makeMeasureSet(select) {
42
+ const set = {};
43
+ for (const [alias, fieldId] of Object.entries(select)) set[alias] = {
44
+ sum: measure("SUM", fieldId),
45
+ avg: measure("AVG", fieldId),
46
+ min: measure("MIN", fieldId),
47
+ max: measure("MAX", fieldId),
48
+ distinct: measure("DISTINCT", fieldId)
49
+ };
50
+ set["$count"] = {
51
+ aggregation: "COUNT",
52
+ fieldId: null,
53
+ [INTERNAL_KIND]: "MEASURE"
54
+ };
55
+ return set;
56
+ }
57
+ function measure(aggregation, fieldId) {
58
+ return {
59
+ aggregation,
60
+ fieldId,
61
+ [INTERNAL_KIND]: "MEASURE"
62
+ };
63
+ }
64
+ function isHandle(value, kind) {
65
+ return typeof value === "object" && value !== null && value[INTERNAL_KIND] === kind;
66
+ }
67
+ /**
68
+ * Aggregates records into chart rows: one row per group, keyed by the caller's own names.
69
+ *
70
+ * `groupBy` is required and takes one or two columns. `metrics` is optional; without it the
71
+ * rows hold the distinct group values. `orderBy` comes last, over the chart's own keys.
72
+ *
73
+ * @example
74
+ * const { data } = await ds
75
+ * .chart(sales)
76
+ * .groupBy((q) => ({ month: q.closedAt.byMonth }))
77
+ * .metrics((q) => ({ revenue: q.amount.sum, orders: q.$count }))
78
+ * .where((q) => q.status.is("paid"))
79
+ * .orderBy((q) => q.month.asc)
80
+ * // data: Array<{ month: string | null; revenue: number | null; orders: number }>
81
+ */
82
+ function chart(query) {
83
+ const { select } = resolveTarget(query, "read");
84
+ const aliases = Object.keys(select);
85
+ const state = {
86
+ filters: [],
87
+ orderBys: []
88
+ };
89
+ let result;
90
+ const run = () => result ??= executeChart(query, state);
91
+ const builder = {
92
+ [Symbol.toStringTag]: "ChartQuery",
93
+ groupBy(build) {
94
+ assert(!state.groups, "groupBy may only be called once");
95
+ const entries = Object.entries(build(makeDimensionSet(select)));
96
+ for (const [, handle] of entries) assert(isHandle(handle, "DIMENSION"), "a chart column must come from the callback a chart hands you");
97
+ assert(entries.length <= 2, "a chart may group by at most two columns");
98
+ const groups = entries;
99
+ assert(groups.length < 2 || groups[0][1].fieldId !== groups[1][1].fieldId, "a chart may group by a field only once");
100
+ state.groups = groups;
101
+ return builder;
102
+ },
103
+ metrics(build) {
104
+ assert(!state.metrics, "metrics may only be called once");
105
+ const entries = Object.entries(build(makeMeasureSet(select)));
106
+ for (const [, handle] of entries) assert(isHandle(handle, "MEASURE"), "a chart column must come from the callback a chart hands you");
107
+ const metrics = entries;
108
+ const seen = /* @__PURE__ */ new Set();
109
+ for (const [, handle] of metrics) {
110
+ const pair = `${handle.aggregation} ${handle.fieldId ?? ""}`;
111
+ assert(!seen.has(pair), "two metrics ask for the same value");
112
+ seen.add(pair);
113
+ }
114
+ state.metrics = metrics;
115
+ return builder;
116
+ },
117
+ where(build) {
118
+ state.filters.push(build(makeFieldSet(aliases)));
119
+ return builder;
120
+ },
121
+ orderBy(build) {
122
+ const sorting = build(makeSortSet([...state.groups ?? [], ...state.metrics ?? []].map(([key]) => key)));
123
+ state.orderBys.push(...buildOrderBy(Array.isArray(sorting) ? sorting : [sorting]));
124
+ return builder;
125
+ },
126
+ then(onfulfilled, onrejected) {
127
+ return run().then(onfulfilled, onrejected);
128
+ },
129
+ catch(onrejected) {
130
+ return run().catch(onrejected);
131
+ },
132
+ finally(onfinally) {
133
+ return run().finally(onfinally);
134
+ }
135
+ };
136
+ return builder;
137
+ }
138
+ function buildChartRequest(state, select) {
139
+ const groups = state.groups;
140
+ assert(groups && groups.length > 0, "a chart needs at least one column to group by");
141
+ const metrics = state.metrics ?? [];
142
+ const grouped = new Set(groups.map(([, handle]) => handle.fieldId));
143
+ for (const [, handle] of metrics) assert(handle.fieldId === null || !grouped.has(handle.fieldId), "a field cannot be grouped and aggregated in one chart");
144
+ const suffixed = metrics.length > 1;
145
+ const columns = [...groups.map(([key, handle]) => ({
146
+ key,
147
+ responseKey: handle.fieldId
148
+ })), ...metrics.map(([key, handle]) => ({
149
+ key,
150
+ responseKey: handle.aggregation === "COUNT" && !handle.fieldId ? COUNT_CNT : `${handle.fieldId}${suffixed ? `_${handle.aggregation.toLowerCase()}` : ""}`
151
+ }))];
152
+ let orderBys = state.orderBys;
153
+ if (orderBys.length === 0) {
154
+ const bucketed = groups.find(([, handle]) => handle.options !== void 0);
155
+ const [key, sortType] = bucketed ? [bucketed[0], "ASC"] : metrics.length > 0 ? [metrics[0][0], "DESC"] : [groups[0][0], "ASC"];
156
+ orderBys = [{
157
+ sortingField: key,
158
+ sortType
159
+ }];
160
+ }
161
+ const responseKeyOf = new Map(columns.map((column) => [column.key, column.responseKey]));
162
+ const sortingOptions = orderBys.map((orderBy) => ({
163
+ sortingField: responseKeyOf.get(orderBy.sortingField) ?? panic(() => `"${orderBy.sortingField}" is not a column of this chart`),
164
+ sortType: orderBy.sortType
165
+ }));
166
+ return {
167
+ body: {
168
+ filterCriteria: toFilterCriteria(state.filters, select),
169
+ sortingOptions,
170
+ groupBy: groups.map(([, handle]) => handle.options ? {
171
+ field: handle.fieldId,
172
+ options: handle.options
173
+ } : { field: handle.fieldId }),
174
+ metrics: metrics.map(([, handle]) => ({
175
+ field: handle.fieldId ?? "",
176
+ aggregation: handle.aggregation
177
+ }))
178
+ },
179
+ columns
180
+ };
181
+ }
182
+ function mapChartRows(columns, data) {
183
+ return data.map((item) => {
184
+ const row = {};
185
+ for (const { key, responseKey } of columns) {
186
+ assert(responseKey in item, () => `the response has no column for "${key}"`);
187
+ row[key] = item[responseKey];
188
+ }
189
+ return row;
190
+ });
191
+ }
192
+ async function executeChart(query, state) {
193
+ const { datasourceId, select } = resolveTarget(query, "read");
194
+ const { body, columns } = buildChartRequest(state, select);
195
+ const res = await fetch(`${dsUrl(datasourceId)}/chart/data`, {
196
+ method: "POST",
197
+ headers: {
198
+ "Content-Type": "application/json",
199
+ "X-User-Timezone": readUserTimezone() ?? "UTC",
200
+ Authorization: `Bearer ${readSessionToken()}`
201
+ },
202
+ body: JSON.stringify(body)
203
+ });
204
+ if (!res.ok) throw new Error(`Softr datasource chart failed for "${datasourceId}": ${res.status}`);
205
+ const payload = await res.json();
206
+ return {
207
+ data: mapChartRows(columns, payload.data),
208
+ executionTimestamp: /* @__PURE__ */ new Date(payload.executionTimestamp * 1e3)
209
+ };
210
+ }
211
+ /**
212
+ * Aggregates records into one value, with no grouping.
213
+ *
214
+ * @example
215
+ * const { value } = await ds
216
+ * .metric(sales, (q) => q.amount.sum)
217
+ * .where((q) => q.status.is("paid"))
218
+ * // value: number | null
219
+ */
220
+ function metric(query, pick) {
221
+ const { select } = resolveTarget(query, "read");
222
+ const aliases = Object.keys(select);
223
+ const picked = pick(makeMeasureSet(select));
224
+ assert(isHandle(picked, "MEASURE"), "a metric must come from the callback a metric hands you");
225
+ const handle = picked;
226
+ const filters = [];
227
+ let result;
228
+ const run = () => result ??= executeMetric(query, handle, filters);
229
+ const builder = {
230
+ [Symbol.toStringTag]: "MetricQuery",
231
+ where(build) {
232
+ filters.push(build(makeFieldSet(aliases)));
233
+ return builder;
234
+ },
235
+ then(onfulfilled, onrejected) {
236
+ return run().then(onfulfilled, onrejected);
237
+ },
238
+ catch(onrejected) {
239
+ return run().catch(onrejected);
240
+ },
241
+ finally(onfinally) {
242
+ return run().finally(onfinally);
243
+ }
244
+ };
245
+ return builder;
246
+ }
247
+ function buildMetricRequest(handle, filters, select) {
248
+ return {
249
+ body: {
250
+ groupBy: [],
251
+ metrics: [{
252
+ field: handle.fieldId ?? void 0,
253
+ aggregation: handle.aggregation
254
+ }],
255
+ filterCriteria: toFilterCriteria(filters, select)
256
+ },
257
+ responseKey: handle.aggregation === "COUNT" && !handle.fieldId ? COUNT_CNT : handle.fieldId
258
+ };
259
+ }
260
+ function readMetricValue(responseKey, data) {
261
+ const row = data.find((item) => responseKey in item);
262
+ assert(row, "the response has no value for the metric");
263
+ return row[responseKey];
264
+ }
265
+ async function executeMetric(query, handle, filters) {
266
+ const { datasourceId, select } = resolveTarget(query, "read");
267
+ const { body, responseKey } = buildMetricRequest(handle, filters, select);
268
+ const res = await fetch(`${dsUrl(datasourceId)}/chart/data`, {
269
+ method: "POST",
270
+ headers: {
271
+ "Content-Type": "application/json",
272
+ "X-User-Timezone": readUserTimezone() ?? "UTC",
273
+ Authorization: `Bearer ${readSessionToken()}`
274
+ },
275
+ body: JSON.stringify(body)
276
+ });
277
+ if (!res.ok) throw new Error(`Softr datasource metric failed for "${datasourceId}": ${res.status}`);
278
+ const payload = await res.json();
279
+ return {
280
+ value: readMetricValue(responseKey, payload.data),
281
+ executionTimestamp: /* @__PURE__ */ new Date(payload.executionTimestamp * 1e3)
282
+ };
283
+ }
284
+ function toFilterCriteria(filters, select) {
285
+ return filters.length ? dealiasFilterCriteria(buildFilterCriteria(andAll(filters)), select) : {};
286
+ }
287
+
288
+ //#endregion
289
+ export { chart, metric };
@@ -0,0 +1,16 @@
1
+ import { createRecord, deleteRecord, getRecord, listRecords, updateRecord } from "./record.js";
2
+ import { chart, metric } from "./chart.js";
3
+ import "@tanstack/react-start/server-only";
4
+
5
+ //#region src/datasource/datasource.d.ts
6
+ declare const ds: {
7
+ list: typeof listRecords;
8
+ get: typeof getRecord;
9
+ create: typeof createRecord;
10
+ update: typeof updateRecord;
11
+ delete: typeof deleteRecord;
12
+ metric: typeof metric;
13
+ chart: typeof chart;
14
+ };
15
+ //#endregion
16
+ export { ds };
@@ -0,0 +1,17 @@
1
+ import { createRecord, deleteRecord, getRecord, listRecords, updateRecord } from "./record.js";
2
+ import { chart, metric } from "./chart.js";
3
+ import "@tanstack/react-start/server-only";
4
+
5
+ //#region src/datasource/datasource.ts
6
+ const ds = {
7
+ list: listRecords,
8
+ get: getRecord,
9
+ create: createRecord,
10
+ update: updateRecord,
11
+ delete: deleteRecord,
12
+ metric,
13
+ chart
14
+ };
15
+
16
+ //#endregion
17
+ export { ds };
@@ -0,0 +1,122 @@
1
+ import { FieldTypeOf, ReadOf, ReadShape, SoftrDatasources } from "../action.js";
2
+ import { AnyConstructable, ConstructableFilter, ConstructableFilterGroup, ConstructableOrderBy } from "./filter.js";
3
+
4
+ //#region src/datasource/fields.d.ts
5
+ type EmptyChain = {
6
+ isEmpty(): ConstructableFilter;
7
+ isNotEmpty(): ConstructableFilter;
8
+ };
9
+ type BooleanChain = EmptyChain & {
10
+ is(value: boolean): ConstructableFilter;
11
+ isNot(value: boolean): ConstructableFilter;
12
+ };
13
+ type NumberChain = EmptyChain & {
14
+ is(value: number): ConstructableFilter;
15
+ isNot(value: number): ConstructableFilter;
16
+ gt(value: number): ConstructableFilter;
17
+ gte(value: number): ConstructableFilter;
18
+ lt(value: number): ConstructableFilter;
19
+ lte(value: number): ConstructableFilter;
20
+ between(start: number, end: number): ConstructableFilter;
21
+ };
22
+ type ArrayChain<TMember$1 extends string> = EmptyChain & {
23
+ is(values: Array<TMember$1>): ConstructableFilter;
24
+ isOneOf(values: Array<TMember$1>): ConstructableFilter;
25
+ isNoneOf(values: Array<TMember$1>): ConstructableFilter;
26
+ hasAllOf(values: Array<TMember$1>): ConstructableFilter;
27
+ };
28
+ type ChoiceChain<TId$1 extends string> = EmptyChain & {
29
+ is(value: TId$1): ConstructableFilter;
30
+ isNot(value: TId$1): ConstructableFilter;
31
+ isOneOf(values: Array<TId$1>): ConstructableFilter;
32
+ isNoneOf(values: Array<TId$1>): ConstructableFilter;
33
+ };
34
+ /** What DSv2 filters generically: matched whole or scanned, never against a set. */
35
+ type GenericChain = EmptyChain & {
36
+ is(value: string): ConstructableFilter;
37
+ isNot(value: string): ConstructableFilter;
38
+ contains(value: string): ConstructableFilter;
39
+ };
40
+ type TextChain<TValue extends string> = EmptyChain & {
41
+ is(value: TValue): ConstructableFilter;
42
+ isNot(value: TValue): ConstructableFilter;
43
+ contains(value: string): ConstructableFilter;
44
+ startsWith(value: string): ConstructableFilter;
45
+ endsWith(value: string): ConstructableFilter;
46
+ isOneOf(values: Array<string>): ConstructableFilter;
47
+ isNoneOf(values: Array<string>): ConstructableFilter;
48
+ };
49
+ type DateChain = EmptyChain & {
50
+ is(value: string): ConstructableFilter;
51
+ isNot(value: string): ConstructableFilter;
52
+ gt(value: string): ConstructableFilter;
53
+ gte(value: string): ConstructableFilter;
54
+ lt(value: string): ConstructableFilter;
55
+ lte(value: string): ConstructableFilter;
56
+ between(start: string, end: string): ConstructableFilter;
57
+ isNotBetween(start: string, end: string): ConstructableFilter;
58
+ };
59
+ /** Every operator, for a field whose type nothing has generated yet. */
60
+ type AnyChain = EmptyChain & {
61
+ is(value: unknown): ConstructableFilter;
62
+ isNot(value: unknown): ConstructableFilter;
63
+ contains(value: string): ConstructableFilter;
64
+ startsWith(value: string): ConstructableFilter;
65
+ endsWith(value: string): ConstructableFilter;
66
+ isOneOf(values: Array<string>): ConstructableFilter;
67
+ isNoneOf(values: Array<string>): ConstructableFilter;
68
+ hasAllOf(values: Array<string>): ConstructableFilter;
69
+ gt(value: number | string): ConstructableFilter;
70
+ gte(value: number | string): ConstructableFilter;
71
+ lt(value: number | string): ConstructableFilter;
72
+ lte(value: number | string): ConstructableFilter;
73
+ between(start: number | string, end: number | string): ConstructableFilter;
74
+ isNotBetween(start: number | string, end: number | string): ConstructableFilter;
75
+ };
76
+ type IdOf<TValue> = TValue extends {
77
+ id: infer TId extends string;
78
+ } ? TId : string;
79
+ /**
80
+ * What a choice-shaped field compares against: its own ids where codegen read the choice
81
+ * list, and any string where it did not. A linked record is the open case, and it is the
82
+ * only thing distinguishing one from a select at the type level.
83
+ */
84
+ type ChoiceIds<TValue> = string extends IdOf<TValue> ? string : IdOf<TValue>;
85
+ type TextFieldType = 'SINGLE_LINE_TEXT' | 'LONG_TEXT' | 'URL' | 'EMAIL' | 'PHONE' | 'ADDRESS' | 'RECORD_ID' | 'LOCATION' | 'TIME' | 'BUTTON';
86
+ type NumberFieldType = 'NUMBER' | 'PERCENT' | 'CURRENCY' | 'PROGRESS' | 'RATING' | 'DURATION' | 'COUNT' | 'AUTONUMBER';
87
+ type DateFieldType = 'DATE' | 'DATETIME' | 'CREATED_AT' | 'UPDATED_AT';
88
+ type ChoiceFieldType = 'SELECT' | 'LINKED_RECORD' | 'CREATED_BY' | 'UPDATED_BY';
89
+ /** The types with no single value to compare: only whether the field holds one. */
90
+ type EmptyFieldType = 'ATTACHMENT' | 'DATE_RANGE';
91
+ type ElementOf<TValue> = TValue extends Array<infer TMember> ? TMember : TValue;
92
+ /**
93
+ * The operators one alias offers, chosen from the type codegen declared for the field rather
94
+ * than from what it reads as: a `DATETIME` and a text field both read as `string`, and only
95
+ * the declared type tells them apart.
96
+ *
97
+ * `TValue` is still needed for the literal choice ids, which live on the read type and
98
+ * nowhere else. A field with no declared type lands on the permissive chain rather than on
99
+ * none.
100
+ */
101
+ type ChainFor<TFieldType, TValue> = [TFieldType] extends [never] ? AnyChain : TFieldType extends `${infer TBase}[]` ? TBase extends EmptyFieldType ? EmptyChain : ArrayChain<ChoiceIds<ElementOf<TValue>>> : TFieldType extends TextFieldType ? TextChain<TValue & string> : TFieldType extends NumberFieldType ? NumberChain : TFieldType extends 'USER' ? GenericChain : TFieldType extends 'CHECKBOX' ? BooleanChain : TFieldType extends DateFieldType ? DateChain : TFieldType extends ChoiceFieldType ? ChoiceChain<ChoiceIds<TValue>> : TFieldType extends EmptyFieldType ? EmptyChain : AnyChain;
102
+ type Combinators = {
103
+ $and(...members: Array<AnyConstructable>): ConstructableFilterGroup;
104
+ $or(...members: Array<AnyConstructable>): ConstructableFilterGroup;
105
+ };
106
+ /**
107
+ * What a `where` callback is handed: every alias the action declares, already a chain of the
108
+ * operators that alias accepts, plus `$and` and `$or` to nest them.
109
+ *
110
+ * Keyed off the action's own `fields` map, so an alias resolves to a field id and from there
111
+ * to the declared type. With nothing generated there is no field id to resolve, and an alias
112
+ * the action never declared has to resolve too, so that case is open by name and permissive
113
+ * by operator.
114
+ */
115
+ type FieldSet<TShape extends ReadShape> = keyof SoftrDatasources extends never ? Record<string, AnyChain> & Combinators : { [Alias in keyof TShape['fields']]: ChainFor<FieldTypeOf<TShape['datasource'], TShape['fields'][Alias]>, NonNullable<ReadOf<TShape['datasource']>[TShape['fields'][Alias] & keyof ReadOf<TShape['datasource']>]>> } & Combinators;
116
+ /** What an `orderBy` callback is handed: two handles per alias and nothing else. */
117
+ type SortSet<TRecord> = { [K in keyof TRecord]: {
118
+ readonly asc: ConstructableOrderBy;
119
+ readonly desc: ConstructableOrderBy;
120
+ } };
121
+ //#endregion
122
+ export { DateFieldType, FieldSet, NumberFieldType, SortSet };