@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 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
- interface ListResult<T = SoftrRecord> {
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, unknown>[];
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, unknown>;
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, unknown>;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softr/sdk",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
@@ -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 this before any
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.17
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.17.
126
+ Targets `@softr/sdk` v0.0.20.
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.17
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.
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.17
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 wiring that puts a visitor on route context.
@@ -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.17
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.
@@ -12,7 +12,7 @@ description: >-
12
12
  metadata:
13
13
  type: sub-skill
14
14
  library: softr-sdk
15
- library_version: 0.0.17
15
+ library_version: 0.0.20
16
16
  ---
17
17
 
18
18
  This skill builds on softr-sdk. Read it first for the entrypoints and the wiring that puts a visitor on route context.