@softr/sdk 0.0.7 → 0.0.11

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/index.js CHANGED
@@ -1,7 +1,534 @@
1
- import { a as q } from "./query-C6022JhD.js";
1
+ import { a as readSessionToken, c as writeCookies, i as readSessionData, l as assert, n as makeSessionDataCookie, o as fetchVisitor, t as clearSession } from "./session-GUEI96C8.js";
2
2
  import { redirect } from "@tanstack/react-router";
3
3
  import { createMiddleware, createServerFn, getGlobalStartContext } from "@tanstack/react-start";
4
4
 
5
+ //#region src/core/query.ts
6
+ const INTERNAL_KIND = Symbol("KIND");
7
+ /**
8
+ * Query builder for filter queries. This exist to alleviate the (LLM?) mental overhead. API filter schema is a bit
9
+ * verbose and confusing, even when dealing with very basic queries. So to fall into the pit of success, aim here
10
+ * is to simplify filtering as much as possible.
11
+ *
12
+ * @example
13
+ * single filter
14
+ * ```tsx
15
+ * const query = q.text("title").contains("hello");
16
+ *
17
+ * const json = buildFilterCriteria(query);
18
+ * // result
19
+ * {
20
+ * "filterCriteria": {
21
+ * "filterConditionGroup": {
22
+ * "logicalOperator": "AND"
23
+ * "filterGroups": [{
24
+ * "logicalOperator": "AND",
25
+ * "filters": [{
26
+ * "subject": "title",
27
+ * "type": "TEXT",
28
+ * "operator": "CONTAINS",
29
+ * "value": "hello"
30
+ * }],
31
+ * }]
32
+ * }
33
+ * }
34
+ * }
35
+ *
36
+ * ```
37
+ *
38
+ * @example
39
+ * combining multiple filters
40
+ * ```tsx
41
+ * const query = q.and(
42
+ * q.text("title").contains("hello"),
43
+ * q.number("views").is(100),
44
+ * q.or(
45
+ * q.text("author").is("John"),
46
+ * q.boolean("isPublished").is(true)
47
+ * )
48
+ * );
49
+ * ```
50
+ */
51
+ const q = {
52
+ select(mapping) {
53
+ return mapping;
54
+ },
55
+ and(...filters) {
56
+ return {
57
+ logicalOperator: "AND",
58
+ filters,
59
+ [INTERNAL_KIND]: "LOGICAL_GROUP"
60
+ };
61
+ },
62
+ or(...filters) {
63
+ return {
64
+ logicalOperator: "OR",
65
+ filters,
66
+ [INTERNAL_KIND]: "LOGICAL_GROUP"
67
+ };
68
+ },
69
+ text(subject) {
70
+ const filter = {
71
+ subject,
72
+ type: "TEXT",
73
+ [INTERNAL_KIND]: "FILTER"
74
+ };
75
+ return {
76
+ is(value) {
77
+ return {
78
+ ...filter,
79
+ operator: "IS",
80
+ value
81
+ };
82
+ },
83
+ isNot(value) {
84
+ return {
85
+ ...filter,
86
+ operator: "IS_NOT",
87
+ value
88
+ };
89
+ },
90
+ contains(value) {
91
+ return {
92
+ ...filter,
93
+ operator: "CONTAINS",
94
+ value
95
+ };
96
+ },
97
+ isOneOf(value) {
98
+ return {
99
+ ...filter,
100
+ operator: "IS_ONE_OF",
101
+ value
102
+ };
103
+ },
104
+ isNoneOf(value) {
105
+ return {
106
+ ...filter,
107
+ operator: "IS_NONE_OF",
108
+ value
109
+ };
110
+ },
111
+ startsWith(value) {
112
+ return {
113
+ ...filter,
114
+ operator: "STARTS_WITH",
115
+ value
116
+ };
117
+ },
118
+ endsWith(value) {
119
+ return {
120
+ ...filter,
121
+ operator: "ENDS_WITH",
122
+ value
123
+ };
124
+ },
125
+ hasAllOf(value) {
126
+ return {
127
+ ...filter,
128
+ operator: "HAS_ALL_OF",
129
+ value
130
+ };
131
+ },
132
+ isEmpty() {
133
+ return {
134
+ ...filter,
135
+ operator: "IS_EMPTY"
136
+ };
137
+ },
138
+ isNotEmpty() {
139
+ return {
140
+ ...filter,
141
+ operator: "IS_NOT_EMPTY"
142
+ };
143
+ }
144
+ };
145
+ },
146
+ boolean(subject) {
147
+ const filter = {
148
+ subject,
149
+ type: "BOOLEAN",
150
+ [INTERNAL_KIND]: "FILTER"
151
+ };
152
+ return {
153
+ is(value) {
154
+ return {
155
+ ...filter,
156
+ operator: "IS",
157
+ value
158
+ };
159
+ },
160
+ isNot(value) {
161
+ return {
162
+ ...filter,
163
+ operator: "IS_NOT",
164
+ value
165
+ };
166
+ },
167
+ isEmpty() {
168
+ return {
169
+ ...filter,
170
+ operator: "IS_EMPTY"
171
+ };
172
+ },
173
+ isNotEmpty() {
174
+ return {
175
+ ...filter,
176
+ operator: "IS_NOT_EMPTY"
177
+ };
178
+ }
179
+ };
180
+ },
181
+ number(subject) {
182
+ const filter = {
183
+ subject,
184
+ type: "NUMERIC",
185
+ [INTERNAL_KIND]: "FILTER"
186
+ };
187
+ return {
188
+ is(value) {
189
+ return {
190
+ ...filter,
191
+ operator: "IS",
192
+ value: value.toString()
193
+ };
194
+ },
195
+ isNot(value) {
196
+ return {
197
+ ...filter,
198
+ operator: "IS_NOT",
199
+ value: value.toString()
200
+ };
201
+ },
202
+ gt(value) {
203
+ return {
204
+ ...filter,
205
+ operator: "GT",
206
+ value: value.toString()
207
+ };
208
+ },
209
+ gte(value) {
210
+ return {
211
+ ...filter,
212
+ operator: "GTE",
213
+ value: value.toString()
214
+ };
215
+ },
216
+ lt(value) {
217
+ return {
218
+ ...filter,
219
+ operator: "LT",
220
+ value: value.toString()
221
+ };
222
+ },
223
+ lte(value) {
224
+ return {
225
+ ...filter,
226
+ operator: "LTE",
227
+ value: value.toString()
228
+ };
229
+ },
230
+ between(start, end) {
231
+ return {
232
+ ...filter,
233
+ operator: "BETWEEN",
234
+ value: [start.toString(), end.toString()]
235
+ };
236
+ },
237
+ isEmpty() {
238
+ return {
239
+ ...filter,
240
+ operator: "IS_EMPTY"
241
+ };
242
+ },
243
+ isNotEmpty() {
244
+ return {
245
+ ...filter,
246
+ operator: "IS_NOT_EMPTY"
247
+ };
248
+ }
249
+ };
250
+ },
251
+ array(subject) {
252
+ const filter = {
253
+ subject,
254
+ type: "ARRAY",
255
+ [INTERNAL_KIND]: "FILTER"
256
+ };
257
+ return {
258
+ is(value) {
259
+ return {
260
+ ...filter,
261
+ operator: "IS",
262
+ value
263
+ };
264
+ },
265
+ isOneOf(value) {
266
+ return {
267
+ ...filter,
268
+ operator: "IS_ONE_OF",
269
+ value
270
+ };
271
+ },
272
+ isNoneOf(value) {
273
+ return {
274
+ ...filter,
275
+ operator: "IS_NONE_OF",
276
+ value
277
+ };
278
+ },
279
+ hasAllOf(value) {
280
+ return {
281
+ ...filter,
282
+ operator: "HAS_ALL_OF",
283
+ value
284
+ };
285
+ },
286
+ isEmpty() {
287
+ return {
288
+ ...filter,
289
+ operator: "IS_EMPTY"
290
+ };
291
+ },
292
+ isNotEmpty() {
293
+ return {
294
+ ...filter,
295
+ operator: "IS_NOT_EMPTY"
296
+ };
297
+ }
298
+ };
299
+ },
300
+ date(subject) {
301
+ const filter = {
302
+ subject,
303
+ type: "DATE",
304
+ [INTERNAL_KIND]: "FILTER"
305
+ };
306
+ return {
307
+ is(value) {
308
+ return {
309
+ ...filter,
310
+ operator: "IS",
311
+ value
312
+ };
313
+ },
314
+ isNot(value) {
315
+ return {
316
+ ...filter,
317
+ operator: "IS_NOT",
318
+ value
319
+ };
320
+ },
321
+ gt(value) {
322
+ return {
323
+ ...filter,
324
+ operator: "GT",
325
+ value
326
+ };
327
+ },
328
+ gte(value) {
329
+ return {
330
+ ...filter,
331
+ operator: "GTE",
332
+ value
333
+ };
334
+ },
335
+ lt(value) {
336
+ return {
337
+ ...filter,
338
+ operator: "LT",
339
+ value
340
+ };
341
+ },
342
+ lte(value) {
343
+ return {
344
+ ...filter,
345
+ operator: "LTE",
346
+ value
347
+ };
348
+ },
349
+ between(start, end) {
350
+ return {
351
+ ...filter,
352
+ operator: "BETWEEN",
353
+ value: [start, end]
354
+ };
355
+ },
356
+ isNotBetween(start, end) {
357
+ return {
358
+ ...filter,
359
+ operator: "IS_NOT_BETWEEN",
360
+ value: [start, end]
361
+ };
362
+ },
363
+ isEmpty() {
364
+ return {
365
+ ...filter,
366
+ operator: "IS_EMPTY"
367
+ };
368
+ },
369
+ isNotEmpty() {
370
+ return {
371
+ ...filter,
372
+ operator: "IS_NOT_EMPTY"
373
+ };
374
+ }
375
+ };
376
+ },
377
+ asc(subject) {
378
+ return {
379
+ sortingField: subject,
380
+ sortType: "ASC",
381
+ [INTERNAL_KIND]: "ORDER_BY"
382
+ };
383
+ },
384
+ desc(subject) {
385
+ return {
386
+ sortingField: subject,
387
+ sortType: "DESC",
388
+ [INTERNAL_KIND]: "ORDER_BY"
389
+ };
390
+ }
391
+ };
392
+ /**
393
+ * Converts filter utility conditions to a format our backend supports
394
+ *
395
+ * @example
396
+ * ```tsx
397
+ * const query = q.text("title").contains("hello");
398
+ *
399
+ * const json = buildFilterCriteria(query);
400
+ * // result
401
+ * {
402
+ * "filterCriteria": {
403
+ * "filterConditionGroup": {
404
+ * "logicalOperator": "AND"
405
+ * "filterGroups": [{
406
+ * "logicalOperator": "AND",
407
+ * "filters": [{
408
+ * "subject": "title",
409
+ * "type": "TEXT",
410
+ * "operator": "CONTAINS",
411
+ * "value": "hello"
412
+ * }],
413
+ * }]
414
+ * }
415
+ * }
416
+ * }
417
+ *
418
+ * ```
419
+ */
420
+ function buildFilterCriteria(input) {
421
+ if (!(INTERNAL_KIND in input)) throw new Error("Filters cannot be constructed manually. Use the 'q' helper.");
422
+ if (isFilter(input)) return { filterConditionGroup: {
423
+ logicalOperator: "AND",
424
+ filterGroups: [{
425
+ logicalOperator: "AND",
426
+ filters: [stripInternalKind(input)]
427
+ }]
428
+ } };
429
+ if (isFilterGroup(input)) {
430
+ if (input.filters.length === 0) return { filterConditionGroup: {
431
+ logicalOperator: input.logicalOperator,
432
+ filterGroups: []
433
+ } };
434
+ return { filterConditionGroup: {
435
+ logicalOperator: input.logicalOperator,
436
+ filterGroups: input.filters.map((inner) => {
437
+ if (isFilter(inner)) return {
438
+ logicalOperator: "AND",
439
+ filters: [stripInternalKind(inner)]
440
+ };
441
+ if (!inner.filters.every(isFilter)) throw new Error("Cannot nest filter groups more than three levels deep.");
442
+ return {
443
+ logicalOperator: inner.logicalOperator,
444
+ filters: inner.filters.map(stripInternalKind)
445
+ };
446
+ })
447
+ } };
448
+ }
449
+ throw new Error("Unknown filter kind: " + input[INTERNAL_KIND]);
450
+ }
451
+ /**
452
+ * Converts order-by utility conditions to a format our backend supports
453
+ *
454
+ * @example
455
+ * single orderBy
456
+ * ```tsx
457
+ * const orderBy = buildOrderBy(q.asc("title"));
458
+ * // result: [{ sortingField: "title", sortType: "ASC" }]
459
+ * ```
460
+ *
461
+ * @example
462
+ * multiple orderBys
463
+ * ```tsx
464
+ * const orderBy = buildOrderBy([q.desc("createdAt"), q.asc("title")]);
465
+ * // result: [
466
+ * // { sortingField: "createdAt", sortType: "DESC" },
467
+ * // { sortingField: "title", sortType: "ASC" }
468
+ * // ]
469
+ * ```
470
+ */
471
+ function buildOrderBy(input) {
472
+ const orderBys = Array.isArray(input) ? input : [input];
473
+ if (!orderBys.every((ob) => ob[INTERNAL_KIND] === "ORDER_BY")) throw new Error("orderBy conditions cannot be constructed manually. Use the 'q' helper.");
474
+ return orderBys.map((orderBy) => ({
475
+ sortingField: orderBy.sortingField,
476
+ sortType: orderBy.sortType
477
+ }));
478
+ }
479
+ function dealiasOrderBy(orderbys, aliases) {
480
+ return orderbys.map((orderBy) => {
481
+ const subject = aliases[orderBy.sortingField];
482
+ if (!subject) throw new Error(`Could not find an alias for subject "${orderBy.sortingField}"`);
483
+ return {
484
+ ...orderBy,
485
+ sortingField: subject
486
+ };
487
+ });
488
+ }
489
+ /**
490
+ * Replaces filter subjects with their aliased values.
491
+ *
492
+ * Throws an error if any filter subject does not have a corresponding alias in the provided mapping.
493
+ *
494
+ * @example
495
+ * ```tsx
496
+ * const criteria = buildFilterCriteria(q.text("name").contains("John"));
497
+ * const aliases = { name: "abcxy123" };
498
+ * const dealiased = dealiasFilterCriteria(criteria, aliases);
499
+ * // Result: filter subject is now "abcxyz123" instead of "name"
500
+ * ```
501
+ */
502
+ function dealiasFilterCriteria(criteria, aliases) {
503
+ if (!criteria.filterConditionGroup) return criteria;
504
+ return { filterConditionGroup: {
505
+ ...criteria.filterConditionGroup,
506
+ filterGroups: criteria.filterConditionGroup.filterGroups.map((group) => ({
507
+ ...group,
508
+ filters: group.filters.map(dealiasFilter)
509
+ }))
510
+ } };
511
+ function dealiasFilter(filter) {
512
+ const subject = aliases[filter.subject];
513
+ if (!subject) throw new Error(`Could not find an alias for subject "${subject}"`);
514
+ return {
515
+ ...filter,
516
+ subject
517
+ };
518
+ }
519
+ }
520
+ function stripInternalKind(item) {
521
+ const { [INTERNAL_KIND]: _,...rest } = item;
522
+ return rest;
523
+ }
524
+ function isFilterGroup(item) {
525
+ return item[INTERNAL_KIND] === "LOGICAL_GROUP";
526
+ }
527
+ function isFilter(item) {
528
+ return item[INTERNAL_KIND] === "FILTER";
529
+ }
530
+
531
+ //#endregion
5
532
  //#region src/routes/visibility.ts
