@softr/sdk 0.0.25 → 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/auth/isomorphic.d.ts +1 -2
- package/dist/auth/permissions.d.ts +7 -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 +9 -5
- 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/data-mutations/SKILL.md +75 -55
- package/skills/softr-sdk/reading-records/SKILL.md +76 -92
- 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/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,12 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: reading-records
|
|
3
3
|
description: >-
|
|
4
|
-
Fetch records from a Softr datasource inside a server function:
|
|
5
|
-
ds.get from @softr/sdk/server, the
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
and chart data are not covered.
|
|
4
|
+
Fetch records from a Softr datasource inside a server function: declaring a
|
|
5
|
+
read with defineAction, ds.list and ds.get from @softr/sdk/server, the where
|
|
6
|
+
and orderBy callbacks, field aliases, the 20-record default and 100-record
|
|
7
|
+
cap, and nextOffset pagination. Reads only. Mutations, workflows,
|
|
8
|
+
integrations, charts and metrics are not covered.
|
|
10
9
|
metadata:
|
|
11
10
|
type: sub-skill
|
|
12
11
|
library: softr-sdk
|
|
@@ -23,53 +22,37 @@ metadata:
|
|
|
23
22
|
|
|
24
23
|
```ts
|
|
25
24
|
import { createServerFn } from '@tanstack/react-start'
|
|
25
|
+
import { defineAction } from '@softr/sdk'
|
|
26
26
|
import { ds } from '@softr/sdk/server'
|
|
27
|
-
import { q } from '@softr/sdk'
|
|
28
27
|
```
|
|
29
28
|
|
|
30
|
-
|
|
29
|
+
`ds` is server-only. `defineAction` only declares data operations, so it ships on the root entrypoint and the declaration can sit anywhere.
|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
## Declare the read
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
// correct
|
|
36
|
-
ds.list('8ffb4814-b028-4496-b679-7face2167ebc', { ... })
|
|
37
|
-
|
|
38
|
-
// wrong
|
|
39
|
-
const POSTS = '8ffb4814-b028-4496-b679-7face2167ebc'
|
|
40
|
-
ds.list(POSTS, { ... })
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
A record comes back as `{ id: string; fields: Record<string, unknown> }`. When you write a TypeScript type for the fields, the shape of each value depends on the Softr field type — see [references/field-types.md](./references/field-types.md) for the full mapping.
|
|
44
|
-
|
|
45
|
-
## Typing record fields
|
|
46
|
-
|
|
47
|
-
Declare a local type for the records you fetch using `SoftrRecord<Fields>`. Use the shapes from the field-types reference, not guesses from the field name. The most common mistake is treating SELECT and LINKED_RECORD fields as strings — they are objects:
|
|
33
|
+
Every `ds` call takes an action: which datasource, which fields, who may run it.
|
|
48
34
|
|
|
49
35
|
```ts
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
36
|
+
const publishedPosts = defineAction({
|
|
37
|
+
id: 'publishedPosts',
|
|
38
|
+
datasource: '8ffb4814-b028-4496-b679-7face2167ebc',
|
|
39
|
+
operation: 'read',
|
|
40
|
+
fields: {
|
|
41
|
+
title: 'fZk3Qm9pLw2Xa1', // SINGLE_LINE_TEXT
|
|
42
|
+
published: 'Ru7bVx2cNq0Ed9', // CHECKBOX
|
|
43
|
+
publishedAt: 'Xp2dLh6vKz9Fa3',// DATETIME
|
|
44
|
+
views: 'mB4kQ8yTn1sWc7', // NUMBER
|
|
45
|
+
},
|
|
46
|
+
permission: { userGroup: 'ALL_USERS' },
|
|
47
|
+
})
|
|
58
48
|
```
|
|
59
49
|
|
|
60
|
-
`
|
|
61
|
-
|
|
62
|
-
|
|
50
|
+
- `datasource` is the opaque UUID from the table's registration on the application (Softr Studio's Data tab, or the `register_application_data_source` tool when an agent does it). It is not the table id and is never derived from the table's name. `'ds_posts'` or a name glued to a uuid fails at request time as not found or forbidden.
|
|
51
|
+
- The keys of `fields` are names you choose. The values are field ids, opaque strings copied from `softr-env.d.ts` (see [Typing](#typing)) or the table's schema. Everything downstream speaks your names: the returned records, `where`, `orderBy`.
|
|
52
|
+
- `permission` declares who may run the action.
|
|
53
|
+
- Declare at module scope, every property a literal. The build reads the declaration without executing it, so a computed id, a shared constant or a spread does not work.
|
|
63
54
|
|
|
64
|
-
|
|
65
|
-
import type { ListResult, SoftrRecord } from '@softr/sdk'
|
|
66
|
-
|
|
67
|
-
const fetchProducts = createServerFn().handler(() =>
|
|
68
|
-
ds.list('3c1e8a52-6f0d-4b7e-9a21-5d84c0f7b2e6', { select, limit: 100 }) as Promise<ListResult<Product>>
|
|
69
|
-
)
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
`loaderData` is then already typed as `ListResult<Product>` — no second cast needed in the component. All the shapes in [references/field-types.md](./references/field-types.md) satisfy the `JsonValue` constraint.
|
|
55
|
+
An action fixes the table and the columns, not the rows: filters, sorting and paging stay at the call site, because a list screen changes them as the user types.
|
|
73
56
|
|
|
74
57
|
## Core Patterns
|
|
75
58
|
|
|
@@ -77,73 +60,62 @@ const fetchProducts = createServerFn().handler(() =>
|
|
|
77
60
|
|
|
78
61
|
```ts
|
|
79
62
|
const listPublishedPosts = createServerFn().handler(async () => {
|
|
80
|
-
return await ds
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
publishedAt: 'Xp2dLh6vKz9Fa3',
|
|
86
|
-
}),
|
|
87
|
-
where: q.boolean('published').is(true),
|
|
88
|
-
orderBy: q.desc('publishedAt'),
|
|
89
|
-
limit: 50,
|
|
90
|
-
})
|
|
63
|
+
return await ds
|
|
64
|
+
.list(publishedPosts)
|
|
65
|
+
.where((q) => q.published.is(true))
|
|
66
|
+
.orderBy((q) => q.publishedAt.desc)
|
|
67
|
+
.limit(50)
|
|
91
68
|
})
|
|
92
69
|
```
|
|
93
70
|
|
|
94
|
-
Returns `{ records, total, nextOffset }`. `
|
|
95
|
-
|
|
96
|
-
**Filtering or sorting requires a `select`, and it has to cover every field you name.** `where` and `orderBy` speak aliases, so without a map to translate through they throw at request time. That is why `published` is in the map above even though the table never displays it.
|
|
71
|
+
Returns `{ records, total, nextOffset }`. Each record is `{ id, fields }`, with `fields` keyed by the action's own names: every declared field is present, `null` when empty, and a field the action does not declare is not returned. `ds.list(publishedPosts)` with no calls chained returns the first 20 records. `limit` is clamped to 100; a larger number is silently reduced, so more than a hundred records means paginating.
|
|
97
72
|
|
|
98
|
-
###
|
|
73
|
+
### Filter with the `where` callback
|
|
99
74
|
|
|
100
|
-
|
|
75
|
+
The callback is handed one handle per declared field, carrying only the operators that field's type supports, plus `$and` and `$or`:
|
|
101
76
|
|
|
102
77
|
```ts
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// records[0].fields is { title: ..., views: ... }
|
|
78
|
+
await ds.list(publishedPosts).where((q) =>
|
|
79
|
+
q.$and(
|
|
80
|
+
q.published.is(true),
|
|
81
|
+
q.views.gte(100),
|
|
82
|
+
q.$or(q.title.contains('cafe'), q.title.startsWith('The')),
|
|
83
|
+
),
|
|
84
|
+
)
|
|
111
85
|
```
|
|
112
86
|
|
|
113
|
-
|
|
87
|
+
Calling `.where` more than once ands the filters together, which is how a fixed scope and a user-supplied filter combine. Only the action's own fields can be filtered or sorted on; a field you need in a `where` must be in the action's `fields`, even when the page never displays it. The full operator matrix is in [references/query-builder.md](./references/query-builder.md).
|
|
114
88
|
|
|
115
|
-
###
|
|
89
|
+
### Sort
|
|
116
90
|
|
|
117
91
|
```ts
|
|
118
|
-
|
|
119
|
-
|
|
92
|
+
.orderBy((q) => q.publishedAt.desc)
|
|
93
|
+
.orderBy((q) => [q.views.desc, q.title.asc]) // array for tie-breakers
|
|
120
94
|
```
|
|
121
95
|
|
|
122
|
-
|
|
96
|
+
### Get one record
|
|
123
97
|
|
|
124
|
-
|
|
98
|
+
```ts
|
|
99
|
+
const post = await ds.get(publishedPosts, { id: postId })
|
|
100
|
+
if (post === null) throw notFound()
|
|
101
|
+
```
|
|
125
102
|
|
|
126
|
-
|
|
103
|
+
A record that does not exist comes back as `null`, not an error. A chained `.where` narrows which record the id may resolve to. A record that does not match reads as `null`, the same as one that does not exist:
|
|
127
104
|
|
|
128
105
|
```ts
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
q.or(
|
|
133
|
-
q.text('owner').is(user.id),
|
|
134
|
-
q.array('collaborators').hasAllOf([user.id]),
|
|
135
|
-
),
|
|
136
|
-
)
|
|
106
|
+
const post = await ds
|
|
107
|
+
.get(publishedPosts, { id: postId })
|
|
108
|
+
.where((q) => q.published.is(true))
|
|
137
109
|
```
|
|
138
110
|
|
|
139
|
-
|
|
111
|
+
There is no `orderBy`, `limit` or `offset` on a get. To find a record by a field instead of its id, use `ds.list` with a `limit` of 1.
|
|
140
112
|
|
|
141
113
|
### Paginate
|
|
142
114
|
|
|
143
115
|
```ts
|
|
144
|
-
const first = await ds.list(
|
|
116
|
+
const first = await ds.list(publishedPosts).limit(50)
|
|
145
117
|
if (first.nextOffset !== null) {
|
|
146
|
-
const second = await ds.list(
|
|
118
|
+
const second = await ds.list(publishedPosts).limit(50).offset(first.nextOffset)
|
|
147
119
|
}
|
|
148
120
|
```
|
|
149
121
|
|
|
@@ -152,13 +124,18 @@ if (first.nextOffset !== null) {
|
|
|
152
124
|
### Scope the read to the visitor
|
|
153
125
|
|
|
154
126
|
```ts
|
|
127
|
+
const myInvoices = defineAction({
|
|
128
|
+
id: 'myInvoices',
|
|
129
|
+
datasource: 'e4b7c2a1-9d3f-4e68-b05a-7f1c6d2e8a93',
|
|
130
|
+
operation: 'read',
|
|
131
|
+
fields: { owner: 'Gq5wNt8rJy3Hb1', amount: 'Vk9cXm2pSz6Ld4' },
|
|
132
|
+
permission: { userGroup: 'LOGGED_IN_USERS' },
|
|
133
|
+
})
|
|
134
|
+
|
|
155
135
|
const listMyInvoices = createServerFn().handler(async ({ context }) => {
|
|
156
136
|
if (!context.user) throw new Error('Not signed in')
|
|
157
|
-
|
|
158
|
-
return await ds.list(
|
|
159
|
-
select: q.select({ owner: 'Gq5wNt8rJy3Hb1', amount: 'Vk9cXm2pSz6Ld4' }),
|
|
160
|
-
where: q.text('owner').is(context.user.id),
|
|
161
|
-
})
|
|
137
|
+
const userId = context.user.id
|
|
138
|
+
return await ds.list(myInvoices).where((q) => q.owner.is(userId))
|
|
162
139
|
})
|
|
163
140
|
```
|
|
164
141
|
|
|
@@ -168,9 +145,16 @@ A server function is its own endpoint, reachable whether or not the visitor ever
|
|
|
168
145
|
|
|
169
146
|
A read the visitor is not allowed to make throws rather than coming back empty, and it throws a plain `Error` carrying the status. There is no typed predicate to narrow it with. Handle it where you would handle any other failed fetch: an `errorComponent` on the route, or a check in the handler.
|
|
170
147
|
|
|
148
|
+
## Typing
|
|
149
|
+
|
|
150
|
+
Records, filters and sorts are typed from `softr-env.d.ts`, the generated file at the app root describing every datasource registered on the application. Regenerate it with `npx @softr/sdk generate` after the data changes. With it in place, field ids autocomplete inside `fields`, each `where` handle offers only its field's operators, and `records[0].fields` carries real value types with no cast anywhere. Without it everything still runs, but field values type as `unknown` and every handle offers every operator.
|
|
151
|
+
|
|
152
|
+
What each Softr field type reads back as is in [references/field-types.md](./references/field-types.md). The two most commonly mistyped: SELECT and LINKED_RECORD values are `{ id, label }` objects, never plain strings. Render the `label`; filter by the `id`.
|
|
153
|
+
|
|
171
154
|
## References
|
|
172
155
|
|
|
173
|
-
- [references/
|
|
174
|
-
- [references/
|
|
156
|
+
- [references/query-builder.md](./references/query-builder.md): every filter operator, by field type
|
|
157
|
+
- [references/field-types.md](./references/field-types.md): the value shape every Softr field type reads back as
|
|
175
158
|
- [current-user/SKILL.md](../current-user/SKILL.md): reading the visitor off `context` to scope a query
|
|
176
159
|
- [routes-and-visibility/SKILL.md](../routes-and-visibility/SKILL.md): deciding who sees the page the records land on
|
|
160
|
+
- [charts-and-metrics/SKILL.md](../charts-and-metrics/SKILL.md): aggregating records instead of listing them
|
|
@@ -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 |
|
|
@@ -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 };
|