@uniweb/api 1.2.0 → 1.2.2
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 +139 -2
- package/package.json +1 -1
- package/src/client.js +232 -5
- package/src/hooks/useChoices.js +102 -0
- package/src/hooks/useDefinition.js +78 -0
- package/src/index.js +5 -0
- package/src/mock/definition.js +119 -0
- package/src/mock/index.js +61 -7
- package/src/mock/schema-shape.js +151 -6
- package/src/mock/store.js +375 -41
- package/src/wire.js +149 -11
package/src/wire.js
CHANGED
|
@@ -40,6 +40,18 @@
|
|
|
40
40
|
* | `acting_unit_id` was a membership signal | every signed-in member of a site acts in the same unit — it cannot tell an operator from a member; `roles` can |
|
|
41
41
|
* | the viewer carried `actingUnitId` | it carries `workspace` — where the request landed: on a site's service, its home org (2026-09-23; `acting_unit_id` leaves the wire) |
|
|
42
42
|
*
|
|
43
|
+
* ## ⭐ MEASURED 2026-10-14 — a Model's definition in full, and a Section's items
|
|
44
|
+
*
|
|
45
|
+
* Against a backend holding data schemas registered the way a site's are, and items
|
|
46
|
+
* written through `/entities`: what a definition carries for each field and section
|
|
47
|
+
* (`SCHEMA`), the route that lists a Section's items (`SECTION_ITEMS`), and the
|
|
48
|
+
* refusals a field's rules answer (`PROBLEM`). That pass corrected:
|
|
49
|
+
*
|
|
50
|
+
* | was | is |
|
|
51
|
+
* |---|---|
|
|
52
|
+
* | the mock's definition gave each field `{ key, kind }` and nothing else | a field's whole declaration is under `type.data`; an `enum` is a `one_of` rule in its section's `constraints`; a section's `label` and `description` are in `other_data`; a nested section is a row of its own naming its `parent_section_id` |
|
|
53
|
+
* | the list took `include_disabled` | the backend refuses it — `400 Unexpected parameters: include_disabled` — and lists a disabled entity like any other |
|
|
54
|
+
*
|
|
43
55
|
* @module @uniweb/api/wire
|
|
44
56
|
*/
|
|
45
57
|
|
|
@@ -49,19 +61,22 @@
|
|
|
49
61
|
* `/sites/*`. Those routes create sites, which is the app's job, not a
|
|
50
62
|
* foundation's, and a site's own `backend` service has no site to address.
|
|
51
63
|
*
|
|
52
|
-
* Its routes are under `/entities`, `/models` and `/auth`, plus what a
|
|
53
|
-
* with an invitation — claim an offer (`OFFERS`), answer an invitation
|
|
64
|
+
* Its routes are under `/entities`, `/models`, `/sections` and `/auth`, plus what a
|
|
65
|
+
* member does with an invitation — claim an offer (`OFFERS`), answer an invitation
|
|
54
66
|
* (`INVITATIONS`) — the steps the backend's mail asks a member to finish on the site
|
|
55
67
|
* (2026-10-11). ⛔ Never `/sites/*`, and nothing that administers: the backend's
|
|
56
68
|
* administration is reached through the Admin link (`AUTH.memberHandoff`), never
|
|
57
69
|
* wrapped. *(This said "no route that is not under `/entities`, `/models` or
|
|
58
|
-
* `/auth`" until 2026-10-11.)*
|
|
70
|
+
* `/auth`" until 2026-10-11, and left out `/sections` until 2026-10-14.)*
|
|
59
71
|
*/
|
|
60
72
|
export const ENTITIES = '/entities'
|
|
61
73
|
|
|
62
74
|
/** The Models lane — read-only, one route: a Model's definition. MEASURED. */
|
|
63
75
|
export const MODELS = '/models'
|
|
64
76
|
|
|
77
|
+
/** The Sections lane — read-only, one route: a Section's items (`SECTION_ITEMS`). MEASURED. */
|
|
78
|
+
export const SECTIONS = '/sections'
|
|
79
|
+
|
|
65
80
|
/**
|
|
66
81
|
* The base, and how a route joins it. **MEASURED.**
|
|
67
82
|
*
|
|
@@ -397,6 +412,8 @@ export const ROUTES = {
|
|
|
397
412
|
removeBatch: () => `${ENTITIES}/delete`,
|
|
398
413
|
/** `GET /models/@scope/name` — the Model's definition, its sections among it. */
|
|
399
414
|
schema: (model) => `${MODELS}/${modelPath(model)}`,
|
|
415
|
+
/** `GET /sections/{uuid}/items` — one page of a Section's items (`SECTION_ITEMS`). */
|
|
416
|
+
sectionItems: (uuid) => `${SECTIONS}/${segment(uuid)}/items`,
|
|
400
417
|
}
|
|
401
418
|
|
|
402
419
|
/**
|
|
@@ -407,13 +424,18 @@ export const ROUTES = {
|
|
|
407
424
|
*
|
|
408
425
|
* | route | takes |
|
|
409
426
|
* |---|---|
|
|
410
|
-
* | list | `model` (required) · `scope` · `limit` · `offset` · `paginate` · `locale`
|
|
427
|
+
* | list | `model` (required) · `scope` · `limit` · `offset` · `paginate` · `locale` |
|
|
411
428
|
* | read | `model` (required) · `via` · `depth` · `max_depth` · `locale` |
|
|
412
429
|
* | items | `model` (required) · `readback` |
|
|
413
430
|
* | create | `model` (required) |
|
|
414
431
|
* | remove | `rev_ref_policy` |
|
|
432
|
+
* | section items | `entity` · `parent` · `field` · `q` · `locale` · `cursor` · `limit` |
|
|
415
433
|
* | verify (`AUTH.verify`) | `token` (required) |
|
|
416
434
|
*
|
|
435
|
+
* ⛔ **The list takes no `include_disabled`** — `400 Unexpected parameters:
|
|
436
|
+
* include_disabled` (MEASURED 2026-10-14). A disabled entity lists like any other,
|
|
437
|
+
* and its row says `disabled`; the flag only keeps it off a site's public reads.
|
|
438
|
+
*
|
|
417
439
|
* ✅ **`via` and `depth` compose** (an assumption retired 2026-09-18): both are
|
|
418
440
|
* parameters of the same read and answer different questions — `via` *who may
|
|
419
441
|
* read* (through a container the viewer is entitled to), `depth` *how much is
|
|
@@ -431,6 +453,7 @@ export const PARAM = {
|
|
|
431
453
|
offset: 'offset',
|
|
432
454
|
locale: 'locale',
|
|
433
455
|
paginate: 'paginate',
|
|
456
|
+
/** ⛔ Refused by the backend (see above) and never sent. Kept so an import of it does not break. */
|
|
434
457
|
includeDisabled: 'include_disabled',
|
|
435
458
|
via: 'via',
|
|
436
459
|
depth: 'depth',
|
|
@@ -438,6 +461,16 @@ export const PARAM = {
|
|
|
438
461
|
readback: 'readback',
|
|
439
462
|
revRefPolicy: 'rev_ref_policy',
|
|
440
463
|
token: 'token',
|
|
464
|
+
/** Section items: only the items this entity holds. */
|
|
465
|
+
entity: 'entity',
|
|
466
|
+
/** Section items: the items under this item — the level below it. */
|
|
467
|
+
parent: 'parent',
|
|
468
|
+
/** Section items: the field `q` searches. */
|
|
469
|
+
field: 'field',
|
|
470
|
+
/** Section items: the search term. */
|
|
471
|
+
q: 'q',
|
|
472
|
+
/** Section items: the previous page's `next_cursor`. */
|
|
473
|
+
cursor: 'cursor',
|
|
441
474
|
}
|
|
442
475
|
|
|
443
476
|
/**
|
|
@@ -608,11 +641,40 @@ export const READ = {
|
|
|
608
641
|
}
|
|
609
642
|
|
|
610
643
|
/**
|
|
611
|
-
* A Model's definition — `GET /models/@scope/name`. **MEASURED
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
*
|
|
615
|
-
*
|
|
644
|
+
* A Model's definition — `GET /models/@scope/name`. **MEASURED** 2026-10-14, on data
|
|
645
|
+
* schemas registered the way a site's are.
|
|
646
|
+
*
|
|
647
|
+
* `{ model, sections }`:
|
|
648
|
+
*
|
|
649
|
+
* - **`model`** — `{ id, uuid, name, data, version, owner_id, unit_id, owned,
|
|
650
|
+
* owner_cardinality, unit_cardinality, role, created_at, updated_at }`. `data` holds
|
|
651
|
+
* what the schema declares of the Model — `label`, `description`, `icon`, `plural`
|
|
652
|
+
* among them — and `grantable`.
|
|
653
|
+
* - **each section** — `{ id, model_id, name, kind, is_brief, parent_section_id, fields,
|
|
654
|
+
* other_data, constraints }`. `kind` is `single`, `multi`, or `binder`: a section that
|
|
655
|
+
* holds only sections. A nested section is a row of its own, `parent_section_id`
|
|
656
|
+
* naming its parent; top-level rows come first, and each group by name. ⛔ **A
|
|
657
|
+
* section's uuid is not in the definition** — a field whose choices are a section's
|
|
658
|
+
* items names that section's uuid itself (below).
|
|
659
|
+
* - **`other_data`** — what the schema declares of the section: `label`, `description`,
|
|
660
|
+
* `append_only` (insert-only), `allow_self_nesting` (its items nest — `self_nesting:
|
|
661
|
+
* true` in a schema), `date_field`.
|
|
662
|
+
* - **`constraints`** — `{ rules: [...] }`, or `{}` with none: the section's own rules
|
|
663
|
+
* first, then `{ kind: 'one_of', field, values }` for each field that declares an
|
|
664
|
+
* `enum`. ⛔ **An `enum` is not on its field** — it is that rule.
|
|
665
|
+
* - **each field** — `{ key, required, multi, type: { id: -1, name, kind, data } }`. ⭐
|
|
666
|
+
* **`type.data` is the field's whole declaration**: `key`, `kind`, and what the field
|
|
667
|
+
* declares — `required`, `multi`, `label`, `description`, `localized`, `format`,
|
|
668
|
+
* `max_length`, `min`, `max` among them. `type` is a wrapper kept from an older
|
|
669
|
+
* shape: its `id` is always `-1`, and its `name` is the kind. ⭐ A `date` field's
|
|
670
|
+
* `format` is its precision — `year-month` or `year`, ISO 8601's reduced precision, its
|
|
671
|
+
* values `YYYY-MM` or `YYYY` as written — and a `date` with none holds a day (since
|
|
672
|
+
* 2026-10-14).
|
|
673
|
+
* - **a field of kind `item_ref`** — a choice from a list — carries `section_uuid`, the
|
|
674
|
+
* section whose items are its choices (a schema's `options:` path becomes it), and
|
|
675
|
+
* `entity_uuid` when it pins the entry that holds them. Its choices are
|
|
676
|
+
* `ROUTES.sectionItems(section_uuid)`. A field of kind `entity_ref` carries the
|
|
677
|
+
* `model_uuid` it pins (read in the backend's source).
|
|
616
678
|
*
|
|
617
679
|
* ⭐ **Readable by every signed-in account, whatever it may create** — closing a
|
|
618
680
|
* Model gates creating its entries, never reading its definition (ruled 2026-10-14;
|
|
@@ -628,6 +690,67 @@ export const SCHEMA = {
|
|
|
628
690
|
name: 'name',
|
|
629
691
|
kind: 'kind',
|
|
630
692
|
parent: 'parent_section_id',
|
|
693
|
+
isBrief: 'is_brief',
|
|
694
|
+
fields: 'fields',
|
|
695
|
+
otherData: 'other_data',
|
|
696
|
+
constraints: 'constraints',
|
|
697
|
+
rules: 'rules',
|
|
698
|
+
/** A field's declaration is `field[fieldType][fieldDecl]` — `field.type.data`. */
|
|
699
|
+
fieldType: 'type',
|
|
700
|
+
fieldDecl: 'data',
|
|
701
|
+
/** The rule an `enum` becomes, and the keys it names its field and values by. */
|
|
702
|
+
oneOf: 'one_of',
|
|
703
|
+
ruleField: 'field',
|
|
704
|
+
ruleValues: 'values',
|
|
705
|
+
/** On an `item_ref` field: the section its choices are in, and the entry it pins. */
|
|
706
|
+
choicesSection: 'section_uuid',
|
|
707
|
+
choicesEntity: 'entity_uuid',
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* A Section's items — `GET /sections/{uuid}/items`. **MEASURED** 2026-10-14.
|
|
712
|
+
*
|
|
713
|
+
* The choices of an `item_ref` field: the field declares the section by uuid
|
|
714
|
+
* (`SCHEMA.choicesSection`), and that uuid is all the route needs — never the Model,
|
|
715
|
+
* which the section determines.
|
|
716
|
+
*
|
|
717
|
+
* `{ items: [{ uuid, entity, parent?, data, has_children? }], next_cursor }`:
|
|
718
|
+
*
|
|
719
|
+
* - **`uuid`** is what an `item_ref` field stores — its value is that string. `entity`
|
|
720
|
+
* is the entry that holds the item; `parent`, the item it sits under (absent at a
|
|
721
|
+
* root); `has_children`, whether items of the same section sit under it — only for a
|
|
722
|
+
* section whose items nest. `data` is projected to `locale` as every read is.
|
|
723
|
+
* - **Whose items:** those of every entry of the section's Model the viewer may read —
|
|
724
|
+
* the list's rule (`SCOPE`'s `accessible`) — or, with `entity`, that entry's alone:
|
|
725
|
+
* `404` when the viewer may not read it, and when it is of another Model.
|
|
726
|
+
* - **One level at a time.** Without `parent`: the section's top level — every item of a
|
|
727
|
+
* list that does not nest, the roots of one that does, and all the items of a section
|
|
728
|
+
* nested in another. With `parent`: the items under that item — the next level of a
|
|
729
|
+
* nesting list, or a **dependent list's** choices under the item chosen in the field
|
|
730
|
+
* it depends on. A parent the viewer cannot read, or that does not exist, gives an
|
|
731
|
+
* empty page.
|
|
732
|
+
* - **In the section's own order** — an author's arrangement, by entry.
|
|
733
|
+
* - **`field` + `q`** — the items whose `field` (a single `string` or `text` field of
|
|
734
|
+
* the section) holds `q`, ignoring case, anywhere in it; a localized field is matched
|
|
735
|
+
* in the `locale` asked for, and in any language without one. ⚠️ **Such a page may be
|
|
736
|
+
* short while more follow**: only `next_cursor: null` ends the list.
|
|
737
|
+
* - **Paging** — `limit` 50 by default, at most 1000; `cursor` is the opaque
|
|
738
|
+
* `next_cursor` of the page before, and `null` on the last page.
|
|
739
|
+
*
|
|
740
|
+
* Refused: signed out `401`; a section uuid that is not one `400 "invalid section
|
|
741
|
+
* uuid: …"`, an unknown one `404 { kind: 'section' }`; a stale cursor `400 "cursor
|
|
742
|
+
* expired, restart pagination"`; a `binder`, which holds no items, `400 Validation`; a
|
|
743
|
+
* `q` with no `field` `400 Validation`; a `field` the section does not declare, or that
|
|
744
|
+
* is not a single `string` or `text`, `400 Validation` naming it in `field`.
|
|
745
|
+
*/
|
|
746
|
+
export const SECTION_ITEMS = {
|
|
747
|
+
items: 'items',
|
|
748
|
+
next: 'next_cursor',
|
|
749
|
+
uuid: 'uuid',
|
|
750
|
+
entity: 'entity',
|
|
751
|
+
parent: 'parent',
|
|
752
|
+
data: 'data',
|
|
753
|
+
hasChildren: 'has_children',
|
|
631
754
|
}
|
|
632
755
|
|
|
633
756
|
/**
|
|
@@ -639,12 +762,27 @@ export const SCHEMA = {
|
|
|
639
762
|
* | `409 Conflict` + `item_id`, `current_updated_at` | that item changed since this viewer's token — the ONLY stale-token answer. `item_id` since 2026-09-18; an older backend sends the token alone |
|
|
640
763
|
* | `409 Conflict`, no token | an invitation that no longer waits — answered, or withdrawn (`INVITATIONS`, 2026-10-12) |
|
|
641
764
|
* | `409 Append-Only Section` + `section` | an insert-only section: its items can be added and moved, not edited or deleted |
|
|
642
|
-
* | `409 Schema Rule Violation` | a rule of the Model: a second item in a one-item section, a section of another Model, a reference that still points here |
|
|
765
|
+
* | `409 Schema Rule Violation` | a rule of the Model: a second item in a one-item section, a section of another Model, a reference that still points here, an item of a nested section without a parent of its parent section |
|
|
766
|
+
* | `409 Bad Reference` | a reference the field does not accept — for an `item_ref`, an item of another section than the one it pins (MEASURED 2026-10-14) |
|
|
643
767
|
* | `403 Forbidden` + `op`, `target` | may read it, may not do this to it — `edit`, `delete`, `use_model` (create) |
|
|
644
768
|
* | `403 Email Not Verified` | right password, unverified address |
|
|
645
769
|
* | `403 CSRF Header Required` | a cookie-authenticated mutation without `X-Uniweb-Csrf` |
|
|
646
|
-
* | `400 Validation` + `field` | content that does not fit the Model (`field: 'data.minutes'`) |
|
|
770
|
+
* | `400 Validation` + `field` | content that does not fit the Model (`field: 'data.minutes'`) — see below |
|
|
647
771
|
* | `404 Not Found` + `kind`, `key` | nothing here for you — not found or not permitted |
|
|
772
|
+
*
|
|
773
|
+
* ⭐ **A field's rule, refused** — `400 Validation`, `reason: 'invalid_field'`, `rule`
|
|
774
|
+
* naming the declaration the value broke, and the declaration's bound beside it
|
|
775
|
+
* (MEASURED 2026-10-14):
|
|
776
|
+
*
|
|
777
|
+
* | rule | the problem's own keys |
|
|
778
|
+
* |---|---|
|
|
779
|
+
* | `required` | `field: 'data.title'` |
|
|
780
|
+
* | `max_length` — in characters, per language for a localized value | `field: 'data.title'` (`'data.label.en'`), `max_length` |
|
|
781
|
+
* | `min` · `max` | `field`, and `min` or `max` |
|
|
782
|
+
* | `format` — a `date` at another precision than its field declares: a day `YYYY-MM-DD`, or with `format: year-month` a month `YYYY-MM`, with `format: year` a year `YYYY` | `field`, `format` — the precision it should be: `'date'`, `'year-month'`, `'year'` |
|
|
783
|
+
* | `format` — a `string` or `text` that is not its `format`: `email`, `url`, `currency` | `field`, `format` |
|
|
784
|
+
* | `type` | `field` |
|
|
785
|
+
* | `one_of` — a value outside a field's `enum` | `field` — since 2026-10-14. ⚠️ A backend from before names the field only in `detail` (`item.level = "x" is not one of the allowed values …`), so read `field` when it is there |
|
|
648
786
|
*/
|
|
649
787
|
export const PROBLEM = {
|
|
650
788
|
csrf: 'CSRF Header Required',
|