6
533
  /**
7
534
  * Why a visitor was denied.
@@ -124,13 +651,36 @@ function defineVisibility(rule) {
124
651
  }
125
652
 
126
653
  //#endregion
127
- //#region src/auth/session.ts
128
- async function resolveVisitor(_request) {
129
- return null;
130
- }
131
- function visitorFromContext(context) {
132
- if (typeof context !== "object" || context === null || !("user" in context)) throw new Error("softrAuthMiddleware has not run: the Start global context carries no `user`. Register it as a global request middleware, e.g. createStart(() => ({ requestMiddleware: [softrAuthMiddleware()] })).");
133
- return context.user ?? null;
654
+ //#region src/auth/isomorphic.ts
655
+ /**
656
+ * Resolves the Softr visitor for every request, so that `getSoftrUser` and the
657
+ * route guards have someone to check.
658
+ *
659
+ * Register it once, as a global request middleware in the app's `src/start.ts`.
660
+ * It touches nothing but the visitor.
661
+ *
662
+ * @example
663
+ * export const startInstance = createStart(() => ({
664
+ * requestMiddleware: [softrAuthMiddleware()],
665
+ * }))
666
+ */
667
+ function softrAuthMiddleware() {
668
+ return createMiddleware({ type: "request" }).server(async ({ next }) => {
669
+ async function getUser() {
670
+ const token = readSessionToken();
671
+ if (token === null) return null;
672
+ const cached = readSessionData(token);
673
+ if (cached !== null) return cached;
674
+ const user = await fetchVisitor(token);
675
+ if (user === null) {
676
+ clearSession();
677
+ return null;
678
+ }
679
+ writeCookies([makeSessionDataCookie(user, token)]);
680
+ return user;
681
+ }
682
+ return next({ context: { user: await getUser() } });
683
+ });
134
684
  }
