@colixsystems/widget-sdk 0.222.0 → 0.227.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/README.md +58 -3
- package/dist/contract.cjs +63 -3
- package/dist/contract.js +63 -3
- package/dist/hooks.js +8 -5
- package/dist/index.d.ts +127 -3
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/dist/stats-hooks.js +60 -4
- package/package.json +1 -1
- package/src/contract.js +63 -3
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
24
24
|
| **CORE** | `useUser()` | `{ id, email, displayName, photoUrl, roles, groupIds }` | `ctx.user` (host-built context, **camelCase** — not a wire payload; `id` null when anonymous) — no scope. `photoUrl` is the user's own profile photo as an absolute URL, or `null`. |
|
|
25
25
|
| **CORE** | `useUserPhoto()` | `{ url, canEdit, upload, remove, uploading, error }` | `ctx.user` + the optional `ctx.account` — no scope. Changes the signed-in user's OWN profile photo. `canEdit` is false when nobody is signed in or the host cannot change it; `upload` / `remove` then reject. |
|
|
26
26
|
| **CORE** | `useAnonymousAccount()` | `{ supported, canCreate, creating, error, create }` | The optional `ctx.account.anonymous` — no scope. Offers an account with a generated login code and password and no email. `create()` opens the app's own dialog, which confirms, creates the account, shows the pair once and signs the visitor in; the widget never receives the pair and `create()` resolves `{ created: true }`. `supported` is false where the workspace has not turned anonymous accounts on, on the Builder canvas, in a preview and for a badge counter; `canCreate` is false while someone is signed in. |
|
|
27
|
-
| **CORE** | `useNavigation()` | `{ goTo, goBack, push, replace, back, currentRoute, openLink }` | `ctx.navigation` — no scope. `goTo(pageIdOrSlug, params?)` accepts a page UUID (a `pageRef` prop) **or its slug** (a row's page-key field) — both hosts resolve either (`openLink` for a link of unknown shape; a known external URL can also use the `Linking` primitive) |
|
|
27
|
+
| **CORE** | `useNavigation()` | `{ goTo, goToStartPage, goBack, push, replace, back, currentRoute, openLink }` | `ctx.navigation` — no scope. `goTo(pageIdOrSlug, params?)` accepts a page UUID (a `pageRef` prop) **or its slug** (a row's page-key field) — both hosts resolve either; `goToStartPage()` opens the signed-in user's own start page (`openLink` for a link of unknown shape; a known external URL can also use the `Linking` primitive) |
|
|
28
28
|
| **CORE** | `useFilePreview(file)` | `{ uri }` | No scope. A temporary address an `<Image>` can show for a file the person picked but has not uploaded yet (a photo in a composer). Native picks use their own uri; on the web the SDK creates and releases an object URL, which widgets may not do themselves. `uri` is null for no file. |
|
|
29
29
|
| **CORE** | `usePageBadge(count)` | `{ supported }` | `ctx.navigation.setBadge` — no scope. Puts `count` on the menu entry or tab of the page the widget is on (a chat's unread messages). The host sums the page's widgets and keeps the last count after the widget leaves the screen, until it reports again; `0` clears this widget's share. `supported` is false where the host draws no page badges (the Builder canvas). |
|
|
30
30
|
| **CORE** | `useRouteParams()` | `{ [paramKey]: value }` | `ctx.navigation.currentRoute.params` — no scope. The nav params the previous page passed via `goTo(pageId, params)`; the flat accessor for master→detail (read `recordId` on a detail page). Empty object when none. |
|
|
@@ -84,7 +84,62 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
84
84
|
|
|
85
85
|
## Status
|
|
86
86
|
|
|
87
|
-
`v0.
|
|
87
|
+
`v0.227.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
|
|
88
|
+
|
|
89
|
+
### What's new in 0.227.0 (contract 1.183.0)
|
|
90
|
+
|
|
91
|
+
**Columns the server keeps, and rules every client meets.** A `datastoreTemplate` can now say what a widget used to write itself and the server could not check:
|
|
92
|
+
|
|
93
|
+
- `latestVia: "<relation column on the target that points back>"` on a ONE_TO_ONE / ONE_TO_MANY relation (with `onDelete` left at `SET_NULL`; the relation back must have `inheritAcl: true` and `immutable: true`) keeps it pointing at the newest related row — the newest message of a conversation.
|
|
94
|
+
- `copyOf: { via, column }` keeps a column equal to `column` on the row that latest-row column points at — the last message's text, time and author on the conversation row. Text copied into a STRING column is cut to 140 characters; a copy of an encrypted column must be `encrypted` too, and a copy cannot `inheritAcl`.
|
|
95
|
+
|
|
96
|
+
Both are written only by the server, in the same write as the related row, so a list reads who wrote last, and when, without trusting whoever wrote the parent row. A client value for them is ignored; read them back.
|
|
97
|
+
|
|
98
|
+
A table may also declare `constraints`, checked on every write and refused as `CONSTRAINT_VIOLATION`: `unique` (now with `unlessRelated`, waived for rows whose related row has a yes/no column set), `no-overlap`, `related-open` (no row is added, changed or removed while the related row's column is filled — votes on a closed poll) and `related-index` (a whole number that must be a position in the related row's list — a poll option). Additive.
|
|
99
|
+
|
|
100
|
+
### What's new in 0.226.0 (contract 1.182.0)
|
|
101
|
+
|
|
102
|
+
**Rank by a column — `useDatastoreAggregate(tableId, { groupBy, order, limit })`.** Group a table by an ordinary column (text, number, yes/no, date or user) and get `groups: [{ group, count, sum }]`, counted on the server over every row the viewer can read, so it stays exact past the 1000-row page a list read returns. A user column's `group` is the user id, so a ranking is one call:
|
|
103
|
+
|
|
104
|
+
```jsx
|
|
105
|
+
const { groups } = useDatastoreAggregate(props.tableId, {
|
|
106
|
+
groupBy: "Reporter",
|
|
107
|
+
order: "count:desc",
|
|
108
|
+
limit: 10,
|
|
109
|
+
filter: { Reporter: "nempty:" },
|
|
110
|
+
});
|
|
111
|
+
// groups: [{ group: "<user id>", count: 42, sum: 0 }, …]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`order` is `count`, `sum` or `group`, then `:asc` or `:desc` (ascending when left out; by value when there is no `order`); `limit` is 1–1000. Rows with no value form one `Uncategorized` group. Encrypted, array, file and relation columns cannot be grouped.
|
|
115
|
+
|
|
116
|
+
### What's new in 0.225.0 (contract 1.181.0)
|
|
117
|
+
|
|
118
|
+
**Chart a table over time — `useDatastoreAggregate(tableId, spec)`.** Counts rows per UTC day or ISO week (Monday) on the server, over every row the viewer may read, so the series stays exact past the 1000-row page a list read returns. Every bucket of the range comes back, empty ones as `count: 0`, so a chart has no gaps. `groupBy` is `"created_at"` (new rows, the default) or `"updated_at"` (changed rows); any other column is refused with a `VALIDATION` error. `sumColumn` adds up a plain number column per bucket. At most 31 buckets.
|
|
119
|
+
|
|
120
|
+
```jsx
|
|
121
|
+
const { buckets, loading } = useDatastoreAggregate(props.tableId, { bucket: "week", from: twelveWeeksAgo });
|
|
122
|
+
// buckets: [{ date: "2026-09-28", count: 14, sum: 0 }, …]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### What's new in 0.224.0 (contract 1.180.0)
|
|
126
|
+
|
|
127
|
+
**A logged-out visitor can attach files to the row they just created.** On a table that lets everyone create rows, the create → `upload(file, { recordId: row.id })` → `update(row.id, { File: stored.id })` flow now works without signing in, for a few minutes after the create. The host carries a grant for that one row from the create to its upload and update, so widget code does not change. The visitor cannot edit the row's other fields, read the file back on a private table, attach more than ten files, or attach to any other row; editing an existing row's files still needs a signed-in user. Additive.
|
|
128
|
+
|
|
129
|
+
### What's new in 0.223.0 (contract 1.179.0)
|
|
130
|
+
|
|
131
|
+
**A widget can send the user back to their start page — `useNavigation().goToStartPage()`.** An app opens on a start page that can differ per user, because page access is restricted per group. A "Back to start" or "Home" button should take each user to their own start page rather than to one hard-coded page they may not be allowed to open.
|
|
132
|
+
|
|
133
|
+
```jsx
|
|
134
|
+
import { useNavigation, Button } from "@colixsystems/widget-sdk";
|
|
135
|
+
|
|
136
|
+
export default function BackToStart() {
|
|
137
|
+
const { goToStartPage } = useNavigation();
|
|
138
|
+
return <Button title="Back to the start page" onPress={() => goToStartPage()} />;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The web Player and the exported app both go to the page the app opens on for the signed-in user. It takes no arguments and carries no params. It is a no-op where there is no live router (the Studio canvas, a badge counter). Additive.
|
|
88
143
|
|
|
89
144
|
### What's new in 0.222.0 (contract 1.178.0)
|
|
90
145
|
|
|
@@ -2752,7 +2807,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
|
|
|
2752
2807
|
|
|
2753
2808
|
- `defineWidget({ manifest, component })` — validates the manifest and produces a widget module the host can register.
|
|
2754
2809
|
- `validateManifest(m)` / `validatePropertySchema(s)` / `validateProps(schema, props)` — shape validation; no third-party deps.
|
|
2755
|
-
- `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useRecordPermissions`, `useAsset`, `useWidgetEvent`, `useWidgetInput`, `usePayments`, `useSendNotification`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useRouteParams`, `usePageContext`, `useWidgetRoute`, `useChildRenderer`, `useClipboard`, `useToast` — hooks that read from the host-provided `WidgetContext` (or, for `useClipboard`, the platform clipboard API directly). `useDirectory(query?)` returns `{ users, loading, error, refetch }` (each user `{ id, name, role }`) and requires the `directory.read:users` scope. `useUsers(query?)` returns `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` and requires `users.read:*` (mutations also need `users.write:*`); rejections are a `DirectoryError`. `useGroups(query?)` returns `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` and requires `groups.read:*` (mutations also need `groups.write:*`). `usePayments()` returns `{ requestPayment, getPayment }` and requires the `payments.charge:appUser` scope; `requestPayment(...)` rejects with a `PaymentError` carrying `code`, the server's user-safe `message`, and `retryable` (`false` = this charge cannot succeed until the workspace, manifest, or amount changes — show the message, not a retry). `useSendNotification()` returns `{ send, sending, error }` and requires the `notifications.send:appUser` scope; `send({ recipient_user_id, title, body, link?, payload? })` notifies one app user in the same workspace (cross-workspace `recipient_user_id` is rejected), must be called from an event handler rather than render, and rejects with a `NotificationError`. `useUser()` returns the active end-user identity `{ id, email, displayName, roles, groupIds }` (camelCase — the host-built context object, not a wire payload; `id` is `null` for anonymous / preview). `useNavigation()` returns `{ goTo, goBack, push, replace, back, currentRoute, openLink }` for internal page navigation; `openLink(link)` follows an author- or data-supplied link of unknown shape through the host's shared resolver (in-app page → internal route, off-app http(s) → opened outside, unsafe → refused) and is the safe choice for any value your code did not construct, while `Linking.openURL(url)` is for an external URL you built yourself. `useRouteParams()` returns the current route's params object (`currentRoute.params`) — the flat master→detail accessor; read a param off it (e.g. `recordId`), never call it. `useDatastoreRecord(tableId, recordId)` returns `{ data, loading, error, refetch }` for a single record (data is one row or null). `useDatastoreSchema(tableId)` returns `{ schema, loading, error, refetch }` where `schema` is `{ id, name, columns: [{ id, name, data_type, required, relation_type, target_table_id, is_identification }] }` (structure only, no row data; snake_case verbatim) — use it to resolve a stored `columnId` to its column type at runtime; requires the `datastore.read:<table>` scope. `useAsset(fileId)` returns `{ url, file, loading, error, refetch }` — the `url` is an absolute URL composed against the host's API base. `useChildRenderer()` returns `{ renderNode(node) }` — container widgets call it to render arbitrary child page-tree nodes (prefer the `WidgetTree` component for the common case). `useWidgetInput(inputName)` returns the latest payload a sibling widget published on the event the page author wired to this widget's declared `inputs` entry (`undefined` when unwired or not yet published).
|
|
2810
|
+
- `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useRecordPermissions`, `useAsset`, `useWidgetEvent`, `useWidgetInput`, `usePayments`, `useSendNotification`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useRouteParams`, `usePageContext`, `useWidgetRoute`, `useChildRenderer`, `useClipboard`, `useToast` — hooks that read from the host-provided `WidgetContext` (or, for `useClipboard`, the platform clipboard API directly). `useDirectory(query?)` returns `{ users, loading, error, refetch }` (each user `{ id, name, role }`) and requires the `directory.read:users` scope. `useUsers(query?)` returns `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` and requires `users.read:*` (mutations also need `users.write:*`); rejections are a `DirectoryError`. `useGroups(query?)` returns `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` and requires `groups.read:*` (mutations also need `groups.write:*`). `usePayments()` returns `{ requestPayment, getPayment }` and requires the `payments.charge:appUser` scope; `requestPayment(...)` rejects with a `PaymentError` carrying `code`, the server's user-safe `message`, and `retryable` (`false` = this charge cannot succeed until the workspace, manifest, or amount changes — show the message, not a retry). `useSendNotification()` returns `{ send, sending, error }` and requires the `notifications.send:appUser` scope; `send({ recipient_user_id, title, body, link?, payload? })` notifies one app user in the same workspace (cross-workspace `recipient_user_id` is rejected), must be called from an event handler rather than render, and rejects with a `NotificationError`. `useUser()` returns the active end-user identity `{ id, email, displayName, roles, groupIds }` (camelCase — the host-built context object, not a wire payload; `id` is `null` for anonymous / preview). `useNavigation()` returns `{ goTo, goToStartPage, goBack, push, replace, back, currentRoute, openLink }` for internal page navigation; `goToStartPage()` opens the signed-in user's own start page (the page the app opens on for them); `openLink(link)` follows an author- or data-supplied link of unknown shape through the host's shared resolver (in-app page → internal route, off-app http(s) → opened outside, unsafe → refused) and is the safe choice for any value your code did not construct, while `Linking.openURL(url)` is for an external URL you built yourself. `useRouteParams()` returns the current route's params object (`currentRoute.params`) — the flat master→detail accessor; read a param off it (e.g. `recordId`), never call it. `useDatastoreRecord(tableId, recordId)` returns `{ data, loading, error, refetch }` for a single record (data is one row or null). `useDatastoreSchema(tableId)` returns `{ schema, loading, error, refetch }` where `schema` is `{ id, name, columns: [{ id, name, data_type, required, relation_type, target_table_id, is_identification }] }` (structure only, no row data; snake_case verbatim) — use it to resolve a stored `columnId` to its column type at runtime; requires the `datastore.read:<table>` scope. `useAsset(fileId)` returns `{ url, file, loading, error, refetch }` — the `url` is an absolute URL composed against the host's API base. `useChildRenderer()` returns `{ renderNode(node) }` — container widgets call it to render arbitrary child page-tree nodes (prefer the `WidgetTree` component for the common case). `useWidgetInput(inputName)` returns the latest payload a sibling widget published on the event the page author wired to this widget's declared `inputs` entry (`undefined` when unwired or not yet published).
|
|
2756
2811
|
- `WidgetTree({ node })` — component that renders an author-authored child node through the host's renderer; used by Tabs / Card / custom containers to host arbitrary child widgets.
|
|
2757
2812
|
- `Text`, `View`, `Pressable`, `Image`, `ScrollView`, `TextInput`, `FlatList`, `SectionList`, `ActivityIndicator`, `Switch`, `StyleSheet`, `Linking`, `Icon`, `DateTimePicker` — re-exported from `react-native` (the RN primitives) or implemented in the SDK (`Icon` wraps `lucide-react-native`; `DateTimePicker` wraps `@react-native-community/datetimepicker` on native and renders `<input type="date|time|datetime-local">` directly on web because the RN library has no react-native-web mapping). The web build aliases `react-native` to `react-native-web` so the RN-re-exported primitives render in the browser without any per-platform code; the exported Expo app's Metro bundler resolves the real `react-native` library. `Linking` is a static API (`Linking.openURL(url)`) — use it for external URLs, and use `useNavigation().goTo(pageId)` for internal page navigation. See https://reactnative.dev/docs/ for per-component props.
|
|
2758
2813
|
- `Overlay` — the screen-level overlay (sc-6607). `<Overlay visible={!!preview} onRequestClose={() => setPreview(null)} size="full">…</Overlay>` renders its children OUTSIDE the widget's layout box on both hosts (web portals it to the document root, native uses the OS modal), so no clipping card, `ScrollView` or neighbouring widget can cut off a document/media preview, lightbox or confirm dialog — which absolute positioning inside the widget cannot achieve on either platform. Children stay in your own React tree, so state and hooks work normally. `onRequestClose` carries the backdrop press, Escape on web, and the Android back button; `size` is `sm` | `md` (default) | `lg` | `full`; the scrim, surface, radius, padding and elevation come from the workspace theme. An anchored dropdown still belongs inside your own root.
|
package/dist/contract.cjs
CHANGED
|
@@ -1128,6 +1128,8 @@ const HOOKS = [
|
|
|
1128
1128
|
goTo:
|
|
1129
1129
|
"(pageIdOrSlug: string, params?: object) => void — a page's UUID (a pageRef prop) OR its SLUG (e.g. a row's page-key field); both hosts resolve either. There is NO page-name lookup: a slug is the only data-carried navigation key.",
|
|
1130
1130
|
openLink: "(link: string) => boolean",
|
|
1131
|
+
goToStartPage:
|
|
1132
|
+
"() => void — opens the signed-in user's own start page, the page the app opens on for them (it can differ per user, since page access is restricted per group). Use it for a 'back to start' or 'home' button instead of hard-coding a page. A no-op where there is no live router (the Studio canvas, a badge counter).",
|
|
1131
1133
|
goBack: "() => void",
|
|
1132
1134
|
push: "(pageIdOrSlug: string, params?: object) => void",
|
|
1133
1135
|
replace: "(pageIdOrSlug: string, params?: object) => void",
|
|
@@ -1395,7 +1397,8 @@ const HOOKS = [
|
|
|
1395
1397
|
"create the row, call upload(file, { recordId: row.id }) — the per-call " +
|
|
1396
1398
|
"recordId names the row the hook could not know at render — then write " +
|
|
1397
1399
|
"the returned id into the FILE column with update(). A logged-out " +
|
|
1398
|
-
"visitor
|
|
1400
|
+
"visitor may do this only for a row they just created, within a few " +
|
|
1401
|
+
"minutes, and never edits other rows' files. For a file that belongs to " +
|
|
1399
1402
|
"no row: 'public' for content the app shows to anyone browsing, " +
|
|
1400
1403
|
"'project' for signed-in workspace users only, 'personal' for a file " +
|
|
1401
1404
|
"private to the uploading app user.",
|
|
@@ -1794,6 +1797,45 @@ const HOOKS = [
|
|
|
1794
1797
|
requiredContextSlice: ["datastore.records"],
|
|
1795
1798
|
scopes: ["datastore.read:*"],
|
|
1796
1799
|
},
|
|
1800
|
+
// sc-10942 — rows per day or week, counted on the server over every row the
|
|
1801
|
+
// viewer may read. Date columns only until column grouping is exact too.
|
|
1802
|
+
{
|
|
1803
|
+
name: "useDatastoreAggregate",
|
|
1804
|
+
signature:
|
|
1805
|
+
"useDatastoreAggregate(tableId, { groupBy, bucket, from, to, sumColumn, filter, order, limit })",
|
|
1806
|
+
optionalArgs: ["groupBy", "bucket", "from", "to", "sumColumn", "filter", "order", "limit"],
|
|
1807
|
+
description:
|
|
1808
|
+
"Counts rows on the server over EVERY row the viewer can read, so it is " +
|
|
1809
|
+
"exact past the 1000-row page a list read returns — never group " +
|
|
1810
|
+
"useDatastoreQuery rows by hand. With `sumColumn` it also totals a plain " +
|
|
1811
|
+
"number column. BY DATE (`groupBy` \"created_at\", the default, or " +
|
|
1812
|
+
"\"updated_at\"): a SERIES for a chart, one entry per UTC day or ISO week " +
|
|
1813
|
+
"(Monday) in `buckets`; `bucket` is \"day\" (default) or \"week\"; " +
|
|
1814
|
+
"`from` / `to` are ISO dates (default: the 30 days, or 12 weeks, up to " +
|
|
1815
|
+
"now), at most 31 buckets, empty ones as count 0 so a chart has no gaps. " +
|
|
1816
|
+
"BY A COLUMN (any other `groupBy`: a text, number, yes/no, date or user " +
|
|
1817
|
+
"column): one entry per value in `groups`. A USER column's `group` is the " +
|
|
1818
|
+
"user id (resolve names with useDirectory({ ids })), so `order: \"count:desc\", " +
|
|
1819
|
+
"limit: 10` ranks the ten people with the most rows. `order` is count, sum " +
|
|
1820
|
+
"or group, then :asc or :desc (default: by value); `limit` is 1-1000. Rows " +
|
|
1821
|
+
"with no value form one \"Uncategorized\" group; on a user column, leave " +
|
|
1822
|
+
"them out with a `nempty:` filter. Encrypted, array, file and relation " +
|
|
1823
|
+
"columns cannot be grouped (VALIDATION). `filter` takes the same shapes " +
|
|
1824
|
+
"as useDatastoreQuery's, created_at ranges included. Pass null as the " +
|
|
1825
|
+
"table until a bound table is known.",
|
|
1826
|
+
returnShape: {
|
|
1827
|
+
buckets:
|
|
1828
|
+
"Array<{ date, count, sum }> // date = the bucket's start, YYYY-MM-DD; sum is 0 without sumColumn",
|
|
1829
|
+
groups:
|
|
1830
|
+
"Array<{ group, count, sum }> // by a column; a USER column's group is the user id",
|
|
1831
|
+
loading: "boolean",
|
|
1832
|
+
fetching: "boolean",
|
|
1833
|
+
error: "DatastoreError | null",
|
|
1834
|
+
refetch: "() => Promise<void>",
|
|
1835
|
+
},
|
|
1836
|
+
requiredContextSlice: ["datastore.records"],
|
|
1837
|
+
scopes: ["datastore.read:*"],
|
|
1838
|
+
},
|
|
1797
1839
|
{
|
|
1798
1840
|
name: "useDatastoreSchema",
|
|
1799
1841
|
signature: "useDatastoreSchema(tableId)",
|
|
@@ -3561,7 +3603,7 @@ const MANIFEST_SCHEMA = {
|
|
|
3561
3603
|
type: "object",
|
|
3562
3604
|
required: false,
|
|
3563
3605
|
description:
|
|
3564
|
-
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 8 tables, 24 columns per table. RELATION columns address siblings by `targetSuffix` (any table in the same array, in any order; tables may relate to each other both ways). Tables persist across uninstalls. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for. sc-7530: a table may also declare `roleGrants` — group-scoped access named SYMBOLICALLY, because a group id belongs to the workspace that installs the widget and could never travel in the manifest. Declare the audiences once at template level as `roles: [{ key, label, description? }]` (key is lowercase kebab, max 8 roles), then per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`. The installing workspace binds each role to one of its own user groups (or has one created) during the install consent step, so a permission model where an admin group does everything and signed-in users only read survives the trip between workspaces. Use `publicGrant` for the two id-less audiences (everyone / any signed-in user) and `roleGrants` for anything that names a group. A role nobody binds simply writes no grant — it never fails the install. sc-7557: a column may declare `encrypted: true` — its values are stored AES-256-GCM encrypted at rest under a per-workspace subkey. The widget still reads and writes PLAINTEXT (the platform encrypts on write, decrypts for end users); studio users see `🔒` instead, and the column cannot be searched, filtered or sorted on. Sample `rows` you seed for it are encrypted the same way. Not valid on `RELATION` — its value is a foreign key the backend must resolve — and publishing one is rejected. An `inheritAcl` RELATION column may add `inheritAclReadOnly: true` — rows then inherit READ access only, so members of the parent read every child and may add children, but change or delete only the rows they created (pair with `grantCreatorPermissions`). A column may declare `defaultValue`, written when a create omits it: `@currentUser` on USER, `@now` on DATE / DATETIME, or a literal of the column's own type (none on FILE, RELATION or USER_GROUP — ids belong to the installing workspace). `defaultLocked: true` on an `@currentUser` / `@now` default makes the server always stamp it and ignore any client value on create and update, so an author column cannot be forged. `immutable: true` makes a column write-once: a create sets it as usual and any value sent for it on update is ignored (not valid on a MANY_TO_MANY relation). A table may declare `presence: true` so the people who share a row see which of each other are online (useOnlineMembers); it takes effect only while the workspace records user activity, and an upgrade that declares it turns it on for the existing table.",
|
|
3606
|
+
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 8 tables, 24 columns per table. RELATION columns address siblings by `targetSuffix` (any table in the same array, in any order; tables may relate to each other both ways). Tables persist across uninstalls. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for. sc-7530: a table may also declare `roleGrants` — group-scoped access named SYMBOLICALLY, because a group id belongs to the workspace that installs the widget and could never travel in the manifest. Declare the audiences once at template level as `roles: [{ key, label, description? }]` (key is lowercase kebab, max 8 roles), then per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`. The installing workspace binds each role to one of its own user groups (or has one created) during the install consent step, so a permission model where an admin group does everything and signed-in users only read survives the trip between workspaces. Use `publicGrant` for the two id-less audiences (everyone / any signed-in user) and `roleGrants` for anything that names a group. A role nobody binds simply writes no grant — it never fails the install. sc-7557: a column may declare `encrypted: true` — its values are stored AES-256-GCM encrypted at rest under a per-workspace subkey. The widget still reads and writes PLAINTEXT (the platform encrypts on write, decrypts for end users); studio users see `🔒` instead, and the column cannot be searched, filtered or sorted on. Sample `rows` you seed for it are encrypted the same way. Not valid on `RELATION` — its value is a foreign key the backend must resolve — and publishing one is rejected. An `inheritAcl` RELATION column may add `inheritAclReadOnly: true` — rows then inherit READ access only, so members of the parent read every child and may add children, but change or delete only the rows they created (pair with `grantCreatorPermissions`). A column may declare `defaultValue`, written when a create omits it: `@currentUser` on USER, `@now` on DATE / DATETIME, or a literal of the column's own type (none on FILE, RELATION or USER_GROUP — ids belong to the installing workspace). `defaultLocked: true` on an `@currentUser` / `@now` default makes the server always stamp it and ignore any client value on create and update, so an author column cannot be forged. `immutable: true` makes a column write-once: a create sets it as usual and any value sent for it on update is ignored (not valid on a MANY_TO_MANY relation). A table may declare `presence: true` so the people who share a row see which of each other are online (useOnlineMembers); it takes effect only while the workspace records user activity, and an upgrade that declares it turns it on for the existing table. A ONE_TO_ONE / ONE_TO_MANY RELATION may declare `latestVia: \"<relation column on its target that points back>\"` (onDelete left at SET_NULL; that relation must have inheritAcl and be immutable): the server keeps it at the newest row of the target table that points back at this row. A column may declare `copyOf: { via, column }`: the server keeps it equal to `column` on the row the latest-row column `via` points at (same type, or text into text, cut to 140 characters in a STRING column; a copy of an encrypted column must be encrypted, and a copy cannot inheritAcl). Both are written only by the server, in the same write as the related row; a client value is ignored. A table may declare `constraints`, checked on every write for every client and refused as CONSTRAINT_VIOLATION: `unique` ({ columns, partitionColumns?, unlessRelated?: { relation, column } } — waived for rows whose related row has that yes/no column set), `no-overlap` ({ startColumn, endColumn, partitionColumns? }), `related-open` ({ relation, closedColumn } — no row is added, changed or removed while the related row's column is filled) and `related-index` ({ column, relation, listColumn } — the number must be a position in the related row's list). An upgrade adds a declared constraint to an existing table without judging the rows already in it.",
|
|
3565
3607
|
},
|
|
3566
3608
|
translations: {
|
|
3567
3609
|
type: "object",
|
|
@@ -5878,7 +5920,25 @@ const CONTRACT = deepFreeze({
|
|
|
5878
5920
|
// edge behind the card's content, with a `scrim` on the text's edge and
|
|
5879
5921
|
// an `imagePosition` for the crop. Children may be a function taking
|
|
5880
5922
|
// `{ colors, onImage }`: the theme colours as they read over the picture.
|
|
5881
|
-
|
|
5923
|
+
// 1.179.0: additive (sc-11253) — `useNavigation().goToStartPage()` opens
|
|
5924
|
+
// the signed-in user's own start page on both hosts.
|
|
5925
|
+
// 1.180.0: additive (sc-11244) — a logged-out visitor may attach files to
|
|
5926
|
+
// the row they just created, for a few minutes: the host carries a grant
|
|
5927
|
+
// from the create to that row's upload and update. Widget code is the
|
|
5928
|
+
// same create → upload → update flow.
|
|
5929
|
+
// 1.181.0: additive (sc-10942) — `useDatastoreAggregate(tableId, spec)`
|
|
5930
|
+
// counts rows per day or week on the server (date columns only), through
|
|
5931
|
+
// `records(t).aggregate`'s new bucket/from/to.
|
|
5932
|
+
// 1.182.0: additive (sc-10737) — `useDatastoreAggregate` groups by an
|
|
5933
|
+
// ordinary column too, into `groups` ([{ group, count, sum }]), with
|
|
5934
|
+
// `order` and `limit` for a top N; counted on the server over every
|
|
5935
|
+
// readable row. A user column groups by user id.
|
|
5936
|
+
// 1.183.0: additive (sc-10766) — a datastoreTemplate column may declare
|
|
5937
|
+
// `latestVia` (a server-kept pointer to the newest related row) or
|
|
5938
|
+
// `copyOf: { via, column }` (a server-kept copy read through one), and
|
|
5939
|
+
// a table may declare `constraints` (`unique` with `unlessRelated`,
|
|
5940
|
+
// `no-overlap`, `related-open`, `related-index`).
|
|
5941
|
+
version: "1.183.0",
|
|
5882
5942
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5883
5943
|
hooks: HOOKS,
|
|
5884
5944
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -1128,6 +1128,8 @@ const HOOKS = [
|
|
|
1128
1128
|
goTo:
|
|
1129
1129
|
"(pageIdOrSlug: string, params?: object) => void — a page's UUID (a pageRef prop) OR its SLUG (e.g. a row's page-key field); both hosts resolve either. There is NO page-name lookup: a slug is the only data-carried navigation key.",
|
|
1130
1130
|
openLink: "(link: string) => boolean",
|
|
1131
|
+
goToStartPage:
|
|
1132
|
+
"() => void — opens the signed-in user's own start page, the page the app opens on for them (it can differ per user, since page access is restricted per group). Use it for a 'back to start' or 'home' button instead of hard-coding a page. A no-op where there is no live router (the Studio canvas, a badge counter).",
|
|
1131
1133
|
goBack: "() => void",
|
|
1132
1134
|
push: "(pageIdOrSlug: string, params?: object) => void",
|
|
1133
1135
|
replace: "(pageIdOrSlug: string, params?: object) => void",
|
|
@@ -1395,7 +1397,8 @@ const HOOKS = [
|
|
|
1395
1397
|
"create the row, call upload(file, { recordId: row.id }) — the per-call " +
|
|
1396
1398
|
"recordId names the row the hook could not know at render — then write " +
|
|
1397
1399
|
"the returned id into the FILE column with update(). A logged-out " +
|
|
1398
|
-
"visitor
|
|
1400
|
+
"visitor may do this only for a row they just created, within a few " +
|
|
1401
|
+
"minutes, and never edits other rows' files. For a file that belongs to " +
|
|
1399
1402
|
"no row: 'public' for content the app shows to anyone browsing, " +
|
|
1400
1403
|
"'project' for signed-in workspace users only, 'personal' for a file " +
|
|
1401
1404
|
"private to the uploading app user.",
|
|
@@ -1794,6 +1797,45 @@ const HOOKS = [
|
|
|
1794
1797
|
requiredContextSlice: ["datastore.records"],
|
|
1795
1798
|
scopes: ["datastore.read:*"],
|
|
1796
1799
|
},
|
|
1800
|
+
// sc-10942 — rows per day or week, counted on the server over every row the
|
|
1801
|
+
// viewer may read. Date columns only until column grouping is exact too.
|
|
1802
|
+
{
|
|
1803
|
+
name: "useDatastoreAggregate",
|
|
1804
|
+
signature:
|
|
1805
|
+
"useDatastoreAggregate(tableId, { groupBy, bucket, from, to, sumColumn, filter, order, limit })",
|
|
1806
|
+
optionalArgs: ["groupBy", "bucket", "from", "to", "sumColumn", "filter", "order", "limit"],
|
|
1807
|
+
description:
|
|
1808
|
+
"Counts rows on the server over EVERY row the viewer can read, so it is " +
|
|
1809
|
+
"exact past the 1000-row page a list read returns — never group " +
|
|
1810
|
+
"useDatastoreQuery rows by hand. With `sumColumn` it also totals a plain " +
|
|
1811
|
+
"number column. BY DATE (`groupBy` \"created_at\", the default, or " +
|
|
1812
|
+
"\"updated_at\"): a SERIES for a chart, one entry per UTC day or ISO week " +
|
|
1813
|
+
"(Monday) in `buckets`; `bucket` is \"day\" (default) or \"week\"; " +
|
|
1814
|
+
"`from` / `to` are ISO dates (default: the 30 days, or 12 weeks, up to " +
|
|
1815
|
+
"now), at most 31 buckets, empty ones as count 0 so a chart has no gaps. " +
|
|
1816
|
+
"BY A COLUMN (any other `groupBy`: a text, number, yes/no, date or user " +
|
|
1817
|
+
"column): one entry per value in `groups`. A USER column's `group` is the " +
|
|
1818
|
+
"user id (resolve names with useDirectory({ ids })), so `order: \"count:desc\", " +
|
|
1819
|
+
"limit: 10` ranks the ten people with the most rows. `order` is count, sum " +
|
|
1820
|
+
"or group, then :asc or :desc (default: by value); `limit` is 1-1000. Rows " +
|
|
1821
|
+
"with no value form one \"Uncategorized\" group; on a user column, leave " +
|
|
1822
|
+
"them out with a `nempty:` filter. Encrypted, array, file and relation " +
|
|
1823
|
+
"columns cannot be grouped (VALIDATION). `filter` takes the same shapes " +
|
|
1824
|
+
"as useDatastoreQuery's, created_at ranges included. Pass null as the " +
|
|
1825
|
+
"table until a bound table is known.",
|
|
1826
|
+
returnShape: {
|
|
1827
|
+
buckets:
|
|
1828
|
+
"Array<{ date, count, sum }> // date = the bucket's start, YYYY-MM-DD; sum is 0 without sumColumn",
|
|
1829
|
+
groups:
|
|
1830
|
+
"Array<{ group, count, sum }> // by a column; a USER column's group is the user id",
|
|
1831
|
+
loading: "boolean",
|
|
1832
|
+
fetching: "boolean",
|
|
1833
|
+
error: "DatastoreError | null",
|
|
1834
|
+
refetch: "() => Promise<void>",
|
|
1835
|
+
},
|
|
1836
|
+
requiredContextSlice: ["datastore.records"],
|
|
1837
|
+
scopes: ["datastore.read:*"],
|
|
1838
|
+
},
|
|
1797
1839
|
{
|
|
1798
1840
|
name: "useDatastoreSchema",
|
|
1799
1841
|
signature: "useDatastoreSchema(tableId)",
|
|
@@ -3557,7 +3599,7 @@ const MANIFEST_SCHEMA = {
|
|
|
3557
3599
|
type: "object",
|
|
3558
3600
|
required: false,
|
|
3559
3601
|
description:
|
|
3560
|
-
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 8 tables, 24 columns per table. RELATION columns address siblings by `targetSuffix` (any table in the same array, in any order; tables may relate to each other both ways). Tables persist across uninstalls. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for. sc-7530: a table may also declare `roleGrants` — group-scoped access named SYMBOLICALLY, because a group id belongs to the workspace that installs the widget and could never travel in the manifest. Declare the audiences once at template level as `roles: [{ key, label, description? }]` (key is lowercase kebab, max 8 roles), then per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`. The installing workspace binds each role to one of its own user groups (or has one created) during the install consent step, so a permission model where an admin group does everything and signed-in users only read survives the trip between workspaces. Use `publicGrant` for the two id-less audiences (everyone / any signed-in user) and `roleGrants` for anything that names a group. A role nobody binds simply writes no grant — it never fails the install. sc-7557: a column may declare `encrypted: true` — its values are stored AES-256-GCM encrypted at rest under a per-workspace subkey. The widget still reads and writes PLAINTEXT (the platform encrypts on write, decrypts for end users); studio users see `🔒` instead, and the column cannot be searched, filtered or sorted on. Sample `rows` you seed for it are encrypted the same way. Not valid on `RELATION` — its value is a foreign key the backend must resolve — and publishing one is rejected. An `inheritAcl` RELATION column may add `inheritAclReadOnly: true` — rows then inherit READ access only, so members of the parent read every child and may add children, but change or delete only the rows they created (pair with `grantCreatorPermissions`). A column may declare `defaultValue`, written when a create omits it: `@currentUser` on USER, `@now` on DATE / DATETIME, or a literal of the column's own type (none on FILE, RELATION or USER_GROUP — ids belong to the installing workspace). `defaultLocked: true` on an `@currentUser` / `@now` default makes the server always stamp it and ignore any client value on create and update, so an author column cannot be forged. `immutable: true` makes a column write-once: a create sets it as usual and any value sent for it on update is ignored (not valid on a MANY_TO_MANY relation). A table may declare `presence: true` so the people who share a row see which of each other are online (useOnlineMembers); it takes effect only while the workspace records user activity, and an upgrade that declares it turns it on for the existing table.",
|
|
3602
|
+
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 8 tables, 24 columns per table. RELATION columns address siblings by `targetSuffix` (any table in the same array, in any order; tables may relate to each other both ways). Tables persist across uninstalls. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for. sc-7530: a table may also declare `roleGrants` — group-scoped access named SYMBOLICALLY, because a group id belongs to the workspace that installs the widget and could never travel in the manifest. Declare the audiences once at template level as `roles: [{ key, label, description? }]` (key is lowercase kebab, max 8 roles), then per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`. The installing workspace binds each role to one of its own user groups (or has one created) during the install consent step, so a permission model where an admin group does everything and signed-in users only read survives the trip between workspaces. Use `publicGrant` for the two id-less audiences (everyone / any signed-in user) and `roleGrants` for anything that names a group. A role nobody binds simply writes no grant — it never fails the install. sc-7557: a column may declare `encrypted: true` — its values are stored AES-256-GCM encrypted at rest under a per-workspace subkey. The widget still reads and writes PLAINTEXT (the platform encrypts on write, decrypts for end users); studio users see `🔒` instead, and the column cannot be searched, filtered or sorted on. Sample `rows` you seed for it are encrypted the same way. Not valid on `RELATION` — its value is a foreign key the backend must resolve — and publishing one is rejected. An `inheritAcl` RELATION column may add `inheritAclReadOnly: true` — rows then inherit READ access only, so members of the parent read every child and may add children, but change or delete only the rows they created (pair with `grantCreatorPermissions`). A column may declare `defaultValue`, written when a create omits it: `@currentUser` on USER, `@now` on DATE / DATETIME, or a literal of the column's own type (none on FILE, RELATION or USER_GROUP — ids belong to the installing workspace). `defaultLocked: true` on an `@currentUser` / `@now` default makes the server always stamp it and ignore any client value on create and update, so an author column cannot be forged. `immutable: true` makes a column write-once: a create sets it as usual and any value sent for it on update is ignored (not valid on a MANY_TO_MANY relation). A table may declare `presence: true` so the people who share a row see which of each other are online (useOnlineMembers); it takes effect only while the workspace records user activity, and an upgrade that declares it turns it on for the existing table. A ONE_TO_ONE / ONE_TO_MANY RELATION may declare `latestVia: \"<relation column on its target that points back>\"` (onDelete left at SET_NULL; that relation must have inheritAcl and be immutable): the server keeps it at the newest row of the target table that points back at this row. A column may declare `copyOf: { via, column }`: the server keeps it equal to `column` on the row the latest-row column `via` points at (same type, or text into text, cut to 140 characters in a STRING column; a copy of an encrypted column must be encrypted, and a copy cannot inheritAcl). Both are written only by the server, in the same write as the related row; a client value is ignored. A table may declare `constraints`, checked on every write for every client and refused as CONSTRAINT_VIOLATION: `unique` ({ columns, partitionColumns?, unlessRelated?: { relation, column } } — waived for rows whose related row has that yes/no column set), `no-overlap` ({ startColumn, endColumn, partitionColumns? }), `related-open` ({ relation, closedColumn } — no row is added, changed or removed while the related row's column is filled) and `related-index` ({ column, relation, listColumn } — the number must be a position in the related row's list). An upgrade adds a declared constraint to an existing table without judging the rows already in it.",
|
|
3561
3603
|
},
|
|
3562
3604
|
translations: {
|
|
3563
3605
|
type: "object",
|
|
@@ -5874,7 +5916,25 @@ const CONTRACT = deepFreeze({
|
|
|
5874
5916
|
// edge behind the card's content, with a `scrim` on the text's edge and
|
|
5875
5917
|
// an `imagePosition` for the crop. Children may be a function taking
|
|
5876
5918
|
// `{ colors, onImage }`: the theme colours as they read over the picture.
|
|
5877
|
-
|
|
5919
|
+
// 1.179.0: additive (sc-11253) — `useNavigation().goToStartPage()` opens
|
|
5920
|
+
// the signed-in user's own start page on both hosts.
|
|
5921
|
+
// 1.180.0: additive (sc-11244) — a logged-out visitor may attach files to
|
|
5922
|
+
// the row they just created, for a few minutes: the host carries a grant
|
|
5923
|
+
// from the create to that row's upload and update. Widget code is the
|
|
5924
|
+
// same create → upload → update flow.
|
|
5925
|
+
// 1.181.0: additive (sc-10942) — `useDatastoreAggregate(tableId, spec)`
|
|
5926
|
+
// counts rows per day or week on the server (date columns only), through
|
|
5927
|
+
// `records(t).aggregate`'s new bucket/from/to.
|
|
5928
|
+
// 1.182.0: additive (sc-10737) — `useDatastoreAggregate` groups by an
|
|
5929
|
+
// ordinary column too, into `groups` ([{ group, count, sum }]), with
|
|
5930
|
+
// `order` and `limit` for a top N; counted on the server over every
|
|
5931
|
+
// readable row. A user column groups by user id.
|
|
5932
|
+
// 1.183.0: additive (sc-10766) — a datastoreTemplate column may declare
|
|
5933
|
+
// `latestVia` (a server-kept pointer to the newest related row) or
|
|
5934
|
+
// `copyOf: { via, column }` (a server-kept copy read through one), and
|
|
5935
|
+
// a table may declare `constraints` (`unique` with `unlessRelated`,
|
|
5936
|
+
// `no-overlap`, `related-open`, `related-index`).
|
|
5937
|
+
version: "1.183.0",
|
|
5878
5938
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5879
5939
|
hooks: HOOKS,
|
|
5880
5940
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -682,10 +682,11 @@ export function useContainerWidth() {
|
|
|
682
682
|
|
|
683
683
|
/**
|
|
684
684
|
* Returns the host-provided navigation surface:
|
|
685
|
-
* `{ goTo, goBack, push, replace, back, currentRoute }`.
|
|
685
|
+
* `{ goTo, goToStartPage, goBack, push, replace, back, currentRoute }`.
|
|
686
686
|
*
|
|
687
|
-
* `goTo(pageId, params?)` navigates to an internal app page. `
|
|
688
|
-
*
|
|
687
|
+
* `goTo(pageId, params?)` navigates to an internal app page. `goToStartPage()`
|
|
688
|
+
* opens the signed-in user's own start page, which can differ per user.
|
|
689
|
+
* `goBack()` pops the stack. `push` / `replace` / `back` are aliases that map to
|
|
689
690
|
* the platform's native navigator (react-router on web, react-navigation
|
|
690
691
|
* on native). Missing methods degrade to no-ops on the Studio canvas
|
|
691
692
|
* preview, where there is no live router.
|
|
@@ -2705,7 +2706,8 @@ function isRetryableDomainStatus(status) {
|
|
|
2705
2706
|
return status >= 500;
|
|
2706
2707
|
}
|
|
2707
2708
|
|
|
2708
|
-
|
|
2709
|
+
// Internal: shared with stats-hooks.js, not re-exported by the index.
|
|
2710
|
+
export function toDatastoreError(err) {
|
|
2709
2711
|
// An already-mapped SDK error passes straight through. NOTE: the datastore
|
|
2710
2712
|
// CLIENT exports its own class of the same name, so a client rejection does
|
|
2711
2713
|
// NOT match here — it is handled by the envelope read below, which is exactly
|
|
@@ -4803,7 +4805,8 @@ export function useFilestoreUpload(options) {
|
|
|
4803
4805
|
setUploading(true);
|
|
4804
4806
|
setError(null);
|
|
4805
4807
|
try {
|
|
4806
|
-
|
|
4808
|
+
// sc-11244: names the row, so a visitor's grant for it can travel.
|
|
4809
|
+
const created = await filesRef.current.upload(form, isRecordSpace ? { recordId: ownerId } : undefined);
|
|
4807
4810
|
setLastUploaded(created || null);
|
|
4808
4811
|
setUploading(false);
|
|
4809
4812
|
return created;
|
package/dist/index.d.ts
CHANGED
|
@@ -228,8 +228,59 @@ export interface WidgetDatastoreTemplateColumn {
|
|
|
228
228
|
* `MANY_TO_MANY` relation, whose links live outside the row.
|
|
229
229
|
*/
|
|
230
230
|
immutable?: boolean;
|
|
231
|
+
/**
|
|
232
|
+
* ONE_TO_ONE / ONE_TO_MANY RELATION only, with `onDelete` left at
|
|
233
|
+
* `SET_NULL`: the server keeps this column pointing at the newest row of its
|
|
234
|
+
* target table whose relation column named here points back at this row.
|
|
235
|
+
* That relation must have `inheritAcl: true`, so only rows this row's
|
|
236
|
+
* readers can read are followed, and `immutable: true`, so a row never
|
|
237
|
+
* moves under another parent. Written only by the server; any value a
|
|
238
|
+
* client sends is ignored.
|
|
239
|
+
*/
|
|
240
|
+
latestVia?: string;
|
|
241
|
+
/**
|
|
242
|
+
* The server keeps this column equal to `column` on the row the latest-row
|
|
243
|
+
* column `via` (on this table) points at: the same type, or text into
|
|
244
|
+
* text, cut to 140 characters in a STRING column. A copy of an encrypted
|
|
245
|
+
* column must be `encrypted` too, and a copy cannot `inheritAcl`. Written
|
|
246
|
+
* only by the server, so a list can show who wrote the newest row, and
|
|
247
|
+
* when, without trusting whoever wrote this one.
|
|
248
|
+
*/
|
|
249
|
+
copyOf?: { via: string; column: string };
|
|
231
250
|
}
|
|
232
251
|
|
|
252
|
+
/**
|
|
253
|
+
* A rule the server checks on every write to the table, for every client. A
|
|
254
|
+
* refused write is a `CONSTRAINT_VIOLATION`.
|
|
255
|
+
*/
|
|
256
|
+
export type WidgetDatastoreTemplateConstraint =
|
|
257
|
+
| {
|
|
258
|
+
/** No two rows share these values; `unlessRelated` waives it for rows
|
|
259
|
+
* whose related row has that yes/no column set. */
|
|
260
|
+
type: "unique";
|
|
261
|
+
config: {
|
|
262
|
+
columns: string[];
|
|
263
|
+
partitionColumns?: string[];
|
|
264
|
+
unlessRelated?: { relation: string; column: string };
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
| {
|
|
268
|
+
/** No two rows' `startColumn`..`endColumn` intervals overlap. */
|
|
269
|
+
type: "no-overlap";
|
|
270
|
+
config: { startColumn: string; endColumn: string; partitionColumns?: string[] };
|
|
271
|
+
}
|
|
272
|
+
| {
|
|
273
|
+
/** No row is added, changed or removed while its related row's
|
|
274
|
+
* `closedColumn` is filled (a yes/no column: while it is yes). */
|
|
275
|
+
type: "related-open";
|
|
276
|
+
config: { relation: string; closedColumn: string };
|
|
277
|
+
}
|
|
278
|
+
| {
|
|
279
|
+
/** `column` must be a position (0-based) in the related row's list. */
|
|
280
|
+
type: "related-index";
|
|
281
|
+
config: { column: string; relation: string; listColumn: string };
|
|
282
|
+
};
|
|
283
|
+
|
|
233
284
|
export interface WidgetDatastoreTemplateTable {
|
|
234
285
|
/** Stable identifier within the template; appears in the created table name. */
|
|
235
286
|
suffix: string;
|
|
@@ -267,6 +318,8 @@ export interface WidgetDatastoreTemplateTable {
|
|
|
267
318
|
rows?: Array<
|
|
268
319
|
Record<string, string | number | boolean | string[] | number[] | null>
|
|
269
320
|
>;
|
|
321
|
+
/** Rules the server checks on every write, by column name. */
|
|
322
|
+
constraints?: WidgetDatastoreTemplateConstraint[];
|
|
270
323
|
}
|
|
271
324
|
|
|
272
325
|
/**
|
|
@@ -765,6 +818,7 @@ export interface WidgetContext<TProps = unknown> {
|
|
|
765
818
|
};
|
|
766
819
|
navigation: {
|
|
767
820
|
goTo(pageId: string, params?: Record<string, string>): void;
|
|
821
|
+
goToStartPage(): void;
|
|
768
822
|
goBack(): void;
|
|
769
823
|
push(pageId: string, params?: Record<string, unknown>): void;
|
|
770
824
|
replace(pageId: string, params?: Record<string, unknown>): void;
|
|
@@ -1047,6 +1101,65 @@ export function useDatastoreFigure(
|
|
|
1047
1101
|
options?: FigureOptions,
|
|
1048
1102
|
): FigureResult;
|
|
1049
1103
|
|
|
1104
|
+
export interface AggregateBucketSpec {
|
|
1105
|
+
/** Bucket a system date column: `created_at` (default) or `updated_at`. */
|
|
1106
|
+
groupBy?: "created_at" | "updated_at";
|
|
1107
|
+
/** UTC days or ISO weeks (Monday); `day` by default. */
|
|
1108
|
+
bucket?: "day" | "week";
|
|
1109
|
+
/** ISO 8601. Defaults to 30 days (or 12 weeks) before `to`. */
|
|
1110
|
+
from?: string;
|
|
1111
|
+
/** ISO 8601. Defaults to now. At most 31 buckets in the range. */
|
|
1112
|
+
to?: string;
|
|
1113
|
+
/** A plain number column to add up per bucket. */
|
|
1114
|
+
sumColumn?: string;
|
|
1115
|
+
filter?: FigureOptions["filter"];
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
export interface AggregateGroupSpec {
|
|
1119
|
+
/** A text, number, yes/no, date or user column. */
|
|
1120
|
+
groupBy: string;
|
|
1121
|
+
/** A plain number column to add up per group. */
|
|
1122
|
+
sumColumn?: string;
|
|
1123
|
+
filter?: FigureOptions["filter"];
|
|
1124
|
+
/** Default: by value. `count:desc` with a `limit` ranks the biggest groups. */
|
|
1125
|
+
order?: "count:desc" | "count:asc" | "sum:desc" | "sum:asc" | "group:asc" | "group:desc";
|
|
1126
|
+
/** 1–1000. */
|
|
1127
|
+
limit?: number;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
export interface AggregateBucket {
|
|
1131
|
+
/** The bucket's start date, `YYYY-MM-DD`. */
|
|
1132
|
+
date: string;
|
|
1133
|
+
count: number;
|
|
1134
|
+
/** 0 unless `sumColumn` was given. */
|
|
1135
|
+
sum: number;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
export interface AggregateGroup {
|
|
1139
|
+
/** The value; a user column's user id; "Uncategorized" for rows with none. */
|
|
1140
|
+
group: string;
|
|
1141
|
+
count: number;
|
|
1142
|
+
/** 0 unless `sumColumn` was given. */
|
|
1143
|
+
sum: number;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
export interface AggregateResult {
|
|
1147
|
+
/** By date: every bucket of the range, empty ones as `count: 0`. */
|
|
1148
|
+
buckets: AggregateBucket[];
|
|
1149
|
+
/** By a column: one entry per value. */
|
|
1150
|
+
groups: AggregateGroup[];
|
|
1151
|
+
loading: boolean;
|
|
1152
|
+
fetching: boolean;
|
|
1153
|
+
error: Error | null;
|
|
1154
|
+
refetch(): Promise<void>;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/** Rows per day, week or column value over every row the viewer can read, counted on the server. */
|
|
1158
|
+
export function useDatastoreAggregate(
|
|
1159
|
+
table: string | null | undefined,
|
|
1160
|
+
spec?: AggregateBucketSpec | AggregateGroupSpec,
|
|
1161
|
+
): AggregateResult;
|
|
1162
|
+
|
|
1050
1163
|
export interface UserStatsQuery {
|
|
1051
1164
|
days?: 7 | 30 | 90;
|
|
1052
1165
|
/** A `useGroups()` row id: narrows user and invite figures to that group. */
|
|
@@ -1491,6 +1604,15 @@ export interface DatastoreSchemaColumn {
|
|
|
1491
1604
|
display_column_id?: string | null;
|
|
1492
1605
|
/** True when this column is the table's display/identification column. */
|
|
1493
1606
|
is_identification?: boolean;
|
|
1607
|
+
/**
|
|
1608
|
+
* Set on a relation the server keeps at the newest related row; the id of
|
|
1609
|
+
* the related table's relation back. Clients cannot write such a column.
|
|
1610
|
+
*/
|
|
1611
|
+
latest_via_column_id?: string | null;
|
|
1612
|
+
/** Set on a copy the server keeps: the latest-row column it reads through. */
|
|
1613
|
+
copy_via_column_id?: string | null;
|
|
1614
|
+
/** Set on a copy the server keeps: the related table's column it copies. */
|
|
1615
|
+
copy_of_column_id?: string | null;
|
|
1494
1616
|
}
|
|
1495
1617
|
|
|
1496
1618
|
export interface DatastoreSchema {
|
|
@@ -2019,12 +2141,14 @@ export function useFill(): boolean;
|
|
|
2019
2141
|
|
|
2020
2142
|
/**
|
|
2021
2143
|
* The host-provided navigation surface. `goTo(pageId, params?)` navigates
|
|
2022
|
-
* to an internal app page; `goBack()` pops the stack.
|
|
2023
|
-
*
|
|
2024
|
-
*
|
|
2144
|
+
* to an internal app page; `goBack()` pops the stack. `goToStartPage()`
|
|
2145
|
+
* opens the signed-in user's own start page, which can differ per user.
|
|
2146
|
+
* Missing methods degrade to no-ops on the Studio canvas preview where no
|
|
2147
|
+
* live router is mounted. For external URLs use the `Linking` primitive.
|
|
2025
2148
|
*/
|
|
2026
2149
|
export function useNavigation(): {
|
|
2027
2150
|
goTo(pageId: string, params?: Record<string, unknown>): void;
|
|
2151
|
+
goToStartPage(): void;
|
|
2028
2152
|
goBack(): void;
|
|
2029
2153
|
push(pageId: string, params?: Record<string, unknown>): void;
|
|
2030
2154
|
replace(pageId: string, params?: Record<string, unknown>): void;
|
package/dist/index.js
CHANGED
package/dist/index.native.js
CHANGED
package/dist/stats-hooks.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
// sc-10691 — read-only hooks
|
|
2
|
-
//
|
|
3
|
-
// re-
|
|
1
|
+
// sc-10691 — read-only aggregate hooks: record counts per day, week or column
|
|
2
|
+
// value, and an app's user totals. Both are one keyed read, stale-while-revalidate like
|
|
3
|
+
// useDatastoreQuery, and both re-run on the page's pull-to-refresh.
|
|
4
4
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
toDatastoreError,
|
|
7
|
+
toDirectoryError,
|
|
8
|
+
useRefresh,
|
|
9
|
+
useWidgetContextOrThrow,
|
|
10
|
+
} from "./hooks.js";
|
|
11
|
+
|
|
12
|
+
// The system date columns bucket by day or week; any other column groups by value.
|
|
13
|
+
const DATE_GROUP_FIELDS = new Set(["created_at", "updated_at"]);
|
|
6
14
|
|
|
7
15
|
const EMPTY = { identity: null, key: null, data: null, error: null };
|
|
8
16
|
|
|
@@ -66,6 +74,54 @@ function useKeyedRead(read, identity, key, mapError) {
|
|
|
66
74
|
};
|
|
67
75
|
}
|
|
68
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Counts (and optionally sums) a table's rows on the server, over every row
|
|
79
|
+
* the viewer may read — not just one page. `spec`: `{ groupBy, sumColumn?,
|
|
80
|
+
* filter? }` plus, by date (`groupBy` "created_at" | "updated_at"), `bucket?:
|
|
81
|
+
* "day" | "week", from?, to?`, or, by a column, `order?` ("count:desc",
|
|
82
|
+
* "count:asc", "sum:desc", "sum:asc", "group:asc", "group:desc") and `limit?`
|
|
83
|
+
* (1–1000). Returns `{ buckets, groups, loading, fetching, error, refetch }`:
|
|
84
|
+
* by date, `buckets` is `[{ date, count, sum }]` with every bucket of the range
|
|
85
|
+
* present (empty ones as 0); by a column, `groups` is `[{ group, count, sum }]`,
|
|
86
|
+
* a user column's `group` being the user id and rows with no value one
|
|
87
|
+
* "Uncategorized" group. A falsy `table` settles to nothing.
|
|
88
|
+
*/
|
|
89
|
+
export function useDatastoreAggregate(table, spec) {
|
|
90
|
+
const ctx = useWidgetContextOrThrow("useDatastoreAggregate");
|
|
91
|
+
if (!ctx.datastore || typeof ctx.datastore.records !== "function") {
|
|
92
|
+
throw new Error("useDatastoreAggregate: host did not inject a datastore client");
|
|
93
|
+
}
|
|
94
|
+
const { groupBy = "created_at", bucket, from, to, sumColumn, filter, order, limit } = spec || {};
|
|
95
|
+
const byDate = DATE_GROUP_FIELDS.has(groupBy);
|
|
96
|
+
const key = table
|
|
97
|
+
? serialize(byDate ? [groupBy, bucket, from, to, sumColumn, filter] : [groupBy, sumColumn, filter, order, limit])
|
|
98
|
+
: null;
|
|
99
|
+
const records = ctx.datastore.records;
|
|
100
|
+
|
|
101
|
+
const result = useKeyedRead(
|
|
102
|
+
async () => {
|
|
103
|
+
const rows = await records(table).aggregate(
|
|
104
|
+
byDate
|
|
105
|
+
? { groupBy, bucket, from, to, sumField: sumColumn, filter }
|
|
106
|
+
: { groupBy, sumField: sumColumn, filter, order, limit },
|
|
107
|
+
);
|
|
108
|
+
return Array.isArray(rows) ? rows : [];
|
|
109
|
+
},
|
|
110
|
+
// The mode is part of the identity: switching it must not show the other's rows.
|
|
111
|
+
table ? `${table}|${byDate ? "date" : "column"}` : null,
|
|
112
|
+
key,
|
|
113
|
+
toDatastoreError,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const { data, ...state } = result;
|
|
117
|
+
const rows = data || [];
|
|
118
|
+
return {
|
|
119
|
+
...state,
|
|
120
|
+
buckets: byDate ? rows.map((row) => ({ date: row.group, count: row.count, sum: row.sum })) : [],
|
|
121
|
+
groups: byDate ? [] : rows.map((row) => ({ group: row.group, count: row.count, sum: row.sum })),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
69
125
|
/**
|
|
70
126
|
* The app's user totals for its own admins — never a list of people.
|
|
71
127
|
* `query`: `{ days?: 7 | 30 | 90, group_id? }`. Returns `{ stats, loading,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.227.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"homepage": "https://github.com/Colix-AB/AppStudio",
|
|
6
6
|
"type": "module",
|
package/src/contract.js
CHANGED
|
@@ -1128,6 +1128,8 @@ const HOOKS = [
|
|
|
1128
1128
|
goTo:
|
|
1129
1129
|
"(pageIdOrSlug: string, params?: object) => void — a page's UUID (a pageRef prop) OR its SLUG (e.g. a row's page-key field); both hosts resolve either. There is NO page-name lookup: a slug is the only data-carried navigation key.",
|
|
1130
1130
|
openLink: "(link: string) => boolean",
|
|
1131
|
+
goToStartPage:
|
|
1132
|
+
"() => void — opens the signed-in user's own start page, the page the app opens on for them (it can differ per user, since page access is restricted per group). Use it for a 'back to start' or 'home' button instead of hard-coding a page. A no-op where there is no live router (the Studio canvas, a badge counter).",
|
|
1131
1133
|
goBack: "() => void",
|
|
1132
1134
|
push: "(pageIdOrSlug: string, params?: object) => void",
|
|
1133
1135
|
replace: "(pageIdOrSlug: string, params?: object) => void",
|
|
@@ -1395,7 +1397,8 @@ const HOOKS = [
|
|
|
1395
1397
|
"create the row, call upload(file, { recordId: row.id }) — the per-call " +
|
|
1396
1398
|
"recordId names the row the hook could not know at render — then write " +
|
|
1397
1399
|
"the returned id into the FILE column with update(). A logged-out " +
|
|
1398
|
-
"visitor
|
|
1400
|
+
"visitor may do this only for a row they just created, within a few " +
|
|
1401
|
+
"minutes, and never edits other rows' files. For a file that belongs to " +
|
|
1399
1402
|
"no row: 'public' for content the app shows to anyone browsing, " +
|
|
1400
1403
|
"'project' for signed-in workspace users only, 'personal' for a file " +
|
|
1401
1404
|
"private to the uploading app user.",
|
|
@@ -1794,6 +1797,45 @@ const HOOKS = [
|
|
|
1794
1797
|
requiredContextSlice: ["datastore.records"],
|
|
1795
1798
|
scopes: ["datastore.read:*"],
|
|
1796
1799
|
},
|
|
1800
|
+
// sc-10942 — rows per day or week, counted on the server over every row the
|
|
1801
|
+
// viewer may read. Date columns only until column grouping is exact too.
|
|
1802
|
+
{
|
|
1803
|
+
name: "useDatastoreAggregate",
|
|
1804
|
+
signature:
|
|
1805
|
+
"useDatastoreAggregate(tableId, { groupBy, bucket, from, to, sumColumn, filter, order, limit })",
|
|
1806
|
+
optionalArgs: ["groupBy", "bucket", "from", "to", "sumColumn", "filter", "order", "limit"],
|
|
1807
|
+
description:
|
|
1808
|
+
"Counts rows on the server over EVERY row the viewer can read, so it is " +
|
|
1809
|
+
"exact past the 1000-row page a list read returns — never group " +
|
|
1810
|
+
"useDatastoreQuery rows by hand. With `sumColumn` it also totals a plain " +
|
|
1811
|
+
"number column. BY DATE (`groupBy` \"created_at\", the default, or " +
|
|
1812
|
+
"\"updated_at\"): a SERIES for a chart, one entry per UTC day or ISO week " +
|
|
1813
|
+
"(Monday) in `buckets`; `bucket` is \"day\" (default) or \"week\"; " +
|
|
1814
|
+
"`from` / `to` are ISO dates (default: the 30 days, or 12 weeks, up to " +
|
|
1815
|
+
"now), at most 31 buckets, empty ones as count 0 so a chart has no gaps. " +
|
|
1816
|
+
"BY A COLUMN (any other `groupBy`: a text, number, yes/no, date or user " +
|
|
1817
|
+
"column): one entry per value in `groups`. A USER column's `group` is the " +
|
|
1818
|
+
"user id (resolve names with useDirectory({ ids })), so `order: \"count:desc\", " +
|
|
1819
|
+
"limit: 10` ranks the ten people with the most rows. `order` is count, sum " +
|
|
1820
|
+
"or group, then :asc or :desc (default: by value); `limit` is 1-1000. Rows " +
|
|
1821
|
+
"with no value form one \"Uncategorized\" group; on a user column, leave " +
|
|
1822
|
+
"them out with a `nempty:` filter. Encrypted, array, file and relation " +
|
|
1823
|
+
"columns cannot be grouped (VALIDATION). `filter` takes the same shapes " +
|
|
1824
|
+
"as useDatastoreQuery's, created_at ranges included. Pass null as the " +
|
|
1825
|
+
"table until a bound table is known.",
|
|
1826
|
+
returnShape: {
|
|
1827
|
+
buckets:
|
|
1828
|
+
"Array<{ date, count, sum }> // date = the bucket's start, YYYY-MM-DD; sum is 0 without sumColumn",
|
|
1829
|
+
groups:
|
|
1830
|
+
"Array<{ group, count, sum }> // by a column; a USER column's group is the user id",
|
|
1831
|
+
loading: "boolean",
|
|
1832
|
+
fetching: "boolean",
|
|
1833
|
+
error: "DatastoreError | null",
|
|
1834
|
+
refetch: "() => Promise<void>",
|
|
1835
|
+
},
|
|
1836
|
+
requiredContextSlice: ["datastore.records"],
|
|
1837
|
+
scopes: ["datastore.read:*"],
|
|
1838
|
+
},
|
|
1797
1839
|
{
|
|
1798
1840
|
name: "useDatastoreSchema",
|
|
1799
1841
|
signature: "useDatastoreSchema(tableId)",
|
|
@@ -3557,7 +3599,7 @@ const MANIFEST_SCHEMA = {
|
|
|
3557
3599
|
type: "object",
|
|
3558
3600
|
required: false,
|
|
3559
3601
|
description:
|
|
3560
|
-
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 8 tables, 24 columns per table. RELATION columns address siblings by `targetSuffix` (any table in the same array, in any order; tables may relate to each other both ways). Tables persist across uninstalls. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for. sc-7530: a table may also declare `roleGrants` — group-scoped access named SYMBOLICALLY, because a group id belongs to the workspace that installs the widget and could never travel in the manifest. Declare the audiences once at template level as `roles: [{ key, label, description? }]` (key is lowercase kebab, max 8 roles), then per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`. The installing workspace binds each role to one of its own user groups (or has one created) during the install consent step, so a permission model where an admin group does everything and signed-in users only read survives the trip between workspaces. Use `publicGrant` for the two id-less audiences (everyone / any signed-in user) and `roleGrants` for anything that names a group. A role nobody binds simply writes no grant — it never fails the install. sc-7557: a column may declare `encrypted: true` — its values are stored AES-256-GCM encrypted at rest under a per-workspace subkey. The widget still reads and writes PLAINTEXT (the platform encrypts on write, decrypts for end users); studio users see `🔒` instead, and the column cannot be searched, filtered or sorted on. Sample `rows` you seed for it are encrypted the same way. Not valid on `RELATION` — its value is a foreign key the backend must resolve — and publishing one is rejected. An `inheritAcl` RELATION column may add `inheritAclReadOnly: true` — rows then inherit READ access only, so members of the parent read every child and may add children, but change or delete only the rows they created (pair with `grantCreatorPermissions`). A column may declare `defaultValue`, written when a create omits it: `@currentUser` on USER, `@now` on DATE / DATETIME, or a literal of the column's own type (none on FILE, RELATION or USER_GROUP — ids belong to the installing workspace). `defaultLocked: true` on an `@currentUser` / `@now` default makes the server always stamp it and ignore any client value on create and update, so an author column cannot be forged. `immutable: true` makes a column write-once: a create sets it as usual and any value sent for it on update is ignored (not valid on a MANY_TO_MANY relation). A table may declare `presence: true` so the people who share a row see which of each other are online (useOnlineMembers); it takes effect only while the workspace records user activity, and an upgrade that declares it turns it on for the existing table.",
|
|
3602
|
+
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 8 tables, 24 columns per table. RELATION columns address siblings by `targetSuffix` (any table in the same array, in any order; tables may relate to each other both ways). Tables persist across uninstalls. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for. sc-7530: a table may also declare `roleGrants` — group-scoped access named SYMBOLICALLY, because a group id belongs to the workspace that installs the widget and could never travel in the manifest. Declare the audiences once at template level as `roles: [{ key, label, description? }]` (key is lowercase kebab, max 8 roles), then per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`. The installing workspace binds each role to one of its own user groups (or has one created) during the install consent step, so a permission model where an admin group does everything and signed-in users only read survives the trip between workspaces. Use `publicGrant` for the two id-less audiences (everyone / any signed-in user) and `roleGrants` for anything that names a group. A role nobody binds simply writes no grant — it never fails the install. sc-7557: a column may declare `encrypted: true` — its values are stored AES-256-GCM encrypted at rest under a per-workspace subkey. The widget still reads and writes PLAINTEXT (the platform encrypts on write, decrypts for end users); studio users see `🔒` instead, and the column cannot be searched, filtered or sorted on. Sample `rows` you seed for it are encrypted the same way. Not valid on `RELATION` — its value is a foreign key the backend must resolve — and publishing one is rejected. An `inheritAcl` RELATION column may add `inheritAclReadOnly: true` — rows then inherit READ access only, so members of the parent read every child and may add children, but change or delete only the rows they created (pair with `grantCreatorPermissions`). A column may declare `defaultValue`, written when a create omits it: `@currentUser` on USER, `@now` on DATE / DATETIME, or a literal of the column's own type (none on FILE, RELATION or USER_GROUP — ids belong to the installing workspace). `defaultLocked: true` on an `@currentUser` / `@now` default makes the server always stamp it and ignore any client value on create and update, so an author column cannot be forged. `immutable: true` makes a column write-once: a create sets it as usual and any value sent for it on update is ignored (not valid on a MANY_TO_MANY relation). A table may declare `presence: true` so the people who share a row see which of each other are online (useOnlineMembers); it takes effect only while the workspace records user activity, and an upgrade that declares it turns it on for the existing table. A ONE_TO_ONE / ONE_TO_MANY RELATION may declare `latestVia: \"<relation column on its target that points back>\"` (onDelete left at SET_NULL; that relation must have inheritAcl and be immutable): the server keeps it at the newest row of the target table that points back at this row. A column may declare `copyOf: { via, column }`: the server keeps it equal to `column` on the row the latest-row column `via` points at (same type, or text into text, cut to 140 characters in a STRING column; a copy of an encrypted column must be encrypted, and a copy cannot inheritAcl). Both are written only by the server, in the same write as the related row; a client value is ignored. A table may declare `constraints`, checked on every write for every client and refused as CONSTRAINT_VIOLATION: `unique` ({ columns, partitionColumns?, unlessRelated?: { relation, column } } — waived for rows whose related row has that yes/no column set), `no-overlap` ({ startColumn, endColumn, partitionColumns? }), `related-open` ({ relation, closedColumn } — no row is added, changed or removed while the related row's column is filled) and `related-index` ({ column, relation, listColumn } — the number must be a position in the related row's list). An upgrade adds a declared constraint to an existing table without judging the rows already in it.",
|
|
3561
3603
|
},
|
|
3562
3604
|
translations: {
|
|
3563
3605
|
type: "object",
|
|
@@ -5874,7 +5916,25 @@ const CONTRACT = deepFreeze({
|
|
|
5874
5916
|
// edge behind the card's content, with a `scrim` on the text's edge and
|
|
5875
5917
|
// an `imagePosition` for the crop. Children may be a function taking
|
|
5876
5918
|
// `{ colors, onImage }`: the theme colours as they read over the picture.
|
|
5877
|
-
|
|
5919
|
+
// 1.179.0: additive (sc-11253) — `useNavigation().goToStartPage()` opens
|
|
5920
|
+
// the signed-in user's own start page on both hosts.
|
|
5921
|
+
// 1.180.0: additive (sc-11244) — a logged-out visitor may attach files to
|
|
5922
|
+
// the row they just created, for a few minutes: the host carries a grant
|
|
5923
|
+
// from the create to that row's upload and update. Widget code is the
|
|
5924
|
+
// same create → upload → update flow.
|
|
5925
|
+
// 1.181.0: additive (sc-10942) — `useDatastoreAggregate(tableId, spec)`
|
|
5926
|
+
// counts rows per day or week on the server (date columns only), through
|
|
5927
|
+
// `records(t).aggregate`'s new bucket/from/to.
|
|
5928
|
+
// 1.182.0: additive (sc-10737) — `useDatastoreAggregate` groups by an
|
|
5929
|
+
// ordinary column too, into `groups` ([{ group, count, sum }]), with
|
|
5930
|
+
// `order` and `limit` for a top N; counted on the server over every
|
|
5931
|
+
// readable row. A user column groups by user id.
|
|
5932
|
+
// 1.183.0: additive (sc-10766) — a datastoreTemplate column may declare
|
|
5933
|
+
// `latestVia` (a server-kept pointer to the newest related row) or
|
|
5934
|
+
// `copyOf: { via, column }` (a server-kept copy read through one), and
|
|
5935
|
+
// a table may declare `constraints` (`unique` with `unlessRelated`,
|
|
5936
|
+
// `no-overlap`, `related-open`, `related-index`).
|
|
5937
|
+
version: "1.183.0",
|
|
5878
5938
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5879
5939
|
hooks: HOOKS,
|
|
5880
5940
|
primitives: PRIMITIVES,
|