@softr/sdk 0.0.24 → 0.0.26-alpha-0

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 (62) hide show
  1. package/dist/action.d.ts +195 -0
  2. package/dist/action.js +21 -0
  3. package/dist/assertions.js +1 -7
  4. package/dist/auth/isomorphic.d.ts +1 -2
  5. package/dist/auth/methods/authenticator.d.ts +80 -0
  6. package/dist/auth/methods/authenticator.js +175 -0
  7. package/dist/auth/methods/code.d.ts +40 -0
  8. package/dist/auth/methods/code.js +74 -0
  9. package/dist/auth/methods/magicLink.d.ts +24 -0
  10. package/dist/auth/methods/magicLink.js +53 -0
  11. package/dist/auth/methods/password.d.ts +19 -5
  12. package/dist/auth/methods/password.js +36 -15
  13. package/dist/auth/methods/signup.d.ts +22 -14
  14. package/dist/auth/methods/signup.js +48 -54
  15. package/dist/auth/methods/social.d.ts +35 -0
  16. package/dist/auth/methods/social.js +58 -0
  17. package/dist/auth/methods/utils.d.ts +1 -1
  18. package/dist/auth/permissions.d.ts +7 -2
  19. package/dist/auth/platform.js +185 -7
  20. package/dist/auth/server.d.ts +15 -0
  21. package/dist/auth/server.js +16 -1
  22. package/dist/auth/session.js +35 -2
  23. package/dist/cli.d.ts +1 -0
  24. package/dist/cli.js +23 -0
  25. package/dist/codegen/datasource-types.js +471 -0
  26. package/dist/codegen/fetch-metadata.js +46 -0
  27. package/dist/codegen/softr-env.js +102 -0
  28. package/dist/codegen/user-groups.js +11 -0
  29. package/dist/core/integration.js +1 -1
  30. package/dist/datasource/chart.d.ts +196 -0
  31. package/dist/datasource/chart.js +289 -0
  32. package/dist/datasource/datasource.d.ts +16 -0
  33. package/dist/datasource/datasource.js +17 -0
  34. package/dist/datasource/fields.d.ts +122 -0
  35. package/dist/datasource/fields.js +82 -0
  36. package/dist/datasource/filter.d.ts +30 -0
  37. package/dist/datasource/filter.js +146 -0
  38. package/dist/datasource/record.d.ts +138 -0
  39. package/dist/datasource/record.js +258 -0
  40. package/dist/index.d.ts +3 -5
  41. package/dist/index.js +2 -2
  42. package/dist/package.js +5 -0
  43. package/dist/routes/visibility.d.ts +1 -1
  44. package/dist/server.d.ts +1 -1
  45. package/dist/server.js +1 -1
  46. package/package.json +31 -8
  47. package/skills/softr-sdk/SKILL.md +11 -7
  48. package/skills/softr-sdk/auth/SKILL.md +222 -36
  49. package/skills/softr-sdk/charts-and-metrics/SKILL.md +178 -0
  50. package/skills/softr-sdk/charts-and-metrics/references/aggregations.md +76 -0
  51. package/skills/softr-sdk/current-user/SKILL.md +1 -1
  52. package/skills/softr-sdk/data-mutations/SKILL.md +76 -56
  53. package/skills/softr-sdk/reading-records/SKILL.md +77 -93
  54. package/skills/softr-sdk/reading-records/references/field-types.md +41 -48
  55. package/skills/softr-sdk/reading-records/references/query-builder.md +66 -70
  56. package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
  57. package/dist/core/datasource.d.ts +0 -31
  58. package/dist/core/datasource.js +0 -226
  59. package/dist/core/query.d.ts +0 -354
  60. package/dist/core/query.js +0 -528
  61. package/dist/core/utils.js +0 -14
  62. package/dist/types.d.ts +0 -128
@@ -1,70 +1,63 @@
1
- # Softr field types — TypeScript shapes
1
+ # Softr field types and what their values look like
2
2
 
3
- When you write a TypeScript type for a record's fields, use these shapes. The field's **type** (shown in the datasource metadata or described by the user) determines the shape, not the field's name.
3
+ The generated `softr-env.d.ts` is the authority for your own tables: it types every field of every registered datasource, choice ids included, with the field's name and constraints in a docstring. This reference is the general mapping behind it, for reading a record's values without opening that file.
4
4
 
