@softr/sdk 0.0.18 → 0.0.20
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.d.ts +2 -2
- package/dist/types.d.ts +12 -9
- package/package.json +1 -1
- package/skills/softr-sdk/SKILL.md +9 -4
- package/skills/softr-sdk/auth/SKILL.md +1 -1
- package/skills/softr-sdk/current-user/SKILL.md +1 -1
- package/skills/softr-sdk/data-mutations/SKILL.md +115 -0
- package/skills/softr-sdk/reading-records/SKILL.md +32 -2
- package/skills/softr-sdk/reading-records/references/field-types.md +70 -0
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { q } from "./core/query.js";
|
|
2
2
|
import { SoftrUserGroupId, SoftrUserGroups, User } from "./auth/user.js";
|
|
3
|
-
import { AggregationFunction, ChartDataOptions, ChartDataResult, CreateOptions, DatasourcePermissions, GetOptions, ListOptions, ListResult, MetricOptions, MetricResult, SoftrMe, SoftrPermissions, SoftrRecord, SoftrUser, UpdateOptions } from "./types.js";
|
|
3
|
+
import { AggregationFunction, ChartDataOptions, ChartDataResult, CreateOptions, DatasourcePermissions, GetOptions, JsonValue, ListOptions, ListResult, MetricOptions, MetricResult, SoftrMe, SoftrPermissions, SoftrRecord, SoftrUser, UpdateOptions } from "./types.js";
|
|
4
4
|
import { Permissions } from "./auth/permissions.js";
|
|
5
5
|
import { SoftrContext } from "./auth/context.js";
|
|
6
6
|
import { AccessBehaviour, AccessCheck, AccessError, AccessReason, RouteVisibility, VisibilityHandle, defineVisibility, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError } from "./routes/visibility.js";
|
|
7
7
|
import { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser, usePermissions } from "./auth/isomorphic.js";
|
|
8
8
|
import { useUserTimezone } from "./timezone.js";
|
|
9
|
-
export { type AccessBehaviour, type AccessCheck, type AccessError, type AccessReason, type AggregationFunction, type ChartDataOptions, type ChartDataResult, type CreateOptions, type DatasourcePermissions, type GetOptions, type ListOptions, type ListResult, type MetricOptions, type MetricResult, type Permissions, type RouteVisibility, type SoftrContext, type SoftrMe, type SoftrPermissions, type SoftrRecord, type SoftrUser, type SoftrUserGroupId, type SoftrUserGroups, type UpdateOptions, type User, type VisibilityHandle, defineVisibility, getSoftrContext, initialSoftrContext, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, q, softrAuthMiddleware, useCurrentUser, usePermissions, useUserTimezone };
|
|
9
|
+
export { type AccessBehaviour, type AccessCheck, type AccessError, type AccessReason, type AggregationFunction, type ChartDataOptions, type ChartDataResult, type CreateOptions, type DatasourcePermissions, type GetOptions, type JsonValue, type ListOptions, type ListResult, type MetricOptions, type MetricResult, type Permissions, type RouteVisibility, type SoftrContext, type SoftrMe, type SoftrPermissions, type SoftrRecord, type SoftrUser, type SoftrUserGroupId, type SoftrUserGroups, type UpdateOptions, type User, type VisibilityHandle, defineVisibility, getSoftrContext, initialSoftrContext, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, q, softrAuthMiddleware, useCurrentUser, usePermissions, useUserTimezone };
|
package/dist/types.d.ts
CHANGED
|
@@ -53,16 +53,19 @@ interface ListOptions {
|
|
|
53
53
|
/** Override the timezone for this request. */
|
|
54
54
|
timeZone?: string;
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
57
|
+
[key: string]: JsonValue;
|
|
58
|
+
};
|
|
59
|
+
interface SoftrRecord<T extends Record<string, JsonValue> = Record<string, JsonValue>> {
|
|
60
|
+
id: string;
|
|
61
|
+
fields: T;
|
|
62
|
+
}
|
|
63
|
+
interface ListResult<T extends SoftrRecord = SoftrRecord> {
|
|
57
64
|
records: T[];
|
|
58
65
|
total: number;
|
|
59
66
|
/** Offset to pass as `offset` in the next call, or `null` when there are no more pages. */
|
|
60
67
|
nextOffset: number | null;
|
|
61
68
|
}
|
|
62
|
-
interface SoftrRecord {
|
|
63
|
-
id: string;
|
|
64
|
-
fields: Record<string, unknown>;
|
|
65
|
-
}
|
|
66
69
|
type AggregationFunction = 'SUM' | 'AVG' | 'MAX' | 'MIN' | 'COUNT' | 'DISTINCT';
|
|
67
70
|
interface MetricOptions {
|
|
68
71
|
select?: Record<string, string>;
|
|
@@ -91,7 +94,7 @@ interface ChartDataOptions {
|
|
|
91
94
|
timeZone?: string;
|
|
92
95
|
}
|
|
93
96
|
interface ChartDataResult {
|
|
94
|
-
data: Record<string,
|
|
97
|
+
data: Record<string, JsonValue>[];
|
|
95
98
|
executionTimestamp: Date;
|
|
96
99
|
}
|
|
97
100
|
interface CreateOptions {
|
|
@@ -100,7 +103,7 @@ interface CreateOptions {
|
|
|
100
103
|
* When provided, `data` keys are interpreted as aliases.
|
|
101
104
|
*/
|
|
102
105
|
select?: Record<string, string>;
|
|
103
|
-
data: Record<string,
|
|
106
|
+
data: Record<string, JsonValue>;
|
|
104
107
|
}
|
|
105
108
|
interface UpdateOptions {
|
|
106
109
|
id: string;
|
|
@@ -109,7 +112,7 @@ interface UpdateOptions {
|
|
|
109
112
|
* When provided, `data` keys are interpreted as aliases.
|
|
110
113
|
*/
|
|
111
114
|
select?: Record<string, string>;
|
|
112
|
-
data: Record<string,
|
|
115
|
+
data: Record<string, JsonValue>;
|
|
113
116
|
}
|
|
114
117
|
//#endregion
|
|
115
|
-
export { AggregationFunction, ChartDataOptions, ChartDataResult, CreateOptions, DatasourcePermissions, GetOptions, ListOptions, ListResult, MetricOptions, MetricResult, SoftrMe, SoftrPermissions, SoftrRecord, SoftrUser, UpdateOptions };
|
|
118
|
+
export { AggregationFunction, ChartDataOptions, ChartDataResult, CreateOptions, DatasourcePermissions, GetOptions, JsonValue, ListOptions, ListResult, MetricOptions, MetricResult, SoftrMe, SoftrPermissions, SoftrRecord, SoftrUser, UpdateOptions };
|
package/package.json
CHANGED
|
@@ -5,12 +5,12 @@ description: >-
|
|
|
5
5
|
four entrypoints (@softr/sdk, /server, /client, /vite) and the client-server
|
|
6
6
|
boundary between them, softrAuthMiddleware, getSoftrContext,
|
|
7
7
|
initialSoftrContext, SoftrContext, and which sub-skill to load for route
|
|
8
|
-
visibility, the current user, reading records, or auth. Load
|
|
9
|
-
other Softr SDK skill.
|
|
8
|
+
visibility, the current user, reading records, writing records, or auth. Load
|
|
9
|
+
this before any other Softr SDK skill.
|
|
10
10
|
metadata:
|
|
11
11
|
type: core
|
|
12
12
|
library: softr-sdk
|
|
13
|
-
library_version: 0.0.
|
|
13
|
+
library_version: 0.0.20
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
# Softr SDK core
|
|
@@ -20,6 +20,7 @@ metadata:
|
|
|
20
20
|
> **CRITICAL**: The SDK is split across four entrypoints and the split is enforced. `ds` and `auth` exist only on `@softr/sdk/server` and only run inside a `createServerFn` handler. Importing them anywhere else fails the build.
|
|
21
21
|
> **CRITICAL**: Every route file must declare its audience with `defineVisibility`. There is no default and nothing is inherited from a sibling. A route file without a declaration is a lint error.
|
|
22
22
|
> **CRITICAL**: The app is already wired. `src/start.ts`, `src/router.tsx` and `src/routes/__root.tsx` register the middleware and resolve the session. Do not add a second registration.
|
|
23
|
+
> **CRITICAL**: `ds.metric`, `ds.chartData`, `workflow.trigger` and `integration.fetch` are exploratory. They exist on the entrypoints below and they typecheck, but their endpoints are not final. Do not build a feature on any of them. Tell the user the capability is not ready and ask how they want to proceed.
|
|
23
24
|
|
|
24
25
|
Load this skill, then load **one** sub-skill for the task at hand. Add a second only when the work genuinely crosses that boundary. A page that lists records behind a group restriction needs `reading-records` plus `routes-and-visibility`.
|
|
25
26
|
|
|
@@ -39,6 +40,7 @@ Load this skill, then load **one** sub-skill for the task at hand. Add a second
|
|
|
39
40
|
| Add a route, or change who can see one | [routes-and-visibility/SKILL.md](./routes-and-visibility/SKILL.md) |
|
|
40
41
|
| Read the signed-in visitor or what they may do | [current-user/SKILL.md](./current-user/SKILL.md) |
|
|
41
42
|
| Fetch records from a datasource | [reading-records/SKILL.md](./reading-records/SKILL.md) |
|
|
43
|
+
| Create, update or delete a record | [data-mutations/SKILL.md](./data-mutations/SKILL.md) |
|
|
42
44
|
| Sign a visitor in, out, or up | [auth/SKILL.md](./auth/SKILL.md) |
|
|
43
45
|
|
|
44
46
|
## Quick Decision Tree
|
|
@@ -53,6 +55,9 @@ Showing the visitor's name, or hiding UI they cannot use?
|
|
|
53
55
|
Putting records on a page, in a table, a list or a detail view?
|
|
54
56
|
load softr-sdk/reading-records
|
|
55
57
|
|
|
58
|
+
Submitting a form, editing a record, or deleting one?
|
|
59
|
+
load softr-sdk/data-mutations
|
|
60
|
+
|
|
56
61
|
Wiring a login, signup, or sign-out?
|
|
57
62
|
load softr-sdk/auth
|
|
58
63
|
|
|
@@ -118,4 +123,4 @@ type User = {
|
|
|
118
123
|
|
|
119
124
|
## Version
|
|
120
125
|
|
|
121
|
-
Targets `@softr/sdk` v0.0.
|
|
126
|
+
Targets `@softr/sdk` v0.0.20.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: data-mutations
|
|
3
|
+
description: >-
|
|
4
|
+
Create, update and delete records in a Softr datasource from inside a server
|
|
5
|
+
function: ds.create, ds.update and ds.delete from @softr/sdk/server, the data
|
|
6
|
+
payload key, select field aliasing on the way in and out, and what each call
|
|
7
|
+
returns or throws. Writes only. Reads, metrics and chart data are not covered.
|
|
8
|
+
metadata:
|
|
9
|
+
type: sub-skill
|
|
10
|
+
library: softr-sdk
|
|
11
|
+
library_version: 0.0.20
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
This skill builds on softr-sdk. Read it first for the entrypoints and the client/server boundary.
|
|
15
|
+
|
|
16
|
+
# Writing records
|
|
17
|
+
|
|
18
|
+
`ds` writes records to a Softr datasource. Like every read, a write lives on `@softr/sdk/server` and runs on the server only, so it happens inside a `createServerFn` handler and never in a component.
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
24
|
+
import { z } from 'zod'
|
|
25
|
+
import { ds } from '@softr/sdk/server'
|
|
26
|
+
import { q } from '@softr/sdk'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Pass a zod schema straight to `.validator`. Start accepts it and types `data` from it.
|
|
30
|
+
|
|
31
|
+
## Core Patterns
|
|
32
|
+
|
|
33
|
+
### Create a record
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const NewProject = z.object({ name: z.string(), status: z.string() })
|
|
37
|
+
|
|
38
|
+
const createProject = createServerFn({ method: 'POST' })
|
|
39
|
+
.validator(NewProject)
|
|
40
|
+
.handler(async ({ data }) =>
|
|
41
|
+
ds.create('ds_projects', {
|
|
42
|
+
data,
|
|
43
|
+
select: q.select({ name: 'fld_name', status: 'fld_status' }),
|
|
44
|
+
}),
|
|
45
|
+
)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Returns the created record as `{ id, fields }`.
|
|
49
|
+
|
|
50
|
+
**The payload key is `data`, not `fields`.** A record answers with `fields`, which is where the confusion comes from, but the option you pass is `data`. `CreateOptions` requires it, so a call keyed by `fields` does not type-check.
|
|
51
|
+
|
|
52
|
+
**`data` speaks whatever `select` maps.** The keys above are the schema's own names because `select` maps them to field ids, which is why the validated input can go straight through. With no `select`, `data` has to be keyed by raw field ids instead.
|
|
53
|
+
|
|
54
|
+
### Update a record
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const Rename = z.object({ id: z.string(), name: z.string() })
|
|
58
|
+
|
|
59
|
+
const renameProject = createServerFn({ method: 'POST' })
|
|
60
|
+
.validator(Rename)
|
|
61
|
+
.handler(async ({ data }) =>
|
|
62
|
+
ds.update('ds_projects', {
|
|
63
|
+
id: data.id,
|
|
64
|
+
data: { name: data.name },
|
|
65
|
+
select: q.select({ name: 'fld_name' }),
|
|
66
|
+
}),
|
|
67
|
+
)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The record id is its own option, so keep it out of `data`.
|
|
71
|
+
|
|
72
|
+
Returns the updated record. `update` writes the keys in `data` and leaves every other field alone, so a field you leave out is a field you are not writing. An empty `id` throws.
|
|
73
|
+
|
|
74
|
+
### Delete a record
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const RecordId = z.object({ id: z.string() })
|
|
78
|
+
|
|
79
|
+
const deleteProject = createServerFn({ method: 'POST' })
|
|
80
|
+
.validator(RecordId)
|
|
81
|
+
.handler(async ({ data }) => ds.delete('ds_projects', data.id))
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**`delete` takes the record id as a positional argument**, unlike `create` and `update`, which take an options object. It resolves to nothing.
|
|
85
|
+
|
|
86
|
+
### Naming fields with `select`
|
|
87
|
+
|
|
88
|
+
`select` works in both directions on a write. Going in, it turns your names into Softr field ids. Coming back, it turns the returned field ids into your names. Without it, `data` has to be keyed by raw field ids and the returned record answers with them too.
|
|
89
|
+
|
|
90
|
+
The map has to cover every key in `data`. A key it does not map is not translated, so the write does not land on the field you meant.
|
|
91
|
+
|
|
92
|
+
### Writing on behalf of the visitor
|
|
93
|
+
|
|
94
|
+
Stamping a record with who created it, or scoping an update to the person making it, reads the visitor off the handler's `context`. That is [current-user](../current-user/SKILL.md)'s subject, including the null guest and how Start types it. Load that skill rather than looking for it here.
|
|
95
|
+
|
|
96
|
+
## Failure Modes
|
|
97
|
+
|
|
98
|
+
**A write that sends `fields`.** The option is `data`. Only typecheck catches this, so it survives any run that does not typecheck.
|
|
99
|
+
|
|
100
|
+
**A key missing from `select`.** The key is not translated, so the field it was meant for is not the field written. Check the map covers every key rather than trusting the call to complain.
|
|
101
|
+
|
|
102
|
+
**A write with no `select` and your own key names.** Without a map, `data` must be keyed by raw field ids.
|
|
103
|
+
|
|
104
|
+
**Passing an options object to `delete`.** The second argument is the id itself. `delete` throws when the record cannot be deleted.
|
|
105
|
+
|
|
106
|
+
**Expecting `select` on a read-then-write round trip.** `ds.get` takes no `select`, so a record fetched by id answers with raw field ids. Load `reading-records` before pairing a read with a write.
|
|
107
|
+
|
|
108
|
+
**A write outside a server function.** `ds` is server-only. Importing it into a component fails the build.
|
|
109
|
+
|
|
110
|
+
**Reading the visitor from somewhere other than `context`.** There is nothing to import. See [current-user](../current-user/SKILL.md).
|
|
111
|
+
|
|
112
|
+
## References
|
|
113
|
+
|
|
114
|
+
- [reading-records/SKILL.md](../reading-records/SKILL.md): fetching the record a write is about to change
|
|
115
|
+
- [current-user/SKILL.md](../current-user/SKILL.md): reading the visitor off `context` to stamp or scope a write
|
|
@@ -10,7 +10,7 @@ description: >-
|
|
|
10
10
|
metadata:
|
|
11
11
|
type: sub-skill
|
|
12
12
|
library: softr-sdk
|
|
13
|
-
library_version: 0.0.
|
|
13
|
+
library_version: 0.0.20
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
This skill builds on softr-sdk. Read it first for the entrypoints and the client/server boundary.
|
|
@@ -29,7 +29,36 @@ import { q } from '@softr/sdk'
|
|
|
29
29
|
|
|
30
30
|
`ds` is server-only and lives on `@softr/sdk/server`. `q` builds queries and nothing more, so it ships on the root entrypoint and can be imported anywhere.
|
|
31
31
|
|
|
32
|
-
A record comes back as `{ id: string; fields: Record<string, unknown> }`.
|
|
32
|
+
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.
|
|
33
|
+
|
|
34
|
+
## Typing record fields
|
|
35
|
+
|
|
36
|
+
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:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import type { SoftrRecord } from '@softr/sdk'
|
|
40
|
+
|
|
41
|
+
type Product = SoftrRecord<{
|
|
42
|
+
name: string // SINGLE_LINE_TEXT
|
|
43
|
+
price: number | null // CURRENCY
|
|
44
|
+
category: { id: string; label: string } // SELECT — not a string
|
|
45
|
+
supplier: { id: string; label: string } // LINKED_RECORD — not a string
|
|
46
|
+
}>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`SoftrRecord<Fields>` is shorthand for `{ id: string; fields: Fields }` — the `Fields` type parameter constrains every value to `JsonValue`, so TypeScript rejects non-serializable types like `Date` objects.
|
|
50
|
+
|
|
51
|
+
Cast at the `ds.list` call using the `ListResult` generic so the type flows through the loader automatically:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import type { ListResult, SoftrRecord } from '@softr/sdk'
|
|
55
|
+
|
|
56
|
+
const fetchProducts = createServerFn().handler(() =>
|
|
57
|
+
ds.list('products', { select, limit: 100 }) as Promise<ListResult<Product>>
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`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.
|
|
33
62
|
|
|
34
63
|
## Core Patterns
|
|
35
64
|
|
|
@@ -130,6 +159,7 @@ A read the visitor is not allowed to make throws rather than coming back empty,
|
|
|
130
159
|
|
|
131
160
|
## References
|
|
132
161
|
|
|
162
|
+
- [references/field-types.md](./references/field-types.md): TypeScript shape for every Softr field type
|
|
133
163
|
- [references/query-builder.md](./references/query-builder.md): every `q` field type and operator
|
|
134
164
|
- [current-user/SKILL.md](../current-user/SKILL.md): reading the visitor off `context` to scope a query
|
|
135
165
|
- [routes-and-visibility/SKILL.md](../routes-and-visibility/SKILL.md): deciding who sees the page the records land on
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Softr field types — TypeScript shapes
|
|
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.
|
|
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 }
|
|
35
|
+
|
|
36
|
+
DATE_RANGE: { from: string; to: string }
|
|
37
|
+
|
|
38
|
+
ATTACHMENT: {
|
|
39
|
+
filename: string | null | undefined
|
|
40
|
+
id: string | null | undefined
|
|
41
|
+
type: string | null | undefined
|
|
42
|
+
url: string
|
|
43
|
+
thumbnails?: Array<{ size: 'small' | 'medium' | 'large'; url: string }>
|
|
44
|
+
}
|
|
45
|
+
|
|
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
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## The non-obvious ones
|
|
57
|
+
|
|
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`.
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
// Correct
|
|
62
|
+
<td>{record.fields.status?.label}</td>
|
|
63
|
+
|
|
64
|
+
// Wrong — status is not a string
|
|
65
|
+
<td>{record.fields.status}</td>
|
|
66
|
+
```
|
|
67
|
+
|
|
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`.
|
|
69
|
+
|
|
70
|
+
**CREATED_BY / UPDATED_BY** can be a plain id string, an array of ids, or a full user object. Narrow before using.
|