@foir/sdk 0.5.0 → 0.8.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/CHANGELOG.md +1126 -0
- package/README.md +94 -0
- package/dist/auth/auth.d.ts +243 -0
- package/dist/auth/hosted.d.ts +40 -0
- package/dist/auth/index.d.ts +21 -0
- package/dist/auth/operations.d.ts +144 -0
- package/dist/auth/ops.d.ts +99 -0
- package/dist/auth/session-client.d.ts +76 -0
- package/dist/auth-session.js +137 -0
- package/dist/auth.js +973 -92
- package/dist/chunk-IGNLWT3H.js +35 -0
- package/dist/chunk-OHYVKTGB.js +45 -0
- package/dist/chunk-TAL5H6VA.js +39 -0
- package/dist/client/platform-client.d.ts +29 -0
- package/dist/client.js +275 -37
- package/dist/codegen/block-registry.d.ts +13 -23
- package/dist/codegen/block-registry.js +3 -31
- package/dist/codegen/block-scaffold.d.ts +12 -25
- package/dist/codegen/block-scaffold.js +3 -41
- package/dist/codegen/index.d.ts +12 -61
- package/dist/codegen/index.js +7 -11
- package/dist/codegen/item-registry.d.ts +28 -0
- package/dist/codegen/item-registry.js +7 -0
- package/dist/codegen/item-scaffold.d.ts +29 -0
- package/dist/codegen/item-scaffold.js +7 -0
- package/dist/codegen/preset.d.ts +67 -0
- package/dist/codegen/shared.d.ts +36 -0
- package/dist/components/EditorProvider.d.ts +36 -0
- package/dist/content/content-renderer.d.ts +113 -0
- package/dist/content/index.d.ts +18 -0
- package/dist/content/richtext.d.ts +118 -0
- package/dist/content.js +227 -80
- package/dist/data/builtins.d.ts +506 -0
- package/dist/data/client.d.ts +166 -0
- package/dist/data/document.d.ts +251 -0
- package/dist/data/index.d.ts +73 -0
- package/dist/data/manifest.d.ts +424 -0
- package/dist/data/schema.d.ts +313 -0
- package/dist/data/selection.d.ts +524 -0
- package/dist/data/subjects.d.ts +221 -0
- package/dist/data/subscription-document.d.ts +75 -0
- package/dist/data/subscriptions.d.ts +181 -0
- package/dist/data/transport.d.ts +325 -0
- package/dist/data/ws-transport.d.ts +197 -0
- package/dist/data.js +1683 -0
- package/dist/graphql/cdn-route.d.ts +26 -0
- package/dist/graphql/client.d.ts +30 -0
- package/dist/graphql/content-client.d.ts +66 -0
- package/dist/graphql/foir-request.d.ts +47 -0
- package/dist/hooks/useAutoResize.d.ts +22 -0
- package/dist/hooks/useEditor.d.ts +16 -0
- package/dist/index.d.ts +10 -394
- package/dist/media/BlurhashCanvas.d.ts +10 -0
- package/dist/media/ProgressiveMedia.d.ts +56 -0
- package/dist/media/index.d.ts +18 -0
- package/dist/media/media.d.ts +139 -0
- package/dist/media/screen-capture.d.ts +81 -0
- package/dist/media/useProgressiveLoad.d.ts +23 -0
- package/dist/media.js +102 -0
- package/dist/next/cookieAdapter.d.ts +26 -0
- package/dist/next/index.d.ts +15 -0
- package/dist/next/middleware.d.ts +49 -0
- package/dist/next/session.d.ts +53 -0
- package/dist/next.js +763 -78
- package/dist/protocol/constants.d.ts +6 -0
- package/dist/protocol/messages.d.ts +155 -0
- package/dist/protocol/validation.d.ts +8 -0
- package/dist/push/index.d.ts +14 -0
- package/dist/push/operations.d.ts +22 -0
- package/dist/push/web-push.d.ts +39 -0
- package/dist/push.js +892 -78
- package/dist/react/FoirFeedback.d.ts +62 -0
- package/dist/react/FoirProvider.d.ts +116 -0
- package/dist/react/OAuthButtons.d.ts +20 -0
- package/dist/react/SignIn.d.ts +22 -0
- package/dist/react/UserButton.d.ts +13 -0
- package/dist/react/feedback-notes.d.ts +101 -0
- package/dist/react/index.d.ts +25 -0
- package/dist/react/localStorageAdapter.d.ts +10 -0
- package/dist/react.js +1373 -91
- package/dist/{realtime.d.ts → realtime/index.d.ts} +35 -16
- package/dist/realtime.js +1 -1
- package/dist/server/callback-client.d.ts +92 -0
- package/dist/server/events-client.d.ts +279 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/operation-types.d.ts +134 -0
- package/dist/server/require-scoped-token.d.ts +76 -0
- package/dist/server/server-client.d.ts +34 -0
- package/dist/server/types.d.ts +54 -0
- package/dist/server/verify-scoped-token.d.ts +100 -0
- package/dist/server/webhook-helpers.d.ts +36 -0
- package/dist/server.js +298 -0
- package/dist/transport/foir-http.d.ts +234 -0
- package/dist/transport/persisted-query.d.ts +68 -0
- package/dist/transport/retry.d.ts +61 -0
- package/dist/types/index.d.ts +124 -0
- package/package.json +35 -13
- package/dist/auth.d.ts +0 -382
- package/dist/chunk-ADO5FJNM.js +0 -30
- package/dist/client.d.ts +0 -135
- package/dist/content.d.ts +0 -96
- package/dist/media.d.ts +0 -234
- package/dist/next.d.ts +0 -178
- package/dist/push.d.ts +0 -109
- package/dist/react.d.ts +0 -319
- package/dist/server.d.ts +0 -534
- package/dist/shared-DkWlEd2Z.d.ts +0 -18
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,1126 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@foir/sdk`.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
Versioning follows the policy below.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Semver policy
|
|
11
|
+
|
|
12
|
+
`@foir/sdk` is published to npm and installed by people outside this repo. Its
|
|
13
|
+
**public API is the set of named exports reachable through the subpaths in
|
|
14
|
+
`package.json#exports`** — nothing else. Anything reachable only by deep-importing
|
|
15
|
+
`@foir/sdk/dist/...` is private and may change in any release without notice.
|
|
16
|
+
|
|
17
|
+
That surface is pinned in [`public-exports.snapshot.json`](./public-exports.snapshot.json)
|
|
18
|
+
and enforced on every build by `scripts/assert-public-exports.mjs`. If a change
|
|
19
|
+
reds that guard, it belongs in this file.
|
|
20
|
+
|
|
21
|
+
### Pre-1.0 note — breaking changes bump the MINOR
|
|
22
|
+
|
|
23
|
+
While the version is `0.x`, a breaking change bumps the **minor** (`0.5.0` →
|
|
24
|
+
`0.6.0`), not the major. This is not laziness about semver, it is what actually
|
|
25
|
+
protects consumers: npm resolves `^0.5.0` to `>=0.5.0 <0.6.0`, so for a `0.x`
|
|
26
|
+
package the minor — not the major — is the boundary an existing `pnpm install`
|
|
27
|
+
will not cross on its own. Once `1.0.0` ships, breaking changes move to the
|
|
28
|
+
major and this note goes away.
|
|
29
|
+
|
|
30
|
+
Read "MAJOR" below as "minor, while we are pre-1.0".
|
|
31
|
+
|
|
32
|
+
### MAJOR — breaking
|
|
33
|
+
|
|
34
|
+
A change is breaking if code that compiled and ran against the previous version
|
|
35
|
+
can stop compiling, stop resolving, or start behaving differently:
|
|
36
|
+
|
|
37
|
+
- **Removing a named export** from any published subpath.
|
|
38
|
+
- **Renaming a named export.** A rename is a removal plus an addition; shipping
|
|
39
|
+
the old name as a deprecated alias for at least one minor is preferred.
|
|
40
|
+
- **Removing a subpath** from `package.json#exports`, or repointing one at a
|
|
41
|
+
module with a different surface.
|
|
42
|
+
- **Demoting a value export to type-only** (`export { X }` → `export type { X }`).
|
|
43
|
+
The type-checker stays happy at the re-export site and the runtime import
|
|
44
|
+
starts throwing, which is the worst possible failure shape.
|
|
45
|
+
- **Narrowing a type**: making an optional parameter or property required,
|
|
46
|
+
removing a member from a union or an enum, tightening a parameter's accepted
|
|
47
|
+
type, widening a return type, or adding a required field to an options object.
|
|
48
|
+
Rule of thumb — narrowing what we *accept* or widening what we *return* is
|
|
49
|
+
breaking; the reverse is not.
|
|
50
|
+
- **Changing the runtime behaviour of an existing call**: different network
|
|
51
|
+
endpoint, different request or response shape on the wire, different default
|
|
52
|
+
value, throwing where it used to return, returning where it used to throw, or
|
|
53
|
+
a change in what gets persisted (cookie name, storage key, header name).
|
|
54
|
+
- **Raising the floor**: a higher minimum Node version, a higher required
|
|
55
|
+
version of a peer dependency, or making an optional peer dependency required.
|
|
56
|
+
|
|
57
|
+
Every breaking change needs a `### Removed` or `### Changed` entry below that
|
|
58
|
+
**names each affected symbol** and states the migration.
|
|
59
|
+
|
|
60
|
+
### MINOR — additive
|
|
61
|
+
|
|
62
|
+
- A new subpath, a new named export, or a new optional parameter/property.
|
|
63
|
+
- Widening what an existing call accepts, or narrowing what it returns.
|
|
64
|
+
- Promoting a type-only export to a value export.
|
|
65
|
+
- A new optional peer dependency.
|
|
66
|
+
- Deprecating something (the deprecation itself is additive; the eventual
|
|
67
|
+
removal is the breaking part).
|
|
68
|
+
|
|
69
|
+
Additive changes still red the export-surface guard — deliberately, so the
|
|
70
|
+
snapshot cannot rot into a rubber stamp. Refresh it with
|
|
71
|
+
`pnpm --filter @foir/sdk run snapshot:exports` and commit the result.
|
|
72
|
+
|
|
73
|
+
### PATCH — fixes
|
|
74
|
+
|
|
75
|
+
- A bug fix that moves behaviour **toward** what the types and docs already
|
|
76
|
+
promised. If a caller could reasonably have been depending on the buggy
|
|
77
|
+
behaviour, it is a MAJOR, not a PATCH.
|
|
78
|
+
- Internal refactors, build/bundling changes, dependency bumps and performance
|
|
79
|
+
work with no observable change to the public surface.
|
|
80
|
+
- Documentation, comments and JSDoc.
|
|
81
|
+
|
|
82
|
+
### Releasing
|
|
83
|
+
|
|
84
|
+
Bump `packages/sdk/package.json#version`, move `Unreleased` into a dated
|
|
85
|
+
section here, then push a `sdk-v<version>` tag — the tag, not the commit,
|
|
86
|
+
triggers `.github/workflows/sdk-publish.yml`.
|
|
87
|
+
|
|
88
|
+
`pnpm --filter @foir/sdk release:minor` does the bump and the tag together, but
|
|
89
|
+
only because `packages/sdk/.npmrc` sets `tag-version-prefix=sdk-v`. Without it
|
|
90
|
+
`pnpm version` tags `v<version>`, which does **not** match the workflow's
|
|
91
|
+
`sdk-v*` trigger — the bump would land and no publish would ever run. If you
|
|
92
|
+
move or drop that `.npmrc`, tag by hand:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
git tag sdk-v<version> && git push origin sdk-v<version>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The tag must point at a commit on `main`: `main` is what is released, and a tag
|
|
99
|
+
on `dev` would publish unreviewed work to npm.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## [Unreleased]
|
|
104
|
+
|
|
105
|
+
Changes on `dev` that have not been published to npm.
|
|
106
|
+
|
|
107
|
+
**The next release is BREAKING** — see `foir.raw()` under "Changed". A MINOR,
|
|
108
|
+
per the pre-1.0 note above.
|
|
109
|
+
|
|
110
|
+
### Removed
|
|
111
|
+
|
|
112
|
+
- **BREAKING — `FoirBlockMeta`, `MediaBlock`, `TextBlock` and `FlexibleBlock`
|
|
113
|
+
are gone from `./data`.** They have no replacement because the things they
|
|
114
|
+
described are not in the schema:
|
|
115
|
+
|
|
116
|
+
- `FoirBlockMeta` declared `_id`/`_key`/`_label`/`_type` on every member. That
|
|
117
|
+
was the wrapper's envelope, removed at TTS-1391 — an item IS its value. No
|
|
118
|
+
member carries any of the four, so every one was a field the types promised
|
|
119
|
+
and the API never returned.
|
|
120
|
+
- `MediaBlock` — `media` stopped being a primitive at TTS-1391. It was never
|
|
121
|
+
in the proto FieldType enum, and it is the one primitive name a project had
|
|
122
|
+
also used as a model key.
|
|
123
|
+
- `TextBlock` — removed at TTS-1406. GraphQL requires a union member to be an
|
|
124
|
+
object type and a text item's value is a bare scalar, which is the whole
|
|
125
|
+
reason a wrapper existed. An inline model with a text field is how a text
|
|
126
|
+
slot is authored.
|
|
127
|
+
- `FlexibleBlock` — there is no fallback member. An item whose `_type` names
|
|
128
|
+
nothing is nulled out of its list and reported, which is already what a
|
|
129
|
+
stored null reads as.
|
|
130
|
+
|
|
131
|
+
**Migration:** delete the import. Code narrowing on `__typename ===
|
|
132
|
+
'MediaBlock' | 'TextBlock' | 'FlexibleBlock'` was matching nothing, so the
|
|
133
|
+
branch was already dead. Code reading `_id`/`_key`/`_label`/`_type` off a
|
|
134
|
+
member was reading `undefined` through a type that said `string | null`.
|
|
135
|
+
|
|
136
|
+
Nothing generates them either: `foir types` emitted `FoirBlockMeta` and the
|
|
137
|
+
three tokens, and no longer does — see the CLI entry below.
|
|
138
|
+
|
|
139
|
+
### Changed
|
|
140
|
+
|
|
141
|
+
- **BREAKING (`foir types`) — a flexible union member is the model's own type,
|
|
142
|
+
and the per-model wrapper is gone.** `foir types` used to emit a second
|
|
143
|
+
interface for every inline model — `ActHero` AND `ActHeroBlock` — where the
|
|
144
|
+
wrapper was `FoirBlockMeta` plus the model's non-`_`-prefixed fields. On the
|
|
145
|
+
Marketing fixture that was 20 of 47 interfaces describing a shape the platform
|
|
146
|
+
stopped serving at TTS-1391.
|
|
147
|
+
|
|
148
|
+
A union branch is now `(<Model> & { __typename: '<Model>' })`: the model
|
|
149
|
+
itself, intersected with the discriminant the union position guarantees. The
|
|
150
|
+
intersection is not a wrapper — it adds no field the server does not send. It
|
|
151
|
+
is there because the op compiler selects `__typename` on a union's own
|
|
152
|
+
selection set and nowhere else, so the model interface cannot declare it
|
|
153
|
+
outright, and because `@foir/sdk/data`'s narrowing surface is written as
|
|
154
|
+
`U extends { __typename: infer T }` — an optional key does not satisfy that,
|
|
155
|
+
and `pick: { on: … }` would silently stop working.
|
|
156
|
+
|
|
157
|
+
Where a model is reached BOTH as a member and as a model, the two answers now
|
|
158
|
+
live on one declaration keyed on `C`: `PageContent<D, C>` degrades `sections`
|
|
159
|
+
at `C extends true` and expands it otherwise, which is exactly what
|
|
160
|
+
`PageContentBlock` and `PageContent` said separately before.
|
|
161
|
+
|
|
162
|
+
**Migration:** re-run `foir types`. Narrow on `ActHero` rather than
|
|
163
|
+
`ActHeroBlock`; `_type`, `_key` and `_label` are gone with the envelope, and
|
|
164
|
+
`__typename` is the discriminant. Aliased response keys are unchanged — they
|
|
165
|
+
are a wire contract published in the manifest, so `imageBlockHeight` stays
|
|
166
|
+
`imageBlockHeight` until the platform republishes it.
|
|
167
|
+
|
|
168
|
+
- **BREAKING (`foir types`) — a manifest carrying a retired primitive is now
|
|
169
|
+
refused by name.** `text`, `media` and a `flexible` fallback member stopped
|
|
170
|
+
being union members at TTS-1391/1406. A manifest listing one predates that, so
|
|
171
|
+
the generator says so and names re-publishing as the fix rather than emitting
|
|
172
|
+
a branch for a type the server never sends.
|
|
173
|
+
|
|
174
|
+
- **BREAKING — the primitive members are the value types, under the schema's own
|
|
175
|
+
names.** `ImageValue` and `ImageBlock` are one type called `Image`, and the
|
|
176
|
+
same for the rest:
|
|
177
|
+
|
|
178
|
+
| was | now |
|
|
179
|
+
| --- | --- |
|
|
180
|
+
| `ImageValue` · `ImageBlock` | `Image` |
|
|
181
|
+
| `VideoValue` · `VideoBlock` | `Video` |
|
|
182
|
+
| `RichtextValue` · `RichtextBlock` | `Richtext` |
|
|
183
|
+
| `GeoPointValue` | `GeoPoint` |
|
|
184
|
+
| `ReferenceValue` · `ReferenceBlock` | `Reference` |
|
|
185
|
+
| `FileValue` · `FileBlock` | `FileValue` |
|
|
186
|
+
|
|
187
|
+
Every old name is kept as a deprecated alias of the new one, so imports keep
|
|
188
|
+
resolving. What changes underneath them:
|
|
189
|
+
|
|
190
|
+
- **`__typename` is now on each type**, optional, carrying the schema's name —
|
|
191
|
+
`'Image'`, not `'ImageBlock'`. The old literals matched nothing in a current
|
|
192
|
+
project, so an exhaustive `switch` over a flexible field fell through every
|
|
193
|
+
case.
|
|
194
|
+
- **`Image.url` and `Video.url` are nullable.** They were `string`. The
|
|
195
|
+
platform relaxed them to `String` at TTS-1391 — non-null on the value object
|
|
196
|
+
and nullable on the wrapper is a §5.3.2 response-shape conflict once the two
|
|
197
|
+
are one type — so `img.url.startsWith(…)` could throw on a value the types
|
|
198
|
+
said was present.
|
|
199
|
+
- **`Reference` loses `_id` and `_type`, and gains `_targetModelKey`,
|
|
200
|
+
`_targetId`, `_targetNaturalKey` and `record`.** `_id` was the ITEM's id and
|
|
201
|
+
`_type` read `"reference"` on every reference ever stored; neither is in the
|
|
202
|
+
schema. The target coordinates are what actually make a reference
|
|
203
|
+
followable (TTS-1349/1371), and both identifier shapes are live — on the dev
|
|
204
|
+
database 101 of 193 stored references carry an id and 92 carry a natural key
|
|
205
|
+
and no id.
|
|
206
|
+
|
|
207
|
+
**Migration:** narrow on the new `__typename` literals; treat `url` as
|
|
208
|
+
nullable; replace `ref._id` with `ref._targetId ?? ref._targetNaturalKey` and
|
|
209
|
+
`ref._type` with `ref.__typename`.
|
|
210
|
+
|
|
211
|
+
**Why this was wrong for so long:** `builtins.ts` is the one part of the typed
|
|
212
|
+
data surface that is not generated per project, and nothing compared it to a
|
|
213
|
+
real schema. `src/data/__tests__/builtins.test-d.ts` now pins every member
|
|
214
|
+
against graphql-codegen output taken from a live project.
|
|
215
|
+
|
|
216
|
+
- **BREAKING (behaviour) — the codegen plugins now default to the `Flexible`
|
|
217
|
+
union, not `Block`.** `DEFAULT_UNIONS` in `@foir/sdk/codegen` was `['Block']`;
|
|
218
|
+
it is `['Flexible']`.
|
|
219
|
+
|
|
220
|
+
The platform stopped emitting a union named `Block` at TTS-1391 — a flexible
|
|
221
|
+
field's members are the value types and inline models under their own names,
|
|
222
|
+
and the project-wide union an unconstrained field resolves to is `Flexible`.
|
|
223
|
+
So `foirCodegen()` on defaults could not have worked against any current
|
|
224
|
+
project: `collectItemMembers` refuses rather than emitting an empty registry,
|
|
225
|
+
so it threw `[foir codegen] no union members found. Looked for union(s):
|
|
226
|
+
Block`. This is listed as breaking because it changes a default, but the
|
|
227
|
+
default it replaces had no working configuration.
|
|
228
|
+
|
|
229
|
+
It stayed invisible because the plugin tests built their own fixture schema
|
|
230
|
+
declaring `union Block = RichtextBlock | ImageBlock | TextBlock` — the
|
|
231
|
+
pre-TTS-1391 shape. The fixture is now the post-1391 shape, and
|
|
232
|
+
`DEFAULT_UNIONS` is asserted against it directly.
|
|
233
|
+
|
|
234
|
+
**Migration:** none if you pass `blockUnions`/`itemUnions` explicitly. If you
|
|
235
|
+
relied on the default and codegen was failing, it now resolves. If your
|
|
236
|
+
project genuinely has a union named `Block`, pass `itemUnions: ['Block']`.
|
|
237
|
+
|
|
238
|
+
- **`createContentRenderer` passes `item` to every component, and accepts
|
|
239
|
+
`items` as well as `blocks`.** `props.block` is still passed on every render
|
|
240
|
+
and `blocks=` is still accepted, so scaffolded components and existing call
|
|
241
|
+
sites are unaffected. Passing both `items` and `blocks` is a type error rather
|
|
242
|
+
than a silent precedence rule.
|
|
243
|
+
|
|
244
|
+
### Added
|
|
245
|
+
|
|
246
|
+
- **`@foir/sdk/codegen/item-registry` and `@foir/sdk/codegen/item-scaffold`** —
|
|
247
|
+
the two graphql-codegen plugins under names that do not claim `block` is a
|
|
248
|
+
platform concept. `@foir/sdk/codegen/block-registry` and `/block-scaffold`
|
|
249
|
+
re-export them unchanged and keep working.
|
|
250
|
+
|
|
251
|
+
- **`ItemLike`, `ItemComponent`, `ItemComponentProps`, `ItemRegistry`** on
|
|
252
|
+
`./content`, and **`ItemPluginConfig`**, **`ItemRegistryConfig`**,
|
|
253
|
+
**`ItemScaffoldConfig`** on the codegen subpaths. Each is the canonical name
|
|
254
|
+
for the `Block*` type of the same shape.
|
|
255
|
+
|
|
256
|
+
- **`foirCodegen({ itemUnions })` / `foirScaffold({ itemUnions })`** — the
|
|
257
|
+
canonical spelling of `blockUnions`, which is still honoured when `itemUnions`
|
|
258
|
+
is unset.
|
|
259
|
+
|
|
260
|
+
### Deprecated
|
|
261
|
+
|
|
262
|
+
- **Every `Block*` spelling in the SDK.** `BlockLike`, `BlockComponent`,
|
|
263
|
+
`BlockComponentProps`, `BlockRegistry`, `BlockPluginConfig`,
|
|
264
|
+
`BlockRegistryConfig`, `BlockScaffoldConfig`, `collectBlockMembers`, the
|
|
265
|
+
`blockUnions` option, the `blocks` prop on `createContentRenderer`, the
|
|
266
|
+
`props.block` field, and the `./codegen/block-registry` and
|
|
267
|
+
`./codegen/block-scaffold` subpaths.
|
|
268
|
+
|
|
269
|
+
`block` was never a platform concept — it was one project's NAME for a
|
|
270
|
+
flexible field, and a modeller may equally call one `items`, `sections` or
|
|
271
|
+
`modules`. The platform never infers anything from a field's name and the SDK
|
|
272
|
+
should not teach otherwise (TTS-1391).
|
|
273
|
+
|
|
274
|
+
Nothing is removed in this release; every old name still resolves and still
|
|
275
|
+
behaves identically. Removal is a later, separately announced break.
|
|
276
|
+
|
|
277
|
+
- **BREAKING (generated types) — a union the compiler stopped in front of one
|
|
278
|
+
level BELOW a block union member is now typed as the degraded union, where it
|
|
279
|
+
used to be typed as the whole one.** Same direction as the composite entry
|
|
280
|
+
below: the type promised more than arrives, so code reading through it
|
|
281
|
+
compiled and got `undefined`.
|
|
282
|
+
|
|
283
|
+
The entry below fixed the composites cut at a union member's OWN fields, which
|
|
284
|
+
are emitted per position and could simply be degraded there. A cut can also
|
|
285
|
+
land one level deeper. `ProductDetailsAccordionBlock.items` is a composite the
|
|
286
|
+
server did NOT cut, so it expands and is typed with `Detail`'s own interface —
|
|
287
|
+
and `Detail.body` IS cut inside that subtree, because reaching it means
|
|
288
|
+
`Block` is already on the compiler's path.
|
|
289
|
+
|
|
290
|
+
`Detail` is one interface referenced from a cut position and an uncut one
|
|
291
|
+
(`product_details_accordion.items` at the model position), so it could not
|
|
292
|
+
simply be degraded: that would have been wrong at the other reference. The
|
|
293
|
+
answer now travels with the REFERENCE, as a second parameter threaded the way
|
|
294
|
+
`<D>` already is:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
export interface Detail<D extends FoirDepthLevel = 1, C extends boolean = false> {
|
|
298
|
+
label: string | null;
|
|
299
|
+
body: Array<(C extends true ? … : DetailBody<D>) | null> | null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// inside the union member — cut
|
|
303
|
+
export interface ProductDetailsAccordionBlock<D extends FoirDepthLevel = 1> … {
|
|
304
|
+
items: Array<Detail<D, true> | null> | null;
|
|
305
|
+
}
|
|
306
|
+
// at the model position — whole
|
|
307
|
+
export interface ProductDetailsAccordion<D extends FoirDepthLevel = 1, C extends boolean = false> {
|
|
308
|
+
items: Array<Detail<D, C> | null> | null;
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Nothing a caller writes ever passes `C`.** It defaults to `false`, so every
|
|
313
|
+
existing reference resolves exactly as it did; the position it came from is
|
|
314
|
+
what sets it.
|
|
315
|
+
|
|
316
|
+
On the live dev project **one position narrows**, and the full recursive
|
|
317
|
+
census is what says so: ten `truncated` positions anywhere in `models[*]
|
|
318
|
+
.selection` or `selectionUnions`, of which six were already correct, three
|
|
319
|
+
were fixed by the entry below, and this is the tenth. Re-run after the change,
|
|
320
|
+
the count of over-promising positions is **zero of ten**.
|
|
321
|
+
|
|
322
|
+
| position | was | is |
|
|
323
|
+
| --- | --- | --- |
|
|
324
|
+
| `ProductDetailsAccordionBlock.items[].body` | `Array<DetailBody \| null> \| null` | `Array<DetailBodyDegraded \| null> \| null` |
|
|
325
|
+
|
|
326
|
+
**`depth` DOES help here**, which is the difference from the composite case.
|
|
327
|
+
The position carries a union name, so `printFields` expands it when the budget
|
|
328
|
+
allows and the type follows: at `depth: 2` the same read resolves to the whole
|
|
329
|
+
`DetailBody` union and narrows on `__typename` with no cast. Verified against
|
|
330
|
+
deployed dev — at the level where the member arrives expanded, all ten live
|
|
331
|
+
`body` values are `{ "__typename": "RichtextBlock" }` and nothing else; one
|
|
332
|
+
level further, every one of them is a whole `RichtextBlock` carrying its
|
|
333
|
+
`html` and `json`.
|
|
334
|
+
|
|
335
|
+
**The wire is untouched.** Types only: 192 documents compared across five
|
|
336
|
+
manifests (the four committed fixtures and a live capture) at every level
|
|
337
|
+
1–4, all byte-identical, and every emitted `foirSelections` /
|
|
338
|
+
`foirNamespaceSelections` / `foirSelectionUnions` const unchanged byte for
|
|
339
|
+
byte. **Regenerate with `foir types` to get the narrowing.**
|
|
340
|
+
|
|
341
|
+
A manifest with no selection trees is unaffected, as before: no `truncated`
|
|
342
|
+
means no cut, no interface gains the parameter, and the generated file is what
|
|
343
|
+
it was.
|
|
344
|
+
|
|
345
|
+
- **BREAKING (generated types) — a composite the compiler stopped in front of
|
|
346
|
+
inside a block union member is now typed `FoirDegraded`, where it used to be
|
|
347
|
+
typed whole.** The DEFAULT result type narrows at those positions, and code
|
|
348
|
+
reading through one stops compiling. That is the point: it compiles today
|
|
349
|
+
against a value that never arrives.
|
|
350
|
+
|
|
351
|
+
The op compiler never expands a type already on the current path, and the path
|
|
352
|
+
includes the union the composite was reached through. `foir types` was deciding
|
|
353
|
+
truncation from the composite GRAPH alone, which cannot see that edge — so a
|
|
354
|
+
composite reached from inside a union member got the target's full interface
|
|
355
|
+
for a position the server sends `{ "__typename": "PageContent" }` at. A caller
|
|
356
|
+
wrote `block.pageContent.sections[0].anchor`, it type-checked, and it was
|
|
357
|
+
`undefined` at runtime with no null check to have skipped.
|
|
358
|
+
|
|
359
|
+
On the live dev project (50 models, one 56-member `Block` union) **four
|
|
360
|
+
positions narrow**, and they are the whole population — the manifest's own
|
|
361
|
+
`truncated` flag is now what decides, so this is not an estimate:
|
|
362
|
+
|
|
363
|
+
| position | was | is |
|
|
364
|
+
| --- | --- | --- |
|
|
365
|
+
| `ProductBlock.pageContent` | `PageContent` | `FoirDegraded<'PageContent'>` |
|
|
366
|
+
| `PageBlock.pageContent` | `PageContent` | `FoirDegraded<'PageContent'>` |
|
|
367
|
+
| `CollectionBlock.pageContent` | `PageContent` | `FoirDegraded<'PageContent'>` |
|
|
368
|
+
| `PageContentBlock.sections` | `Array<Section \| null> \| null` | `Array<FoirDegraded<'Section'> \| null> \| null` |
|
|
369
|
+
|
|
370
|
+
**What a caller does about it.** The values were never arriving, so there is no
|
|
371
|
+
code to preserve — only a read to move:
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
// was: compiled, `sections` undefined at runtime
|
|
375
|
+
const anchor = block.pageContent.sections[0].anchor;
|
|
376
|
+
|
|
377
|
+
// now: `__typename` is all that arrives, and the type says so
|
|
378
|
+
if (block.pageContent?.__typename === 'PageContent') { /* … */ }
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
To actually get the content, address the record it belongs to in a second call
|
|
382
|
+
(`foir.page.findUnique({ … })`), or reach it through the position where it is
|
|
383
|
+
NOT cut — the same composite read from the model rather than from inside a
|
|
384
|
+
block still arrives whole.
|
|
385
|
+
|
|
386
|
+
**`depth` does not help here, deliberately.** A UNION stopping point is buyable
|
|
387
|
+
a level at a time (see `depth` under "Added"); a COMPOSITE one has no union to
|
|
388
|
+
expand, so `printFields` prints `{ __typename }` for it at every level. Typing
|
|
389
|
+
it behind `<D>` would promise a level that never arrives, which is the defect
|
|
390
|
+
being fixed rather than a fix for it. Measured on the live project: the number
|
|
391
|
+
of expanded union positions rises 1 → 2 → 3 → 4 with `depth`, and the four
|
|
392
|
+
composite cuts are merely replicated by that expansion, never opened.
|
|
393
|
+
|
|
394
|
+
**The wire is untouched.** This is a types-only change: the document printed
|
|
395
|
+
for a call passing no `depth` is byte-identical — verified across 119 default
|
|
396
|
+
documents (73 ops of the live manifest, 39 of the `orderby` fixture, 7 of the
|
|
397
|
+
`jacket` one), and the emitted `foirSelections` runtime const is unchanged
|
|
398
|
+
byte for byte. **Regenerate with `foir types` to get the narrowing**; an
|
|
399
|
+
existing generated file keeps its old, over-promising types.
|
|
400
|
+
|
|
401
|
+
A manifest with no selection trees — a server predating `selectionUnions` —
|
|
402
|
+
keeps widening at these positions, unchanged. Absence of the flag must not read
|
|
403
|
+
as "cut", or an old server would silently degrade every composite in every
|
|
404
|
+
block, which is a worse lie than the one being fixed. `compositeCycleMembers`
|
|
405
|
+
is still what cuts a composite CYCLE there, from the graph alone.
|
|
406
|
+
|
|
407
|
+
- **`@foir/sdk/data` prints a GraphQL document and executes it over `/graphql`**,
|
|
408
|
+
the same endpoint every other consumer of the API uses — one execution surface,
|
|
409
|
+
one executor, one set of scope checks, one response cache. The public API is
|
|
410
|
+
unchanged: `foir.page.findMany({ where, include, omit })` is the same call it
|
|
411
|
+
always was. What changed is what goes on the wire. The client loads that op's
|
|
412
|
+
selection tree (published by `GET /v1/manifest` as `models.<key>.selection`),
|
|
413
|
+
applies `omit`/`pick`/`include`, prints a document, hashes it and sends the
|
|
414
|
+
hash.
|
|
415
|
+
|
|
416
|
+
Still **no `graphql` dependency** — the tree arrives pre-resolved, so printing
|
|
417
|
+
it is string building. The import-graph assertion that pins this is unchanged
|
|
418
|
+
and still passes.
|
|
419
|
+
|
|
420
|
+
What this buys, immediately: a block-bearing model reads complete-by-default.
|
|
421
|
+
`page`, `product` and `collection` need no caller-supplied `omit` to be read at
|
|
422
|
+
all. A document is the only form that can alias two union members which
|
|
423
|
+
disagree on a field's response shape, and the manifest publishes those aliases,
|
|
424
|
+
so those models return 200 on a bare `findMany()`.
|
|
425
|
+
|
|
426
|
+
Two new config fields carry the runtime half of `foir types`:
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
createFoirClient<FoirSchema>({
|
|
430
|
+
apiUrl, apiKey,
|
|
431
|
+
selections: foirSelections,
|
|
432
|
+
selectionUnions: foirSelectionUnions,
|
|
433
|
+
});
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Both are optional. Without them the client fetches `GET /v1/manifest` once at
|
|
437
|
+
first use and reads the same trees off it — a FALLBACK, so a zero-codegen start
|
|
438
|
+
still works, at the cost of a round trip in front of the first query on every
|
|
439
|
+
cold edge invocation.
|
|
440
|
+
|
|
441
|
+
Reads take the shared, cacheable lane where they can:
|
|
442
|
+
`GET /cdn/graphql?key=pk_…` with the persisted-query hash in the URL when the
|
|
443
|
+
call is a query, on a `pk_` key, with no customer bearer, on the published
|
|
444
|
+
channel — and `POST /graphql` otherwise. The server's gate refuses on five
|
|
445
|
+
conditions and the client decides all five before the request: the fifth,
|
|
446
|
+
"the credential is in the URL", it satisfies by construction. The server is
|
|
447
|
+
still the authority — it refuses an ineligible read by name, and the refusal
|
|
448
|
+
is retried on `/graphql`.
|
|
449
|
+
|
|
450
|
+
- **BREAKING — `foir.raw()` takes a GraphQL document.** A document string and an
|
|
451
|
+
optional variables object, where it used to take a JSON op
|
|
452
|
+
(`{ model, op, args }`):
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
// before
|
|
456
|
+
await foir.raw({ model: 'page', op: 'findMany', args: { first: 5 } });
|
|
457
|
+
// after
|
|
458
|
+
await foir.raw('query ($n: Int) { pages(first: $n) { totalCount } }', { n: 5 });
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
It returns the whole `data` object rather than a single root field, because a
|
|
462
|
+
hand-written document may legitimately have several. As a document it reaches
|
|
463
|
+
everything the credential can reach — including the two dozen root fields no
|
|
464
|
+
model and no namespace claims (`designTokens`, `record`, `locales`, the
|
|
465
|
+
customer-auth mutations), which is the point of an escape hatch and is more
|
|
466
|
+
than the old shape could address.
|
|
467
|
+
|
|
468
|
+
- **Every op prints, and there is one wire format rather than two.** The
|
|
469
|
+
descriptor now publishes a `payload` per op — the model it IS, the model it
|
|
470
|
+
CARRIES, its own resolved fields, or an explicit `leaf` — so every shape the
|
|
471
|
+
client can name is a shape it can print. That covers the 18 the client could
|
|
472
|
+
previously only name: every `foir.ns.*` op, plus `myAccess` and the other model
|
|
473
|
+
ops whose result is not the model. They execute over `/graphql` like everything
|
|
474
|
+
else, and nothing is kept beside it that a call could quietly land on.
|
|
475
|
+
|
|
476
|
+
This also removes the last place the client knew something the descriptor did
|
|
477
|
+
not tell it. A connection was wrapped in `edges { cursor node { … } }` from a
|
|
478
|
+
table of op names compiled into the client; it is now printed from the wrapper
|
|
479
|
+
the manifest published, so a server that changes that shape does not need a
|
|
480
|
+
client release to match.
|
|
481
|
+
|
|
482
|
+
**An op the manifest describes no payload for is refused, by name.** It is not
|
|
483
|
+
synthesised and there is no second protocol to fall back to.
|
|
484
|
+
|
|
485
|
+
- **Arguments are sent as GraphQL variables, not printed as literals.** An op's
|
|
486
|
+
published `argTypes` becomes a real variable definition —
|
|
487
|
+
`query PageFindMany($orderBy: [PageOrderByInput])` — and the value travels in
|
|
488
|
+
`variables`, where the executor coerces it against the declared input type.
|
|
489
|
+
|
|
490
|
+
This is the fix for enum arguments. A printer working from argument names
|
|
491
|
+
alone has to infer each value's literal form from the JavaScript value, which
|
|
492
|
+
is exact for strings, numbers, booleans and objects and ambiguous for enums:
|
|
493
|
+
`orderBy: { createdAt: DESC }` needs `DESC` unquoted and the client holds the
|
|
494
|
+
string `'DESC'`. On one real project that was `orderBy` on every `findMany`
|
|
495
|
+
plus `where` on any enum field. Those calls used to be refused by the server,
|
|
496
|
+
by name; they work now.
|
|
497
|
+
|
|
498
|
+
Second-order, and worth having on its own: the document text no longer varies
|
|
499
|
+
with the argument VALUE, so the persisted-query registry holds one entry per
|
|
500
|
+
op instead of one per argument tuple. Argument names are emitted sorted for
|
|
501
|
+
the same reason.
|
|
502
|
+
|
|
503
|
+
**An argument the manifest publishes no type for is refused.** Falling back to
|
|
504
|
+
inferring a literal would reinstate the ambiguity under a condition no caller
|
|
505
|
+
can see.
|
|
506
|
+
|
|
507
|
+
- **`@foir/sdk/auth` prints its documents too.** The flows used the same JSON
|
|
508
|
+
lane the data client did; they now print from `namespaces.auth.ops.<op>` on
|
|
509
|
+
the caller's own descriptor and execute over `/graphql`. Same functions, same
|
|
510
|
+
arguments, same return values. The descriptor is fetched once per credential
|
|
511
|
+
and memoised, so a `currentUser()` on every render does not refetch it.
|
|
512
|
+
|
|
513
|
+
- **Schema drift now means "regenerate", not "be aware".** The trees the client
|
|
514
|
+
prints from are the ones the generated version published, so a field added
|
|
515
|
+
since is not merely untyped — it is not requested. The warning says so.
|
|
516
|
+
|
|
517
|
+
### Removed
|
|
518
|
+
|
|
519
|
+
- **`FoirRawOp` from `@foir/sdk/data`** — the JSON op shape (`{ model, op, args }`)
|
|
520
|
+
`foir.raw()` used to take. There is no replacement type because a call is a
|
|
521
|
+
GraphQL document now: `foir.raw()` takes a document string and variables. See
|
|
522
|
+
`foir.raw()` under "Changed" above for the migration.
|
|
523
|
+
- **`foirOpResultKind` and `FoirResultKind` from `@foir/sdk/data`** — a table
|
|
524
|
+
mapping op NAMES to how their result wrapped the model, which is exactly the
|
|
525
|
+
schema knowledge the client is not allowed to hold. The manifest publishes
|
|
526
|
+
each op's result shape, so the table has nothing left to answer. Neither
|
|
527
|
+
symbol has appeared in a published release.
|
|
528
|
+
- **`customerResendVerificationEmail` from `@foir/sdk/auth`** — and the
|
|
529
|
+
`AUTH_OPS.customerResendVerificationEmail` entry with it. **It never sent an
|
|
530
|
+
email.** The resolver behind it checked that the request carried a customer id,
|
|
531
|
+
returned `true`, and called no RPC; there is no customer verification-code
|
|
532
|
+
column and no customer verification email in the action catalogue for it to
|
|
533
|
+
have used. A caller got a success response every time and nothing was
|
|
534
|
+
delivered, which is worse than an error, because there is no signal to act on.
|
|
535
|
+
|
|
536
|
+
**Use the OTP flow for mailbox proof:** `customerRequestOTP(env, email)` emails
|
|
537
|
+
a code, and `customerLoginOTP(env, { email, otp })` redeems it — activating a
|
|
538
|
+
pending account on redemption and returning a session, so possession of the
|
|
539
|
+
mailbox is proven by the same call that signs the customer in. That is a live,
|
|
540
|
+
delivered path today; `customerVerifyEmail` remains for redeeming a token that
|
|
541
|
+
arrived by some other route.
|
|
542
|
+
|
|
543
|
+
Like the two entries above, this symbol has not appeared in a published
|
|
544
|
+
release — the whole `@foir/sdk/auth` surface it belongs to is unreleased — so
|
|
545
|
+
no installed version loses a function and there is nothing to migrate off.
|
|
546
|
+
|
|
547
|
+
### Added
|
|
548
|
+
|
|
549
|
+
- **`<FoirFeedback>` — a feedback widget anchored to the record, not the page.**
|
|
550
|
+
On `./react`, beside `<SignIn>` and `<UserButton>`.
|
|
551
|
+
|
|
552
|
+
```tsx
|
|
553
|
+
const subjects = createFoirSubjectRegistry({ label: (r) => r.title as string });
|
|
554
|
+
const foir = createFoirClient({ apiUrl, apiKey, generated, subjects });
|
|
555
|
+
|
|
556
|
+
<FoirFeedback client={foir} subjects={subjects} />
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
That is the whole wiring. There is no route table and no per-page
|
|
560
|
+
configuration: the subject is the last record the client addressed by name, so
|
|
561
|
+
a comment attaches to the record and survives a redesign, a URL change and a
|
|
562
|
+
locale variant. Nothing in the component reads `window.location`.
|
|
563
|
+
|
|
564
|
+
A page whose data all came out of lists has no subject, and the widget says so
|
|
565
|
+
rather than picking one of the twelve or falling back to the address bar. Pass
|
|
566
|
+
`subject` to name one yourself, or `subject={null}` to state there is nothing
|
|
567
|
+
here — which beats whatever the registry still holds from the previous page.
|
|
568
|
+
|
|
569
|
+
The visitor sees a human label or nothing at all: the model key and the record
|
|
570
|
+
id are what the note is filed against and never appear on screen.
|
|
571
|
+
|
|
572
|
+
Opening the panel reads `noteConfig`, which the API charges `notes:write` and
|
|
573
|
+
refuses to a caller with no customer session, so one probe settles whether the
|
|
574
|
+
visitor is signed in, whether their role may write notes, and whether
|
|
575
|
+
screenshot capture may be offered. A missing scope reads like a bad API key and
|
|
576
|
+
is not one, so it is reported as what it is: the signed-in customer's ROLE is
|
|
577
|
+
short `notes:write`, which a new project's default customer role does not
|
|
578
|
+
carry.
|
|
579
|
+
|
|
580
|
+
Screenshot capture is offered only when the project allows it and the browser
|
|
581
|
+
can do it, and is opt-in per submission — the browser's own picker is the
|
|
582
|
+
consent gate. A still is uploaded through the ordinary
|
|
583
|
+
`createFileUpload` → PUT → `confirmFileUpload` flow, so the mime gate, the
|
|
584
|
+
plan's per-file limit and the storage allowance all apply; an upload that fails
|
|
585
|
+
fails the whole submission rather than quietly sending a note without the
|
|
586
|
+
picture.
|
|
587
|
+
|
|
588
|
+
Props: `client`, `subjects`, `subject`, `label`, `triggerLabel`, `title`,
|
|
589
|
+
`placeholder`, `defaultOpen`, `onSignIn`, `onSubmitted`, `onError`,
|
|
590
|
+
`className`. Passing `className` turns the built-in inline styles off entirely;
|
|
591
|
+
the `data-foir-feedback-*` attributes are the styling hooks.
|
|
592
|
+
|
|
593
|
+
New exports on `./react`: `FoirFeedback`, `FoirFeedbackProps`,
|
|
594
|
+
`FoirFeedbackClient`.
|
|
595
|
+
|
|
596
|
+
- **Screen capture on `./media`.** `isFoirScreenCaptureSupported()` says whether
|
|
597
|
+
the browser can do it at all; `captureFoirScreen()` captures one still of a
|
|
598
|
+
surface the reader picks, downscaled to 1600px and encoded as JPEG at 0.7. It
|
|
599
|
+
answers `null` when the picker is dismissed, because that is a decision rather
|
|
600
|
+
than a failure, and it stops every track before returning on every path. The
|
|
601
|
+
blob and the data URL are one encode read two ways, so the size the upload
|
|
602
|
+
declares is the size the bytes are.
|
|
603
|
+
|
|
604
|
+
New exports on `./media`: `captureFoirScreen`, `isFoirScreenCaptureSupported`,
|
|
605
|
+
`FoirScreenCapture`, `FoirScreenCaptureOptions`.
|
|
606
|
+
|
|
607
|
+
- **Note attachments and note config on `./data`.** `FoirCustomerNote` gains
|
|
608
|
+
`attachments`, `FoirCreateMyNoteArgs` gains an optional `attachmentFileIds`,
|
|
609
|
+
and `FoirNoteConfig` is the shape `foir.ns.notes.noteConfig()` answers with.
|
|
610
|
+
|
|
611
|
+
New exports on `./data`: `FoirCustomerNoteAttachment`, `FoirNoteConfig`.
|
|
612
|
+
|
|
613
|
+
- **A subject registry — the records a client fetched, so an annotation can
|
|
614
|
+
anchor to the record instead of to a URL.** `createFoirSubjectRegistry()`
|
|
615
|
+
builds one; passing it as `subjects` on the client config records what each
|
|
616
|
+
call fetched.
|
|
617
|
+
|
|
618
|
+
```ts
|
|
619
|
+
const subjects = createFoirSubjectRegistry({ label: (r) => r.title as string });
|
|
620
|
+
const foir = createFoirClient({ apiUrl, apiKey, generated, subjects });
|
|
621
|
+
|
|
622
|
+
await foir.article.findUnique({ naturalKey: 'about-us' });
|
|
623
|
+
subjects.subject(); // { modelKey: 'article', recordId: 'kx7p…', label: 'About Us' }
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
`subject()` answers with the last record the caller addressed **by name**; a
|
|
627
|
+
record that only appeared inside a list is recorded but never promoted, so a
|
|
628
|
+
page whose data all came out of lists answers `undefined` rather than picking
|
|
629
|
+
one. That distinction is read off the op's published payload — a `record`
|
|
630
|
+
payload IS one record — not inferred from result counts or field names. The
|
|
631
|
+
rest of the surface is `fetched()`, `setSubject()`, `clear()`, `subscribe()`
|
|
632
|
+
and `add()`, the last for seeding a record read on the server or through
|
|
633
|
+
`foir.raw()`, neither of which the data path can see.
|
|
634
|
+
|
|
635
|
+
**Off unless you build one, and there is no flag that turns it on.** A list of
|
|
636
|
+
the records a page fetched names content the reader may never have seen
|
|
637
|
+
rendered, so it exists only when the host constructs it and the host holds the
|
|
638
|
+
only reference. Build one per client, never at module scope — a shared registry
|
|
639
|
+
on a multi-tenant edge isolate is shared across tenants.
|
|
640
|
+
|
|
641
|
+
A failing `label` function or `subscribe` listener raises rather than being
|
|
642
|
+
swallowed, in a message that says the request itself succeeded: a subject that
|
|
643
|
+
silently fails to record leaves the PREVIOUS one standing, and a widget then
|
|
644
|
+
files feedback against the wrong page.
|
|
645
|
+
|
|
646
|
+
`FoirDocumentPlan` also now carries an optional `records` — where the records a
|
|
647
|
+
document selects sit in the response, and which model each is — derived from
|
|
648
|
+
the same payload walk that decides where `pick`/`omit` land. Optional so a
|
|
649
|
+
hand-built plan need not invent positions.
|
|
650
|
+
|
|
651
|
+
New exports on `./data`: `createFoirSubjectRegistry`, `recordFoirSubjects`,
|
|
652
|
+
`FoirSubject`, `FoirSubjectEntry`, `FoirSubjectOptions`, `FoirSubjectOrigin`,
|
|
653
|
+
`FoirSubjectRegistry`, `FoirRecordPath`.
|
|
654
|
+
|
|
655
|
+
- **Types for the customer notes surface.** `foir.ns.notes.myNotes()` and
|
|
656
|
+
`foir.ns.notes.createMyNote()` already worked — `notes` is a namespace the
|
|
657
|
+
manifest publishes like any other — but their shapes had to be written out by
|
|
658
|
+
hand at each call site. They are platform-owned and identical in every project,
|
|
659
|
+
so they now ship: `FoirCustomerNote`, `FoirMyNotesArgs`, `FoirMyNotesResult`,
|
|
660
|
+
`FoirCreateMyNoteArgs`, `FoirNoteSubjectType`.
|
|
661
|
+
|
|
662
|
+
```ts
|
|
663
|
+
const mine = (await foir.ns.notes.myNotes({ first: 20 })) as FoirMyNotesResult;
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
`FoirNoteSubjectType` is the closed set the platform's write path enforces
|
|
667
|
+
(`record`, `file`, `model`, `operation`, `schedule`, `customer`) and is never a
|
|
668
|
+
model key — a note on a record is `'record'` plus the key on
|
|
669
|
+
`subjectModelKey`. Like every mirrored platform table in `builtins.ts`, a kind
|
|
670
|
+
added upstream widens this union in a minor release.
|
|
671
|
+
|
|
672
|
+
- **`depth` on a model or namespace call — a per-request budget for nested block
|
|
673
|
+
content.** A block union's members can themselves carry block unions, and the
|
|
674
|
+
server refuses to expand a union already on the current path, so the
|
|
675
|
+
complete-by-default document stops one level in. Those stopping points now
|
|
676
|
+
publish the union they stopped in front of, and `depth` spends a level on one:
|
|
677
|
+
|
|
678
|
+
```ts
|
|
679
|
+
// Default: a stopping point arrives as `{ __typename }`, exactly as before.
|
|
680
|
+
await foir.product.findMany({ first: 5 });
|
|
681
|
+
|
|
682
|
+
// One further union expansion — the nested blocks' own fields arrive.
|
|
683
|
+
await foir.product.findMany({ first: 5, depth: 2 });
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
It is a **budget**, not a setting: the field count is exponential per level.
|
|
687
|
+
On one real project the same op measures 1,203 fields at the default, 4,308 at
|
|
688
|
+
`depth: 2` and 13,623 at `depth: 3`, against a server ceiling of 10,000 — so
|
|
689
|
+
the third level is reachable there only alongside `pick`/`omit`. A call whose
|
|
690
|
+
document would cross the ceiling is refused **client-side**, with the
|
|
691
|
+
arithmetic in the message and the ceiling named, rather than being sent for the
|
|
692
|
+
server to answer with a complexity number the caller cannot act on.
|
|
693
|
+
|
|
694
|
+
Additive in the strict sense: **the document printed for a call that does not
|
|
695
|
+
pass `depth` is byte-identical to the one printed before this change** —
|
|
696
|
+
verified across all 73 ops of a live manifest, and asserted in
|
|
697
|
+
`document.test.ts`. `FoirDocumentPlan` also now carries an optional `fields`,
|
|
698
|
+
the count the server's complexity ceiling is denominated in — optional so a
|
|
699
|
+
hand-built plan need not invent a number, and always set on one this SDK
|
|
700
|
+
printed.
|
|
701
|
+
|
|
702
|
+
**The result type follows the level.** A stopping point the budget reaches is
|
|
703
|
+
typed as the expanded union rather than `FoirDegraded`, so the value is read
|
|
704
|
+
with a `__typename` narrowing and no cast:
|
|
705
|
+
|
|
706
|
+
```ts
|
|
707
|
+
const page = await foir.product.findMany({ first: 5, depth: 2 });
|
|
708
|
+
const block = page.edges[0].node.pageContent?.sections?.[0]?.blocks?.[0];
|
|
709
|
+
if (block?.__typename === 'SectionBlock') {
|
|
710
|
+
const nested = block.blocks?.[0]; // the expanded union, not `{ __typename }`
|
|
711
|
+
if (nested?.__typename === 'ProductMainBlock') nested.enableShopPay; // reads
|
|
712
|
+
}
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
`foir types` gives the composites between the record and a stopping point a
|
|
716
|
+
`<D extends FoirDepthLevel = 1>` parameter and names the model's shape at each
|
|
717
|
+
level under a new optional `FoirModelDefinition.atDepth`; the client picks one
|
|
718
|
+
with a single lookup. **Regenerate to get it** — an existing generated file
|
|
719
|
+
keeps working and keeps typing stopping points as `FoirDegraded` at every
|
|
720
|
+
level, which is what it did before.
|
|
721
|
+
|
|
722
|
+
The default is untouched, in the same strict sense the document is: `D`
|
|
723
|
+
defaults to `1`, so **the result type of a call that does not pass `depth` is
|
|
724
|
+
the type it was before this change**. A `depth` that is not a literal at the
|
|
725
|
+
call site (a variable typed `number`), or a level past the four `atDepth`
|
|
726
|
+
names, falls back to that same default shape — the values still arrive, the
|
|
727
|
+
type just declines to describe them, which is the direction that cannot
|
|
728
|
+
mislead.
|
|
729
|
+
|
|
730
|
+
- **`FoirDepthLevel` and `FoirDepthBelow` on `@foir/sdk/data`** — the two type
|
|
731
|
+
exports the generated file needs to declare its `<D>` parameter (see `depth`
|
|
732
|
+
above). Platform-owned rather than emitted per project because `FoirDepthBelow`
|
|
733
|
+
mirrors the printer's `depth - 1` at an expanded stopping point, and one
|
|
734
|
+
definition beside it is how the two stay agreeing. Named exports, so the
|
|
735
|
+
export-surface snapshot moves 372 → 374.
|
|
736
|
+
|
|
737
|
+
- **`generated` on `createFoirClient` and `createFoirClientFromEnv`, with the
|
|
738
|
+
`FoirGeneratedModule` type** — the generated module passed whole, which is now
|
|
739
|
+
the documented way to build a client:
|
|
740
|
+
|
|
741
|
+
```ts
|
|
742
|
+
import { createFoirClient } from '@foir/sdk/data';
|
|
743
|
+
import * as generated from './foir.generated';
|
|
744
|
+
|
|
745
|
+
const foir = createFoirClient({ apiUrl, apiKey, generated });
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
One import, no per-export knowledge at the call site, and `FoirSchema` inferred
|
|
749
|
+
from the module rather than restated as `createFoirClient<FoirSchema>`. An
|
|
750
|
+
artifact added to `foir types` later then costs a call site nothing.
|
|
751
|
+
|
|
752
|
+
**Additive.** `schemaVersion`, `selections`, `namespaceSelections` and
|
|
753
|
+
`selectionUnions` are unchanged, still accepted, and still win where both are
|
|
754
|
+
given — a client written against them compiles and behaves exactly as before.
|
|
755
|
+
|
|
756
|
+
What the module form removes is a failure that had no symptom. Passing four of
|
|
757
|
+
the five exports produced a client that silently fetched `GET /v1/manifest` on
|
|
758
|
+
every cold start and answered identically, so a `foirSelections` emitted in a
|
|
759
|
+
shape the printer could not read went unnoticed. A `generated` module is now
|
|
760
|
+
checked where it is passed: a missing export is a compile error naming it and a
|
|
761
|
+
refusal at construction, and so is a `foirSelections` entry that is a bare
|
|
762
|
+
selection tree rather than a selection model.
|
|
763
|
+
|
|
764
|
+
Needs a file from a current `foir types`: the inference reads `foirSchemaType`,
|
|
765
|
+
a type carrier the generator now emits, because a namespace import carries
|
|
766
|
+
values and `FoirSchema` is an interface. An older generated file, or one from a
|
|
767
|
+
server that publishes no selection trees, does not type-check as a
|
|
768
|
+
`FoirGeneratedModule` — re-run `foir types`, or keep using the individual
|
|
769
|
+
fields, which is what that file's own header now tells you to do.
|
|
770
|
+
- **`announceManifestFallback` on `createFoirClient`** — set `false` to silence
|
|
771
|
+
the one-line `console.info` a client prints when it is built with neither
|
|
772
|
+
`generated` nor `selections` and will therefore read its selection trees from
|
|
773
|
+
`GET /v1/manifest`.
|
|
774
|
+
|
|
775
|
+
The fallback itself is unchanged and stays supported: it is what makes a
|
|
776
|
+
zero-codegen start work. What is new is that it announces itself, because a
|
|
777
|
+
fallback nobody can see is indistinguishable from the wiring mistake above.
|
|
778
|
+
- **`planFoirNamespaceCall` on `@foir/sdk/data`**, with `FoirNamespaceCall`,
|
|
779
|
+
`FoirNamespaceSelections` and `FoirSelectionNamespace`. A namespace op is a
|
|
780
|
+
`FoirManifestOp` carrying a payload exactly as a model op is, so the function
|
|
781
|
+
that prints `foir.page.findMany()` prints `foir.ns.search.searchRecords()` —
|
|
782
|
+
one printer rather than two that agree by inspection.
|
|
783
|
+
- **`namespaceSelections` on `createFoirClient`** — the namespace half of what
|
|
784
|
+
`foir types` emits, beside `selections`. Optional, like `selections`: without
|
|
785
|
+
it the client reads the same descriptor from `GET /v1/manifest` at first use.
|
|
786
|
+
- **`FoirManifestOpPayload` on `@foir/sdk/data`** — an op's published result
|
|
787
|
+
shape, in four kinds (`record`, `object`, `union`, `leaf`). `leaf` is a stated
|
|
788
|
+
kind rather than an absent field list because a leaf takes NO selection set
|
|
789
|
+
and printing `{}` after one is a parse error.
|
|
790
|
+
- **Selection-tree types on `@foir/sdk/data`** — `FoirManifestSelection`,
|
|
791
|
+
`FoirManifestSelectionField`, `FoirManifestSelectionMember` and
|
|
792
|
+
`FoirManifestSelectionUnion`, the resolved complete-by-default selections
|
|
793
|
+
`GET /v1/manifest` now publishes alongside the field buckets.
|
|
794
|
+
|
|
795
|
+
They are what `foir types` emits as a runtime value, so a client can print a
|
|
796
|
+
GraphQL document without a schema and without recomputing a selection in a
|
|
797
|
+
second language. Every field on them is optional on `FoirManifest` and
|
|
798
|
+
`FoirManifestModel`, because a server that predates trees omits them and a
|
|
799
|
+
consumer must not assume the shape.
|
|
800
|
+
- **`foirSelections` / `foirNamespaceSelections` / `foirSelectionUnions` on
|
|
801
|
+
`@foir/sdk/data`** — the runtime contract `foir types` emits against, and the
|
|
802
|
+
printer that reads it: `planFoirModelCall`, `planFoirNamespaceCall`,
|
|
803
|
+
`foirRoute`, and the types `FoirSelections`, `FoirNamespaceSelections`,
|
|
804
|
+
`FoirSelectionUnions`, `FoirSelectionModel`, `FoirSelectionNamespace`,
|
|
805
|
+
`FoirSelectionSource`, `FoirCall`, `FoirNamespaceCall`, `FoirDocumentPlan`,
|
|
806
|
+
`FoirRouteDecision`, `FoirRouteInput`, `FoirLane`, `FoirLaneReason`. Plus the
|
|
807
|
+
manifest's own per-op and selection types — `FoirManifestOpPayload`,
|
|
808
|
+
`FoirManifestSelection`, `FoirManifestSelectionField`,
|
|
809
|
+
`FoirManifestSelectionUnion`, `FoirManifestSelectionMember`.
|
|
810
|
+
|
|
811
|
+
- **The event-log client on `@foir/sdk/server`** (TTS-983) — `createEventsClient`,
|
|
812
|
+
cursor-resumable consumption of a project's durable event history over
|
|
813
|
+
`GET /v1/events`. `list()` returns one page; `stream()` pages forward to the
|
|
814
|
+
current end of the log and yields events, each carrying the `seq` that is the
|
|
815
|
+
durable cursor.
|
|
816
|
+
|
|
817
|
+
Also `FoirEventCursorExpiredError` and `isFoirEventCursorExpired` for the
|
|
818
|
+
retention refusal, the constants `FOIR_EVENTS_PATH`, `FOIR_EVENTS_SCOPE` and
|
|
819
|
+
`FOIR_EVENTS_RETENTION_DAYS`, and the types `FoirEvent`, `FoirEventActor`,
|
|
820
|
+
`FoirEventPage`, `FoirEventRetention`, `FoirEventsClient`,
|
|
821
|
+
`FoirEventsClientConfig`, `FoirEventsListParams`, `FoirEventsStreamParams`.
|
|
822
|
+
|
|
823
|
+
**On `@foir/sdk/server` and no other subpath, deliberately.** The event log is
|
|
824
|
+
an OPERATOR surface (DECIDED 2026-08-04, TTS-981): reachable by an `sk_` key or
|
|
825
|
+
an admin session, never by a customer token, and `events:list.project` is not
|
|
826
|
+
mintable on a `pk_` key at all. `events.view` is plain tenant+project
|
|
827
|
+
containment while `record.view` carries owner/grantee/borrow terms on top, so a
|
|
828
|
+
containment-only read over the log describes rows whose records the same caller
|
|
829
|
+
could not read — and each row's `actor` names which *other* customer acted.
|
|
830
|
+
`createEventsClient` therefore throws on a `pk_` key and on an `Authorization`
|
|
831
|
+
header rather than letting either reach the wire, and an import-graph test
|
|
832
|
+
fails if the module becomes reachable from a subpath that ships to a browser.
|
|
833
|
+
|
|
834
|
+
Requires foir-io/platform#757 to be deployed. Against an api-public without it,
|
|
835
|
+
`GET /v1/events` 404s.
|
|
836
|
+
|
|
837
|
+
- **The package error taxonomy on `@foir/sdk/server`** — `FoirRequestError`,
|
|
838
|
+
`isFoirRateLimited`, `isFoirUnauthenticated`, `isFoirForbidden`,
|
|
839
|
+
`isFoirNotFound`, and the `FoirErrorCode` / `FoirErrorExtensions` /
|
|
840
|
+
`FoirGraphQLErrorItem` types, re-exported from `@foir/sdk/client` so an
|
|
841
|
+
events-client consumer classifies failures without importing a second subpath.
|
|
842
|
+
Same classes, not copies.
|
|
843
|
+
|
|
844
|
+
- **The whole auth surface on `@foir/sdk/auth`.** Password management, email
|
|
845
|
+
verification and account lifecycle were reachable NOWHERE in the package — a
|
|
846
|
+
storefront doing "forgot password" had to drop to hand-written GraphQL, which
|
|
847
|
+
is the entire cost of GraphQL paid for one screen. Ten root fields closed:
|
|
848
|
+
|
|
849
|
+
`getAuthConfig`, `getDefaultAuthProvider`, `currentUser`, `myProviderToken`,
|
|
850
|
+
`customerLogout`, `customerRequestPasswordReset`, `customerResetPassword`,
|
|
851
|
+
`customerUpdatePassword`, `customerVerifyEmail`, `deleteMyAccount`.
|
|
852
|
+
|
|
853
|
+
Plus the session-layer pairings for the flows that need one:
|
|
854
|
+
`resetPasswordAndSignIn` (redeem the emailed token and persist the session it
|
|
855
|
+
mints — the peer of `loginWithPassword` for the branch where the customer
|
|
856
|
+
cannot log in), `signOut` (revoke server-side, then clear locally, and clear
|
|
857
|
+
locally even if the revoke fails), and `authedOp` (`authedRequest`'s twin for
|
|
858
|
+
auth ops: refresh once, retry once, on a 401-shaped failure).
|
|
859
|
+
|
|
860
|
+
And the vocabulary: `AUTH_OPS`, `AuthOpName`, `AuthOpOptions`, with the result
|
|
861
|
+
types `AuthConfig`, `AuthConfigProvider`, `DefaultAuthProvider`, `CurrentUser`,
|
|
862
|
+
`ProviderToken`, `DeleteAccountResult`.
|
|
863
|
+
|
|
864
|
+
Requires an api-public that publishes the full `auth` namespace in
|
|
865
|
+
`GET /v1/manifest`. Against an older one every auth call is refused with
|
|
866
|
+
`operation "…" is not available in namespace "auth" for this credential`, so
|
|
867
|
+
**the platform change deploys first**.
|
|
868
|
+
|
|
869
|
+
### Removed
|
|
870
|
+
|
|
871
|
+
- **The nine auth operation documents on `@foir/sdk/auth`** —
|
|
872
|
+
`CustomerLoginDocument`, `CustomerRegisterDocument`,
|
|
873
|
+
`CustomerRefreshTokenDocument`, `CustomerRequestOTPDocument`,
|
|
874
|
+
`CustomerLoginOTPDocument`, `CustomerLoginWithProviderDocument`,
|
|
875
|
+
`CustomerProviderCallbackDocument`, `CustomerProviderVerifyOTPDocument`,
|
|
876
|
+
`AuthProvidersDocument`.
|
|
877
|
+
|
|
878
|
+
**Breaking, so the next release is a MINOR** per the pre-1.0 note above.
|
|
879
|
+
|
|
880
|
+
*Why.* They were a second wire protocol for one substrate inside one package:
|
|
881
|
+
hand-written GraphQL posted to `/graphql` while every other typed call in the
|
|
882
|
+
SDK went through the op transport. That split is what produced the gap above —
|
|
883
|
+
the hand-written set covered nine root fields and nobody noticed the other
|
|
884
|
+
eleven were unreachable. The flows now address the server's generated `auth`
|
|
885
|
+
namespace, so `@foir/sdk/auth` carries no transport of its own.
|
|
886
|
+
|
|
887
|
+
*Migration.* Nothing that called a FUNCTION changes: `customerLogin(env, …)`,
|
|
888
|
+
`getAuthProviders(env)`, `loginWithPassword(adapter, env, …)`, `refreshSession`,
|
|
889
|
+
`requireAuth`, `authedRequest` and every React hook in `@foir/sdk/react` keep
|
|
890
|
+
their exact signatures. Only code importing a `*Document` const is affected,
|
|
891
|
+
and only if it was executing the document itself through its own GraphQL
|
|
892
|
+
client. Two options:
|
|
893
|
+
|
|
894
|
+
- call the SDK function instead (`customerLogin(env, vars)` rather than running
|
|
895
|
+
`CustomerLoginDocument` through Apollo/urql), which is what the exports were
|
|
896
|
+
for; or
|
|
897
|
+
- keep your own client and write the document in your own codebase — these are
|
|
898
|
+
fixed-shape platform operations, so the text does not vary by project.
|
|
899
|
+
|
|
900
|
+
`authedRequest` is unchanged and still takes a `TypedDocumentNode`: it exists
|
|
901
|
+
for the project's OWN operations, whose shape the SDK cannot know.
|
|
902
|
+
|
|
903
|
+
- **`graphql` from `@foir/sdk/auth`'s path.** The removed documents were
|
|
904
|
+
`parse()`d at module load, which no bundler could tree-shake, and that is the
|
|
905
|
+
reason `@foir/sdk/auth/session` had to exist as a separate graphql-free door.
|
|
906
|
+
The subpath stays — it is a published surface and consumers import it — but it
|
|
907
|
+
is no longer the only graphql-free way into this package.
|
|
908
|
+
|
|
909
|
+
---
|
|
910
|
+
|
|
911
|
+
## [0.7.0] — 2026-08-08
|
|
912
|
+
|
|
913
|
+
Everything additive, so a MINOR per the pre-1.0 note.
|
|
914
|
+
|
|
915
|
+
Ships 0.6.0 with it. That version was cut in the changelog and in
|
|
916
|
+
`package.json` on 2026-07-27 but no `sdk-v0.6.0` tag was ever pushed, and the
|
|
917
|
+
tag is what publishes, so npm went straight from 0.5.0 to here. The 0.6.0
|
|
918
|
+
section below is left as written rather than folded in, so the history says what
|
|
919
|
+
actually happened.
|
|
920
|
+
|
|
921
|
+
### Added
|
|
922
|
+
|
|
923
|
+
- **Failure classification on `@foir/sdk/client`** — `isFoirRateLimited`,
|
|
924
|
+
`isFoirUnauthenticated`, `isFoirForbidden`, `isFoirNotFound`, plus `code`,
|
|
925
|
+
`requestId`, `retryable` and `retryAfterMs` on `FoirRequestError`, and the
|
|
926
|
+
`FoirErrorCode` / `FoirErrorExtensions` types behind them.
|
|
927
|
+
|
|
928
|
+
`FoirGraphQLErrorItem` now carries `extensions`, which is what makes any of
|
|
929
|
+
this possible: the platform has been sending `extensions.code` on every
|
|
930
|
+
rejection and the SDK's error type had nowhere to put it, so the only way to
|
|
931
|
+
tell one failure from another was to match the message.
|
|
932
|
+
|
|
933
|
+
- **Retry, on by default, in `createFoirContentClient`** — `withFoirRetry` and
|
|
934
|
+
`foirRetryDelayMs` are exported for callers running their own loop.
|
|
935
|
+
|
|
936
|
+
It covers no-response, `5xx`, and **`429`**. The last one is the reason this
|
|
937
|
+
is in the SDK rather than in a docs snippet. A 429 sits among the 4xx statuses
|
|
938
|
+
that are permanent, so the rule everyone writes — retry 5xx, never retry 4xx —
|
|
939
|
+
excludes the one failure in that family that clears on its own. A storefront
|
|
940
|
+
wrote exactly that rule; when its key saturated, every read failed on the
|
|
941
|
+
first attempt for an hour and the site served empty pages while the API was
|
|
942
|
+
healthy.
|
|
943
|
+
|
|
944
|
+
A server-stated `Retry-After` wins over backoff, capped by `maxDelayMs`
|
|
945
|
+
(default 20s) so an hour-long window does not block a render. Opt out with
|
|
946
|
+
`retry: { attempts: 1 }`.
|
|
947
|
+
|
|
948
|
+
- **`onRateLimit`** on `createFoirContentClient` and on `FoirRequestOptions`,
|
|
949
|
+
with `readFoirRateLimit` and the `FoirRateLimitSnapshot` type. Called with the
|
|
950
|
+
`X-RateLimit-*` headers of every response, so an app can log that a key is
|
|
951
|
+
approaching its ceiling instead of finding out when it hits one.
|
|
952
|
+
|
|
953
|
+
- **New subpath `@foir/sdk/auth/session`** — `createFoirSessionClient` and its
|
|
954
|
+
types (`FoirSessionClient`, `FoirSessionConfig`, `FoirSessionUser`) on their
|
|
955
|
+
own, without the rest of `./auth`.
|
|
956
|
+
|
|
957
|
+
Purely additive: `./auth` keeps exporting all four, unchanged, and nothing is
|
|
958
|
+
moved or renamed. What changes is the cost of reaching them. `./auth` also
|
|
959
|
+
exports the customer-auth GraphQL documents, which build their
|
|
960
|
+
TypedDocumentNodes with a top-level `parse()` that no bundler can treat as
|
|
961
|
+
pure — so a browser app importing only `createFoirSessionClient` was shipping
|
|
962
|
+
the whole `graphql` package to get it. `src/auth/session-client.ts` imports
|
|
963
|
+
nothing at all, and this subpath bundles it alone.
|
|
964
|
+
|
|
965
|
+
Like `./data`, it adds **no dependency and no peer dependency**:
|
|
966
|
+
`dist/auth-session.js` has no imports. An app on this subpath needs `graphql`
|
|
967
|
+
neither installed nor bundled. Use `./auth` when you want the login /
|
|
968
|
+
register / OTP / provider flows too; use this one for a backend-less SPA whose
|
|
969
|
+
only credential is the Fóir-hosted session cookie.
|
|
970
|
+
|
|
971
|
+
- **New subpath `@foir/sdk/data`** — the typed data client (TTS-874).
|
|
972
|
+
`createFoirClient<FoirSchema>({ apiUrl, apiKey })` gives `foir.<model>.<op>(…)`,
|
|
973
|
+
`foir.ns.<namespace>.<op>(…)` and an untyped `foir.raw(op)`. The client's surface is shaped by the credential: an op that
|
|
974
|
+
is not in that key's manifest is not on the type, so `foir.page.create(…)`
|
|
975
|
+
under a `pk_` key is a compile error rather than a 400.
|
|
976
|
+
|
|
977
|
+
Selection is complete-by-default — every scalar always arrives, so the model
|
|
978
|
+
type is unconditionally true — with `include` to opt into relations and
|
|
979
|
+
`omit` to drop a heavy field. `_id` is not omittable, matching the server.
|
|
980
|
+
|
|
981
|
+
**This subpath has no `graphql` dependency**: it imports neither `graphql`,
|
|
982
|
+
`@graphql-typed-document-node/core` nor `@graphql-codegen/*`, and adds no new
|
|
983
|
+
peer dependency, required or optional. `dist/data.js` has no imports at all.
|
|
984
|
+
|
|
985
|
+
New exports on `./data`: `createFoirClient`, `createFoirClientFromEnv`,
|
|
986
|
+
`fetchFoirManifest`, `foirDataEndpoint`, `FoirRequestError`, and the type
|
|
987
|
+
surface (`FoirSchemaDefinition`, `FoirModelDefinition`,
|
|
988
|
+
`FoirRelationDefinition`, `FoirOpDefinition`, `FoirReturnDefinition`,
|
|
989
|
+
`FoirNamespaceDefinition`, `FoirNamespaceOpDefinition`, `FoirFieldsShape`,
|
|
990
|
+
`FoirConnection`, `FoirClient`, `FoirClientCore`, `FoirClientConfig`,
|
|
991
|
+
`FoirModelClient`, `FoirNamespaceClient`, `FoirModelOperation`,
|
|
992
|
+
`FoirNamespaceOperation`, `FoirQuery`, `FoirInclude`, `FoirOmittableKey`,
|
|
993
|
+
`FoirRecordResult`, `FoirOpResult`, `FoirRawOp`, `FoirSchemaDrift`,
|
|
994
|
+
`FoirManifest` + its members, and `FoirEnv` / `FoirGraphQLErrorItem` /
|
|
995
|
+
`FoirTokenProvider` re-exported for `./data`-only consumers).
|
|
996
|
+
|
|
997
|
+
Additive, so a MINOR bump at release time. No existing subpath's surface
|
|
998
|
+
changed — the export snapshot diff for this change is one new subpath and
|
|
999
|
+
nothing else.
|
|
1000
|
+
|
|
1001
|
+
### Changed
|
|
1002
|
+
|
|
1003
|
+
- Internal only, no public surface change: the `graphql`-free half of
|
|
1004
|
+
`src/graphql/foir-request.ts` moved to `src/transport/foir-http.ts` so
|
|
1005
|
+
`./data` and `./client` share one header builder and one `FoirRequestError`
|
|
1006
|
+
rather than forking the transport. `@foir/sdk/client` re-exports every symbol
|
|
1007
|
+
it exported before, unchanged, and `FoirTokenProvider` is now declared in the
|
|
1008
|
+
shared module and re-exported from its old home.
|
|
1009
|
+
|
|
1010
|
+
---
|
|
1011
|
+
|
|
1012
|
+
## [0.6.0] — 2026-07-27
|
|
1013
|
+
|
|
1014
|
+
First release since the public surface was pinned. Everything in it is
|
|
1015
|
+
additive, so per the pre-1.0 note this is a MINOR: `^0.5.0` will not pick it up
|
|
1016
|
+
on its own, which is the intended behaviour for a release that adds exports.
|
|
1017
|
+
|
|
1018
|
+
### Added
|
|
1019
|
+
|
|
1020
|
+
- `@foir/sdk/auth`: `customerSignupContext` and `customerJoinWaitlist`, plus the
|
|
1021
|
+
`SignupContext`, `SignupMode` and `WaitlistJoinResult` types — waitlist and
|
|
1022
|
+
restricted-signup flows for projects that are not open-registration.
|
|
1023
|
+
- A pinned public-export surface: `public-exports.snapshot.json` plus
|
|
1024
|
+
`scripts/assert-public-exports.mjs`, wired into `pnpm build`. Deleting a named
|
|
1025
|
+
export previously passed the full SDK CI lane — 125 tests and the export-map
|
|
1026
|
+
guard all green — because nothing checked the contents of the declared files,
|
|
1027
|
+
only that they existed.
|
|
1028
|
+
- This changelog, and the semver policy above.
|
|
1029
|
+
|
|
1030
|
+
### Changed
|
|
1031
|
+
|
|
1032
|
+
- Type declarations are emitted by a dedicated `tsc --emitDeclarationOnly` pass
|
|
1033
|
+
instead of `tsup`'s built-in `dts`, whose parallel rollup workers
|
|
1034
|
+
intermittently failed to flush a subpath's `.d.ts`; consumers saw
|
|
1035
|
+
"Could not find a declaration file for module '@foir/sdk/realtime'".
|
|
1036
|
+
`scripts/assert-export-types.mjs` now fails the build if any file the export
|
|
1037
|
+
map promises is missing. No change to the public surface.
|
|
1038
|
+
- The test suite pins `NODE_ENV=test` so an inherited `NODE_ENV=production`
|
|
1039
|
+
cannot load React's production build and break every render test. Internal.
|
|
1040
|
+
|
|
1041
|
+
---
|
|
1042
|
+
|
|
1043
|
+
## [0.5.0] — 2026-07-15
|
|
1044
|
+
|
|
1045
|
+
### Added
|
|
1046
|
+
|
|
1047
|
+
- `@foir/sdk/client`: `createFoirContentClient` — forwards the customer bearer
|
|
1048
|
+
token on content reads, so authenticated and segment-targeted content works
|
|
1049
|
+
without hand-rolling the header.
|
|
1050
|
+
|
|
1051
|
+
## [0.4.0] — 2026-07-12
|
|
1052
|
+
|
|
1053
|
+
### Added
|
|
1054
|
+
|
|
1055
|
+
- `@foir/sdk/auth`: `createFoirSessionClient`, plus the `FoirSessionClient`,
|
|
1056
|
+
`FoirSessionConfig` and `FoirSessionUser` types — Foir-hosted first-party
|
|
1057
|
+
sessions for backend-less SPAs, over the `fetchHostedSession` /
|
|
1058
|
+
`hostedLoginUrl` hosted-login bridge that already shipped in 0.1.0 (EID-516).
|
|
1059
|
+
|
|
1060
|
+
## [0.3.0] — 2026-07-12
|
|
1061
|
+
|
|
1062
|
+
### Added
|
|
1063
|
+
|
|
1064
|
+
- **New subpath `@foir/sdk/react`** — drop-in React customer auth: `FoirProvider`,
|
|
1065
|
+
`SignIn`, `OAuthButtons`, `UserButton`, `useFoirAuth`, `useFoirSession`,
|
|
1066
|
+
`useFoirUser` (EID-515).
|
|
1067
|
+
- **New subpath `@foir/sdk/next`** — Next.js App Router BFF auth adapter: cookie
|
|
1068
|
+
session store, `createFoirMiddleware`, server-side sign-in/out actions and the
|
|
1069
|
+
provider callback handler (EID-515).
|
|
1070
|
+
|
|
1071
|
+
## [0.2.0] — 2026-07-10
|
|
1072
|
+
|
|
1073
|
+
### Added
|
|
1074
|
+
|
|
1075
|
+
- **New subpath `@foir/sdk/push`** — native Web Push (VAPID) enrolment:
|
|
1076
|
+
`enableWebPush`, `disableWebPush`, `isWebPushSupported`.
|
|
1077
|
+
- `@foir/sdk/server`: `requireScopedToken` — a one-line ingress guard for
|
|
1078
|
+
operation BFF routes, over the existing `verifyScopedToken` (EID-498).
|
|
1079
|
+
|
|
1080
|
+
## [0.1.0] — 2026-06-23
|
|
1081
|
+
|
|
1082
|
+
First public release. Ten subpaths:
|
|
1083
|
+
|
|
1084
|
+
- `@foir/sdk` — editor iframe client: `EditorProvider`, `useEditor`, the
|
|
1085
|
+
host↔editor message protocol, and `createPlatformClient` (EID-88, EID-91).
|
|
1086
|
+
- `@foir/sdk/server` — server-side platform helpers for config API backends:
|
|
1087
|
+
`createServerClient`, scoped-token verification, `verifyWebhookSignature`.
|
|
1088
|
+
- `@foir/sdk/client` — React-free GraphQL transport for SSR/edge (`foirRequest`).
|
|
1089
|
+
- `@foir/sdk/media` — progressive media components and adapters (EID-94).
|
|
1090
|
+
- `@foir/sdk/content` — `createContentRenderer` and `RichTextRenderer` with
|
|
1091
|
+
injected typography tokens (EID-93).
|
|
1092
|
+
- `@foir/sdk/auth` — customer auth flows, token lifecycle, hosted-login bridge
|
|
1093
|
+
(EID-95).
|
|
1094
|
+
- `@foir/sdk/realtime` — `createCollabSession`, a thin y-websocket helper
|
|
1095
|
+
(EID-103, EID-104).
|
|
1096
|
+
- `@foir/sdk/codegen`, `@foir/sdk/codegen/block-registry`,
|
|
1097
|
+
`@foir/sdk/codegen/block-scaffold` — the `foirCodegen` preset and the
|
|
1098
|
+
graphql-codegen plugins (EID-92).
|
|
1099
|
+
|
|
1100
|
+
---
|
|
1101
|
+
|
|
1102
|
+
### A note on the backfilled entries
|
|
1103
|
+
|
|
1104
|
+
`@foir/sdk` shipped 0.1.0 through 0.5.0 with no changelog. The five sections
|
|
1105
|
+
above were reconstructed after the fact from sources that can be re-checked:
|
|
1106
|
+
the `sdk-v*` git tags, the `packages/sdk/package.json#exports` map read at each
|
|
1107
|
+
of those tags, the commits between consecutive tags that touch `packages/sdk/`,
|
|
1108
|
+
and the npm registry's publish timestamps (`npm view @foir/sdk time`), which is
|
|
1109
|
+
where the dates come from.
|
|
1110
|
+
|
|
1111
|
+
They are **headline-level, not exhaustive**. They record subpath additions and
|
|
1112
|
+
the named feature of each release; they do not claim to list every symbol added,
|
|
1113
|
+
because no per-symbol record was kept at the time and reconstructing one would
|
|
1114
|
+
mean guessing. Nothing above is asserted that could not be read out of the tags
|
|
1115
|
+
and the commit history.
|
|
1116
|
+
|
|
1117
|
+
`Unreleased` and every section after it are authored at the time of the change
|
|
1118
|
+
and are exhaustive for the public surface, which is what the export-surface
|
|
1119
|
+
guard exists to keep true.
|
|
1120
|
+
|
|
1121
|
+
[Unreleased]: https://github.com/foir-io/clients/compare/sdk-v0.5.0...dev
|
|
1122
|
+
[0.5.0]: https://github.com/foir-io/clients/compare/sdk-v0.4.0...sdk-v0.5.0
|
|
1123
|
+
[0.4.0]: https://github.com/foir-io/clients/compare/sdk-v0.3.0...sdk-v0.4.0
|
|
1124
|
+
[0.3.0]: https://github.com/foir-io/clients/compare/sdk-v0.2.0...sdk-v0.3.0
|
|
1125
|
+
[0.2.0]: https://github.com/foir-io/clients/compare/sdk-v0.1.0...sdk-v0.2.0
|
|
1126
|
+
[0.1.0]: https://github.com/foir-io/clients/releases/tag/sdk-v0.1.0
|