5
- ```ts
6
- // Simple scalar types
7
- SINGLE_LINE_TEXT: string
8
- LONG_TEXT: string
9
- EMAIL: string
10
- PHONE: string
11
- URL: string
12
- ADDRESS: string
13
- LOCATION: string
14
- BUTTON: string
15
- RECORD_ID: string
16
- DATETIME: string // ISO timestamp string
17
- DATE: string // ISO date string
18
- TIME: string
19
- CREATED_AT: string
20
- UPDATED_AT: string
21
- CHECKBOX: string | boolean
22
- AUTONUMBER: number | null
23
- NUMBER: number | null
24
- PERCENT: number | null
25
- CURRENCY: number | null
26
- PROGRESS: number | null
27
- DURATION: number | null
28
- COUNT: number | null
29
- RATING: string | number | null
30
- FORMULA: string | number
31
-
32
- // Object types — these are NOT plain strings
33
- SELECT: { id: string; label: string }
34
- LINKED_RECORD: { id: string; label: string }
5
+ Three rules cover nullability:
35
6
 
36
- DATE_RANGE: { from: string; to: string }
7
+ - Every field reads back present, `null` when it is empty. Nothing is optional.
8
+ - A multi-entry field is an array, empty rather than `null`.
9
+ - A `CHECKBOX` is a boolean, `false` rather than `null`.
37
10
 
11
+ ```ts
12
+ // Strings
13
+ SINGLE_LINE_TEXT, LONG_TEXT, ADDRESS, LOCATION, BUTTON, RECORD_ID: string
14
+ EMAIL, PHONE, URL: string
15
+ DATETIME: string // ISO timestamp, or an ISO date at date precision
16
+ CREATED_AT, UPDATED_AT: string
17
+ TIME: string // '09:30:00'
18
+ CREATED_BY, UPDATED_BY: string
19
+
20
+ // Numbers
21
+ NUMBER, PERCENT, CURRENCY, PROGRESS, DURATION, COUNT, RATING, AUTONUMBER: number
22
+
23
+ // Booleans
24
+ CHECKBOX: boolean
25
+
26
+ // Objects. These are NOT plain strings
27
+ SELECT: { id: string; label: string } // generated with literal id and label unions
28
+ LINKED_RECORD: { id: string; label: string | null } // label is the linked record's primary field
29
+ DATE_RANGE: { from: string; to: string }
30
+ USER: { id: string; name: string; email: string; avatarUrl: string | null }
38
31
  ATTACHMENT: {
39
- filename: string | null | undefined
40
- id: string | null | undefined
41
- type: string | null | undefined
32
+ id: string | null
33
+ filename: string | null
34
+ type: string | null
42
35
  url: string
36
+ expiresAt: string | null
37
+ size?: number
43
38
  thumbnails?: Array<{ size: 'small' | 'medium' | 'large'; url: string }>
44
39
  }
45
40
 
46
- USER: { avatarUrl: string | null; id: string; name: string; email: string }
47
-
48
- CREATED_BY: string | string[] | { avatarUrl: string | null; id: string; name: string; email: string }
49
- UPDATED_BY: string | string[] | { avatarUrl: string | null; id: string; name: string; email: string }
50
-
51
- // These depend on what the linked field/aggregation resolves to
52
- LOOKUP: unknown
53
- ROLLUP: unknown
41
+ // Computed fields read as whatever they return
42
+ FORMULA, LOOKUP, ROLLUP: the returned field's shape, resolved in softr-env.d.ts
54
43
  ```
55
44
 
45
+ Every scalar above is `| null` on the record unless the field allows multiple entries, in which case the value is an `Array<...>` of the scalar.
46
+
56
47
  ## The non-obvious ones
57
48
 
58
- **SELECT and LINKED_RECORD** are the most commonly mistyped. They are never a plain string — always `{ id: string; label: string }`. To display the value, use `.label`. To filter or compare, use `.id`.
49
+ **SELECT and LINKED_RECORD** are the most commonly mistyped. They are never a plain string, always an `{ id, label }` object. To display the value, use `.label`. To filter or compare, use `.id`. A generated SELECT narrows both to the field's own choices as literal unions.
59
50
 
60
51
  ```tsx
