@softr/sdk 0.0.75 → 0.0.76
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/codegen/datasource-types.js +1 -14
- package/dist/core/options.d.ts +17 -2
- package/dist/core/options.js +30 -2
- package/dist/datasource/datasource.d.ts +2 -1
- package/dist/datasource/datasource.js +3 -2
- package/dist/datasource/fields.d.ts +16 -24
- package/dist/package.js +1 -1
- package/dist/server.d.ts +2 -2
- package/package.json +1 -1
- package/skills/softr-sdk/SKILL.md +1 -1
- package/skills/softr-sdk/auth/SKILL.md +1 -1
- package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
- package/skills/softr-sdk/comments/SKILL.md +1 -1
- package/skills/softr-sdk/current-user/SKILL.md +1 -1
- package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
- package/skills/softr-sdk/formatting/SKILL.md +1 -1
- package/skills/softr-sdk/integrations/SKILL.md +1 -1
- package/skills/softr-sdk/mcp/SKILL.md +1 -1
- package/skills/softr-sdk/pwa/SKILL.md +1 -1
- package/skills/softr-sdk/record-mutations/SKILL.md +2 -2
- package/skills/softr-sdk/record-queries/SKILL.md +8 -1
- package/skills/softr-sdk/record-queries/references/filters-and-sorting.md +1 -1
- package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/workflows/SKILL.md +1 -1
|
@@ -161,7 +161,7 @@ function writeValue(shape, context) {
|
|
|
161
161
|
case "UPDATED_AT": return "IsoDateTime";
|
|
162
162
|
case "TIME": return "IsoTime";
|
|
163
163
|
case "DATE_RANGE": return "{ from: IsoDate; to: IsoDate }";
|
|
164
|
-
case "SELECT": return
|
|
164
|
+
case "SELECT": return "SelectChoiceInput";
|
|
165
165
|
case "ATTACHMENT": return "AttachmentInput";
|
|
166
166
|
case "LINKED_RECORD": return "LinkedRecordInput";
|
|
167
167
|
case "USER": return "WorkspaceUser";
|
|
@@ -208,19 +208,6 @@ function selectValue(options) {
|
|
|
208
208
|
if (choices === null) return "SelectChoice";
|
|
209
209
|
return union(choices.map(({ id, label }) => `{ id: ${quote(id)}; label: ${quote(label)} }`));
|
|
210
210
|
}
|
|
211
|
-
/**
|
|
212
|
-
* A write carries the id alone. The API resolves it against the choice list and writes that
|
|
213
|
-
* choice's own label, so a label sent alongside is dropped.
|
|
214
|
-
*
|
|
215
|
-
* The API also takes `{ label }` with no id, and a label matching nothing creates a choice.
|
|
216
|
-
* The union omits that: it would make every misspelling type-check, and the cost of one is a
|
|
217
|
-
* new choice in the field rather than an error.
|
|
218
|
-
*/
|
|
219
|
-
function selectInput(options) {
|
|
220
|
-
const choices = choiceEntries(options);
|
|
221
|
-
if (choices === null) return "SelectChoiceInput";
|
|
222
|
-
return union(choices.map(({ id }) => `{ id: ${quote(id)} }`));
|
|
223
|
-
}
|
|
224
211
|
function datePrecision(options) {
|
|
225
212
|
const precision = option(options, "precision");
|
|
226
213
|
return typeof precision === "string" ? precision : null;
|
package/dist/core/options.d.ts
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
|
-
import { DatasourceAction, ReadShape } from "../action.js";
|
|
1
|
+
import { ActionShape, DatasourceAction, ReadShape } from "../action.js";
|
|
2
2
|
import "@tanstack/react-start/server-only";
|
|
3
3
|
|
|
4
4
|
//#region src/core/options.d.ts
|
|
5
|
+
/** One choice of a SELECT field, as the field holds it now. */
|
|
6
|
+
type FieldChoice = {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
color: string | null;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The choices a SELECT field holds right now, read from Softr on every call. A choice the
|
|
13
|
+
* builder adds or renames later shows up without a new build. The field must be one the
|
|
14
|
+
* action declares. Any query or mutation over the table works.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const loadCategories = createServerFn().handler(() => ds.choices(products, 'category'))
|
|
18
|
+
*/
|
|
19
|
+
declare function listChoices<TShape extends ActionShape>(action: DatasourceAction<TShape>, field: keyof TShape["fields"] & string): Promise<Array<FieldChoice>>;
|
|
5
20
|
/** One row a linked-record picker offers. */
|
|
6
21
|
type RecordOption = {
|
|
7
22
|
id: string;
|
|
@@ -33,4 +48,4 @@ declare function listOptions<TShape extends ReadShape>(query: DatasourceAction<T
|
|
|
33
48
|
limit?: number;
|
|
34
49
|
}): Promise<RecordOptions>;
|
|
35
50
|
//#endregion
|
|
36
|
-
export { RecordOption, RecordOptions, listOptions };
|
|
51
|
+
export { FieldChoice, RecordOption, RecordOptions, listChoices, listOptions };
|
package/dist/core/options.js
CHANGED
|
@@ -1,9 +1,37 @@
|
|
|
1
|
+
import { assert } from "../assertions.js";
|
|
2
|
+
import { fetch } from "../fetch.js";
|
|
3
|
+
import { readSessionToken } from "../auth/session.js";
|
|
1
4
|
import { makeChain } from "../datasource/fields.js";
|
|
2
|
-
import { listRecords } from "../datasource/record.js";
|
|
5
|
+
import { describeFailure, dsUrl, listRecords, refererHeader, resolveTarget } from "../datasource/record.js";
|
|
3
6
|
import "@tanstack/react-start/server-only";
|
|
4
7
|
|
|
5
8
|
//#region src/core/options.ts
|
|
6
9
|
/**
|
|
10
|
+
* The choices a SELECT field holds right now, read from Softr on every call. A choice the
|
|
11
|
+
* builder adds or renames later shows up without a new build. The field must be one the
|
|
12
|
+
* action declares. Any query or mutation over the table works.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const loadCategories = createServerFn().handler(() => ds.choices(products, 'category'))
|
|
16
|
+
*/
|
|
17
|
+
async function listChoices(action, field) {
|
|
18
|
+
const { actionId, select } = resolveTarget(action, action.resource.operation);
|
|
19
|
+
const fieldId = select[field];
|
|
20
|
+
assert(fieldId, () => `action "${actionId}" declares no field "${field}"`);
|
|
21
|
+
const res = await fetch(`${dsUrl(actionId)}/metadata`, { headers: {
|
|
22
|
+
Authorization: `Bearer ${readSessionToken()}`,
|
|
23
|
+
...refererHeader()
|
|
24
|
+
} });
|
|
25
|
+
if (!res.ok) throw new Error(`datasource metadata failed for "${actionId}": ${await describeFailure(res)}`);
|
|
26
|
+
const metadataField = (await res.json()).fields.find(({ id }) => id === fieldId);
|
|
27
|
+
assert(metadataField?.type === "SELECT", () => `"${field}" on action "${actionId}" is not a SELECT field`);
|
|
28
|
+
return (metadataField.options?.choices ?? []).map(({ id, label, color }) => ({
|
|
29
|
+
id,
|
|
30
|
+
label,
|
|
31
|
+
color: color ?? null
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
7
35
|
* The rows a linked field can point at, for a picker: each record of the query as its id and the text
|
|
8
36
|
* of one field, searched on that field when a term is given, a page at a time.
|
|
9
37
|
*
|
|
@@ -36,4 +64,4 @@ function labelOf(value) {
|
|
|
36
64
|
}
|
|
37
65
|
|
|
38
66
|
//#endregion
|
|
39
|
-
export { listOptions };
|
|
67
|
+
export { listChoices, listOptions };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRecord, deleteRecord, getRecord, listRecords, updateRecord } from "./record.js";
|
|
2
|
-
import { listOptions } from "../core/options.js";
|
|
2
|
+
import { listChoices, listOptions } from "../core/options.js";
|
|
3
3
|
import { chart, metric } from "./chart.js";
|
|
4
4
|
import "@tanstack/react-start/server-only";
|
|
5
5
|
|
|
@@ -13,6 +13,7 @@ declare const ds: {
|
|
|
13
13
|
metric: typeof metric;
|
|
14
14
|
chart: typeof chart;
|
|
15
15
|
options: typeof listOptions;
|
|
16
|
+
choices: typeof listChoices;
|
|
16
17
|
};
|
|
17
18
|
//#endregion
|
|
18
19
|
export { ds };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRecord, deleteRecord, getRecord, listRecords, updateRecord } from "./record.js";
|
|
2
2
|
import { chart, metric } from "./chart.js";
|
|
3
|
-
import { listOptions } from "../core/options.js";
|
|
3
|
+
import { listChoices, listOptions } from "../core/options.js";
|
|
4
4
|
import "@tanstack/react-start/server-only";
|
|
5
5
|
|
|
6
6
|
//#region src/datasource/datasource.ts
|
|
@@ -12,7 +12,8 @@ const ds = {
|
|
|
12
12
|
delete: deleteRecord,
|
|
13
13
|
metric,
|
|
14
14
|
chart,
|
|
15
|
-
options: listOptions
|
|
15
|
+
options: listOptions,
|
|
16
|
+
choices: listChoices
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
//#endregion
|
|
@@ -19,17 +19,21 @@ type NumberChain = EmptyChain & {
|
|
|
19
19
|
lte(value: number): ConstructableFilter;
|
|
20
20
|
between(start: number, end: number): ConstructableFilter;
|
|
21
21
|
};
|
|
22
|
-
type ArrayChain
|
|
23
|
-
is(values: Array<
|
|
24
|
-
isOneOf(values: Array<
|
|
25
|
-
isNoneOf(values: Array<
|
|
26
|
-
hasAllOf(values: Array<
|
|
22
|
+
type ArrayChain = EmptyChain & {
|
|
23
|
+
is(values: Array<string>): ConstructableFilter;
|
|
24
|
+
isOneOf(values: Array<string>): ConstructableFilter;
|
|
25
|
+
isNoneOf(values: Array<string>): ConstructableFilter;
|
|
26
|
+
hasAllOf(values: Array<string>): ConstructableFilter;
|
|
27
27
|
};
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Compares choice ids as plain strings. A select's choices change in Studio after the build,
|
|
30
|
+
* and an id read at runtime from ds.choices has to be a valid filter value.
|
|
31
|
+
*/
|
|
32
|
+
type ChoiceChain = EmptyChain & {
|
|
33
|
+
is(value: string): ConstructableFilter;
|
|
34
|
+
isNot(value: string): ConstructableFilter;
|
|
35
|
+
isOneOf(values: Array<string>): ConstructableFilter;
|
|
36
|
+
isNoneOf(values: Array<string>): ConstructableFilter;
|
|
33
37
|
};
|
|
34
38
|
/** What DSv2 filters generically: matched whole or scanned, never against a set. */
|
|
35
39
|
type GenericChain = EmptyChain & {
|
|
@@ -73,32 +77,20 @@ type AnyChain = EmptyChain & {
|
|
|
73
77
|
between(start: number | string, end: number | string): ConstructableFilter;
|
|
74
78
|
isNotBetween(start: number | string, end: number | string): ConstructableFilter;
|
|
75
79
|
};
|
|
76
|
-
type IdOf<TValue> = TValue extends {
|
|
77
|
-
id: infer TId extends string;
|
|
78
|
-
} ? TId : string;
|
|
79
|
-
/**
|
|
80
|
-
* What a choice-shaped field compares against: its own ids where codegen read the choice
|
|
81
|
-
* list, and any string where it did not. A linked record is the open case, and it is the
|
|
82
|
-
* only thing distinguishing one from a select at the type level.
|
|
83
|
-
*/
|
|
84
|
-
type ChoiceIds<TValue> = string extends IdOf<TValue> ? string : IdOf<TValue>;
|
|
85
80
|
type TextFieldType = 'SINGLE_LINE_TEXT' | 'LONG_TEXT' | 'URL' | 'EMAIL' | 'PHONE' | 'ADDRESS' | 'RECORD_ID' | 'LOCATION' | 'TIME' | 'BUTTON';
|
|
86
81
|
type NumberFieldType = 'NUMBER' | 'PERCENT' | 'CURRENCY' | 'PROGRESS' | 'RATING' | 'DURATION' | 'COUNT' | 'AUTONUMBER';
|
|
87
82
|
type DateFieldType = 'DATE' | 'DATETIME' | 'CREATED_AT' | 'UPDATED_AT';
|
|
88
83
|
type ChoiceFieldType = 'SELECT' | 'LINKED_RECORD' | 'CREATED_BY' | 'UPDATED_BY';
|
|
89
84
|
/** The types with no single value to compare: only whether the field holds one. */
|
|
90
85
|
type EmptyFieldType = 'ATTACHMENT' | 'DATE_RANGE';
|
|
91
|
-
type ElementOf<TValue> = TValue extends Array<infer TMember> ? TMember : TValue;
|
|
92
86
|
/**
|
|
93
87
|
* The operators one alias offers, chosen from the type codegen declared for the field rather
|
|
94
88
|
* than from what it reads as: a `DATETIME` and a text field both read as `string`, and only
|
|
95
89
|
* the declared type tells them apart.
|
|
96
90
|
*
|
|
97
|
-
*
|
|
98
|
-
* nowhere else. A field with no declared type lands on the permissive chain rather than on
|
|
99
|
-
* none.
|
|
91
|
+
* A field with no declared type lands on the permissive chain rather than on none.
|
|
100
92
|
*/
|
|
101
|
-
type ChainFor<TFieldType, TValue> = [TFieldType] extends [never] ? AnyChain : TFieldType extends `${infer TBase}[]` ? TBase extends EmptyFieldType ? EmptyChain : ArrayChain
|
|
93
|
+
type ChainFor<TFieldType, TValue> = [TFieldType] extends [never] ? AnyChain : TFieldType extends `${infer TBase}[]` ? TBase extends EmptyFieldType ? EmptyChain : ArrayChain : TFieldType extends TextFieldType ? TextChain<TValue & string> : TFieldType extends NumberFieldType ? NumberChain : TFieldType extends 'USER' ? GenericChain : TFieldType extends 'CHECKBOX' ? BooleanChain : TFieldType extends DateFieldType ? DateChain : TFieldType extends ChoiceFieldType ? ChoiceChain : TFieldType extends EmptyFieldType ? EmptyChain : AnyChain;
|
|
102
94
|
type Combinators = {
|
|
103
95
|
$and(...members: Array<AnyConstructable>): ConstructableFilterGroup;
|
|
104
96
|
$or(...members: Array<AnyConstructable>): ConstructableFilterGroup;
|
package/dist/package.js
CHANGED
package/dist/server.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { upload } from "./core/file-upload.js";
|
|
2
2
|
import { comments } from "./comments/comments.js";
|
|
3
3
|
import { auth } from "./auth/server.js";
|
|
4
|
-
import { RecordOption, RecordOptions } from "./core/options.js";
|
|
4
|
+
import { FieldChoice, RecordOption, RecordOptions } from "./core/options.js";
|
|
5
5
|
import { ds } from "./datasource/datasource.js";
|
|
6
6
|
import { workflow } from "./core/workflow.js";
|
|
7
7
|
import { integration } from "./core/integration.js";
|
|
8
8
|
import "@tanstack/react-start/server-only";
|
|
9
|
-
export { type RecordOption, type RecordOptions, auth, comments, ds, integration, upload, workflow };
|
|
9
|
+
export { type FieldChoice, type RecordOption, type RecordOptions, auth, comments, ds, integration, upload, workflow };
|
package/package.json
CHANGED
|
@@ -18,7 +18,7 @@ description: >-
|
|
|
18
18
|
metadata:
|
|
19
19
|
type: sub-skill
|
|
20
20
|
library: softr-sdk
|
|
21
|
-
library_version: 0.0.
|
|
21
|
+
library_version: 0.0.76
|
|
22
22
|
---
|
|
23
23
|
|
|
24
24
|
**Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build.
|
|
@@ -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.76
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
**Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
metadata:
|
|
12
12
|
type: sub-skill
|
|
13
13
|
library: softr-sdk
|
|
14
|
-
library_version: 0.0.
|
|
14
|
+
library_version: 0.0.76
|
|
15
15
|
---
|
|
16
16
|
|
|
17
17
|
**Boundary.** `comments` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler. Importing it into a component fails the build. `useCommentComposer` lives on `@softr/sdk/client` and runs in components. `defineComments` comes from `@softr/sdk` and can be imported anywhere.
|
|
@@ -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.76
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
**Boundary.** `upload` lives on `@softr/sdk/server` and runs inside server functions. `useFileUpload` and `FileItem` live on `@softr/sdk` and run in components. `FileAttachment` comes from `@softr/sdk` and can be imported anywhere.
|
|
@@ -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.76
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
**Boundary.** `integration` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineIntegration` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.
|
|
12
|
+
library_version: 0.0.76
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**Boundary.** `softrMcpMiddleware` lives on `@softr/sdk` and is registered once, in `src/start.ts`. The actions you hand it are the same `defineQuery` / `defineMutation` values the app's own pages use — declared wherever they already live, not duplicated for MCP.
|
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
metadata:
|
|
12
12
|
type: sub-skill
|
|
13
13
|
library: softr-sdk
|
|
14
|
-
library_version: 0.0.
|
|
14
|
+
library_version: 0.0.76
|
|
15
15
|
---
|
|
16
16
|
|
|
17
17
|
**Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineMutation` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -155,7 +155,7 @@ A form select for a `LINKED_RECORD` field needs its options from the linked tabl
|
|
|
155
155
|
|
|
156
156
|
Each key of `data` accepts exactly what its field takes (see [record-queries](../record-queries/SKILL.md#typing)):
|
|
157
157
|
|
|
158
|
-
- SELECT and LINKED_RECORD take `{ id }`. Not a bare string, and not the `{ id, label }` a read returns; the label is display data and a write does not accept it.
|
|
158
|
+
- SELECT and LINKED_RECORD take `{ id }`. Not a bare string, and not the `{ id, label }` a read returns; the label is display data and a write does not accept it. For a form select that should follow the field's choices in Studio, read them with `ds.choices` (see [record-queries](../record-queries/SKILL.md)).
|
|
159
159
|
- CHECKBOX takes a boolean. Dates take ISO strings, never `Date` instances.
|
|
160
160
|
- ATTACHMENT takes `{ url }`, optionally with `filename`, `id` and `type`.
|
|
161
161
|
- A multi-entry field takes an array of the value.
|
|
@@ -13,7 +13,7 @@ description: >-
|
|
|
13
13
|
metadata:
|
|
14
14
|
type: sub-skill
|
|
15
15
|
library: softr-sdk
|
|
16
|
-
library_version: 0.0.
|
|
16
|
+
library_version: 0.0.76
|
|
17
17
|
---
|
|
18
18
|
|
|
19
19
|
**Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. The same holds for any helper that calls `ds`: declare it inside the handler. A function at module level that calls `ds` keeps the server-only import in the browser bundle, and every page stops loading until it moves. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -200,6 +200,13 @@ const loadProductStatuses = createServerFn()
|
|
|
200
200
|
|
|
201
201
|
Skip this request when the options are predefined and fixed — hardcode them instead.
|
|
202
202
|
|
|
203
|
+
A `SELECT` field's options are its choices, and the builder can add and rename them in Studio after the app is built. When a dropdown should follow those changes, read them with `ds.choices`. When the app means a fixed choice, such as a "Done" tab, use its id directly. A mistyped id is not caught at compile time, and a write with an id the field lacks adds it as a new choice. `ds.choices` takes any query or mutation that declares the field and answers `[{ id, label, color }]` as the field holds them now. Each `id` is what a `where` and a write take, for a filter dropdown and a form select alike:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
// `products` declares `category`, a SELECT field
|
|
207
|
+
const loadCategories = createServerFn().handler(() => ds.choices(products, 'category'))
|
|
208
|
+
```
|
|
209
|
+
|
|
203
210
|
A read the datasource turns down throws rather than coming back empty, and it throws a plain `Error` carrying the status, followed by the reason the datasource gives when it sends one. 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.
|
|
204
211
|
|
|
205
212
|
## Typing
|
|
@@ -83,7 +83,7 @@ Every value is an ISO date string, either `'2026-01-31'` or a full timestamp. A
|
|
|
83
83
|
| `.isOneOf(values)` / `.isNoneOf(values)` | Any of / none of |
|
|
84
84
|
| `.isEmpty()` / `.isNotEmpty()` | No value / has a value |
|
|
85
85
|
|
|
86
|
-
Values are choice ids, never labels. A
|
|
86
|
+
Values are choice ids, never labels. A SELECT takes any choice id as a string, so a filter dropdown can pass the ids `ds.choices` returns. A label still matches nothing. A LINKED_RECORD compares against the linked record's id.
|
|
87
87
|
|
|
88
88
|
### Multi-value fields
|
|
89
89
|
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.
|
|
12
|
+
library_version: 0.0.76
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**Boundary.** `workflow` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineWorkflow` and every type come from `@softr/sdk` and can be imported anywhere.
|