@formancy/data-client 0.0.0-stage → 0.1.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/LICENSE.md ADDED
@@ -0,0 +1,156 @@
1
+ # Formancy Data Source-Available Licence
2
+
3
+ **Version 1.1 — in force from 11 October 2026. Not reviewed by a lawyer.**
4
+
5
+ This text states the terms Formancy Data is offered under. Version 1.1 is in
6
+ force from 11 October 2026 and replaces Version 1 of the same day, which named
7
+ no address for the agreement Redistribution requires, and Version 0, the draft
8
+ of 8 October 2026. **This version has not been reviewed by a lawyer.** It is
9
+ published in the repository so that the terms are visible to everyone who
10
+ reads the source. Production licences, which section 3 describes, can be had
11
+ as section 9 says.
12
+
13
+ This is not an open-source licence. It is not Apache-2.0, and it is not the
14
+ licence of [formancy](https://github.com/sharkysan/formancy.ai), on which this
15
+ software builds and whose packages keep their own Apache-2.0 terms.
16
+
17
+ ## The terms in one table
18
+
19
+ | Use | Terms |
20
+ | --- | --- |
21
+ | Read the source and fork the repository | Free |
22
+ | Evaluate, develop and test — including in companies | Free |
23
+ | Run in production, internally or customer-facing | Paid licence |
24
+ | Redistribute inside another software product | Separate commercial agreement |
25
+ | Customer data and generated form definitions | Remain the customer's |
26
+
27
+ The rest of this document says what each row means.
28
+
29
+ ## 1. Definitions
30
+
31
+ **The Software** means the source code, declarations, documentation and
32
+ packages in this repository and the packages published from it under the
33
+ `@formancy/data-*` names, in any version, including modified versions.
34
+
35
+ **The Rightsholder** means Daniel Bacher, or whoever the Rightsholder later
36
+ designates in a revision of this document.
37
+
38
+ **Production Use** means running the Software, or anything that incorporates
39
+ it, in a way that serves real end users or real business operations: an
40
+ application people other than its developers and testers rely on, whether
41
+ inside one organisation or offered to others, whether or not it earns money.
42
+ Production is defined by operational use, not by revenue.
43
+
44
+ **Development Use** means any use that is not Production Use: reading,
45
+ evaluating, building, modifying, testing, demonstrating and running in
46
+ development, test, staging and continuous-integration environments, including
47
+ by and within a commercial organisation. A staging environment that real
48
+ users depend on is Production Use whatever it is called.
49
+
50
+ **Redistribution** means delivering the Software, or anything that
51
+ incorporates it, to a third party as part of a software product, a managed
52
+ service, a platform or a toolkit, such that the third party runs it rather
53
+ than you.
54
+
55
+ **Customer Data** means the contents of any database the Software connects to,
56
+ and anything the Software reads from or writes to it.
57
+
58
+ **Generated Definitions** means the form documents, bindings, layouts,
59
+ policies and other configuration the Software generates or that a person
60
+ creates with it.
61
+
62
+ ## 2. What is free
63
+
64
+ Subject to section 4, the Rightsholder grants you a worldwide, royalty-free,
65
+ non-exclusive licence to:
66
+
67
+ 1. read, copy, fork and modify the Software;
68
+ 2. use the Software and your modifications for Development Use, without limit
69
+ of time, number of people or size of organisation;
70
+ 3. share the Software and your modifications with others for their
71
+ Development Use, under this licence and with this document attached.
72
+
73
+ No production licence is required to evaluate the Software for as long as you
74
+ like, and no evaluation period expires.
75
+
76
+ ## 3. What is paid
77
+
78
+ **Production Use requires a production licence from the Rightsholder.** A
79
+ production licence is a separate written agreement. It is expected to be
80
+ granted per application, to cover that application's development, test and
81
+ replica environments without further charge, to specify support and update
82
+ entitlements, and to leave you the right to keep running the versions you
83
+ licensed if you later choose not to renew. Those are intentions; the agreement
84
+ you sign is what governs.
85
+
86
+ **Redistribution requires a separate commercial agreement.** Shipping the
87
+ Software inside a product, a platform or a managed service that other parties
88
+ run is not covered by a production licence for your own applications.
89
+
90
+ Running the Software in production without a production licence is a breach of
91
+ this licence.
92
+
93
+ ## 4. Conditions
94
+
95
+ 1. **Notices travel.** A copy of this document, and the NOTICE file, stays
96
+ with every copy and every modified version of the Software.
97
+ 2. **Modified versions carry these terms.** A modified version of the Software
98
+ is the Software for the purposes of this licence. You may not remove or
99
+ circumvent the terms by modifying it.
100
+ 3. **No trademark grant.** This licence grants no right to use the names
101
+ "Formancy", "Formancy Data", "formancy.ai" or any associated logo, beyond
102
+ what is needed to identify the Software truthfully.
103
+ 4. **Third-party components keep their own licences.** The Software depends on
104
+ packages licensed under the Apache License, Version 2.0, among others. Those
105
+ licences continue to govern those components, their notices are preserved
106
+ in NOTICE, and nothing in this document narrows the rights they grant.
107
+
108
+ ## 5. What stays yours
109
+
110
+ **Customer Data remains the customer's.** This licence grants the Rightsholder
111
+ no right of any kind over the contents of any database the Software connects
112
+ to, and the Software is designed not to send any of it anywhere.
113
+
114
+ **Generated Definitions remain yours.** Form documents, bindings, layouts and
115
+ policies you generate or author with the Software are yours, under whatever
116
+ terms you choose. They are not the Software, and this licence does not
117
+ restrict them — including after a production licence ends.
118
+
119
+ ## 6. Contributions
120
+
121
+ Contributions to the Software are accepted under the Contributor License
122
+ Agreement in `CLA.md`, which grants the Rightsholder the right to distribute
123
+ them under this licence and under commercial agreements, and transfers no
124
+ ownership.
125
+
126
+ ## 7. Termination
127
+
128
+ Your rights under this licence end automatically if you breach it and do not
129
+ cure the breach within thirty days of becoming aware of it. The rights in
130
+ section 5 do not end: your data and your generated definitions stay yours in
131
+ every case.
132
+
133
+ ## 8. No warranty, limited liability
134
+
135
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
136
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
137
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
138
+ RIGHTSHOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
139
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
140
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
141
+
142
+ A production licence may provide warranties and support; this document does
143
+ not.
144
+
145
+ ## 9. Obtaining a production licence
146
+
147
+ Production licences are requested from the Rightsholder at info@formancy.ai.
148
+ The separate commercial agreement that Redistribution requires, which
149
+ section 3 describes, is requested at the same address. The Software is new
150
+ and has no published price; the plan is to set the price after the first
151
+ design partners have established what the Software is worth to them.
152
+
153
+ ## 10. Governing law and place of jurisdiction
154
+
155
+ This licence is governed by Swiss law, excluding its conflict-of-laws rules.
156
+ The place of jurisdiction is the Rightsholder's domicile in Switzerland.
package/NOTICE ADDED
@@ -0,0 +1,9 @@
1
+ Formancy Data
2
+ Copyright 2026 Daniel Bacher
3
+
4
+ This product is distributed under the Formancy Data Source-Available Licence
5
+ (LICENSE.md). It is not open source.
6
+
7
+ This product includes software developed by the formancy project
8
+ (https://github.com/sharkysan/formancy.ai), licensed under the Apache License,
9
+ Version 2.0. Those packages keep their licence; nothing here changes it.
package/README.md CHANGED
@@ -1,3 +1,247 @@
1
- # Temporary Holding Version
1
+ <!-- Part of Formancy Data: https://github.com/sharkysan/formancy.ai-data -->
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # @formancy/data-client
4
+
5
+ The browser's side of Formancy Data's runtime plane: one function per route
6
+ for the forms a person may read, a published form's definition, records and
7
+ lookups, option sources that
8
+ both formancy renderers take, and a refusal's field problems in the shape a
9
+ form engine applies them
10
+ ([0029](../../docs/decisions/0029-a-host-renders-a-published-form-through-one-client.md)).
11
+
12
+ Framework-neutral and fetch-based. It uses no Node API — the package's own
13
+ type check has no Node types, so a `node:` import does not compile — and runs
14
+ wherever `fetch` and `AbortSignal` exist.
15
+
16
+ **Licence.** Source-available under the Formancy Data licence in
17
+ [`LICENSE.md`](LICENSE.md), like every package in this repository; see the
18
+ licence table in the [repository README](../../README.md). It depends on the
19
+ Apache-2.0 `@formancy/spec` for types only.
20
+
21
+ ## What it exports
22
+
23
+ ```ts
24
+ createDataClient({ token, base?, fetch? }) // → { forms, form, read, list, create, update, delete, reconcile, query, resolve }
25
+ isUnknownWrite(outcome) // → whether a create, update or delete's outcome is an UnknownWrite
26
+ lookupSources(client, formId, form, operation | () => operation) // → { [source]: { resolve } }
27
+ sourceNames(form) // → every optionsSource the document names, once each, in document order
28
+ fieldProblems(refusal) // → { [field]: [sentence, …] }, for engine.applyServerErrors
29
+ ```
30
+
31
+ Every client function resolves to an `Outcome`: `{ ok: true, value }` or a
32
+ `Refusal` — `{ ok: false, status, code, message, fieldErrors? }`. It rejects
33
+ only for an abort the caller asked for, with the AbortError it raised.
34
+ `create` and `update` resolve to a `WriteOutcome`, which is that or an
35
+ `UnknownWrite` — see below; `delete` to a `DeleteOutcome`, the server's
36
+ `{ record, deleted: true }` or a refusal, or an `UnknownWrite`.
37
+
38
+ | Function | Route |
39
+ |---|---|
40
+ | `forms(signal?)` | `GET /v1/forms` → `{ forms: [{ id, title, operations, children }] }` |
41
+ | `form(formId)` | `GET /v1/forms/:id` → `{ form, operations, readable, list, deleting }` |
42
+ | `read(formId, record)` | `POST /v1/forms/:id/records/read` |
43
+ | `list(formId, query?, signal?)` | `POST /v1/forms/:id/records/list` → `{ rows, hasMore }` |
44
+ | `create(formId, answers)` | `POST /v1/forms/:id/records/create` |
45
+ | `update(formId, { record, version, answers })` | `POST /v1/forms/:id/records/update` |
46
+ | `delete(formId, { record, version })` | `POST /v1/forms/:id/records/delete` → `{ record, deleted: true }` |
47
+ | `reconcile(formId, unknown)` | `POST /v1/forms/:id/records/read`, or nothing when there is no token |
48
+ | `query(formId, source, { operation, search, offset?, limit? }, signal?)` | `POST /v1/forms/:id/lookups/:source/query` |
49
+ | `resolve(formId, source, { operation, tokens }, signal?)` | `POST /v1/forms/:id/lookups/:source/resolve` |
50
+
51
+ The reply types — `FormMenu`, `FormMenuEntry`, `FormMenuChild`,
52
+ `PublishedForm`, `FormRecord`, `RecordList`, `RecordListQuery`,
53
+ `ListSortTerm`, `ListColumns`, `LookupResult`, `LookupRow`, `FieldError` —
54
+ and `LIST_PAGE_SIZE` are re-exported from
55
+ `@formancy/data-core`, where the server's handlers are checked against the
56
+ same declarations. A form's `list`
57
+ is `null` when the server would refuse this person a page, and otherwise
58
+ says which fields a page may be searched and sorted by
59
+ ([0044](../../docs/decisions/0044-a-forms-records-are-listed-a-page-at-a-time-under-its-read-policy.md));
60
+ a `form` reply without one is `unexpected`, as from a server older than
61
+ lists. `list` sends exactly `search`, `sort`, `parent`, `offset` and `limit`
62
+ in a POST body, each left out the server's default, so the search is in no
63
+ URL. A form's `deleting` says what a delete also does -- whether it
64
+ cascades, clears references, or may be refused while other rows refer to
65
+ the record -- for a page to word its confirmation by, and is `null`
66
+ exactly when `operations` holds no `delete`; a reply that breaks that is
67
+ `unexpected`, as is one without `deleting` from a server older than
68
+ deletes, so a host upgrades its server first
69
+ ([0050](../../docs/decisions/0050-a-record-is-deleted-one-at-a-time-by-its-token-and-the-version-its-person-read.md)).
70
+ `delete` sends exactly `record` and `version`, with a write id of its own,
71
+ and is done only on the server's `{ record, deleted: true }`, whose token
72
+ is the row the statement deleted.
73
+
74
+ `forms` is the menu
75
+ ([0045](../../docs/decisions/0045-the-runtime-plane-lists-the-forms-a-person-may-read-and-nothing-about-the-others.md)):
76
+ the forms the server says this person may read, each with its title, the
77
+ operations its policy grants them as published, and its children -- a
78
+ child form and the lookup field its rows name this form's records by, so a
79
+ host lists one record's children with
80
+ `list(child.form, { parent: { field: child.field, record } })`. The server
81
+ asks no database for it: `form(formId)` says what the database allows now,
82
+ and can still refuse a form the menu named. An answer whose entries lack
83
+ a title, operations or children is `unexpected`.
84
+
85
+ ## The rules it keeps for a host
86
+
87
+ - **The token is the host's, per request.** `token()` is called once for every
88
+ request and its answer goes in `Authorization: Bearer …` and nowhere else —
89
+ not a URL, not a body, not storage. A host whose session renews the token is
90
+ never answered for the old one.
91
+ - **Same origin, or a listed one.** `base` defaults to `''`, the page's own
92
+ origin: served from the server's origin, or through a proxy on the page's
93
+ own ([0024](../../docs/decisions/0024-the-studio-speaks-only-the-admin-plane.md)).
94
+ A page on another origin sets `base` to the server's, and is answered only
95
+ when the server's operator lists the page's origin exactly in
96
+ `FORMANCY_DATA_CORS_ORIGINS`
97
+ ([0060](../../docs/decisions/0060-the-runtime-plane-answers-the-origins-an-operator-lists-and-the-administrators-plane-none.md));
98
+ otherwise every call is a refused preflight, a read `unreachable` and a
99
+ write unknown though nothing was sent, and only the server's log says why.
100
+ Across origins the client sends only headers the server grants --
101
+ `authorization`, `content-type` and `formancy-write-id`, and `accept`,
102
+ which a browser sends without asking -- and asks for no credentials: a
103
+ `fetch` the host passes must not add a header or set `credentials:
104
+ 'include'`, which the server never allows.
105
+ - **A name is one path segment.** A form id or a source name is
106
+ `encodeURIComponent`'d into one segment under the form's routes. Empty, `.`
107
+ and `..` cannot be one — `fetch` would resolve them to another route — and
108
+ are refused as `invalid-name` before any request or token is made. A
109
+ document decides which names exist, and never a URL, a header, a token, a
110
+ tenant or an operation (formancy.ai 0077,
111
+ [0012](../../docs/decisions/0012-a-lookup-token-is-a-reference-not-a-permission.md)).
112
+ - **Refusals are the server's, verbatim.** The client adds words only when
113
+ the server said none: `unreachable` (status 0) when nothing answered,
114
+ `unexpected` when what answered was not the data server's shape — a proxy's
115
+ HTML page, a redirect, a captive portal's `200`. A refusal with a sentence
116
+ and no code is `http-<status>`.
117
+ - **An abort stops the lookup in the database.** The signal a renderer
118
+ passes a source, or a host passes `query` and `resolve`, aborts the
119
+ request; the server notices its client left and stops the statement it
120
+ started, on both engines, and the call rejects with the AbortError
121
+ ([0061](../../docs/decisions/0061-a-lookup-statement-stops-when-its-request-is-abandoned-or-its-deadline-passes.md)).
122
+ A search that runs past the server's deadline is refused 503
123
+ `lookup-timeout`, whose sentence says the same search will take as long
124
+ again, where `unavailable` invites another try; a resolve past it is
125
+ `unavailable`, since trying again helps a read by key. A source rejects
126
+ with either, and a renderer shows "could not be loaded". The 499 `abandoned` the server
127
+ answers an abandoned lookup reaches nobody: it is the audit trail's.
128
+ - **A write is known only when the data server says what happened**
129
+ ([0031](../../docs/decisions/0031-an-answer-lost-after-a-write-is-unknown.md)).
130
+ A record, the server's 502 `unknown-outcome`, a 4xx with a sentence, or a
131
+ 503 `unavailable`. Anything else — a rejected `fetch`, a body that cannot be
132
+ read or is not JSON, a proxy's 502 or 504, a 500, a 2xx that is not a
133
+ record — may have reached the database, so it is an `UnknownWrite`:
134
+ `{ ok: false, status, code: 'unknown-outcome', message, operation, record,
135
+ version, origin }`, `origin` being `database` (the server's 502, whose
136
+ `record` and `version` it carries) or `transport` (the update's or the
137
+ delete's own, or `null` for a create). `ok` is false, so a host that
138
+ checks only `ok` fails closed. Nothing is ever sent again.
139
+ `reconcile(formId, unknown)` reads what it addressed and says `unchanged`
140
+ (an update's or a deletion's record still at the version sent: saving or
141
+ deleting again with it is done at most once), `changed`, `present`,
142
+ `absent` (the read's 404, through this person's read filter: not proof the
143
+ row is not stored, but creating again is safe, because the key stops a
144
+ second row; for a deletion, the record is gone, by it or another) or
145
+ `unverifiable` (no token, and no request) — or passes on the read's own
146
+ refusal. On a read, 0029's rules stand: a proxy's page is
147
+ `unexpected`.
148
+ - **Every write carries a write id of its own**, in the `formancy-write-id`
149
+ header: 128 random bits from `crypto.getRandomValues`, new for every
150
+ `create`, `update` and `delete` call. Over HTTP/1.1, when a connection the
151
+ browser reused closes after the server read a write and before any byte of
152
+ an answer, Chromium and Firefox send it again on their own, below the page;
153
+ one server process answers that copy with the first one's answer instead of
154
+ applying it twice (0031). WebKit on Linux sends every write on a new
155
+ connection and never sends one twice, so there the write is unknown
156
+ ([0058](../../docs/decisions/0058-the-browser-gates-run-in-chromium-firefox-and-webkit-and-each-fact-the-browsers-disagree-on-is-named-and-held.md)). The client itself still never sends anything again.
157
+ - **Field problems are sentences.** The renderers print an error entry as
158
+ it is, beside the field and in the error summary, as observed at 0.3.0 on
159
+ 2026-10-09, at 0.4.0 on 2026-10-10 and at 0.5.0 on 2026-10-11, so
160
+ `fieldProblems` hands them the server's sentence, which never echoes a
161
+ value. The code stays on the
162
+ refusal for a program.
163
+
164
+ ## In React
165
+
166
+ ```tsx
167
+ import { createFormEngine } from '@formancy/core'
168
+ import { ErrorSummary, FormancyForm, FormancyProvider, OptionsSourcesProvider } from '@formancy/react'
169
+ import { createDataClient, fieldProblems, isUnknownWrite, lookupSources } from '@formancy/data-client'
170
+
171
+ const client = createDataClient({ token: () => session.token() })
172
+ const published = await client.form('order') // check .ok
173
+ const { form } = published.value
174
+ let held = { record: undefined as string | undefined, version: '', operation: 'create' as 'create' | 'update' }
175
+ const sources = lookupSources(client, 'order', form, () => held.operation)
176
+ const engine = createFormEngine({ schema: form, formId: 'order-react', capabilities })
177
+
178
+ async function save(outcome) {
179
+ if (!outcome.ok) return // the engine's own errors are showing
180
+ const saved = held.record === undefined
181
+ ? await client.create('order', outcome.data)
182
+ : await client.update('order', { record: held.record, version: held.version, answers: outcome.data })
183
+ if (isUnknownWrite(saved)) return showUnknown(saved) // it may have been saved: client.reconcile, never a resend
184
+ if (!saved.ok) {
185
+ engine.applyServerErrors(fieldProblems(saved)) // {} when the refusal concerns no field
186
+ return show(saved.message) // 409 stale: keep the draft and say so
187
+ }
188
+ held = { record: saved.value.record ?? undefined, version: saved.value.version ?? '', operation: 'update' }
189
+ }
190
+
191
+ <OptionsSourcesProvider value={sources}>
192
+ <FormancyProvider engine={engine}>
193
+ <ErrorSummary />
194
+ <FormancyForm submitLabel="Save" onSubmit={save} />
195
+ </FormancyProvider>
196
+ </OptionsSourcesProvider>
197
+ ```
198
+
199
+ ## In Angular
200
+
201
+ ```ts
202
+ import { provideFormancy, provideFormancyOptionsSources } from '@formancy/angular'
203
+
204
+ const app = await createApplication({
205
+ providers: [provideZonelessChangeDetection(), provideFormancy(engine), provideFormancyOptionsSources(sources)],
206
+ })
207
+ ```
208
+
209
+ The same `sources` object serves both renderers: each declares its own
210
+ `OptionsSource` on purpose, and a `LookupSource` is assignable to both, which
211
+ a host's type check proves. Angular's providers are fixed once its injector
212
+ exists, so the operation is passed as a getter: a form that moves from create
213
+ to update asks under update's filter without a new application.
214
+
215
+ ## What it does not do
216
+
217
+ - **No cache and no retry.** The server asks the policy on every request
218
+ ([0022](../../docs/decisions/0022-the-runtime-plane-asks-the-policy-every-time.md)),
219
+ and a write whose outcome is unknown — `502 unknown-outcome`, "may have been
220
+ saved" — is the host's to reconcile, never the client's to send again
221
+ ([0015](../../docs/decisions/0015-a-record-operation-is-one-guarded-statement.md)).
222
+ What the browser sends again on its own is answered by the server once per
223
+ write id, in its process only: a copy that reaches another replica is
224
+ applied again.
225
+ - **`hasMore` and `omitted` do not reach the person.** A renderer's option
226
+ contract, as read at 0.3.0 on 2026-10-09 and unchanged at 0.4.0 and 0.5.0,
227
+ is a list of `{ value, label }`, so a list of exactly one page looks
228
+ complete. A typeahead keeps narrowing; a plain select does not.
229
+ - **Its types are `@formancy/spec` 0.5.0's.** `PublishedForm.form` is the
230
+ `FormSchema` of the `@formancy/spec` this package pins exactly (0002), and
231
+ there `specVersion` may be `"4"`, so a host whose engine is
232
+ `@formancy/core` 0.3.0 does not compile the React snippet above: TS2322 at
233
+ `createFormEngine`, measured with TypeScript 6.0.3 and `skipLibCheck` off
234
+ on 2026-10-10, and again against 0.5.0's types on 2026-10-11, when a host
235
+ whose engine is at 0.4.0 compiled it. The document itself is spec 3
236
+ ([0042](../../docs/decisions/0042-generated-forms-stay-on-spec-3-after-spec-4-is-released.md)),
237
+ so such a host casts it to its own `@formancy/spec`'s `FormSchema`, or
238
+ moves its renderer to 0.4.0 or later. Nothing here type-checks a 0.3.0 or
239
+ a 0.4.0 host.
240
+ - **Records are addressed by token only.** The definition does not name the
241
+ identity columns, so a host keeps the tokens that create, read and lookups
242
+ return, or encodes its own key with data-core's `encodeKeyToken`.
243
+ - **Error sentences are the server's English.** The codes stay on the refusal
244
+ for a host that translates.
245
+ - **A source answers for every name the document carries.** A host with its
246
+ own named lists merges maps; a name the server does not know fails closed,
247
+ as "could not be loaded".
@@ -0,0 +1,232 @@
1
+ import { Deleting, FieldError, FieldError as FieldError$1, FormMenu, FormMenu as FormMenu$1, FormMenuChild, FormMenuEntry, FormRecord, FormRecord as FormRecord$1, LIST_PAGE_SIZE, ListColumns, ListSortTerm, LookupResult, LookupResult as LookupResult$1, LookupRow, LookupRow as LookupRow$1, PublishedForm, PublishedForm as PublishedForm$1, RecordDeleted, RecordDeleted as RecordDeleted$1, RecordList, RecordList as RecordList$1, RecordListQuery, RecordListQuery as RecordListQuery$1 } from "@formancy/data-core";
2
+ import { FormSchema, RemoteOption } from "@formancy/spec";
3
+ //#region src/writes.d.ts
4
+ /** A write that was sent and whose outcome nobody here can know: the host reconciles it, nothing retries it. */
5
+ interface UnknownWrite {
6
+ /** False, so a host that only checks `ok` treats it as not done, and fails closed. */
7
+ ok: false;
8
+ /** The status that answered, or 0 when nothing did. */
9
+ status: number;
10
+ code: 'unknown-outcome';
11
+ /** The server's sentence when the database lost the answer; the client's own when the page lost it. */
12
+ message: string;
13
+ /** `delete` since 0050. */
14
+ operation: 'create' | 'update' | 'delete';
15
+ /** Update and delete: the token sent. Create: the token the server says it would have, or null. */
16
+ record: string | null;
17
+ /**
18
+ * Update and delete: the version sent -- saving again with exactly it is
19
+ * stored at most once, and deleting again with it deletes nothing else.
20
+ * Create: null.
21
+ */
22
+ version: string | null;
23
+ /** `database`: the data server said so (its 502). `transport`: the answer was lost between page and server. */
24
+ origin: 'database' | 'transport';
25
+ }
26
+ /** What `create` and `update` resolve to. */
27
+ type WriteOutcome = Outcome<FormRecord$1> | UnknownWrite;
28
+ /** What `delete` resolves to (0050): the server's deletion answer, a refusal, or a deletion nobody here can say happened. */
29
+ type DeleteOutcome = Outcome<RecordDeleted$1> | UnknownWrite;
30
+ /** Whether an outcome is an unknown write, rather than a success or a refusal. */
31
+ export declare function isUnknownWrite(outcome: {
32
+ ok: boolean;
33
+ }): outcome is UnknownWrite;
34
+ //#endregion
35
+ //#region src/reconcile.d.ts
36
+ /**
37
+ * What one read says about an unknown write (0031). Never a resend: the
38
+ * person decides what happens next, with this in front of them.
39
+ *
40
+ * - `unchanged` -- an update's or a deletion's record is still at the
41
+ * version it sent: for a deletion, deleting again with it is safe. Not
42
+ * visible yet, which is not "not stored": on PostgreSQL a write orphaned by
43
+ * a cut can still commit. Saving again with that same version is safe; the
44
+ * database stores it at most once, and whichever comes second is stale.
45
+ * - `changed` -- an update's or a deletion's record has moved on: this save, or another's.
46
+ * - `present` -- a create whose token was known is there -- or, where records
47
+ * are deleted, one created again since with the same key, which only a
48
+ * key people type allows, and only on a rowversion table (0050).
49
+ * - `absent` -- it is not (the read's 404). Creating again is safe: the key
50
+ * stops a second row, whichever write lands first. A deletion's record
51
+ * is gone: this deletion's work, or another's, which the version a record
52
+ * cannot share with another makes one and the same (0050).
53
+ * - `unverifiable` -- a create with no token: the database numbers the key,
54
+ * or the answer was lost before the server could name one. No request is
55
+ * made; only the person can tell.
56
+ * - a refusal -- the read's own, which says nothing about the write.
57
+ */
58
+ type Reconciliation = {
59
+ ok: true;
60
+ state: 'unchanged';
61
+ current: FormRecord$1;
62
+ } | {
63
+ ok: true;
64
+ state: 'changed';
65
+ current: FormRecord$1;
66
+ } | {
67
+ ok: true;
68
+ state: 'present';
69
+ current: FormRecord$1;
70
+ } | {
71
+ ok: true;
72
+ state: 'absent';
73
+ } | {
74
+ ok: true;
75
+ state: 'unverifiable';
76
+ } | Refusal;
77
+ //#endregion
78
+ //#region src/client.d.ts
79
+ interface DataClientOptions {
80
+ /** Called once per request. The host's session owns it; it is never stored, and never in a URL or a body. */
81
+ token: () => string | Promise<string>;
82
+ /**
83
+ * Prefixed to every route. Default `''`: the page's own origin. Another
84
+ * origin is answered only when the server's operator lists the page's
85
+ * origin in `FORMANCY_DATA_CORS_ORIGINS`; otherwise every call is a refused
86
+ * preflight, a network error to the page: a read is `unreachable`, a write
87
+ * unknown though nothing was sent (0060).
88
+ */
89
+ base?: string;
90
+ /** Default the global `fetch`, bound. */
91
+ fetch?: typeof fetch;
92
+ }
93
+ /**
94
+ * Why a call did not succeed. `status` is the HTTP status, or 0 when nothing
95
+ * answered or nothing was sent. `code` and `message` are the server's when it
96
+ * gave them; `fieldErrors` is present when the refusal concerns fields.
97
+ */
98
+ type Refusal = {
99
+ ok: false;
100
+ status: number;
101
+ code: string;
102
+ message: string;
103
+ fieldErrors?: FieldError$1[];
104
+ };
105
+ type Outcome<T> = {
106
+ ok: true;
107
+ value: T;
108
+ } | Refusal;
109
+ /** The operation a form is open for, which decides the policy's lookup filter. Never the document's to say. */
110
+ type LookupOperation = 'read' | 'create' | 'update';
111
+ /**
112
+ * A client of one data server's runtime plane. Every function resolves to an
113
+ * outcome and never rejects for an answer -- only for an abort the caller
114
+ * asked for, which reaches it as the AbortError it raised.
115
+ */
116
+ export declare function createDataClient(options: DataClientOptions): {
117
+ /**
118
+ * `GET /v1/forms` (0045): the published forms this person may read, each
119
+ * with its title, the operations the policy grants them and the children
120
+ * a page may list under one of its records -- and nothing about any other
121
+ * form. Opening one with `form` says what the database allows now.
122
+ */
123
+ forms: (signal?: AbortSignal) => Promise<Outcome<FormMenu$1>>;
124
+ /** `GET /v1/forms/:id`: the published document, the operations this person may use, and the fields they may read. */
125
+ form: (formId: string) => Promise<Outcome<PublishedForm$1>>;
126
+ /** `POST …/records/read`: one record by its token, with the version an update sends back. */
127
+ read: (formId: string, record: string) => Promise<Outcome<FormRecord$1>>;
128
+ /**
129
+ * `POST …/records/list` (0044): one page of the records this person may
130
+ * read, each what `read` returns for its token, and whether more follow.
131
+ * A property left out is the server's default -- no search, the key's
132
+ * order, no parent, the first page, the largest page -- and exactly the
133
+ * route's keys are sent, whatever else the object carries. A POST, so the
134
+ * search is never in a URL.
135
+ */
136
+ list: (formId: string, query?: Partial<RecordListQuery$1>, signal?: AbortSignal) => Promise<Outcome<RecordList$1>>;
137
+ /**
138
+ * `POST …/records/create`: a new record from the answers; the reply is what
139
+ * was stored, in the stored spelling -- or an `UnknownWrite` when nobody
140
+ * can say whether it was stored.
141
+ */
142
+ create: (formId: string, answers: Readonly<Record<string, unknown>>) => Promise<WriteOutcome>;
143
+ /**
144
+ * `POST …/records/update`: saved only if the record is still at `version`,
145
+ * otherwise 409 `stale`; an `UnknownWrite` when nobody can say whether it
146
+ * was saved. Exactly the route's keys are sent, whatever else the object
147
+ * passed carries.
148
+ */
149
+ update: (formId: string, change: {
150
+ record: string;
151
+ version: string;
152
+ answers: Readonly<Record<string, unknown>>;
153
+ }) => Promise<WriteOutcome>;
154
+ /**
155
+ * `POST …/records/delete` (0050): the record, only if it is still at
156
+ * `version`, otherwise 409 `stale`; the answer is the deleted row's
157
+ * token and `deleted: true`, and nothing else is done. An `UnknownWrite`
158
+ * when nobody can say whether it was deleted -- deleting again with the
159
+ * same version then deletes nothing else. Exactly the route's keys are
160
+ * sent, with a write id of its own.
161
+ */
162
+ delete: (formId: string, removal: {
163
+ record: string;
164
+ version: string;
165
+ }) => Promise<DeleteOutcome>;
166
+ /** Reads what an unknown write addressed and says what that shows; never sends the write again (0031). */
167
+ reconcile: (formId: string, unknown: UnknownWrite) => Promise<Reconciliation>;
168
+ /** `POST …/lookups/:source/query`: one page of the options this person may pick for this operation. */
169
+ query: (formId: string, source: string, q: {
170
+ operation: LookupOperation;
171
+ search: string;
172
+ offset?: number;
173
+ limit?: number;
174
+ }, signal?: AbortSignal) => Promise<Outcome<LookupResult$1>>;
175
+ /** `POST …/lookups/:source/resolve`: the labels of the tokens this person may see; any other is absent, not an error. */
176
+ resolve: (formId: string, source: string, r: {
177
+ operation: LookupOperation;
178
+ tokens: readonly string[];
179
+ }, signal?: AbortSignal) => Promise<Outcome<LookupRow$1[]>>;
180
+ };
181
+ /** A client of one data server's runtime plane: what `createDataClient` returns. */
182
+ type DataClient = ReturnType<typeof createDataClient>;
183
+ //#endregion
184
+ //#region src/lookups.d.ts
185
+ /** What a source reads of a renderer's request: the kind, the query or the stored values, the limit, and the signal. */
186
+ type LookupRequest = {
187
+ kind: 'search' | 'labels';
188
+ query: string;
189
+ values: readonly string[];
190
+ limit: number;
191
+ signal: AbortSignal;
192
+ };
193
+ /** One named list, answering through the runtime plane. */
194
+ type LookupSource = {
195
+ resolve(request: LookupRequest): Promise<readonly RemoteOption[]>;
196
+ };
197
+ /** Every optionsSource the document names -- in groups and repeaters too -- once each, in document order. */
198
+ export declare function sourceNames(form: FormSchema): string[];
199
+ /**
200
+ * One source per name the document carries, each answering through this
201
+ * form's lookup routes.
202
+ *
203
+ * `operation` may be a getter: it is read on every request, so a form that
204
+ * moves from create to update after its first save asks under update's filter
205
+ * without a new map -- which, in Angular, would mean a new application.
206
+ *
207
+ * A search sends the renderer's query and limit; a label request resolves the
208
+ * stored tokens, and asks nothing when there are none. A token the person may
209
+ * not see is left out, not an error. A refusal rejects with the server's
210
+ * sentence, which the renderer shows as "could not be loaded"; an abort
211
+ * rejects with the AbortError. `hasMore` and `omitted` do not reach the
212
+ * renderer, whose contract, at 0.5.0 as at 0.3.0, has nowhere to put them
213
+ * (0029).
214
+ */
215
+ export declare function lookupSources(client: DataClient, formId: string, form: FormSchema, operation: LookupOperation | (() => LookupOperation)): Readonly<Record<string, LookupSource>>;
216
+ //#endregion
217
+ //#region src/problems.d.ts
218
+ /**
219
+ * A refusal's field problems as `engine.applyServerErrors` takes them: the
220
+ * server's sentences, grouped by field, in the order the server gave them,
221
+ * each once. `{}` when the refusal concerns no field.
222
+ *
223
+ * Sentences and not codes, because the renderers print an entry verbatim, at
224
+ * 0.3.0, 0.4.0 and 0.5.0, beside the field and in the error summary: a code would
225
+ * put `not-an-option` in front of a person. The server promises its sentence is
226
+ * showable and never echoes a value (0011); the code stays on the Refusal for a
227
+ * program to read.
228
+ */
229
+ export declare function fieldProblems(refusal: Refusal): Record<string, string[]>;
230
+ //#endregion
231
+ export { type DataClient, type DataClientOptions, type DeleteOutcome, type Deleting, type FieldError, type FormMenu, type FormMenuChild, type FormMenuEntry, type FormRecord, LIST_PAGE_SIZE, type ListColumns, type ListSortTerm, type LookupOperation, type LookupRequest, type LookupResult, type LookupRow, type LookupSource, type Outcome, type PublishedForm, type Reconciliation, type RecordDeleted, type RecordList, type RecordListQuery, type Refusal, type UnknownWrite, type WriteOutcome };
232
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":["FormRecord","RecordDeleted","FormRecord","FieldError","FormMenu","PublishedForm","FormRecord","RecordListQuery","RecordList","LookupResult","LookupRow"],"sources":["../src/writes.ts","../src/reconcile.ts","../src/client.ts","../src/lookups.ts","../src/problems.ts"],"mappings":";;;;UAciB;;EAEf;;EAEA;EACA;;EAEA;;EAEA;;EAEA;;;;;;EAMA;;EAEA;;;KAIU,eAAe,QAAQA,gBAAc;;KAGrC,gBAAgB,QAAQC,mBAAiB;;wBAGrC,eAAe;EAAW;IAAgB,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;KCjBzD;EACN;EAAU;EAAoB,SAASC;;EACvC;EAAU;EAAkB,SAASA;;EACrC;EAAU;EAAkB,SAASA;;EACrC;EAAU;;EACV;EAAU;IACZ;;;UCGa;;EAEf,sBAAsB;;;;;;;;EAQtB;;EAEA,eAAe;;;;;;;KAQL;EAAY;EAAW;EAAgB;EAAc;EAAiB,cAAcC;;KACpF,QAAQ;EAAO;EAAU,OAAO;IAAM;;KAGtC;;;;;;wBAsEI,iBAAiB,SAAS;;;;;;;EAqErB,QAAA,SAAA,gBAAc,QAAQ,QAAQC;;EAIhC,OAAA,mBAAS,QAAQ,QAAQC;;EAhBpB,OAAA,gBAAM,mBAAmB,QAAQ,QAAQC;;;;;;;;;EA8B9C,OAAA,gBAAM,QAAS,QAAQC,oBAAgB,SAAgB,gBAAc,QAAQ,QAAQC;;;;;;EAenF,SAAA,gBAAM,SAAW,SAAS,6BAA2B,QAAQ;;;;;;;EAS7D,SAAA,gBAAM;IAAY;IAAgB;IAAiB,SAAS,SAAS;QAA6B,QAAQ;;;;;;;;;EAWpG,SAAA,gBAAM;IAAa;IAAgB;QAAoB,QAAQ;;EAOlE,YAAA,gBAAM,SAAW,iBAAe,QAAQ;;EAIlD,QAAA,gBAAM,gBACA;IACT,WAAW;IAAiB;IAAgB;IAAiB;KAAgB,SACzE,gBACR,QAAQ,QAAQC;;EAID,UAAA,gBAAM,gBAAgB;IAAO,WAAW;IAAiB;KAA2B,SAAW,gBAAc,QAAQ,QAAQC;;;KAMvI,aAAa,kBAAkB;;;;KChQ/B;EAAkB;EAA2B;EAAe;EAA2B;EAAe,QAAQ;;;KAG9G;EAAiB,QAAQ,SAAS,gBAAgB,iBAAiB;;;wBAU/D,YAAY,MAAM;;;;;;;;;;;;;;;;;wBAsBlB,cACd,QAAQ,YACR,gBACA,MAAM,YACN,WAAW,yBAAyB,mBACnC,SAAS,eAAe;;;;;;;;;;;;;;wBC/CX,cAAc,SAAS,UAAU"}
package/dist/index.mjs ADDED
@@ -0,0 +1,487 @@
1
+ import { LIST_PAGE_SIZE, WRITE_ID_HEADER } from "@formancy/data-core";
2
+ //#region src/reconcile.ts
3
+ /** Reads the record an unknown write addressed, through `read`, and says what that shows. */
4
+ async function reconcile(read, formId, unknown) {
5
+ if (unknown.record === null) return {
6
+ ok: true,
7
+ state: "unverifiable"
8
+ };
9
+ const found = await read(formId, unknown.record);
10
+ if (unknown.operation === "create") {
11
+ if (found.ok) return {
12
+ ok: true,
13
+ state: "present",
14
+ current: found.value
15
+ };
16
+ return found.status === 404 && found.code === "not-found" ? {
17
+ ok: true,
18
+ state: "absent"
19
+ } : found;
20
+ }
21
+ if (unknown.operation === "delete" && !found.ok && found.status === 404 && found.code === "not-found") return {
22
+ ok: true,
23
+ state: "absent"
24
+ };
25
+ if (!found.ok) return found;
26
+ return {
27
+ ok: true,
28
+ state: found.value.version === unknown.version ? "unchanged" : "changed",
29
+ current: found.value
30
+ };
31
+ }
32
+ //#endregion
33
+ //#region src/shapes.ts
34
+ const UNEXPECTED = "The data server did not answer in a way this client understands.";
35
+ function isRecord(value) {
36
+ return typeof value === "object" && value !== null && !Array.isArray(value);
37
+ }
38
+ function isNullableString(value) {
39
+ return value === null || typeof value === "string";
40
+ }
41
+ function isFieldError(value) {
42
+ return isRecord(value) && typeof value["field"] === "string" && typeof value["code"] === "string" && typeof value["message"] === "string";
43
+ }
44
+ function isRows(value) {
45
+ return Array.isArray(value) && value.every((row) => isRecord(row) && typeof row["token"] === "string" && typeof row["label"] === "string");
46
+ }
47
+ function isStrings(value) {
48
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
49
+ }
50
+ /** What a form's list may be searched and sorted by, or `null` for no list (0044): a reply without one is from no server that has lists. */
51
+ function isListColumns(value) {
52
+ return value === null || isRecord(value) && isStrings(value["searchable"]) && isStrings(value["sortable"]);
53
+ }
54
+ /**
55
+ * What a delete also does (0050): `null` exactly when the form offers no
56
+ * delete, otherwise its three flags. A reply that says delete without them,
57
+ * or them without delete -- or neither, a server older than delete -- is no
58
+ * form this server sent, and a page that worded a confirmation from it
59
+ * would say what nobody decided.
60
+ */
61
+ function isDeleting(value, operations) {
62
+ if (!operations.includes("delete")) return value === null;
63
+ return isRecord(value) && typeof value["cascades"] === "boolean" && typeof value["clears"] === "boolean" && typeof value["restricted"] === "boolean";
64
+ }
65
+ function isPublishedForm(body) {
66
+ return isRecord(body) && isRecord(body["form"]) && isRecord(body["form"]["model"]) && Array.isArray(body["operations"]) && Array.isArray(body["readable"]) && isListColumns(body["list"]) && isDeleting(body["deleting"], body["operations"]);
67
+ }
68
+ /** A child in a menu: the child form, and the field its list is narrowed by. */
69
+ function isMenuChild(value) {
70
+ return isRecord(value) && typeof value["form"] === "string" && typeof value["field"] === "string";
71
+ }
72
+ /** One menu entry: a page draws a link from it, and a grid per child. */
73
+ function isMenuEntry(value) {
74
+ return isRecord(value) && typeof value["id"] === "string" && typeof value["title"] === "string" && isStrings(value["operations"]) && Array.isArray(value["children"]) && value["children"].every(isMenuChild);
75
+ }
76
+ /** The menu (0045): every entry in the server's shape, children included, so an entry of a server with no children is not drawn as a form with none. */
77
+ function isFormMenu(body) {
78
+ return isRecord(body) && Array.isArray(body["forms"]) && body["forms"].every(isMenuEntry);
79
+ }
80
+ /** A delete's answer (0050): the token of the row deleted, and `deleted: true`, nothing else that a 2xx could be mistaken for. */
81
+ function isRecordDeleted(body) {
82
+ return isRecord(body) && typeof body["record"] === "string" && body["deleted"] === true;
83
+ }
84
+ function isFormRecord(body) {
85
+ return isRecord(body) && isNullableString(body["record"]) && isNullableString(body["version"]) && isRecord(body["answers"]);
86
+ }
87
+ /** A page of records (0044): every row a record, and whether more follow. */
88
+ function isRecordList(body) {
89
+ return isRecord(body) && Array.isArray(body["rows"]) && body["rows"].every(isFormRecord) && typeof body["hasMore"] === "boolean";
90
+ }
91
+ function isLookupResult(body) {
92
+ return isRecord(body) && isRows(body["rows"]) && typeof body["hasMore"] === "boolean" && typeof body["omitted"] === "number";
93
+ }
94
+ function isResolved(body) {
95
+ return isRecord(body) && isRows(body["rows"]);
96
+ }
97
+ /** A non-2xx answer: the server's refusal when it carries a sentence, otherwise not the server's to have said. */
98
+ function refusalOf(status, body) {
99
+ if (!isRecord(body) || typeof body["message"] !== "string") return {
100
+ ok: false,
101
+ status,
102
+ code: "unexpected",
103
+ message: UNEXPECTED
104
+ };
105
+ const refusal = {
106
+ ok: false,
107
+ status,
108
+ code: typeof body["code"] === "string" ? body["code"] : `http-${String(status)}`,
109
+ message: body["message"]
110
+ };
111
+ if (Array.isArray(body["fieldErrors"])) refusal.fieldErrors = body["fieldErrors"].filter(isFieldError);
112
+ return refusal;
113
+ }
114
+ //#endregion
115
+ //#region src/writes.ts
116
+ /** Whether an outcome is an unknown write, rather than a success or a refusal. */
117
+ function isUnknownWrite(outcome) {
118
+ if (outcome.ok) return false;
119
+ const candidate = outcome;
120
+ return candidate.code === "unknown-outcome" && (candidate.origin === "database" || candidate.origin === "transport");
121
+ }
122
+ const LOST_ON_THE_WAY = "The answer to this save was lost between this page and the data server. It may have been saved.";
123
+ const DELETION_LOST_ON_THE_WAY = "The answer to this deletion was lost between this page and the data server. It may have been deleted.";
124
+ function lost(intent, status) {
125
+ return {
126
+ ok: false,
127
+ status,
128
+ code: "unknown-outcome",
129
+ message: intent.operation === "delete" ? DELETION_LOST_ON_THE_WAY : LOST_ON_THE_WAY,
130
+ ...intent,
131
+ origin: "transport"
132
+ };
133
+ }
134
+ function writeOutcome(answer, intent) {
135
+ if (!answer.answered) return lost(intent, 0);
136
+ const { status, body } = answer;
137
+ if (!isRecord(body)) return lost(intent, status);
138
+ if (status >= 200 && status <= 299) {
139
+ if (intent.operation === "delete") return isRecordDeleted(body) ? {
140
+ ok: true,
141
+ value: body
142
+ } : lost(intent, status);
143
+ return isFormRecord(body) ? {
144
+ ok: true,
145
+ value: body
146
+ } : lost(intent, status);
147
+ }
148
+ const message = body["message"];
149
+ if (typeof message !== "string") return lost(intent, status);
150
+ if (status === 502 && body["code"] === "unknown-outcome") {
151
+ const record = isNullableString(body["record"]) ? body["record"] : intent.record;
152
+ const version = isNullableString(body["version"]) ? body["version"] : intent.version;
153
+ return {
154
+ ok: false,
155
+ status,
156
+ code: "unknown-outcome",
157
+ message,
158
+ operation: intent.operation,
159
+ record,
160
+ version,
161
+ origin: "database"
162
+ };
163
+ }
164
+ if (status >= 400 && status <= 499) return refusalOf(status, body);
165
+ if (status === 503 && body["code"] === "unavailable") return refusalOf(status, body);
166
+ return lost(intent, status);
167
+ }
168
+ //#endregion
169
+ //#region src/client.ts
170
+ const UNREACHABLE = "The data server could not be reached.";
171
+ const INVALID_NAME = "A form or lookup name that is empty, \".\" or \"..\" cannot address a route.";
172
+ /**
173
+ * `name` as one path segment, or undefined when it cannot be one. `fetch`
174
+ * resolves `.` and `..` before sending, so they would address a different
175
+ * route; an empty segment would address the route above.
176
+ */
177
+ function pathSegment(name) {
178
+ if (name === "" || name === "." || name === "..") return void 0;
179
+ return encodeURIComponent(name);
180
+ }
181
+ /** The body as JSON, or undefined when it is not JSON or could not be read. An abort while reading rethrows. */
182
+ async function bodyOf(response, signal) {
183
+ let text;
184
+ try {
185
+ text = await response.text();
186
+ } catch (error) {
187
+ if (signal?.aborted === true) throw error;
188
+ return;
189
+ }
190
+ try {
191
+ return JSON.parse(text);
192
+ } catch {
193
+ return;
194
+ }
195
+ }
196
+ /**
197
+ * The base without the slashes it ends in, by walking back from the end.
198
+ * `/\/+$/` did the same and CodeQL flagged it (js/polynomial-redos): on a
199
+ * base of many slashes not at the end, the anchored pattern backtracks.
200
+ */
201
+ function withoutTrailingSlashes(base) {
202
+ let end = base.length;
203
+ while (end > 0 && base[end - 1] === "/") end -= 1;
204
+ return base.slice(0, end);
205
+ }
206
+ /**
207
+ * A new write id: 128 random bits as hex, from `crypto.getRandomValues`,
208
+ * which a page served over plain HTTP has too, unlike `randomUUID`.
209
+ */
210
+ function newWriteId() {
211
+ return Array.from(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16)), (byte) => byte.toString(16).padStart(2, "0")).join("");
212
+ }
213
+ /**
214
+ * A client of one data server's runtime plane. Every function resolves to an
215
+ * outcome and never rejects for an answer -- only for an abort the caller
216
+ * asked for, which reaches it as the AbortError it raised.
217
+ */
218
+ function createDataClient(options) {
219
+ const base = withoutTrailingSlashes(options.base ?? "");
220
+ const send = options.fetch ?? globalThis.fetch.bind(globalThis);
221
+ /** The transport: the request sent once, and what came back. Classifying it is the caller's. */
222
+ async function exchange(request) {
223
+ const segments = request.names.map(pathSegment);
224
+ if (segments.some((segment) => segment === void 0)) return { sent: false };
225
+ const headers = {
226
+ ...request.headers,
227
+ authorization: `Bearer ${await options.token()}`,
228
+ accept: "application/json"
229
+ };
230
+ if (request.body !== void 0) headers["content-type"] = "application/json";
231
+ let response;
232
+ try {
233
+ response = await send(`${base}${request.route(segments)}`, {
234
+ method: request.method,
235
+ headers,
236
+ redirect: "manual",
237
+ ...request.body === void 0 ? {} : { body: JSON.stringify(request.body) },
238
+ ...request.signal === void 0 ? {} : { signal: request.signal }
239
+ });
240
+ } catch (error) {
241
+ if (request.signal?.aborted === true) throw error;
242
+ return { answered: false };
243
+ }
244
+ return {
245
+ answered: true,
246
+ status: response.status,
247
+ body: await bodyOf(response, request.signal)
248
+ };
249
+ }
250
+ const invalidName = {
251
+ ok: false,
252
+ status: 0,
253
+ code: "invalid-name",
254
+ message: INVALID_NAME
255
+ };
256
+ /** A read: changes nothing, so an answer that went wrong is a refusal, and asking again is safe (0029). */
257
+ async function call(request) {
258
+ const answer = await exchange(request);
259
+ if ("sent" in answer) return invalidName;
260
+ if (!answer.answered) return {
261
+ ok: false,
262
+ status: 0,
263
+ code: "unreachable",
264
+ message: UNREACHABLE
265
+ };
266
+ if (answer.status < 200 || answer.status > 299) return refusalOf(answer.status, answer.body);
267
+ const value = request.read(answer.body);
268
+ return value === void 0 ? {
269
+ ok: false,
270
+ status: answer.status,
271
+ code: "unexpected",
272
+ message: UNEXPECTED
273
+ } : {
274
+ ok: true,
275
+ value
276
+ };
277
+ }
278
+ /** A write: known only when the data server says what happened; otherwise unknown, and never sent again (0031). */
279
+ async function sendWrite(formId, intent, body) {
280
+ return exchange({
281
+ method: "POST",
282
+ names: [formId],
283
+ route: ([form]) => `/v1/forms/${String(form)}/records/${intent.operation}`,
284
+ body,
285
+ headers: { [WRITE_ID_HEADER]: newWriteId() }
286
+ });
287
+ }
288
+ async function write(formId, intent, body) {
289
+ const answer = await sendWrite(formId, intent, body);
290
+ return "sent" in answer ? invalidName : writeOutcome(answer, intent);
291
+ }
292
+ const read = (formId, record) => call({
293
+ method: "POST",
294
+ names: [formId],
295
+ route: ([form]) => `/v1/forms/${String(form)}/records/read`,
296
+ body: { record },
297
+ read: (body) => isFormRecord(body) ? body : void 0
298
+ });
299
+ const lookups = (formId, source, action, body, read, signal) => call({
300
+ method: "POST",
301
+ names: [formId, source],
302
+ route: ([form, name]) => `/v1/forms/${String(form)}/lookups/${String(name)}/${action}`,
303
+ body,
304
+ read,
305
+ signal
306
+ });
307
+ return {
308
+ /**
309
+ * `GET /v1/forms` (0045): the published forms this person may read, each
310
+ * with its title, the operations the policy grants them and the children
311
+ * a page may list under one of its records -- and nothing about any other
312
+ * form. Opening one with `form` says what the database allows now.
313
+ */
314
+ forms: (signal) => call({
315
+ method: "GET",
316
+ names: [],
317
+ route: () => "/v1/forms",
318
+ read: (body) => isFormMenu(body) ? body : void 0,
319
+ signal
320
+ }),
321
+ /** `GET /v1/forms/:id`: the published document, the operations this person may use, and the fields they may read. */
322
+ form: (formId) => call({
323
+ method: "GET",
324
+ names: [formId],
325
+ route: ([form]) => `/v1/forms/${String(form)}`,
326
+ read: (body) => isPublishedForm(body) ? body : void 0
327
+ }),
328
+ /** `POST …/records/read`: one record by its token, with the version an update sends back. */
329
+ read,
330
+ /**
331
+ * `POST …/records/list` (0044): one page of the records this person may
332
+ * read, each what `read` returns for its token, and whether more follow.
333
+ * A property left out is the server's default -- no search, the key's
334
+ * order, no parent, the first page, the largest page -- and exactly the
335
+ * route's keys are sent, whatever else the object carries. A POST, so the
336
+ * search is never in a URL.
337
+ */
338
+ list: (formId, query = {}, signal) => call({
339
+ method: "POST",
340
+ names: [formId],
341
+ route: ([form]) => `/v1/forms/${String(form)}/records/list`,
342
+ body: {
343
+ search: query.search,
344
+ sort: query.sort,
345
+ parent: query.parent,
346
+ offset: query.offset,
347
+ limit: query.limit
348
+ },
349
+ read: (body) => isRecordList(body) ? body : void 0,
350
+ signal
351
+ }),
352
+ /**
353
+ * `POST …/records/create`: a new record from the answers; the reply is what
354
+ * was stored, in the stored spelling -- or an `UnknownWrite` when nobody
355
+ * can say whether it was stored.
356
+ */
357
+ create: (formId, answers) => write(formId, {
358
+ operation: "create",
359
+ record: null,
360
+ version: null
361
+ }, { answers }),
362
+ /**
363
+ * `POST …/records/update`: saved only if the record is still at `version`,
364
+ * otherwise 409 `stale`; an `UnknownWrite` when nobody can say whether it
365
+ * was saved. Exactly the route's keys are sent, whatever else the object
366
+ * passed carries.
367
+ */
368
+ update: (formId, change) => write(formId, {
369
+ operation: "update",
370
+ record: change.record,
371
+ version: change.version
372
+ }, {
373
+ record: change.record,
374
+ version: change.version,
375
+ answers: change.answers
376
+ }),
377
+ /**
378
+ * `POST …/records/delete` (0050): the record, only if it is still at
379
+ * `version`, otherwise 409 `stale`; the answer is the deleted row's
380
+ * token and `deleted: true`, and nothing else is done. An `UnknownWrite`
381
+ * when nobody can say whether it was deleted -- deleting again with the
382
+ * same version then deletes nothing else. Exactly the route's keys are
383
+ * sent, with a write id of its own.
384
+ */
385
+ delete: async (formId, removal) => {
386
+ const intent = {
387
+ operation: "delete",
388
+ record: removal.record,
389
+ version: removal.version
390
+ };
391
+ const answer = await sendWrite(formId, intent, {
392
+ record: removal.record,
393
+ version: removal.version
394
+ });
395
+ return "sent" in answer ? invalidName : writeOutcome(answer, intent);
396
+ },
397
+ /** Reads what an unknown write addressed and says what that shows; never sends the write again (0031). */
398
+ reconcile: (formId, unknown) => reconcile(read, formId, unknown),
399
+ /** `POST …/lookups/:source/query`: one page of the options this person may pick for this operation. */
400
+ query: (formId, source, q, signal) => lookups(formId, source, "query", {
401
+ operation: q.operation,
402
+ search: q.search,
403
+ offset: q.offset,
404
+ limit: q.limit
405
+ }, (body) => isLookupResult(body) ? body : void 0, signal),
406
+ /** `POST …/lookups/:source/resolve`: the labels of the tokens this person may see; any other is absent, not an error. */
407
+ resolve: (formId, source, r, signal) => lookups(formId, source, "resolve", {
408
+ operation: r.operation,
409
+ tokens: r.tokens
410
+ }, (body) => isResolved(body) ? body.rows : void 0, signal)
411
+ };
412
+ }
413
+ //#endregion
414
+ //#region src/lookups.ts
415
+ function collect(fields, into) {
416
+ for (const field of fields) {
417
+ if (field.optionsSource !== void 0) into.add(field.optionsSource);
418
+ if (field.fields !== void 0) collect(field.fields, into);
419
+ }
420
+ }
421
+ /** Every optionsSource the document names -- in groups and repeaters too -- once each, in document order. */
422
+ function sourceNames(form) {
423
+ const names = /* @__PURE__ */ new Set();
424
+ collect(form.model.fields, names);
425
+ return [...names];
426
+ }
427
+ /**
428
+ * One source per name the document carries, each answering through this
429
+ * form's lookup routes.
430
+ *
431
+ * `operation` may be a getter: it is read on every request, so a form that
432
+ * moves from create to update after its first save asks under update's filter
433
+ * without a new map -- which, in Angular, would mean a new application.
434
+ *
435
+ * A search sends the renderer's query and limit; a label request resolves the
436
+ * stored tokens, and asks nothing when there are none. A token the person may
437
+ * not see is left out, not an error. A refusal rejects with the server's
438
+ * sentence, which the renderer shows as "could not be loaded"; an abort
439
+ * rejects with the AbortError. `hasMore` and `omitted` do not reach the
440
+ * renderer, whose contract, at 0.5.0 as at 0.3.0, has nowhere to put them
441
+ * (0029).
442
+ */
443
+ function lookupSources(client, formId, form, operation) {
444
+ const current = typeof operation === "function" ? operation : () => operation;
445
+ const source = (name) => ({ async resolve(request) {
446
+ if (request.kind === "labels" && request.values.length === 0) return [];
447
+ const outcome = request.kind === "search" ? await client.query(formId, name, {
448
+ operation: current(),
449
+ search: request.query,
450
+ limit: request.limit
451
+ }, request.signal) : await client.resolve(formId, name, {
452
+ operation: current(),
453
+ tokens: request.values
454
+ }, request.signal);
455
+ if (!outcome.ok) throw new Error(outcome.message);
456
+ return (Array.isArray(outcome.value) ? outcome.value : outcome.value.rows).map((row) => ({
457
+ value: row.token,
458
+ label: row.label
459
+ }));
460
+ } });
461
+ return Object.fromEntries(sourceNames(form).map((name) => [name, source(name)]));
462
+ }
463
+ //#endregion
464
+ //#region src/problems.ts
465
+ /**
466
+ * A refusal's field problems as `engine.applyServerErrors` takes them: the
467
+ * server's sentences, grouped by field, in the order the server gave them,
468
+ * each once. `{}` when the refusal concerns no field.
469
+ *
470
+ * Sentences and not codes, because the renderers print an entry verbatim, at
471
+ * 0.3.0, 0.4.0 and 0.5.0, beside the field and in the error summary: a code would
472
+ * put `not-an-option` in front of a person. The server promises its sentence is
473
+ * showable and never echoes a value (0011); the code stays on the Refusal for a
474
+ * program to read.
475
+ */
476
+ function fieldProblems(refusal) {
477
+ const problems = {};
478
+ for (const { field, message } of refusal.fieldErrors ?? []) {
479
+ const sentences = problems[field] ??= [];
480
+ if (!sentences.includes(message)) sentences.push(message);
481
+ }
482
+ return problems;
483
+ }
484
+ //#endregion
485
+ export { LIST_PAGE_SIZE, createDataClient, fieldProblems, isUnknownWrite, lookupSources, sourceNames };
486
+
487
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/reconcile.ts","../src/shapes.ts","../src/writes.ts","../src/client.ts","../src/lookups.ts","../src/problems.ts"],"sourcesContent":["import type { FormRecord } from '@formancy/data-core'\nimport type { Outcome, Refusal } from './client.js'\nimport type { UnknownWrite } from './writes.js'\n\n/**\n * What one read says about an unknown write (0031). Never a resend: the\n * person decides what happens next, with this in front of them.\n *\n * - `unchanged` -- an update's or a deletion's record is still at the\n * version it sent: for a deletion, deleting again with it is safe. Not\n * visible yet, which is not \"not stored\": on PostgreSQL a write orphaned by\n * a cut can still commit. Saving again with that same version is safe; the\n * database stores it at most once, and whichever comes second is stale.\n * - `changed` -- an update's or a deletion's record has moved on: this save, or another's.\n * - `present` -- a create whose token was known is there -- or, where records\n * are deleted, one created again since with the same key, which only a\n * key people type allows, and only on a rowversion table (0050).\n * - `absent` -- it is not (the read's 404). Creating again is safe: the key\n * stops a second row, whichever write lands first. A deletion's record\n * is gone: this deletion's work, or another's, which the version a record\n * cannot share with another makes one and the same (0050).\n * - `unverifiable` -- a create with no token: the database numbers the key,\n * or the answer was lost before the server could name one. No request is\n * made; only the person can tell.\n * - a refusal -- the read's own, which says nothing about the write.\n */\nexport type Reconciliation =\n | { ok: true; state: 'unchanged'; current: FormRecord }\n | { ok: true; state: 'changed'; current: FormRecord }\n | { ok: true; state: 'present'; current: FormRecord }\n | { ok: true; state: 'absent' }\n | { ok: true; state: 'unverifiable' }\n | Refusal\n\n/** Reads the record an unknown write addressed, through `read`, and says what that shows. */\nexport async function reconcile(\n read: (formId: string, record: string) => Promise<Outcome<FormRecord>>,\n formId: string,\n unknown: UnknownWrite,\n): Promise<Reconciliation> {\n if (unknown.record === null) return { ok: true, state: 'unverifiable' }\n const found = await read(formId, unknown.record)\n if (unknown.operation === 'create') {\n if (found.ok) return { ok: true, state: 'present', current: found.value }\n return found.status === 404 && found.code === 'not-found' ? { ok: true, state: 'absent' } : found\n }\n if (unknown.operation === 'delete' && !found.ok && found.status === 404 && found.code === 'not-found') return { ok: true, state: 'absent' }\n if (!found.ok) return found\n return { ok: true, state: found.value.version === unknown.version ? 'unchanged' : 'changed', current: found.value }\n}\n","import type { FieldError, FormMenu, FormRecord, LookupResult, LookupRow, PublishedForm, RecordDeleted, RecordList } from '@formancy/data-core'\nimport type { Refusal } from './client.js'\n\n/*\n * The shape each runtime route answers with, and a refusal's. Shallow on\n * purpose: enough that a captive portal's `{}` or a proxy's page is not taken\n * for the data server's answer, without restating the server's validation of\n * a document. One place, because a read and a write must agree on what a\n * record and a refusal look like (0029, 0031).\n */\n\nexport const UNEXPECTED = 'The data server did not answer in a way this client understands.'\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nexport function isNullableString(value: unknown): value is string | null {\n return value === null || typeof value === 'string'\n}\n\nfunction isFieldError(value: unknown): value is FieldError {\n return isRecord(value) && typeof value['field'] === 'string' && typeof value['code'] === 'string' && typeof value['message'] === 'string'\n}\n\nfunction isRows(value: unknown): value is LookupRow[] {\n return Array.isArray(value) && value.every((row) => isRecord(row) && typeof row['token'] === 'string' && typeof row['label'] === 'string')\n}\n\nfunction isStrings(value: unknown): value is string[] {\n return Array.isArray(value) && value.every((entry) => typeof entry === 'string')\n}\n\n/** What a form's list may be searched and sorted by, or `null` for no list (0044): a reply without one is from no server that has lists. */\nfunction isListColumns(value: unknown): value is PublishedForm['list'] {\n return value === null || (isRecord(value) && isStrings(value['searchable']) && isStrings(value['sortable']))\n}\n\n/**\n * What a delete also does (0050): `null` exactly when the form offers no\n * delete, otherwise its three flags. A reply that says delete without them,\n * or them without delete -- or neither, a server older than delete -- is no\n * form this server sent, and a page that worded a confirmation from it\n * would say what nobody decided.\n */\nfunction isDeleting(value: unknown, operations: readonly unknown[]): value is PublishedForm['deleting'] {\n if (!operations.includes('delete')) return value === null\n return isRecord(value) && typeof value['cascades'] === 'boolean' && typeof value['clears'] === 'boolean' && typeof value['restricted'] === 'boolean'\n}\n\nexport function isPublishedForm(body: unknown): body is PublishedForm {\n return (\n isRecord(body) &&\n isRecord(body['form']) &&\n isRecord(body['form']['model']) &&\n Array.isArray(body['operations']) &&\n Array.isArray(body['readable']) &&\n isListColumns(body['list']) &&\n isDeleting(body['deleting'], body['operations'] as unknown[])\n )\n}\n\n/** A child in a menu: the child form, and the field its list is narrowed by. */\nfunction isMenuChild(value: unknown): boolean {\n return isRecord(value) && typeof value['form'] === 'string' && typeof value['field'] === 'string'\n}\n\n/** One menu entry: a page draws a link from it, and a grid per child. */\nfunction isMenuEntry(value: unknown): boolean {\n return (\n isRecord(value) &&\n typeof value['id'] === 'string' &&\n typeof value['title'] === 'string' &&\n isStrings(value['operations']) &&\n Array.isArray(value['children']) &&\n value['children'].every(isMenuChild)\n )\n}\n\n/** The menu (0045): every entry in the server's shape, children included, so an entry of a server with no children is not drawn as a form with none. */\nexport function isFormMenu(body: unknown): body is FormMenu {\n return isRecord(body) && Array.isArray(body['forms']) && body['forms'].every(isMenuEntry)\n}\n\n/** A delete's answer (0050): the token of the row deleted, and `deleted: true`, nothing else that a 2xx could be mistaken for. */\nexport function isRecordDeleted(body: unknown): body is RecordDeleted {\n return isRecord(body) && typeof body['record'] === 'string' && body['deleted'] === true\n}\n\nexport function isFormRecord(body: unknown): body is FormRecord {\n return isRecord(body) && isNullableString(body['record']) && isNullableString(body['version']) && isRecord(body['answers'])\n}\n\n/** A page of records (0044): every row a record, and whether more follow. */\nexport function isRecordList(body: unknown): body is RecordList {\n return isRecord(body) && Array.isArray(body['rows']) && body['rows'].every(isFormRecord) && typeof body['hasMore'] === 'boolean'\n}\n\nexport function isLookupResult(body: unknown): body is LookupResult {\n return isRecord(body) && isRows(body['rows']) && typeof body['hasMore'] === 'boolean' && typeof body['omitted'] === 'number'\n}\n\nexport function isResolved(body: unknown): body is { rows: LookupRow[] } {\n return isRecord(body) && isRows(body['rows'])\n}\n\n/** A non-2xx answer: the server's refusal when it carries a sentence, otherwise not the server's to have said. */\nexport function refusalOf(status: number, body: unknown): Refusal {\n if (!isRecord(body) || typeof body['message'] !== 'string') return { ok: false, status, code: 'unexpected', message: UNEXPECTED }\n const code = typeof body['code'] === 'string' ? body['code'] : `http-${String(status)}`\n const refusal: Refusal = { ok: false, status, code, message: body['message'] }\n if (Array.isArray(body['fieldErrors'])) refusal.fieldErrors = body['fieldErrors'].filter(isFieldError)\n return refusal\n}\n","import type { FormRecord, RecordDeleted } from '@formancy/data-core'\nimport type { Outcome } from './client.js'\nimport { isFormRecord, isNullableString, isRecord, isRecordDeleted, refusalOf } from './shapes.js'\n\n/*\n * What the answer to a write says happened (0031). A read that went wrong\n * changed nothing and may be asked again; a write may have been stored by\n * the time anything went wrong, and sending it again may store it twice. So\n * a write's answer is known -- stored, or refused -- only when the data\n * server says which; everything else is an unknown write, never a refusal,\n * which a host would show as \"not saved\". Nothing here sends anything again.\n */\n\n/** A write that was sent and whose outcome nobody here can know: the host reconciles it, nothing retries it. */\nexport interface UnknownWrite {\n /** False, so a host that only checks `ok` treats it as not done, and fails closed. */\n ok: false\n /** The status that answered, or 0 when nothing did. */\n status: number\n code: 'unknown-outcome'\n /** The server's sentence when the database lost the answer; the client's own when the page lost it. */\n message: string\n /** `delete` since 0050. */\n operation: 'create' | 'update' | 'delete'\n /** Update and delete: the token sent. Create: the token the server says it would have, or null. */\n record: string | null\n /**\n * Update and delete: the version sent -- saving again with exactly it is\n * stored at most once, and deleting again with it deletes nothing else.\n * Create: null.\n */\n version: string | null\n /** `database`: the data server said so (its 502). `transport`: the answer was lost between page and server. */\n origin: 'database' | 'transport'\n}\n\n/** What `create` and `update` resolve to. */\nexport type WriteOutcome = Outcome<FormRecord> | UnknownWrite\n\n/** What `delete` resolves to (0050): the server's deletion answer, a refusal, or a deletion nobody here can say happened. */\nexport type DeleteOutcome = Outcome<RecordDeleted> | UnknownWrite\n\n/** Whether an outcome is an unknown write, rather than a success or a refusal. */\nexport function isUnknownWrite(outcome: { ok: boolean }): outcome is UnknownWrite {\n if (outcome.ok) return false\n const candidate = outcome as Partial<UnknownWrite>\n return candidate.code === 'unknown-outcome' && (candidate.origin === 'database' || candidate.origin === 'transport')\n}\n\n/** What a write addressed, as the call knows it: the transport can lose an answer, never the call's own arguments. */\nexport interface WriteIntent {\n operation: 'create' | 'update' | 'delete'\n record: string | null\n version: string | null\n}\n\n/** What arrived for a write: nothing, or a status and the body as JSON (undefined when unreadable or not JSON). */\nexport type WriteAnswer = { answered: false } | { answered: true; status: number; body: unknown }\n\nconst LOST_ON_THE_WAY = 'The answer to this save was lost between this page and the data server. It may have been saved.'\nconst DELETION_LOST_ON_THE_WAY = 'The answer to this deletion was lost between this page and the data server. It may have been deleted.'\n\nfunction lost(intent: WriteIntent, status: number): UnknownWrite {\n return { ok: false, status, code: 'unknown-outcome', message: intent.operation === 'delete' ? DELETION_LOST_ON_THE_WAY : LOST_ON_THE_WAY, ...intent, origin: 'transport' }\n}\n\n/**\n * The outcome of a write, from what arrived, in this order:\n *\n * 1. Nothing arrived -- `fetch` rejected -- or the body is unreadable or not\n * JSON, at any status: lost on the way. A proxy's page is not the data\n * server's answer, and the request may have reached it.\n * 2. A 2xx with a record is the record stored -- for a delete, a 2xx with\n * the deleted row's token and `deleted: true` is the record deleted\n * (0050); any other 2xx was lost.\n * 3. The data server's 502 `unknown-outcome` with a sentence: the database\n * lost the answer. What was addressed comes from the body, or the call.\n * 4. A 4xx with a sentence is the server's refusal: nothing was written.\n * This is 0028's 422 `refused`, 409 `stale`, 401 and 403 -- and a rate\n * limit's 429, which carries a sentence and no code.\n * 5. A 503 `unavailable` with a sentence: the database was not reached.\n * 6. Anything else -- a 500, another 502 or 504 -- was lost.\n */\nexport function writeOutcome(answer: WriteAnswer, intent: WriteIntent & { operation: 'delete' }): DeleteOutcome\nexport function writeOutcome(answer: WriteAnswer, intent: WriteIntent & { operation: 'create' | 'update' }): WriteOutcome\nexport function writeOutcome(answer: WriteAnswer, intent: WriteIntent): WriteOutcome | DeleteOutcome {\n if (!answer.answered) return lost(intent, 0)\n const { status, body } = answer\n if (!isRecord(body)) return lost(intent, status)\n if (status >= 200 && status <= 299) {\n if (intent.operation === 'delete') return isRecordDeleted(body) ? { ok: true, value: body } : lost(intent, status)\n return isFormRecord(body) ? { ok: true, value: body } : lost(intent, status)\n }\n const message = body['message']\n if (typeof message !== 'string') return lost(intent, status)\n if (status === 502 && body['code'] === 'unknown-outcome') {\n const record = isNullableString(body['record']) ? body['record'] : intent.record\n const version = isNullableString(body['version']) ? body['version'] : intent.version\n return { ok: false, status, code: 'unknown-outcome', message, operation: intent.operation, record, version, origin: 'database' }\n }\n if (status >= 400 && status <= 499) return refusalOf(status, body)\n if (status === 503 && body['code'] === 'unavailable') return refusalOf(status, body)\n return lost(intent, status)\n}\n","import { WRITE_ID_HEADER } from '@formancy/data-core'\nimport type { FieldError, FormMenu, FormRecord, LookupResult, LookupRow, PublishedForm, RecordDeleted, RecordList, RecordListQuery } from '@formancy/data-core'\nimport { reconcile } from './reconcile.js'\nimport type { Reconciliation } from './reconcile.js'\nimport { isFormMenu, isFormRecord, isLookupResult, isPublishedForm, isRecordList, isResolved, refusalOf, UNEXPECTED } from './shapes.js'\nimport { writeOutcome } from './writes.js'\nimport type { DeleteOutcome, UnknownWrite, WriteAnswer, WriteIntent, WriteOutcome } from './writes.js'\n\n/*\n * One function per runtime route (0029), and the decisions every host would\n * otherwise make again: where the token goes, how a name becomes a path, what\n * an answer that is not a success says, and what a write's lost answer is.\n *\n * - **The token** is asked of the host once per request and sent in the\n * Authorization header and nowhere else: never a query string, which is in\n * every proxy's log, never a body, which is in every request dump.\n * - **A name** -- a form id, a source -- is one encoded path segment. `.` and\n * `..` survive `encodeURIComponent` and are resolved away by `fetch`, so they\n * are refused before anything is sent: `form('..')` would be `GET /v1/`.\n * - **A refusal** is the server's code and sentence, verbatim. The client adds\n * words only when the server said none: nothing answered (`unreachable`), or\n * what answered was not the data server's shape (`unexpected`).\n * - **A write** is known only when the data server says what happened to it.\n * Its answer lost -- by the database, said in the server's 502, or between\n * the page and the server -- it is an `UnknownWrite`, not a refusal, and\n * `reconcile` reads what it addressed (0031). Each write carries a new\n * write id, so a copy the browser itself sends again -- Chromium does, on a\n * reused connection that closed before any answer -- is answered by the\n * server with the first one's answer instead of being applied twice.\n *\n * Nothing is cached and nothing is retried. The server asks the policy on\n * every request (0022), and a write whose outcome is unknown is the host's to\n * reconcile, never the client's to send again (0015, 0031).\n */\n\nexport interface DataClientOptions {\n /** Called once per request. The host's session owns it; it is never stored, and never in a URL or a body. */\n token: () => string | Promise<string>\n /**\n * Prefixed to every route. Default `''`: the page's own origin. Another\n * origin is answered only when the server's operator lists the page's\n * origin in `FORMANCY_DATA_CORS_ORIGINS`; otherwise every call is a refused\n * preflight, a network error to the page: a read is `unreachable`, a write\n * unknown though nothing was sent (0060).\n */\n base?: string\n /** Default the global `fetch`, bound. */\n fetch?: typeof fetch\n}\n\n/**\n * Why a call did not succeed. `status` is the HTTP status, or 0 when nothing\n * answered or nothing was sent. `code` and `message` are the server's when it\n * gave them; `fieldErrors` is present when the refusal concerns fields.\n */\nexport type Refusal = { ok: false; status: number; code: string; message: string; fieldErrors?: FieldError[] }\nexport type Outcome<T> = { ok: true; value: T } | Refusal\n\n/** The operation a form is open for, which decides the policy's lookup filter. Never the document's to say. */\nexport type LookupOperation = 'read' | 'create' | 'update'\n\nconst UNREACHABLE = 'The data server could not be reached.'\nconst INVALID_NAME = 'A form or lookup name that is empty, \".\" or \"..\" cannot address a route.'\n\n/**\n * `name` as one path segment, or undefined when it cannot be one. `fetch`\n * resolves `.` and `..` before sending, so they would address a different\n * route; an empty segment would address the route above.\n */\nfunction pathSegment(name: string): string | undefined {\n if (name === '' || name === '.' || name === '..') return undefined\n return encodeURIComponent(name)\n}\n\n/** The body as JSON, or undefined when it is not JSON or could not be read. An abort while reading rethrows. */\nasync function bodyOf(response: Response, signal: AbortSignal | undefined): Promise<unknown> {\n let text: string\n try {\n text = await response.text()\n } catch (error) {\n if (signal?.aborted === true) throw error\n return undefined\n }\n try {\n return JSON.parse(text) as unknown\n } catch {\n return undefined\n }\n}\n\ninterface Request {\n method: 'GET' | 'POST'\n /** The names that become one segment each; the route is built only when every one can. */\n names: readonly string[]\n /** The route, from the names as segments, in order. */\n route: (segments: readonly string[]) => string\n body?: Record<string, unknown>\n signal?: AbortSignal | undefined\n /** Sent beside the token and the content type: a write's id. */\n headers?: Record<string, string>\n}\n\n/**\n * The base without the slashes it ends in, by walking back from the end.\n * `/\\/+$/` did the same and CodeQL flagged it (js/polynomial-redos): on a\n * base of many slashes not at the end, the anchored pattern backtracks.\n */\nfunction withoutTrailingSlashes(base: string): string {\n let end = base.length\n while (end > 0 && base[end - 1] === '/') end -= 1\n return base.slice(0, end)\n}\n\n/**\n * A new write id: 128 random bits as hex, from `crypto.getRandomValues`,\n * which a page served over plain HTTP has too, unlike `randomUUID`.\n */\nfunction newWriteId(): string {\n return Array.from(crypto.getRandomValues(new Uint8Array(16)), (byte) => byte.toString(16).padStart(2, '0')).join('')\n}\n\n/** What one request came to: a name that is no segment (nothing sent), nothing answered, or a status and its JSON body. */\ntype Exchange = { sent: false } | WriteAnswer\n\n/**\n * A client of one data server's runtime plane. Every function resolves to an\n * outcome and never rejects for an answer -- only for an abort the caller\n * asked for, which reaches it as the AbortError it raised.\n */\nexport function createDataClient(options: DataClientOptions) {\n const base = withoutTrailingSlashes(options.base ?? '')\n const send = options.fetch ?? globalThis.fetch.bind(globalThis)\n\n /** The transport: the request sent once, and what came back. Classifying it is the caller's. */\n async function exchange(request: Request): Promise<Exchange> {\n const segments = request.names.map(pathSegment)\n if (segments.some((segment) => segment === undefined)) return { sent: false }\n const headers: Record<string, string> = { ...request.headers, authorization: `Bearer ${await options.token()}`, accept: 'application/json' }\n if (request.body !== undefined) headers['content-type'] = 'application/json'\n let response: Response\n try {\n response = await send(`${base}${request.route(segments as string[])}`, {\n method: request.method,\n headers,\n // The runtime plane never redirects; following one would carry the token to another route, or another host.\n redirect: 'manual',\n ...(request.body === undefined ? {} : { body: JSON.stringify(request.body) }),\n ...(request.signal === undefined ? {} : { signal: request.signal }),\n })\n } catch (error) {\n if (request.signal?.aborted === true) throw error\n return { answered: false }\n }\n return { answered: true, status: response.status, body: await bodyOf(response, request.signal) }\n }\n\n const invalidName: Refusal = { ok: false, status: 0, code: 'invalid-name', message: INVALID_NAME }\n\n /** A read: changes nothing, so an answer that went wrong is a refusal, and asking again is safe (0029). */\n async function call<T>(request: Request & { read: (body: unknown) => T | undefined }): Promise<Outcome<T>> {\n const answer = await exchange(request)\n if ('sent' in answer) return invalidName\n if (!answer.answered) return { ok: false, status: 0, code: 'unreachable', message: UNREACHABLE }\n if (answer.status < 200 || answer.status > 299) return refusalOf(answer.status, answer.body)\n const value = request.read(answer.body)\n return value === undefined ? { ok: false, status: answer.status, code: 'unexpected', message: UNEXPECTED } : { ok: true, value }\n }\n\n /** A write: known only when the data server says what happened; otherwise unknown, and never sent again (0031). */\n async function sendWrite(formId: string, intent: WriteIntent, body: Record<string, unknown>): Promise<Exchange> {\n return exchange({\n method: 'POST',\n names: [formId],\n route: ([form]) => `/v1/forms/${String(form)}/records/${intent.operation}`,\n body,\n // New for every call: one id on two saves would answer the second with the first's answer.\n headers: { [WRITE_ID_HEADER]: newWriteId() },\n })\n }\n\n async function write(formId: string, intent: WriteIntent & { operation: 'create' | 'update' }, body: Record<string, unknown>): Promise<WriteOutcome> {\n const answer = await sendWrite(formId, intent, body)\n // Nothing was sent: a refusal like any other.\n return 'sent' in answer ? invalidName : writeOutcome(answer, intent)\n }\n\n const read = (formId: string, record: string): Promise<Outcome<FormRecord>> =>\n call({ method: 'POST', names: [formId], route: ([form]) => `/v1/forms/${String(form)}/records/read`, body: { record }, read: (body) => (isFormRecord(body) ? body : undefined) })\n const lookups = <T>(formId: string, source: string, action: 'query' | 'resolve', body: Record<string, unknown>, read: (body: unknown) => T | undefined, signal?: AbortSignal) =>\n call({ method: 'POST', names: [formId, source], route: ([form, name]) => `/v1/forms/${String(form)}/lookups/${String(name)}/${action}`, body, read, signal })\n\n return {\n /**\n * `GET /v1/forms` (0045): the published forms this person may read, each\n * with its title, the operations the policy grants them and the children\n * a page may list under one of its records -- and nothing about any other\n * form. Opening one with `form` says what the database allows now.\n */\n forms: (signal?: AbortSignal): Promise<Outcome<FormMenu>> =>\n call({ method: 'GET', names: [], route: () => '/v1/forms', read: (body) => (isFormMenu(body) ? body : undefined), signal }),\n\n /** `GET /v1/forms/:id`: the published document, the operations this person may use, and the fields they may read. */\n form: (formId: string): Promise<Outcome<PublishedForm>> =>\n call({ method: 'GET', names: [formId], route: ([form]) => `/v1/forms/${String(form)}`, read: (body) => (isPublishedForm(body) ? body : undefined) }),\n\n /** `POST …/records/read`: one record by its token, with the version an update sends back. */\n read,\n\n /**\n * `POST …/records/list` (0044): one page of the records this person may\n * read, each what `read` returns for its token, and whether more follow.\n * A property left out is the server's default -- no search, the key's\n * order, no parent, the first page, the largest page -- and exactly the\n * route's keys are sent, whatever else the object carries. A POST, so the\n * search is never in a URL.\n */\n list: (formId: string, query: Partial<RecordListQuery> = {}, signal?: AbortSignal): Promise<Outcome<RecordList>> =>\n call({\n method: 'POST',\n names: [formId],\n route: ([form]) => `/v1/forms/${String(form)}/records/list`,\n body: { search: query.search, sort: query.sort, parent: query.parent, offset: query.offset, limit: query.limit },\n read: (body) => (isRecordList(body) ? body : undefined),\n signal,\n }),\n\n /**\n * `POST …/records/create`: a new record from the answers; the reply is what\n * was stored, in the stored spelling -- or an `UnknownWrite` when nobody\n * can say whether it was stored.\n */\n create: (formId: string, answers: Readonly<Record<string, unknown>>): Promise<WriteOutcome> =>\n write(formId, { operation: 'create', record: null, version: null }, { answers }),\n\n /**\n * `POST …/records/update`: saved only if the record is still at `version`,\n * otherwise 409 `stale`; an `UnknownWrite` when nobody can say whether it\n * was saved. Exactly the route's keys are sent, whatever else the object\n * passed carries.\n */\n update: (formId: string, change: { record: string; version: string; answers: Readonly<Record<string, unknown>> }): Promise<WriteOutcome> =>\n write(formId, { operation: 'update', record: change.record, version: change.version }, { record: change.record, version: change.version, answers: change.answers }),\n\n /**\n * `POST …/records/delete` (0050): the record, only if it is still at\n * `version`, otherwise 409 `stale`; the answer is the deleted row's\n * token and `deleted: true`, and nothing else is done. An `UnknownWrite`\n * when nobody can say whether it was deleted -- deleting again with the\n * same version then deletes nothing else. Exactly the route's keys are\n * sent, with a write id of its own.\n */\n delete: async (formId: string, removal: { record: string; version: string }): Promise<DeleteOutcome> => {\n const intent = { operation: 'delete', record: removal.record, version: removal.version } as const\n const answer = await sendWrite(formId, intent, { record: removal.record, version: removal.version })\n return 'sent' in answer ? invalidName : writeOutcome(answer, intent)\n },\n\n /** Reads what an unknown write addressed and says what that shows; never sends the write again (0031). */\n reconcile: (formId: string, unknown: UnknownWrite): Promise<Reconciliation> => reconcile(read, formId, unknown),\n\n /** `POST …/lookups/:source/query`: one page of the options this person may pick for this operation. */\n query: (\n formId: string,\n source: string,\n q: { operation: LookupOperation; search: string; offset?: number; limit?: number },\n signal?: AbortSignal,\n ): Promise<Outcome<LookupResult>> =>\n lookups(formId, source, 'query', { operation: q.operation, search: q.search, offset: q.offset, limit: q.limit }, (body) => (isLookupResult(body) ? body : undefined), signal),\n\n /** `POST …/lookups/:source/resolve`: the labels of the tokens this person may see; any other is absent, not an error. */\n resolve: (formId: string, source: string, r: { operation: LookupOperation; tokens: readonly string[] }, signal?: AbortSignal): Promise<Outcome<LookupRow[]>> =>\n lookups(formId, source, 'resolve', { operation: r.operation, tokens: r.tokens }, (body) => (isResolved(body) ? body.rows : undefined), signal),\n }\n}\n\n/** A client of one data server's runtime plane: what `createDataClient` returns. */\nexport type DataClient = ReturnType<typeof createDataClient>\n","import type { FieldDef, FormSchema, RemoteOption } from '@formancy/spec'\nimport type { DataClient, LookupOperation } from './client.js'\n\nexport type { LookupOperation } from './client.js'\n\n/*\n * The option sources a host hands either renderer (formancy.ai 0077). The\n * document names a list; the deployment says where it comes from, and here\n * that is always the runtime plane, under the same form, for the operation\n * the host has the form open for. A document decides which names exist and\n * nothing else: not a URL, a header, a token, a tenant or an operation (0012).\n *\n * `@formancy/react` and `@formancy/angular` each declare their own\n * `OptionsSource`, on purpose. A `LookupSource` declares only what it reads of\n * a request, and is assignable to both; a host's type check of\n * `OptionsSourcesProvider` and `provideFormancyOptionsSources` is what fails\n * when either contract moves.\n */\n\n/** What a source reads of a renderer's request: the kind, the query or the stored values, the limit, and the signal. */\nexport type LookupRequest = { kind: 'search' | 'labels'; query: string; values: readonly string[]; limit: number; signal: AbortSignal }\n\n/** One named list, answering through the runtime plane. */\nexport type LookupSource = { resolve(request: LookupRequest): Promise<readonly RemoteOption[]> }\n\nfunction collect(fields: readonly FieldDef[], into: Set<string>): void {\n for (const field of fields) {\n if (field.optionsSource !== undefined) into.add(field.optionsSource)\n if (field.fields !== undefined) collect(field.fields, into)\n }\n}\n\n/** Every optionsSource the document names -- in groups and repeaters too -- once each, in document order. */\nexport function sourceNames(form: FormSchema): string[] {\n const names = new Set<string>()\n collect(form.model.fields, names)\n return [...names]\n}\n\n/**\n * One source per name the document carries, each answering through this\n * form's lookup routes.\n *\n * `operation` may be a getter: it is read on every request, so a form that\n * moves from create to update after its first save asks under update's filter\n * without a new map -- which, in Angular, would mean a new application.\n *\n * A search sends the renderer's query and limit; a label request resolves the\n * stored tokens, and asks nothing when there are none. A token the person may\n * not see is left out, not an error. A refusal rejects with the server's\n * sentence, which the renderer shows as \"could not be loaded\"; an abort\n * rejects with the AbortError. `hasMore` and `omitted` do not reach the\n * renderer, whose contract, at 0.5.0 as at 0.3.0, has nowhere to put them\n * (0029).\n */\nexport function lookupSources(\n client: DataClient,\n formId: string,\n form: FormSchema,\n operation: LookupOperation | (() => LookupOperation),\n): Readonly<Record<string, LookupSource>> {\n const current = typeof operation === 'function' ? operation : () => operation\n const source = (name: string): LookupSource => ({\n async resolve(request) {\n if (request.kind === 'labels' && request.values.length === 0) return []\n const outcome =\n request.kind === 'search'\n ? await client.query(formId, name, { operation: current(), search: request.query, limit: request.limit }, request.signal)\n : await client.resolve(formId, name, { operation: current(), tokens: request.values }, request.signal)\n if (!outcome.ok) throw new Error(outcome.message)\n const rows = Array.isArray(outcome.value) ? outcome.value : outcome.value.rows\n return rows.map((row) => ({ value: row.token, label: row.label }))\n },\n })\n return Object.fromEntries(sourceNames(form).map((name) => [name, source(name)]))\n}\n","import type { Refusal } from './client.js'\n\n/**\n * A refusal's field problems as `engine.applyServerErrors` takes them: the\n * server's sentences, grouped by field, in the order the server gave them,\n * each once. `{}` when the refusal concerns no field.\n *\n * Sentences and not codes, because the renderers print an entry verbatim, at\n * 0.3.0, 0.4.0 and 0.5.0, beside the field and in the error summary: a code would\n * put `not-an-option` in front of a person. The server promises its sentence is\n * showable and never echoes a value (0011); the code stays on the Refusal for a\n * program to read.\n */\nexport function fieldProblems(refusal: Refusal): Record<string, string[]> {\n const problems: Record<string, string[]> = {}\n for (const { field, message } of refusal.fieldErrors ?? []) {\n const sentences = (problems[field] ??= [])\n if (!sentences.includes(message)) sentences.push(message)\n }\n return problems\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,57 @@
1
1
  {
2
2
  "name": "@formancy/data-client",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "The browser's side of Formancy Data's runtime plane: a fetch-based client for a published form's records and lookups, and option sources both formancy renderers take. Framework-neutral, no Node.",
5
+ "license": "SEE LICENSE IN LICENSE.md",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.mts",
11
+ "default": "./dist/index.mjs"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "LICENSE.md",
17
+ "NOTICE"
18
+ ],
19
+ "dependencies": {
20
+ "@formancy/data-core": "0.1.0",
21
+ "@formancy/spec": "0.5.0"
22
+ },
23
+ "devDependencies": {
24
+ "@arethetypeswrong/cli": "^0.18.5",
25
+ "@formancy/core": "0.5.0",
26
+ "@formancy/data-fixtures": "0.1.0",
27
+ "@formancy/data-server": "0.1.0",
28
+ "@types/mssql": "^12.3.0",
29
+ "@types/node": "^22.10.0",
30
+ "@vitest/coverage-v8": "^5.0.1",
31
+ "jose": "^6.2.12",
32
+ "mssql": "^12.0.0",
33
+ "postgres": "^3.4.9",
34
+ "publint": "^0.3.24",
35
+ "tsdown": "^0.23.0",
36
+ "typescript": "~6.0.3",
37
+ "unrun": "^0.3.1",
38
+ "vitest": "^5.0.1"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/sharkysan/formancy.ai-data.git",
46
+ "directory": "packages/data-client"
47
+ },
48
+ "homepage": "https://github.com/sharkysan/formancy.ai-data",
49
+ "bugs": "https://github.com/sharkysan/formancy.ai-data/issues",
50
+ "scripts": {
51
+ "build": "tsdown",
52
+ "test:coverage": "vitest run --coverage",
53
+ "test": "vitest run",
54
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
55
+ "check:pkg": "publint && attw --pack . --profile esm-only"
56
+ }
6
57
  }