61
52
  // Correct
62
53
  <td>{record.fields.status?.label}</td>
63
54
 
64
- // Wrong — status is not a string
55
+ // Wrong: status is not a string
65
56
  <td>{record.fields.status}</td>
66
57
  ```
67
58
 
68
- **CHECKBOX** can be either a string (`'true'` / `'false'`) or a boolean depending on the datasource. Type it as `string | boolean` and coerce when needed: `!!record.fields.active`.
59
+ **DURATION** is seconds, whatever format the field displays in.
60
+
61
+ **DATETIME at date precision** is generated as a `DATE` field and holds `'2026-03-14'`, not a timestamp.
69
62
 
70
- **CREATED_BY / UPDATED_BY** can be a plain id string, an array of ids, or a full user object. Narrow before using.
63
+ **LOOKUP and FORMULA** take the shape of what they resolve to, so two lookups on one table can have entirely different types. Read the generated type rather than guessing from the field name.
@@ -1,30 +1,27 @@
1
- # `q`, the query builder
1
+ # Filters and sorting
2
2
 
3
- Every filter, sort and field map passed to `ds` is built with `q`, imported from `@softr/sdk`. Filters and sorts built any other way are rejected at runtime.
4
-
5
- ```ts
6
- import { q } from '@softr/sdk'
7
- ```
3
+ Every filter and sort is built inside the callback `where` or `orderBy` hands you. A filter built any other way is rejected at request time.
8
4
 
9
5
  ## Shape
10
6
 
11
7
  ```ts