135
685
  /**
136
686
  * Returns the signed-in visitor, or `null` when nobody is signed in.
@@ -144,27 +694,327 @@ function visitorFromContext(context) {
144
694
  * beforeLoad: async () => ({ user: await getSoftrUser() }),
145
695
  * })
146
696
  */
147
- const getSoftrUser = createServerFn({ method: "GET" }).handler(async () => visitorFromContext(getGlobalStartContext()));
697
+ const getSoftrUser = createServerFn({ method: "GET" }).handler(async () => {
698
+ const context = getGlobalStartContext();
699
+ assert(typeof context === "object" && context !== null && "user" in context, "softrAuthMiddleware has not run: the Start global context carries no `user`. Register it as a global request middleware, e.g. createStart(() => ({ requestMiddleware: [softrAuthMiddleware()] })).");
700
+ return context.user ?? null;
701
+ });
148
702
 
149
703
  //#endregion
150
- //#region src/auth/middleware.ts
704
+ //#region src/core/datasource.ts
705
+ const MAX_RECORD_COUNT = 100;
706
+ const COUNT_CNT = "COUNT_CNT";
707
+ var DatasourceAPIImpl = class {
708
+ id;
709
+ fetch;
710
+ userTimezone;
711
+ constructor(id, fetch$1, userTimezone = "UTC") {
712
+ this.id = id;
713
+ this.fetch = fetch$1;
714
+ this.userTimezone = userTimezone;
715
+ }
716
+ async list(options = {}) {
717
+ const { select, where, orderBy } = options;
718
+ const count = Math.min(options.limit ?? 20, MAX_RECORD_COUNT);
719
+ const offset = options.offset ?? null;
720
+ const filterCriteria = where ? dealiasFilterCriteria(buildFilterCriteria(where), select ?? {}) : {};
721
+ const sortingOption = orderBy ? dealiasOrderBy(buildOrderBy(orderBy), select ?? {}) : void 0;
722
+ const res = await this.fetch(`/api/datasources/${this.id}/records`, {
723
+ method: "POST",
724
+ headers: {
725
+ "Content-Type": "application/json",
726
+ "X-User-Timezone": options.timeZone ?? this.userTimezone
727
+ },
728
+ body: JSON.stringify({
729
+ options: { userLocale: "en-US" },
730
+ filterCriteria,
731
+ sortingOption,
732
+ pagingOption: {
733
+ offset,
734
+ count
735
+ }
736
+ })
737
+ });
738
+ if (!res.ok) throw new Error(`Softr datasource list failed for "${this.id}": ${res.status}`);
739
+ const payload = await res.json();
740
+ const nextOffset = payload.offset < payload.total ? payload.offset : null;
741
+ return {
742
+ records: payload.items.map((item) => ({
743
+ id: item.id,
744
+ fields: select ? aliasFields(item.fields, select) : item.fields
745
+ })),
746
+ total: payload.total,
747
+ nextOffset
748
+ };
749
+ }
750
+ async get(options) {
751
+ if (options.id !== void 0) {
752
+ if (!options.id) throw new Error(`Softr datasource get: id must not be empty`);
753
+ const res = await this.fetch(`/api/datasources/${this.id}/records/${options.id}`, {
754
+ method: "POST",
755
+ headers: {
756
+ "Content-Type": "application/json",
757
+ "X-User-Timezone": options.timeZone ?? this.userTimezone
758
+ },
759
+ body: JSON.stringify({ options: { userLocale: "en-US" } })
760
+ });
761
+ if (!res.ok) throw new Error(`Softr datasource get failed for "${this.id}": ${res.status}`);
762
+ const contentLength = res.headers.get("Content-Length");
763
+ if (contentLength && parseInt(contentLength, 10) === 0) return null;
764
+ return res.json();
765
+ }
766
+ return (await this.list({
767
+ where: options.where,
768
+ limit: 1,
769
+ timeZone: options.timeZone
770
+ })).records[0] ?? null;
771
+ }
772
+ async create(options) {
773
+ const res = await this.fetch(`/api/datasources/${this.id}/records`, {
774
+ method: "POST",
775
+ headers: { "Content-Type": "application/json" },
776
+ body: JSON.stringify({ fields: options.fields })
777
+ });
778
+ if (!res.ok) throw new Error(`Softr datasource create failed for "${this.id}": ${res.status}`);
779
+ return (await res.json()).record;
780
+ }
781
+ async update(options) {
782
+ if (!options.id) throw new Error(`Softr datasource update: id must not be empty`);
783
+ const res = await this.fetch(`/api/datasources/${this.id}/records/${options.id}`, {
784
+ method: "PATCH",
785
+ headers: { "Content-Type": "application/json" },
786
+ body: JSON.stringify({ fields: options.fields })
787
+ });
788
+ if (!res.ok) throw new Error(`Softr datasource update failed for "${this.id}": ${res.status}`);
789
+ return (await res.json()).record;
790
+ }
791
+ async delete(id) {
792
+ const res = await this.fetch(`/api/datasources/${this.id}/records/${id}`, { method: "DELETE" });
793
+ if (!res.ok) throw new Error(`Softr datasource delete failed for "${this.id}": ${res.status}`);
794
+ }
795
+ async metric(options) {
796
+ const { select, where, aggregation, field } = options;
797
+ const fieldId = field ? select?.[field] ?? field : void 0;
798
+ const filterCriteria = where ? dealiasFilterCriteria(buildFilterCriteria(where), select ?? {}) : {};
799
+ const res = await this.fetch(`/api/datasources/${this.id}/chartMetricData`, {
800
+ method: "POST",
801
+ headers: {
802
+ "Content-Type": "application/json",
803
+ "X-User-Timezone": options.timeZone ?? this.userTimezone
804
+ },
805
+ body: JSON.stringify({
806
+ options: { userLocale: "en-US" },
807
+ calculationField: {
808
+ field: fieldId,
809
+ aggregation
810
+ },
811
+ filterCriteria
812
+ })
813
+ });
814
+ if (!res.ok) throw new Error(`Softr datasource metric failed for "${this.id}": ${res.status}`);
815
+ const payload = await res.json();
816
+ const executionTimestamp = /* @__PURE__ */ new Date(payload.executionTimestamp * 1e3);
817
+ let value;
818
+ if (aggregation === "COUNT") value = payload.data.find((item) => COUNT_CNT in item)?.[COUNT_CNT] ?? null;
819
+ else {
820
+ if (!fieldId) throw new Error(`field is required for aggregation "${aggregation}"`);
821
+ value = payload.data.find((item) => fieldId in item)?.[fieldId] ?? null;
822
+ }
823
+ return {
824
+ value,
825
+ executionTimestamp
826
+ };
827
+ }
828
+ async chartData(options) {
829
+ const { select, where, groupBy, metrics, orderBy } = options;
830
+ const filterCriteria = where ? dealiasFilterCriteria(buildFilterCriteria(where), select ?? {}) : {};
831
+ const sortingOption = orderBy ? dealiasOrderBy(buildOrderBy(orderBy), select ?? {})[0] : void 0;
832
+ const groupByFields = (Array.isArray(groupBy) ? groupBy : [groupBy]).map((field) => ({ field: select?.[field] ?? field }));
833
+ const yAxisFields = Object.values(metrics).map(({ field, aggregation }) => ({
834
+ field: field ? select?.[field] ?? field : void 0,
835
+ aggregation
836
+ }));
837
+ const res = await this.fetch(`/api/datasources/${this.id}/chartData`, {
838
+ method: "POST",
839
+ headers: {
840
+ "Content-Type": "application/json",
841
+ "X-User-Timezone": options.timeZone ?? this.userTimezone
842
+ },
843
+ body: JSON.stringify({
844
+ options: { userLocale: "en-US" },
845
+ filterCriteria,
846
+ sortingOption,
847
+ xAxisFields: groupByFields,
848
+ yAxisFields
849
+ })
850
+ });
851
+ if (!res.ok) throw new Error(`Softr datasource chartData failed for "${this.id}": ${res.status}`);
852
+ const payload = await res.json();
853
+ const executionTimestamp = /* @__PURE__ */ new Date(payload.executionTimestamp * 1e3);
854
+ const aliases = {};
855
+ const multipleMetrics = Object.keys(metrics).length > 1;
856
+ for (const [alias, { field, aggregation }] of Object.entries(metrics)) if (aggregation === "COUNT") aliases[COUNT_CNT] = alias;
857
+ else {
858
+ const fieldId = field ? select?.[field] ?? field : field;
859
+ if (fieldId) {
860
+ const suffix = multipleMetrics ? `_${aggregation.toLowerCase()}` : "";
861
+ aliases[fieldId + suffix] = alias;
862
+ }
863
+ }
864
+ for (const field of Array.isArray(groupBy) ? groupBy : [groupBy]) aliases[select?.[field] ?? field] = field;
865
+ return {
866
+ data: payload.data.map((item) => {
867
+ const row = {};
868
+ for (const [rawKey, alias] of Object.entries(aliases)) if (rawKey in item) row[alias] = item[rawKey];
869
+ return row;
870
+ }),
871
+ executionTimestamp
872
+ };
873
+ }
874
+ };
875
+ function aliasFields(fields, aliases) {
876
+ const result = {};
877
+ for (const [alias, fieldId] of Object.entries(aliases)) result[alias] = fields[fieldId];
878
+ return result;
879
+ }
880
+
881
+ //#endregion
882
+ //#region src/core/integration.ts
883
+ var IntegrationAPIImpl = class {
884
+ id;
885
+ request;
886
+ constructor(id, fetch$1) {
887
+ this.id = id;
888
+ this.request = fetch$1;
889
+ }
890
+ async fetch(input, init) {
891
+ const req = new Request(input, init);
892
+ if (req.signal.aborted) throw req.signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
893
+ const headers = {};
894
+ new Headers(req.headers).forEach((value, key) => {
895
+ headers[key] = value;
896
+ });
897
+ const payload = {
898
+ method: req.method,
899
+ url: req.url,
900
+ headers
901
+ };
902
+ const method = req.method.toUpperCase();
903
+ if (method !== "GET" && method !== "HEAD" && req.body !== null) payload.body = await req.clone().text();
904
+ return this.request(`/api/integrations/${this.id}/proxy`, {
905
+ method: "POST",
906
+ headers: { "Content-Type": "application/json" },
907
+ body: JSON.stringify(payload),
908
+ signal: req.signal
909
+ });
910
+ }
911
+ };
912
+
913
+ //#endregion
914
+ //#region src/core/workflow.ts
915
+ var WorkflowAPIImpl = class {
916
+ id;
917
+ fetch;
918
+ constructor(id, fetch$1) {
919
+ this.id = id;
920
+ this.fetch = fetch$1;
921
+ }
922
+ async trigger(payload) {
923
+ const res = await this.fetch(`/api/workflows/${this.id}/trigger`, {
924
+ method: "POST",
925
+ ...payload ? {
926
+ headers: { "Content-Type": "application/json" },
927
+ body: JSON.stringify(payload)
928
+ } : {}
929
+ });
930
+ if (!res.ok) throw new Error(`Softr workflow trigger failed: ${res.status}`);
931
+ }
932
+ };
933
+
934
+ //#endregion
935
+ //#region src/core/sdk.ts
936
+ var SoftrSDKImpl = class {
937
+ baseUrl;
938
+ userTimezone;
939
+ extraHeaders;
940
+ cachedMe = null;
941
+ constructor(config) {
942
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
943
+ this.userTimezone = config.userTimezone ?? import.meta.env.VITE_APP_TIMEZONE ?? "UTC";
944
+ this.extraHeaders = config.headers;
945
+ }
946
+ async me() {
947
+ if (this.cachedMe) return this.cachedMe;
948
+ const res = await this._fetch("/me");
949
+ if (!res.ok) throw new Error(`Softr /me failed: ${res.status}`);
950
+ this.cachedMe = await res.json();
951
+ return this.cachedMe;
952
+ }
953
+ auth = {
954
+ login: async ({ email, password }) => {
955
+ const res = await this._fetch("/auth/login", {
956
+ method: "POST",
957
+ headers: { "Content-Type": "application/json" },
958
+ body: JSON.stringify({
959
+ email,
960
+ password
961
+ })
962
+ });
963
+ if (!res.ok) throw new Error(`Login failed: ${res.status}`);
964
+ this.cachedMe = null;
965
+ },
966
+ logout: async () => {
967
+ const res = await this._fetch("/auth/logout", { method: "POST" });
968
+ if (!res.ok) throw new Error(`Logout failed: ${res.status}`);
969
+ this.cachedMe = null;
970
+ }
971
+ };
972
+ datasource(id) {
973
+ return new DatasourceAPIImpl(id, this._fetch.bind(this), this.userTimezone);
974
+ }
975
+ integration(id) {
976
+ return new IntegrationAPIImpl(id, this._fetch.bind(this));
977
+ }
978
+ workflow(id) {
979
+ return new WorkflowAPIImpl(id, this._fetch.bind(this));
980
+ }
981
+ _fetch(path, init) {
982
+ const headers = new Headers(this.extraHeaders);
983
+ if (init?.headers) new Headers(init.headers).forEach((v, k) => headers.set(k, v));
984
+ const isServer = typeof window === "undefined";
985
+ return fetch(`${this.baseUrl}${path}`, {
986
+ ...init,
987
+ credentials: isServer ? "omit" : "include",
988
+ headers
989
+ });
990
+ }
991
+ };
992
+ function createSoftrSDKFromRequest(request, baseUrl) {
993
+ const cookieHeader = request.headers.get("cookie") ?? "";
994
+ return new SoftrSDKImpl({
995
+ baseUrl,
996
+ userTimezone: parseCookie(cookieHeader, "user-timezone"),
997
+ headers: { cookie: cookieHeader }
998
+ });
999
+ }
1000
+ function parseCookie(cookieHeader, name) {
1001
+ return cookieHeader.split(";").map((c) => c.trim().split("=")).find(([k]) => k === name)?.[1];
1002
+ }
1003
+
1004
+ //#endregion
1005
+ //#region src/core/middleware.ts
151
1006
  /**
152
- * Resolves the Softr visitor for every request, so that `getSoftrUser` and the
153
- * route guards have someone to check.
154
- *
155
- * Register it once, as a global request middleware in the app's `src/start.ts`.
156
- * It touches nothing but the visitor.
1007
+ * TanStack Start middleware. Injects a per-request Softr SDK instance with
1008
+ * cookies forwarded automatically. The base URL is read from VITE_SOFTR_BASE_URL.
157
1009
  *
158
1010
  * @example
159
- * export const startInstance = createStart(() => ({
160
- * requestMiddleware: [softrAuthMiddleware()],
161
- * }))
1011
+ * import { withSDK } from '@softr/sdk'
1012
+ *
1013
+ * export const getProjects = createServerFn()
1014
+ * .middleware([withSDK])
1015
+ * .handler(({ context }) => context.sdk.datasource('ds_projects').list())
162
1016
  */
163
- function softrAuthMiddleware() {
164
- return createMiddleware({ type: "request" }).server(async ({ request, next }) => {
165
- return next({ context: { user: await resolveVisitor(request) } });
166
- });
167
- }
1017
+ const withSDK = createMiddleware().server(({ next, request }) => next({ context: { sdk: createSoftrSDKFromRequest(request, import.meta.env.VITE_SOFTR_BASE_URL) } }));
168
1018
 
169
1019
  //#endregion
170
- export { defineVisibility, getSoftrUser, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, q, softrAuthMiddleware };
1020
+ export { defineVisibility, getSoftrUser, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, q, softrAuthMiddleware, withSDK };