@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.
- package/dist/action.d.ts +195 -0
- package/dist/action.js +21 -0
- package/dist/assertions.js +1 -7
- package/dist/auth/isomorphic.d.ts +1 -2
- package/dist/auth/methods/authenticator.d.ts +80 -0
- package/dist/auth/methods/authenticator.js +175 -0
- package/dist/auth/methods/code.d.ts +40 -0
- package/dist/auth/methods/code.js +74 -0
- package/dist/auth/methods/magicLink.d.ts +24 -0
- package/dist/auth/methods/magicLink.js +53 -0
- package/dist/auth/methods/password.d.ts +19 -5
- package/dist/auth/methods/password.js +36 -15
- package/dist/auth/methods/signup.d.ts +22 -14
- package/dist/auth/methods/signup.js +48 -54
- package/dist/auth/methods/social.d.ts +35 -0
- package/dist/auth/methods/social.js +58 -0
- package/dist/auth/methods/utils.d.ts +1 -1
- package/dist/auth/permissions.d.ts +7 -2
- package/dist/auth/platform.js +185 -7
- package/dist/auth/server.d.ts +15 -0
- package/dist/auth/server.js +16 -1
- package/dist/auth/session.js +35 -2
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +23 -0
- package/dist/codegen/datasource-types.js +471 -0
- package/dist/codegen/fetch-metadata.js +46 -0
- package/dist/codegen/softr-env.js +102 -0
- package/dist/codegen/user-groups.js +11 -0
- package/dist/core/integration.js +1 -1
- package/dist/datasource/chart.d.ts +196 -0
- package/dist/datasource/chart.js +289 -0
- package/dist/datasource/datasource.d.ts +16 -0
- package/dist/datasource/datasource.js +17 -0
- package/dist/datasource/fields.d.ts +122 -0
- package/dist/datasource/fields.js +82 -0
- package/dist/datasource/filter.d.ts +30 -0
- package/dist/datasource/filter.js +146 -0
- package/dist/datasource/record.d.ts +138 -0
- package/dist/datasource/record.js +258 -0
- package/dist/index.d.ts +3 -5
- package/dist/index.js +2 -2
- package/dist/package.js +5 -0
- package/dist/routes/visibility.d.ts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/package.json +31 -8
- package/skills/softr-sdk/SKILL.md +11 -7
- package/skills/softr-sdk/auth/SKILL.md +222 -36
- package/skills/softr-sdk/charts-and-metrics/SKILL.md +178 -0
- package/skills/softr-sdk/charts-and-metrics/references/aggregations.md +76 -0
- package/skills/softr-sdk/current-user/SKILL.md +1 -1
- package/skills/softr-sdk/data-mutations/SKILL.md +76 -56
- package/skills/softr-sdk/reading-records/SKILL.md +77 -93
- package/skills/softr-sdk/reading-records/references/field-types.md +41 -48
- package/skills/softr-sdk/reading-records/references/query-builder.md +66 -70
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
- package/dist/core/datasource.d.ts +0 -31
- package/dist/core/datasource.js +0 -226
- package/dist/core/query.d.ts +0 -354
- package/dist/core/query.js +0 -528
- package/dist/core/utils.js +0 -14
- package/dist/types.d.ts +0 -128
|
@@ -1,70 +1,63 @@
|
|
|
1
|
-
# Softr field types
|
|
1
|
+
# Softr field types and what their values look like
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
type: string | null
|
|
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
|
-
|
|
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
|
|
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
|
|
55
|
+
// Wrong: status is not a string
|
|
65
56
|
<td>{record.fields.status}</td>
|
|
66
57
|
```
|
|
67
58
|
|
|
68
|
-
**
|
|
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
|
-
**
|
|
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
|
-
#
|
|
1
|
+
# Filters and sorting
|
|
2
2
|
|
|
3
|
-
Every filter
|
|
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
|
|
13
|
-
|
|
14
|
-
where
|
|
15
|
-
orderBy
|
|
16
|
-
limit
|
|
17
|
-
offset
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
22
|
+
### Text fields
|
|
26
23
|
|
|
27
|
-
|
|
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
|
-
| `.
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
| `.isEmpty()` / `.isNotEmpty()` | No value / has a value |
|
|
36
|
+
|
|
37
|
+
### Number fields
|
|
41
38
|
|
|
42
|
-
|
|
39
|
+
`NUMBER`, `PERCENT`, `CURRENCY`, `PROGRESS`, `RATING`, `DURATION`, `COUNT`, `AUTONUMBER`.
|
|
43
40
|
|
|
44
41
|
| Call | Meaning |
|
|
45
42
|
| --- | --- |
|
|
46
|
-
| `.is(value: number)` | Equal |
|
|
47
|
-
| `.
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
| `.
|
|
71
|
-
| `.
|
|
72
|
-
| `.
|
|
73
|
-
| `.
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
|
97
|
-
q
|
|
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
|
|
104
|
-
q
|
|
105
|
-
|
|
106
|
-
q.
|
|
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
|
|
118
|
-
orderBy
|
|
127
|
+
.orderBy((q) => q.title.asc)
|
|
128
|
+
.orderBy((q) => [q.publishedAt.desc, q.title.asc])
|
|
119
129
|
```
|
|
120
130
|
|
|
121
|
-
|
|
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
|
-
| `
|
|
141
|
-
| `
|
|
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
|
-
| `
|
|
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.
|
|
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 };
|
package/dist/core/datasource.js
DELETED
|
@@ -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 };
|