12
- await ds.list(datasourceId, {
13
- select: q.select({ alias: '<field id>', ... }), // optional field map
14
- where: <one filter, or q.and/q.or of filters>,
15
- orderBy: q.asc('alias') | [q.desc('a'), q.asc('b')],
16
- limit: number, // default 20, clamped down to 100
17
- offset: number | null,
18
- })
8
+ await ds
9
+ .list(action)
10
+ .where((q) => q.status.is('active')) // one filter, or a $and/$or of filters
11
+ .orderBy((q) => q.title.asc) // or an array of handles
12
+ .limit(20) // default 20, clamped down to 100
13
+ .offset(null) // a previous result's nextOffset
19
14
  ```
20
15
 
21
- `where` takes exactly one value. For more than one condition, wrap them in `q.and(...)` or `q.or(...)`.
16
+ A `where` callback returns exactly one value. For more than one condition, wrap them in `q.$and(...)` or `q.$or(...)`, or call `.where` again: repeated calls are anded.
17
+
18
+ ## Operators by field type
22
19
 
23
- ## Field types and operators
20
+ The handle for each field carries the operators its declared type supports, taken from the generated `softr-env.d.ts`. A field with no generated type gets every operator.
24
21
 
25
- Pick the builder that matches the field's type. The type travels with the operator and decides how the value is compared.
22
+ ### Text fields
26
23
 
27
- ### `q.text(field)`
24
+ `SINGLE_LINE_TEXT`, `LONG_TEXT`, `URL`, `EMAIL`, `PHONE`, `ADDRESS`, `LOCATION`, `TIME`, `BUTTON`, `RECORD_ID`.
28
25
 
29
26
  | Call | Meaning |
30
27
  | --- | --- |
@@ -35,52 +32,56 @@ Pick the builder that matches the field's type. The type travels with the operat
35
32
  | `.endsWith(value: string)` | Suffix match |
36
33
  | `.isOneOf(values: string[])` | Equal to any of |
37
34
  | `.isNoneOf(values: string[])` | Equal to none of |
38
- | `.hasAllOf(values: string[])` | Contains all of |
39
- | `.isEmpty()` | No value |
40
- | `.isNotEmpty()` | Has a value |
35
+ | `.isEmpty()` / `.isNotEmpty()` | No value / has a value |
36
+
37
+ ### Number fields
41
38
 
42
- ### `q.number(field)`
39
+ `NUMBER`, `PERCENT`, `CURRENCY`, `PROGRESS`, `RATING`, `DURATION`, `COUNT`, `AUTONUMBER`.
43
40
 
44
41
  | Call | Meaning |
45
42
  | --- | --- |
46
- | `.is(value: number)` | Equal |
47
- | `.isNot(value: number)` | Not equal |
48
- | `.gt(value: number)` | Greater than |
49
- | `.gte(value: number)` | Greater than or equal |
50
- | `.lt(value: number)` | Less than |
51
- | `.lte(value: number)` | Less than or equal |
43
+ | `.is(value: number)` / `.isNot(value: number)` | Equal / not equal |
44
+ | `.gt(value)` / `.gte(value)` / `.lt(value)` / `.lte(value)` | Comparisons |
52
45
  | `.between(start: number, end: number)` | Inclusive range |
53
46
  | `.isEmpty()` / `.isNotEmpty()` | No value / has a value |
54
47
 
55
- Takes JavaScript numbers.
56
-
57
- ### `q.boolean(field)`
48
+ ### Checkbox fields
58
49
 
59
50
  | Call | Meaning |
60
51
  | --- | --- |
61
- | `.is(value: boolean)` | Equal |
62
- | `.isNot(value: boolean)` | Not equal |
52
+ | `.is(value: boolean)` / `.isNot(value: boolean)` | Equal / not equal |
63
53
  | `.isEmpty()` / `.isNotEmpty()` | No value / has a value |
64
54
 
65
- ### `q.date(field)`
55
+ ### Date fields
56
+
57
+ `DATE`, `DATETIME`, `CREATED_AT`, `UPDATED_AT`.
66
58
 
67
59
  | Call | Meaning |
68
60
  | --- | --- |
69
- | `.is(value: string)` | On that date |
70
- | `.isNot(value: string)` | Not on that date |
71
- | `.gt(value: string)` | After |
72
- | `.gte(value: string)` | On or after |
73
- | `.lt(value: string)` | Before |
74
- | `.lte(value: string)` | On or before |
75
- | `.between(start: string, end: string)` | Inclusive range |
76
- | `.isNotBetween(start: string, end: string)` | Outside the range |
61
+ | `.is(value: string)` / `.isNot(value: string)` | On / not on that date |
62
+ | `.gt(value)` / `.gte(value)` | After / on or after |
63
+ | `.lt(value)` / `.lte(value)` | Before / on or before |
64
+ | `.between(start, end)` | Inclusive range |
65
+ | `.isNotBetween(start, end)` | Outside the range |
77
66
  | `.isEmpty()` / `.isNotEmpty()` | No value / has a value |
78
67
 
79
68
  Every value is an ISO date string, either `'2026-01-31'` or a full timestamp. A `Date` instance is not accepted. Call `.toISOString()` first.
80
69
 
81
- ### `q.array(field)`
70
+ ### Choice fields
71
+
72
+ `SELECT`, `LINKED_RECORD`, `CREATED_BY`, `UPDATED_BY`.
73
+
74
+ | Call | Meaning |
75
+ | --- | --- |
76
+ | `.is(value)` / `.isNot(value)` | Equal / not equal |
77
+ | `.isOneOf(values)` / `.isNoneOf(values)` | Any of / none of |
78
+ | `.isEmpty()` / `.isNotEmpty()` | No value / has a value |
79
+
80
+ Values are choice ids, never labels. A generated SELECT narrows the argument to the field's own choice ids, so a label or a typo is a type error. A LINKED_RECORD compares against the linked record's id.
81
+
82
+ ### Multi-value fields
82
83
 
83
- For multi-value fields: linked records, multi-selects, collaborators.
84
+ Any generated type with a `[]` suffix: multi-selects, multi-entry linked records.
84
85
 
85
86
  | Call | Meaning |
86
87
  | --- | --- |
@@ -90,21 +91,30 @@ For multi-value fields: linked records, multi-selects, collaborators.
90
91
  | `.hasAllOf(values: string[])` | Contains all of |
91
92
  | `.isEmpty()` / `.isNotEmpty()` | Empty / non-empty |
92
93
 
94
+ Values are the members' ids, as with choice fields.
95
+
96
+ ### User fields
97
+
98
+ `USER` offers `.is`, `.isNot`, `.contains`, `.isEmpty` and `.isNotEmpty`.
99
+
100
+ ### Attachment and date range fields
101
+
102
+ `ATTACHMENT` and `DATE_RANGE` have no single value to compare, so they offer only `.isEmpty()` and `.isNotEmpty()`.
103
+
93
104
  ## Combining
94
105
 
95
106
  ```ts
