@uniweb/api 0.6.9 → 0.7.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 +11 -9
- package/package.json +2 -2
- package/src/client.js +28 -19
- package/src/hooks/useRecords.js +1 -1
- package/src/index.js +1 -1
- package/src/mock/index.js +2 -2
- package/src/mock/node.js +2 -2
- package/src/mock/store.js +1 -1
- package/src/wire.js +4 -4
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @uniweb/api
|
|
2
2
|
|
|
3
|
-
The client a foundation uses to talk to **its site's `
|
|
3
|
+
The client a foundation uses to talk to **its site's `backend` service** — sign-in, the
|
|
4
4
|
site's members, and the content they create.
|
|
5
5
|
|
|
6
6
|
You import it the way you import `@uniweb/kit`. It is bundled into your foundation,
|
|
7
|
-
tree-shaken, and **inert on a site that has no `
|
|
7
|
+
tree-shaken, and **inert on a site that has no `backend` service**: nothing throws, no request
|
|
8
8
|
leaves, and your components render the version of themselves that does not need one.
|
|
9
9
|
|
|
10
10
|
```bash
|
|
@@ -17,9 +17,9 @@ Ask before you draw. This is a synchronous read of the site's own configuration,
|
|
|
17
17
|
a probe — there is nothing to await.
|
|
18
18
|
|
|
19
19
|
```jsx
|
|
20
|
-
import {
|
|
20
|
+
import { isBackendEnabled } from '@uniweb/kit'
|
|
21
21
|
|
|
22
|
-
if (!
|
|
22
|
+
if (!isBackendEnabled()) return <StaticVersion />
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
⛔ **And when the answer is no, draw nothing** — not a disabled button, and not an
|
|
@@ -79,7 +79,7 @@ const { status, records, hasMore } = useRecords({ schema: '@acme/session', scope
|
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
⭐ **`absent` and an empty `ready` are different answers.** `absent` means there is no
|
|
82
|
-
live source — no `
|
|
82
|
+
live source — no `backend` service, or nobody signed in — so render your site's own content.
|
|
83
83
|
`ready` with `records: []` means the service answered and there is nothing there.
|
|
84
84
|
Showing "nothing yet" for the first case tells a visitor their content is missing
|
|
85
85
|
when it is simply not being asked for.
|
|
@@ -170,8 +170,9 @@ Building against a live service is slow and puts a shared database behind your
|
|
|
170
170
|
experiments. Name a local one in `site.yml`:
|
|
171
171
|
|
|
172
172
|
```yaml
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
services:
|
|
174
|
+
backend: true # ask your host for it, on a site you publish
|
|
175
|
+
$devBackend: ./mock/api.js # what answers it in `uniweb dev`; never published
|
|
175
176
|
```
|
|
176
177
|
|
|
177
178
|
```js
|
|
@@ -203,8 +204,9 @@ export default createMockBackend({
|
|
|
203
204
|
}).fetch
|
|
204
205
|
```
|
|
205
206
|
|
|
206
|
-
`uniweb dev` mounts it at
|
|
207
|
-
|
|
207
|
+
`uniweb dev` mounts it at an address of its own on the dev server, and hands your site that
|
|
208
|
+
address the way a host hands over its own — same origin, so cookies behave as they will in
|
|
209
|
+
production, and nothing in your site's configuration changes between the two.
|
|
208
210
|
|
|
209
211
|
⭐ **The mock answers what the backend answers** — the same statuses, bodies and
|
|
210
212
|
refusals, for every route this package uses. It enforces what your schemas declare —
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "The client for a Uniweb site's own backend — session, records, entities — imported by a foundation the way it imports @uniweb/kit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"node": ">=20.19"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@uniweb/core": "^0.
|
|
37
|
+
"@uniweb/core": "^0.38.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"react": "^19.0.0"
|
package/src/client.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @uniweb/api — the client half. No React in this module.
|
|
3
3
|
*
|
|
4
4
|
* A foundation's client for the site's own backend — the one a host declares
|
|
5
|
-
* as the site service `
|
|
5
|
+
* as the site service `backend`. Absent that declaration the site has no such
|
|
6
6
|
* backend and everything here is inert: no request leaves, and a component
|
|
7
7
|
* renders for that state rather than retrying it.
|
|
8
8
|
*
|
|
@@ -18,8 +18,16 @@ import { composeUrl, isCrossOrigin, readBody, UNSAFE } from './http.js'
|
|
|
18
18
|
import { AUTH, ROUTES, PARAM, FIELD, LIST, OP, GUARDED_OPS, CREATE, READ, SCHEMA, PAGE, TOTP } from './wire.js'
|
|
19
19
|
import { Ledger } from './ledger.js'
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The site service this package reads its base from — the only name it owns.
|
|
23
|
+
*
|
|
24
|
+
* ⛔ **It was `api` until 2026-10-07.** A service is named for what it gives the
|
|
25
|
+
* site — `search`, `records` — and this one is the site's own backend; `/api` is
|
|
26
|
+
* only the interface it answers on, which is what this package is named for. A
|
|
27
|
+
* foundation built before then asks for `api`, which no host offers now, and draws
|
|
28
|
+
* no sign-in until it is rebuilt.
|
|
29
|
+
*/
|
|
30
|
+
export const SERVICE_NAME = 'backend'
|
|
23
31
|
|
|
24
32
|
/**
|
|
25
33
|
* The shape of the shared instance, as a contract between copies of this
|
|
@@ -31,10 +39,10 @@ export const CONTRACT = 1
|
|
|
31
39
|
/**
|
|
32
40
|
* Where the site's backend is, if it has one.
|
|
33
41
|
*
|
|
34
|
-
* `resolveService` answers with the host's offer first (`config.services.
|
|
35
|
-
* then the site's own
|
|
36
|
-
* names
|
|
37
|
-
* not an error.
|
|
42
|
+
* `resolveService` answers with the host's offer first (`config.services.backend`),
|
|
43
|
+
* then the site's own address (`backend:` under `services:` in `site.yml`), and
|
|
44
|
+
* `null` when neither names one. Absence is the ordinary state of a site with no
|
|
45
|
+
* backend, not an error.
|
|
38
46
|
*
|
|
39
47
|
* @param {object} website - the active Website, or anything shaped `{ config, basePath }`
|
|
40
48
|
* @returns {string|null} the base every request is made against, or null
|
|
@@ -44,34 +52,35 @@ export function resolveBase(website) {
|
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
/**
|
|
47
|
-
* Does this site have an app backend — the `
|
|
55
|
+
* Does this site have an app backend — the `backend` service?
|
|
48
56
|
*
|
|
49
57
|
* The question to ask before drawing a sign-in affordance or any control only a
|
|
50
58
|
* backend can answer. False means: draw nothing, or the static alternative the
|
|
51
59
|
* site already carries.
|
|
52
60
|
*
|
|
53
61
|
* ⭐ **The website argument is OPTIONAL and defaults to the active one**, so this
|
|
54
|
-
* is the same call as `@uniweb/kit`'s `
|
|
62
|
+
* is the same call as `@uniweb/kit`'s `isBackendEnabled()` — one predicate per
|
|
55
63
|
* service, no arguments, the website resolved for you. Pass one explicitly only
|
|
56
64
|
* when you already hold it, or are working outside a render.
|
|
57
65
|
*
|
|
58
66
|
* ⛔ **THE DEFAULT IS NOT A CONVENIENCE, IT CLOSES A TRAP.** For one commit this
|
|
59
67
|
* took a REQUIRED website while every doc and template showed the no-argument
|
|
60
|
-
* spelling — so
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
68
|
+
* spelling — so the documented call resolved `undefined`, returned `false`
|
|
69
|
+
* forever, and drew no sign-in UI on a site that had a backend. Silent, and
|
|
70
|
+
* indistinguishable from a site with no backend: exactly the invisible absence
|
|
71
|
+
* this predicate exists to prevent. Caught in review the same day; pinned by
|
|
72
|
+
* `tests/enabled.test.js`.
|
|
65
73
|
*
|
|
66
|
-
* ⛔ **Renamed
|
|
67
|
-
* its subject and collided with three unrelated `isEnabled`s in the
|
|
68
|
-
* including the field `useSearch()` returns, which made `uniweb
|
|
69
|
-
* search control as gated when it was gated on this instead.
|
|
74
|
+
* ⛔ **Renamed twice.** From `isEnabled` (2026-09-10): the old name said nothing
|
|
75
|
+
* about its subject and collided with three unrelated `isEnabled`s in the
|
|
76
|
+
* framework — including the field `useSearch()` returns, which made `uniweb
|
|
77
|
+
* doctor` read a search control as gated when it was gated on this instead. Then
|
|
78
|
+
* from `isApiEnabled` (2026-10-07), with the service it asks about (`SERVICE_NAME`).
|
|
70
79
|
*
|
|
71
80
|
* @param {object} [website] - defaults to the active website
|
|
72
81
|
* @returns {boolean}
|
|
73
82
|
*/
|
|
74
|
-
export function
|
|
83
|
+
export function isBackendEnabled(website = getUniweb()?.activeWebsite ?? null) {
|
|
75
84
|
return resolveBase(website) !== null
|
|
76
85
|
}
|
|
77
86
|
|
package/src/hooks/useRecords.js
CHANGED
|
@@ -18,7 +18,7 @@ const DISABLED = Object.freeze({ status: 'absent', records: NONE, matched: 0, ha
|
|
|
18
18
|
* ## ⭐ `absent` and an empty `ready` are DIFFERENT, and conflating them is the bug
|
|
19
19
|
*
|
|
20
20
|
* `absent` means **there is no live source for this viewer** — a site with no
|
|
21
|
-
* `
|
|
21
|
+
* `backend` service, which is the ordinary standalone case, or nobody signed in, since
|
|
22
22
|
* the backend lists nothing to an anonymous caller (it answers `401`, so no request
|
|
23
23
|
* is sent once the session is known to be anonymous). `ready` with `records: []`
|
|
24
24
|
* means **the source answered, and there is nothing there.**
|
package/src/index.js
CHANGED
package/src/mock/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { MockStore, isUuid, refuse } from './store.js'
|
|
|
3
3
|
import { DEFAULT_SEED } from './seed.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* A mock of a site's `
|
|
6
|
+
* A mock of a site's `backend` service, for local development.
|
|
7
7
|
*
|
|
8
8
|
* ```js
|
|
9
9
|
* import { createMockBackend } from '@uniweb/api/mock'
|
|
@@ -29,7 +29,7 @@ import { DEFAULT_SEED } from './seed.js'
|
|
|
29
29
|
* ## Where it answers
|
|
30
30
|
*
|
|
31
31
|
* Its routes are the API's own — `/auth/me`, `/entities` — under whatever the
|
|
32
|
-
* site's `
|
|
32
|
+
* site's `backend` address is. It accepts a request with the site's `/_api` prefix
|
|
33
33
|
* still on, and one with an extra `/api` after it (how clients before 0.4
|
|
34
34
|
* composed URLs), so it answers the same whether or not the server mounting it
|
|
35
35
|
* strips its path.
|
package/src/mock/node.js
CHANGED
|
@@ -40,7 +40,7 @@ async function send(response, res) {
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Connect-style middleware — what Vite's `configureServer` takes. Mount it at the
|
|
43
|
-
* site's `
|
|
43
|
+
* site's `backend` address (`/_api`), which is the base the client composes routes
|
|
44
44
|
* onto: `/_api/auth/me`, `/_api/entities`.
|
|
45
45
|
*
|
|
46
46
|
* ```js
|
|
@@ -76,7 +76,7 @@ export function middleware(mock, { prefix = '/_api' } = {}) {
|
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* A standalone server. Resolves once listening; call `close()` to stop. Its routes
|
|
79
|
-
* answer at the root — a site's `
|
|
79
|
+
* answer at the root — a site's `backend` address for it is the server's URL
|
|
80
80
|
* (`http://localhost:8787`), plus `prefix` when one is given.
|
|
81
81
|
*
|
|
82
82
|
* @param {{ fetch: Function }} mock
|
package/src/mock/store.js
CHANGED
|
@@ -6,7 +6,7 @@ import { checkItemWrite, OUTCOME } from './schema-shape.js'
|
|
|
6
6
|
* entities whose content is items.
|
|
7
7
|
*
|
|
8
8
|
* ⭐ **It stores what the backend stores, the way the backend stores it** —
|
|
9
|
-
* measured 2026-09-18 against a real one set up as a site's `
|
|
9
|
+
* measured 2026-09-18 against a real one set up as a site's `backend` service:
|
|
10
10
|
*
|
|
11
11
|
* - **An entity's content is items, each in a section of its Model.** There is no
|
|
12
12
|
* entity-level data. A `single` section holds one item; its `brief` section's item
|
package/src/wire.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* ## ⭐ MEASURED 2026-09-18 — every shape below, in literal responses
|
|
23
23
|
*
|
|
24
24
|
* The whole surface this package touches was exercised against a real backend set
|
|
25
|
-
* up the way a site's `
|
|
25
|
+
* up the way a site's `backend` service is: one operator account, members who sign up
|
|
26
26
|
* themselves, and the site's own Models. Where this module quotes a shape it was
|
|
27
27
|
* copied from a response, not described. That pass corrected the package in these
|
|
28
28
|
* places, each now fixed and pinned by a test:
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
* The lane. ⭐ **RULED** *(2026-09-01)*: this package reads and writes
|
|
48
48
|
* **entities**, reads the definitions of their Models, and touches nothing under
|
|
49
49
|
* `/sites/*`. Those routes create sites, which is the app's job, not a
|
|
50
|
-
* foundation's, and a site's own `
|
|
50
|
+
* foundation's, and a site's own `backend` service has no site to address.
|
|
51
51
|
*
|
|
52
52
|
* ⛔ Do not add a route that is not under `/entities`, `/models` or `/auth`.
|
|
53
53
|
*/
|
|
@@ -59,7 +59,7 @@ export const MODELS = '/models'
|
|
|
59
59
|
/**
|
|
60
60
|
* The base, and how a route joins it. **MEASURED.**
|
|
61
61
|
*
|
|
62
|
-
* The base the site is handed for its `
|
|
62
|
+
* The base the site is handed for its `backend` service is the address of the
|
|
63
63
|
* backend's API route space, and every route here is relative to it:
|
|
64
64
|
* `${base}/auth/me`, `${base}/entities`. On a hosted site that is `/_api` on the
|
|
65
65
|
* site's own origin, which reaches the backend's `/api/…`; wherever a page reaches
|
|
@@ -138,7 +138,7 @@ export const TOTP = {
|
|
|
138
138
|
* `unit_admin`, `content_editor`, `user`. An ordinary member holds none: `[]`.
|
|
139
139
|
*
|
|
140
140
|
* `workspace` is `{ unit_uuid, handle }` — **the workspace the request works in**. This
|
|
141
|
-
* package's requests name none, and on a site's `
|
|
141
|
+
* package's requests name none, and on a site's `backend` service such a request lands in
|
|
142
142
|
* the service's **home org**, `handle: 'home'`, in which every account is enrolled.
|
|
143
143
|
* Both `null` there means an account that is not a member — a service account, a
|
|
144
144
|
* revoked membership. (On a backend with no home org, naming none is personal: both
|