@colixsystems/widget-sdk 0.223.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 +41 -1
- package/dist/contract.cjs +59 -3
- package/dist/contract.js +59 -3
- package/dist/hooks.js +4 -2
- package/dist/index.d.ts +121 -0
- 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 +59 -3
package/README.md
CHANGED
|
@@ -84,7 +84,47 @@ 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.
|
|
88
128
|
|
|
89
129
|
### What's new in 0.223.0 (contract 1.179.0)
|
|
90
130
|
|
package/dist/contract.cjs
CHANGED
|
@@ -1397,7 +1397,8 @@ const HOOKS = [
|
|
|
1397
1397
|
"create the row, call upload(file, { recordId: row.id }) — the per-call " +
|
|
1398
1398
|
"recordId names the row the hook could not know at render — then write " +
|
|
1399
1399
|
"the returned id into the FILE column with update(). A logged-out " +
|
|
1400
|
-
"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 " +
|
|
1401
1402
|
"no row: 'public' for content the app shows to anyone browsing, " +
|
|
1402
1403
|
"'project' for signed-in workspace users only, 'personal' for a file " +
|
|
1403
1404
|
"private to the uploading app user.",
|
|
@@ -1796,6 +1797,45 @@ const HOOKS = [
|
|
|
1796
1797
|
requiredContextSlice: ["datastore.records"],
|
|
1797
1798
|
scopes: ["datastore.read:*"],
|
|
1798
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
|
+
},
|
|
1799
1839
|
{
|
|
1800
1840
|
name: "useDatastoreSchema",
|
|
1801
1841
|
signature: "useDatastoreSchema(tableId)",
|
|
@@ -3563,7 +3603,7 @@ const MANIFEST_SCHEMA = {
|
|
|
3563
3603
|
type: "object",
|
|
3564
3604
|
required: false,
|
|
3565
3605
|
description:
|
|
3566
|
-
"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.",
|
|
3567
3607
|
},
|
|
3568
3608
|
translations: {
|
|
3569
3609
|
type: "object",
|
|
@@ -5882,7 +5922,23 @@ const CONTRACT = deepFreeze({
|
|
|
5882
5922
|
// `{ colors, onImage }`: the theme colours as they read over the picture.
|
|
5883
5923
|
// 1.179.0: additive (sc-11253) — `useNavigation().goToStartPage()` opens
|
|
5884
5924
|
// the signed-in user's own start page on both hosts.
|
|
5885
|
-
|
|
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",
|
|
5886
5942
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5887
5943
|
hooks: HOOKS,
|
|
5888
5944
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -1397,7 +1397,8 @@ const HOOKS = [
|
|
|
1397
1397
|
"create the row, call upload(file, { recordId: row.id }) — the per-call " +
|
|
1398
1398
|
"recordId names the row the hook could not know at render — then write " +
|
|
1399
1399
|
"the returned id into the FILE column with update(). A logged-out " +
|
|
1400
|
-
"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 " +
|
|
1401
1402
|
"no row: 'public' for content the app shows to anyone browsing, " +
|
|
1402
1403
|
"'project' for signed-in workspace users only, 'personal' for a file " +
|
|
1403
1404
|
"private to the uploading app user.",
|
|
@@ -1796,6 +1797,45 @@ const HOOKS = [
|
|
|
1796
1797
|
requiredContextSlice: ["datastore.records"],
|
|
1797
1798
|
scopes: ["datastore.read:*"],
|
|
1798
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
|
+
},
|
|
1799
1839
|
{
|
|
1800
1840
|
name: "useDatastoreSchema",
|
|
1801
1841
|
signature: "useDatastoreSchema(tableId)",
|
|
@@ -3559,7 +3599,7 @@ const MANIFEST_SCHEMA = {
|
|
|
3559
3599
|
type: "object",
|
|
3560
3600
|
required: false,
|
|
3561
3601
|
description:
|
|
3562
|
-
"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.",
|
|
3563
3603
|
},
|
|
3564
3604
|
translations: {
|
|
3565
3605
|
type: "object",
|
|
@@ -5878,7 +5918,23 @@ const CONTRACT = deepFreeze({
|
|
|
5878
5918
|
// `{ colors, onImage }`: the theme colours as they read over the picture.
|
|
5879
5919
|
// 1.179.0: additive (sc-11253) — `useNavigation().goToStartPage()` opens
|
|
5880
5920
|
// the signed-in user's own start page on both hosts.
|
|
5881
|
-
|
|
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",
|
|
5882
5938
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5883
5939
|
hooks: HOOKS,
|
|
5884
5940
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -2706,7 +2706,8 @@ function isRetryableDomainStatus(status) {
|
|
|
2706
2706
|
return status >= 500;
|
|
2707
2707
|
}
|
|
2708
2708
|
|
|
2709
|
-
|
|
2709
|
+
// Internal: shared with stats-hooks.js, not re-exported by the index.
|
|
2710
|
+
export function toDatastoreError(err) {
|
|
2710
2711
|
// An already-mapped SDK error passes straight through. NOTE: the datastore
|
|
2711
2712
|
// CLIENT exports its own class of the same name, so a client rejection does
|
|
2712
2713
|
// NOT match here — it is handled by the envelope read below, which is exactly
|
|
@@ -4804,7 +4805,8 @@ export function useFilestoreUpload(options) {
|
|
|
4804
4805
|
setUploading(true);
|
|
4805
4806
|
setError(null);
|
|
4806
4807
|
try {
|
|
4807
|
-
|
|
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);
|
|
4808
4810
|
setLastUploaded(created || null);
|
|
4809
4811
|
setUploading(false);
|
|
4810
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
|
/**
|
|
@@ -1048,6 +1101,65 @@ export function useDatastoreFigure(
|
|
|
1048
1101
|
options?: FigureOptions,
|
|
1049
1102
|
): FigureResult;
|
|
1050
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
|
+
|
|
1051
1163
|
export interface UserStatsQuery {
|
|
1052
1164
|
days?: 7 | 30 | 90;
|
|
1053
1165
|
/** A `useGroups()` row id: narrows user and invite figures to that group. */
|
|
@@ -1492,6 +1604,15 @@ export interface DatastoreSchemaColumn {
|
|
|
1492
1604
|
display_column_id?: string | null;
|
|
1493
1605
|
/** True when this column is the table's display/identification column. */
|
|
1494
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;
|
|
1495
1616
|
}
|
|
1496
1617
|
|
|
1497
1618
|
export interface DatastoreSchema {
|
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
|
@@ -1397,7 +1397,8 @@ const HOOKS = [
|
|
|
1397
1397
|
"create the row, call upload(file, { recordId: row.id }) — the per-call " +
|
|
1398
1398
|
"recordId names the row the hook could not know at render — then write " +
|
|
1399
1399
|
"the returned id into the FILE column with update(). A logged-out " +
|
|
1400
|
-
"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 " +
|
|
1401
1402
|
"no row: 'public' for content the app shows to anyone browsing, " +
|
|
1402
1403
|
"'project' for signed-in workspace users only, 'personal' for a file " +
|
|
1403
1404
|
"private to the uploading app user.",
|
|
@@ -1796,6 +1797,45 @@ const HOOKS = [
|
|
|
1796
1797
|
requiredContextSlice: ["datastore.records"],
|
|
1797
1798
|
scopes: ["datastore.read:*"],
|
|
1798
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
|
+
},
|
|
1799
1839
|
{
|
|
1800
1840
|
name: "useDatastoreSchema",
|
|
1801
1841
|
signature: "useDatastoreSchema(tableId)",
|
|
@@ -3559,7 +3599,7 @@ const MANIFEST_SCHEMA = {
|
|
|
3559
3599
|
type: "object",
|
|
3560
3600
|
required: false,
|
|
3561
3601
|
description:
|
|
3562
|
-
"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.",
|
|
3563
3603
|
},
|
|
3564
3604
|
translations: {
|
|
3565
3605
|
type: "object",
|
|
@@ -5878,7 +5918,23 @@ const CONTRACT = deepFreeze({
|
|
|
5878
5918
|
// `{ colors, onImage }`: the theme colours as they read over the picture.
|
|
5879
5919
|
// 1.179.0: additive (sc-11253) — `useNavigation().goToStartPage()` opens
|
|
5880
5920
|
// the signed-in user's own start page on both hosts.
|
|
5881
|
-
|
|
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",
|
|
5882
5938
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5883
5939
|
hooks: HOOKS,
|
|
5884
5940
|
primitives: PRIMITIVES,
|