96
- q.and(...filters) // all must match
97
- q.or(...filters) // any may match
107
+ q.$and(...filters) // all must match
108
+ q.$or(...filters) // any may match
98
109
  ```
99
110
 
100
111
  Both take filters or nested groups:
101
112
 
102
113
  ```ts
103
- where: q.and(
104
- q.text('status').is('active'),
105
- q.or(
106
- q.number('amount').gte(1000),
107
- q.array('tags').hasAllOf(['priority']),
114
+ .where((q) =>
115
+ q.$and(
116
+ q.status.is('active'),
117
+ q.$or(q.amount.gte(1000), q.title.contains('priority')),
108
118
  ),
109
119
  )
110
120
  ```
@@ -114,31 +124,17 @@ Nesting goes two levels, a group inside a group. A third level throws `Cannot ne
114
124
  ## Sorting
115
125
 
116
126
  ```ts
117
- orderBy: q.asc('title')
118
- orderBy: [q.desc('publishedAt'), q.asc('title')]
127
+ .orderBy((q) => q.title.asc)
128
+ .orderBy((q) => [q.publishedAt.desc, q.title.asc])
119
129
  ```
120
130
 
121
- Sorts are applied in array order.
122
-
123
- ## Field maps
124
-
125
- ```ts
126
- const select = q.select({ title: 'fZk3Qm9pLw2Xa1', views: 'Ru7bVx2cNq0Ed9' })
127
- ```
128
-
129
- `q.select` maps names you choose onto Softr field ids — opaque strings copied from the table's schema. Two rules follow from that:
130
-
131
- 1. The returned record carries exactly the keys in the map. A field left out is not returned, and no error is raised.
132
- 2. `where` and `orderBy` name aliases, and every one of them must be in the map. A name that is not throws `Could not find an alias for subject "..."` at request time, and so does a `where` or `orderBy` passed with no `select` at all.
133
-
134
- So `select` must list every field the query touches, not just the ones being displayed.
131
+ Each field carries `.asc` and `.desc` handles and nothing else. An array applies sorts in order. Repeated `.orderBy` calls append.
135
132
 
136
133
  ## Errors
137
134
 
138
135
  | Message | Cause |
139
136
  | --- | --- |
140
- | `Filters cannot be constructed manually. Use the 'q' helper.` | A `where` value built as a plain object |
141
- | `orderBy conditions cannot be constructed manually. Use the 'q' helper.` | An `orderBy` value not from `q.asc` / `q.desc` |
137
+ | `a filter must come from the callback a read hands you` | A `where` value built as a plain object |
138
+ | `a sort must come from the callback a read hands you` | An `orderBy` value not from the callback's `.asc` / `.desc` handles |
142
139
  | `Cannot nest filter groups more than three levels deep.` | A group nested one level too far |
143
- | `Could not find an alias for subject "..."` | A sort field missing from `select`, or an `orderBy` with no `select` at all |
144
- | `Could not find an alias for subject "undefined"` | The same thing on a filter, which does not report the field name |
140
+ | A `TypeError` reading an operator off `undefined` | Filtering or sorting on a field the action does not declare |
@@ -12,7 +12,7 @@ description: >-
12
12
  metadata:
13
13
  type: sub-skill
14
14
  library: softr-sdk
15
- library_version: 0.0.24
15
+ library_version: 0.0.25
16
16
  ---
17
17
 
18
18
  **Boundary.** `defineVisibility`, the error predicates and every type come from `@softr/sdk` and work anywhere; `ds` and `auth` live on `@softr/sdk/server` and run only inside a `createServerFn` handler. The app is already wired — `src/start.ts` registers `softrAuthMiddleware`, `src/routes/__root.tsx` publishes the visitor as route context — never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
@@ -1,31 +0,0 @@
1
- import { ChartOptions, ChartResult, CreateOptions, GetOptions, ListOptions, ListResult, MetricOptions, MetricResult, RecordFieldsFor, SoftrRecord, UpdateOptions } from "../types.js";
2
-
3
- //#region src/core/datasource.d.ts
4
- declare function listRecords<Id extends string, Sel extends Record<string, string> = Record<never, never>>(datasourceId: Id, options?: Omit<ListOptions, 'select'> & {
5
- select?: Sel;
6
- }): Promise<ListResult<SoftrRecord<RecordFieldsFor<Id, Sel>>>>;
7
- declare function getRecord<Id extends string, Sel extends Record<string, string> = Record<never, never>>(datasourceId: Id, options: Omit<GetOptions, 'select'> & {
8
- select?: Sel;
9
- }): Promise<SoftrRecord<RecordFieldsFor<Id, Sel>> | null>;
10
- declare function createRecord<Id extends string, Sel extends Record<string, string> = Record<never, never>>(datasourceId: Id, options: Omit<CreateOptions, 'select' | 'data'> & {
11
- select?: Sel;
12
- data: Partial<RecordFieldsFor<Id, Sel>>;
13
- }): Promise<SoftrRecord<RecordFieldsFor<Id, Sel>>>;
14
- declare function updateRecord<Id extends string, Sel extends Record<string, string> = Record<never, never>>(datasourceId: Id, options: Omit<UpdateOptions, 'select' | 'data'> & {
15
- select?: Sel;
16
- data: Partial<RecordFieldsFor<Id, Sel>>;
17
- }): Promise<SoftrRecord<RecordFieldsFor<Id, Sel>>>;
18
- declare function deleteRecord(datasourceId: string, id: string): Promise<void>;
19
- declare function getMetric(datasourceId: string, options: MetricOptions): Promise<MetricResult>;
20
- declare function getChart(datasourceId: string, options: ChartOptions): Promise<ChartResult>;
21
- declare const ds: {
22
- list: typeof listRecords;
23
- get: typeof getRecord;
24
- create: typeof createRecord;
25
- update: typeof updateRecord;
26
- delete: typeof deleteRecord;
27
- metric: typeof getMetric;
28
- chart: typeof getChart;
29
- };
30
- //#endregion
31
- export { ds };
@@ -1,226 +0,0 @@
1
- import { buildFilterCriteria, buildOrderBy, dealiasFilterCriteria, dealiasOrderBy } from "./query.js";
2
- import { panic } from "../assertions.js";
3
- import { readUserTimezone } from "../cookies.js";
4
- import { sessionConfig } from "../auth/config.js";
5
- import { readSessionToken } from "../auth/session.js";
6
- import { assertResponseOk } from "./utils.js";
7
-
8
- //#region src/core/datasource.ts
9
- const MAX_RECORD_COUNT = 100;
10
- const COUNT_CNT = "COUNT_CNT";
11
- const dsUrl = (datasourceId) => {
12
- const datasourceApiUrl = process.env.SOFTR_DATASOURCE_API_URL;
13
- if (!datasourceApiUrl) panic("SOFTR_DATASOURCE_API_URL is not set.");
14
- const { applicationId } = sessionConfig();
15
- return `${withoutTrailingSlash(datasourceApiUrl)}/v1/datasource/vibe/applications/${applicationId}/datasources/${datasourceId}`;
16
- };
17
- async function listRecords(datasourceId, options = {}) {
18
- const { select, where, orderBy } = options;
19
- const count = Math.min(options.limit ?? 20, MAX_RECORD_COUNT);
20
- const offset = options.offset ?? null;
21
- const filterCriteria = where ? dealiasFilterCriteria(buildFilterCriteria(where), select ?? {}) : {};
22
- const sortingOption = orderBy ? dealiasOrderBy(buildOrderBy(orderBy), select ?? {}) : void 0;
23
- const res = await fetch(`${dsUrl(datasourceId)}/records`, {
24
- method: "POST",
25
- headers: {
26
- "Content-Type": "application/json",
27
- "X-User-Timezone": options.timeZone ?? readUserTimezone() ?? "UTC",
28
- "Authorization": `Bearer ${readSessionToken()}`
29
- },
30
- body: JSON.stringify({
31
- options: { userLocale: "en-US" },
32
- filterCriteria,
33
- sortingOption,
34
- pagingOption: {
35
- offset,
36
- count
37
- }
38
- })
39
- });
40
- await assertResponseOk(res, `Softr datasource list failed for "${datasourceId}"`);
41
- const payload = await res.json();
42
- const nextOffset = payload.offset < payload.total ? payload.offset : null;
43
- return {
44
- records: payload.items.map((item) => ({
45
- id: item.id,
46
- fields: select ? aliasFields(item.fields, select) : item.fields
47
- })),
48
- total: payload.total,
49
- nextOffset
50
- };
51
- }
52
- async function getRecord(datasourceId, options) {
53
- if (options.id !== void 0) {
54
- if (!options.id) throw new Error(`Softr datasource get: id must not be empty`);
55
- const res = await fetch(`${dsUrl(datasourceId)}/records/${options.id}`, {
56
- method: "POST",
57
- headers: {
58
- "Content-Type": "application/json",
59
- "X-User-Timezone": options.timeZone ?? readUserTimezone() ?? "UTC",
60
- "Authorization": `Bearer ${readSessionToken()}`
61
- },
62
- body: JSON.stringify({ options: { userLocale: "en-US" } })
63
- });
64
- await assertResponseOk(res, `Softr datasource get failed for "${datasourceId}"`);
65
- const contentLength = res.headers.get("Content-Length");
66
- if (contentLength && parseInt(contentLength, 10) === 0) return null;
67
- const record = await res.json();
68
- return {
69
- id: record.id,
70
- fields: options.select ? aliasFields(record.fields, options.select) : record.fields
71
- };
72
- }
73
- return (await listRecords(datasourceId, {
74
- where: options.where,
75
- select: options.select,
76
- limit: 1,
77
- timeZone: options.timeZone
78
- })).records[0] ?? null;
79
- }
80
- async function createRecord(datasourceId, options) {
81
- const rawFields = options.select ? dealiasData(options.data, options.select) : options.data;
82
- const res = await fetch(`${dsUrl(datasourceId)}/records/new`, {
83
- method: "POST",
84
- headers: {
85
- "Content-Type": "application/json",
86
- "Authorization": `Bearer ${readSessionToken()}`
87
- },
88
- body: JSON.stringify({ fields: rawFields })
89
- });
90
- await assertResponseOk(res, `Softr datasource create failed for "${datasourceId}"`);
91
- const record = await res.json();
92
- return {
93
- id: record.id,
94
- fields: options.select ? aliasFields(record.fields, options.select) : record.fields
95
- };
96
- }
97
- async function updateRecord(datasourceId, options) {
98
- if (!options.id) throw new Error(`Softr datasource update: id must not be empty`);
99
- const rawFields = options.select ? dealiasData(options.data, options.select) : options.data;
100
- const res = await fetch(`${dsUrl(datasourceId)}/records/${options.id}`, {
101
- method: "PATCH",
102
- headers: {
103
- "Content-Type": "application/json",
104
- "Authorization": `Bearer ${readSessionToken()}`
105
- },
106
- body: JSON.stringify({ fields: rawFields })
107
- });
108
- await assertResponseOk(res, `Softr datasource update failed for "${datasourceId}"`);
109
- const record = await res.json();
110
- return {
111
- id: record.id,
112
- fields: options.select ? aliasFields(record.fields, options.select) : record.fields
113
- };
114
- }
115
- async function deleteRecord(datasourceId, id) {
116
- await assertResponseOk(await fetch(`${dsUrl(datasourceId)}/records/${id}`, {
117
- method: "DELETE",
118
- headers: { "Authorization": `Bearer ${readSessionToken()}` }
119
- }), `Softr datasource delete failed for "${datasourceId}"`);
120
- }
121
- async function getMetric(datasourceId, options) {
122
- const { select, where, aggregation, field } = options;
123
- if (aggregation !== "COUNT" && !field) throw new Error(`field is required for aggregation "${aggregation}"`);
124
- const fieldId = field ? select?.[field] ?? field : void 0;
125
- const filterCriteria = where ? dealiasFilterCriteria(buildFilterCriteria(where), select ?? {}) : {};
126
- const res = await fetch(`${dsUrl(datasourceId)}/chart/data`, {
127
- method: "POST",
128
- headers: {
129
- "Content-Type": "application/json",
130
- "X-User-Timezone": options.timeZone ?? readUserTimezone() ?? "UTC",
131
- "Authorization": `Bearer ${readSessionToken()}`
132
- },
133
- body: JSON.stringify({
134
- options: { userLocale: "en-US" },
135
- groupBy: [],
136
- metrics: [{
137
- field: fieldId,
138
- aggregation
139
- }],
140
- filterCriteria
141
- })
142
- });
143
- await assertResponseOk(res, `Softr datasource metric failed for "${datasourceId}"`);
144
- const payload = await res.json();
145
- const executionTimestamp = /* @__PURE__ */ new Date(payload.executionTimestamp * 1e3);
146
- let value;
147
- const responseKey = aggregation === "COUNT" && !fieldId ? COUNT_CNT : fieldId;
148
- value = payload.data.find((item) => responseKey in item)?.[responseKey] ?? null;
149
- return {
150
- value,
151
- executionTimestamp
152
- };
153
- }
154
- async function getChart(datasourceId, options) {
155
- const { select, where, groupBy, metrics, orderBy } = options;
156
- const filterCriteria = where ? dealiasFilterCriteria(buildFilterCriteria(where), select ?? {}) : {};
157
- const sortingOptions = orderBy ? dealiasOrderBy(buildOrderBy(orderBy), select ?? {}) : void 0;
158
- const groupByFields = (Array.isArray(groupBy) ? groupBy : [groupBy]).map((field) => ({ field: select?.[field] ?? field }));
159
- const yAxisFields = Object.values(metrics).map(({ field, aggregation }) => ({
160
- field: field ? select?.[field] ?? field : void 0,
161
- aggregation
162
- }));
163
- if (!yAxisFields.length) throw new Error("chart requires at least one metric");
164
- const res = await fetch(`${dsUrl(datasourceId)}/chart/data`, {
165
- method: "POST",
166
- headers: {
167
- "Content-Type": "application/json",
168
- "X-User-Timezone": options.timeZone ?? readUserTimezone() ?? "UTC",
169
- "Authorization": `Bearer ${readSessionToken()}`
170
- },
171
- body: JSON.stringify({
172
- options: { userLocale: "en-US" },
173
- filterCriteria,
174
- sortingOptions,
175
- groupBy: groupByFields,
176
- metrics: yAxisFields
177
- })
178
- });
179
- await assertResponseOk(res, `Softr datasource chart failed for "${datasourceId}"`);
180
- const payload = await res.json();
181
- const executionTimestamp = /* @__PURE__ */ new Date(payload.executionTimestamp * 1e3);
182
- const aliases = {};
183
- const multipleMetrics = Object.keys(metrics).length > 1;
184
- for (const [alias, { field, aggregation }] of Object.entries(metrics)) if (aggregation === "COUNT") aliases[COUNT_CNT] = alias;
185
- else {
186
- const fieldId = field ? select?.[field] ?? field : field;
187
- if (fieldId) {
188
- const suffix = multipleMetrics ? `_${aggregation.toLowerCase()}` : "";
189
- aliases[fieldId + suffix] = alias;
190
- }
191
- }
192
- for (const field of Array.isArray(groupBy) ? groupBy : [groupBy]) aliases[select?.[field] ?? field] = field;
193
- return {
194
- data: payload.data.map((item) => {
195
- const row = {};
196
- for (const [rawKey, alias] of Object.entries(aliases)) if (rawKey in item) row[alias] = item[rawKey];
197
- return row;
198
- }),
199
- executionTimestamp
200
- };
201
- }
202
- function aliasFields(fields, aliases) {
203
- const result = {};
204
- for (const [alias, fieldId] of Object.entries(aliases)) result[alias] = fields[fieldId];
205
- return result;
206
- }
207
- function dealiasData(data, select) {
208
- const result = {};
209
- for (const [alias, value] of Object.entries(data)) result[select[alias] ?? alias] = value;
210
- return result;
211
- }
212
- const ds = {
213
- list: listRecords,
214
- get: getRecord,
215
- create: createRecord,
216
- update: updateRecord,
217
- delete: deleteRecord,
218
- metric: getMetric,
219
- chart: getChart
220
- };
221
- function withoutTrailingSlash(url) {
222
- return url.replace(/\/+$/, "");
223
- }
224
-
225
- //#endregion
226
- export { ds, dsUrl };