@foir/sdk 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +115 -0
- package/README.md +8 -1
- package/dist/app/client.d.ts +47 -8
- package/dist/app/contract/proto/apps/v1/apps_pb.d.ts +71 -4
- package/dist/app/index.d.ts +1 -1
- package/dist/app/vocabulary.generated.d.ts +1 -1
- package/dist/app-contract.js +24 -22
- package/dist/app.js +33 -14
- package/dist/data/compatibility.d.ts +95 -0
- package/dist/data/document.d.ts +11 -1
- package/dist/data/transport.d.ts +49 -25
- package/dist/data.js +337 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -108,6 +108,121 @@ nobody asking for `@foir/sdk` gets it early. The push that carries the commit to
|
|
|
108
108
|
|
|
109
109
|
Changes on `dev` that have not been published to npm.
|
|
110
110
|
|
|
111
|
+
## [0.19.0] — 2026-10-02
|
|
112
|
+
|
|
113
|
+
Published under npm's `next` tag from `dev`. The data client's change needs no
|
|
114
|
+
platform release. `setConsent` by email calls a field the platform serves from
|
|
115
|
+
the next promotion; `latest` moves to this version then.
|
|
116
|
+
|
|
117
|
+
### Changed
|
|
118
|
+
|
|
119
|
+
- **Breaking: schema drift by itself no longer refuses a typed call**
|
|
120
|
+
(`@foir/sdk/data`). What `strictSchemaDrift` refuses has changed; its name
|
|
121
|
+
and its default have not.
|
|
122
|
+
|
|
123
|
+
Before, a client built with a generated module refused every typed call,
|
|
124
|
+
with `FoirSchemaDriftError`, from the moment it saw the server serving another
|
|
125
|
+
surface version. That version is a hash over everything `foir types` reads,
|
|
126
|
+
so it moves whenever the surface grows. A field published on a model, or an
|
|
127
|
+
op the platform added to every model, made every deployed client refuse its
|
|
128
|
+
second typed call, though nothing it prints had changed.
|
|
129
|
+
|
|
130
|
+
Now the drift is reported once, exactly as before (`onSchemaDrift`, or the
|
|
131
|
+
`console.warn`), and it refuses nothing by itself. When the manifest arrives
|
|
132
|
+
the client compares its generated trees with the served ones, and refuses a
|
|
133
|
+
typed call only when something that call depends on is no longer served the
|
|
134
|
+
way the module describes it:
|
|
135
|
+
|
|
136
|
+
- its op is gone, or the op's root field or result kind changed;
|
|
137
|
+
- an argument the module declares is gone, or has a type the generated one no
|
|
138
|
+
longer fits, or the op now requires an argument the module does not know;
|
|
139
|
+
- a field its selection names is gone or changed shape, in the model the op
|
|
140
|
+
returns or in a union that model embeds;
|
|
141
|
+
- a relation the call includes changed target, became a list or a
|
|
142
|
+
connection, or its target model lost a field.
|
|
143
|
+
|
|
144
|
+
Anything added refuses nothing: a model, a namespace, an op, an optional
|
|
145
|
+
argument, a field, a relation, a union member, a subscription. An argument
|
|
146
|
+
that went from required to optional refuses nothing either.
|
|
147
|
+
|
|
148
|
+
`FoirSchemaDriftError`'s message now names what the refused call depends on
|
|
149
|
+
(`models.order.selection.status (no longer served)`), and still names
|
|
150
|
+
`foir types` as the remedy. The warning for a drift that broke nothing says
|
|
151
|
+
the surface has grown and that every typed call keeps working.
|
|
152
|
+
|
|
153
|
+
Unchanged: `strictSchemaDrift: false` turns refusal off entirely, the first
|
|
154
|
+
call on a client is never blocked on the manifest, `raw()` is never refused,
|
|
155
|
+
and `checkSchemaDrift: false` skips the fetch. `createFoirDriftHandler` in
|
|
156
|
+
`@foir/sdk/live` still resyncs on every drift, whether or not anything is
|
|
157
|
+
incompatible.
|
|
158
|
+
|
|
159
|
+
A client wired by hand with `surfaceVersion` and no `selections` prints from
|
|
160
|
+
the served manifest, so it has nothing older to compare and is no longer
|
|
161
|
+
refused on drift.
|
|
162
|
+
|
|
163
|
+
Migration: none for an app that only catches `FoirSchemaDriftError`. An app
|
|
164
|
+
that used the refusal to learn that its module was stale in ANY way should
|
|
165
|
+
read `onSchemaDrift`, which still fires for every drift, or gate on
|
|
166
|
+
`foir types --check` in CI.
|
|
167
|
+
- **Breaking: `FoirSchemaDrift` has a new required field, `incompatible`**
|
|
168
|
+
(`@foir/sdk/data`). Code that reads a drift is unaffected. Code that builds
|
|
169
|
+
one by hand, for example to test a handler given to `onSchemaDrift` or made by
|
|
170
|
+
`createFoirDriftHandler`, must add `incompatible: []`.
|
|
171
|
+
- **Breaking (types): `AppConsentChange` names its customer one way**
|
|
172
|
+
(`@foir/sdk/app`). It was an interface with a required `customerId`. It is
|
|
173
|
+
now a type that holds a `customerId` or an `email`, never both. A change
|
|
174
|
+
written as `{ customerId, ... }` is unaffected. An interface that extended
|
|
175
|
+
`AppConsentChange` no longer compiles: intersect with it instead.
|
|
176
|
+
|
|
177
|
+
### Added
|
|
178
|
+
|
|
179
|
+
- `notifications.setConsent` takes `email` in place of `customerId`
|
|
180
|
+
(`@foir/sdk/app`), for an opt-in from somebody who may have no customer yet,
|
|
181
|
+
such as a sign-up form. The platform finds the customer holding the address
|
|
182
|
+
at the install's level, or makes one, together with the change, so a change
|
|
183
|
+
it refuses (a category the project does not have, a confirmation email the
|
|
184
|
+
project cannot send) leaves no customer behind. Finding or making the
|
|
185
|
+
customer first with `customers.ensure` did. The operation must declare
|
|
186
|
+
`customers:write` as well as `notifications:consent`. `AppConsentSubject` is
|
|
187
|
+
exported.
|
|
188
|
+
|
|
189
|
+
- `FoirSchemaDrift.incompatible`: what the generated module describes that the
|
|
190
|
+
server no longer serves that way, as sorted paths into the manifest
|
|
191
|
+
(`models.order.selection.status`, `models.order.ops.update`,
|
|
192
|
+
`namespaces.search.ops.searchRecords.args.query`,
|
|
193
|
+
`models.order.relations.customer`, `selectionUnions.Block.HeroBlock.title`).
|
|
194
|
+
Empty when the surface has only grown. `changed` keeps its meaning: what
|
|
195
|
+
moved, not what broke.
|
|
196
|
+
|
|
197
|
+
## [0.18.0] — 2026-10-02
|
|
198
|
+
|
|
199
|
+
Published under npm's `next` tag from `dev`, because it calls fields the
|
|
200
|
+
platform serves from the next promotion; `latest` moves to it then.
|
|
201
|
+
|
|
202
|
+
### Changed
|
|
203
|
+
|
|
204
|
+
- **Breaking: an app finds a customer by identity through its install, not
|
|
205
|
+
through a model** (`@foir/sdk/app`). The customer sign-in an app's customers
|
|
206
|
+
come through is paired with the install as a whole, so the two reads that
|
|
207
|
+
walk it no longer name a model and have moved beside the other customer
|
|
208
|
+
reads:
|
|
209
|
+
- `records.customer.identities(model, customerId)` is now
|
|
210
|
+
`customers.identities(customerId)`.
|
|
211
|
+
- `customers.byIdentity(identity)` is new: the id of the customer who holds
|
|
212
|
+
an identity at the app's own system, or `null`. It replaces reading a
|
|
213
|
+
customer's record for its owner.
|
|
214
|
+
|
|
215
|
+
Both need the new capability `customers:read:identity` (or `customers:read`),
|
|
216
|
+
declared by the operation, and are refused at an install that pairs no
|
|
217
|
+
sign-in. Reading or writing a model is no longer what admits them.
|
|
218
|
+
- `install.config()` asks the platform for `customerSignIn`, a field a platform
|
|
219
|
+
older than this release does not have.
|
|
220
|
+
|
|
221
|
+
### Added
|
|
222
|
+
|
|
223
|
+
- `install.config()` answers `customerSignIn`: the key of the customer sign-in
|
|
224
|
+
the install pairs, or `null` when it pairs none.
|
|
225
|
+
|
|
111
226
|
## [0.17.0] — 2026-10-02
|
|
112
227
|
|
|
113
228
|
Published under npm's `next` tag from `dev`, because it reads a field the
|
package/README.md
CHANGED
|
@@ -88,7 +88,14 @@ was generated from: a hash over exactly what `foir types` reads, which moves onl
|
|
|
88
88
|
when regenerating would write a different file. Pushing an operation, a role or
|
|
89
89
|
a key moves the server's invalidation counter and not the surface, so it is not
|
|
90
90
|
drift. Publishing a field on a model the types include is, and the client
|
|
91
|
-
|
|
91
|
+
reports it once, naming the model.
|
|
92
|
+
|
|
93
|
+
**Drift does not stop a call.** A surface that has grown still answers every
|
|
94
|
+
document the module prints. A typed call is refused, with `FoirSchemaDriftError`,
|
|
95
|
+
only when something it depends on is no longer served as generated: a field its
|
|
96
|
+
selection names, its op, one of its arguments, or the target of a relation it
|
|
97
|
+
includes. The message names that part, and `drift.incompatible` lists every
|
|
98
|
+
such part as a path (`models.order.selection.status`). The remedy is
|
|
92
99
|
`foir types`; `strictSchemaDrift: false` never is.
|
|
93
100
|
|
|
94
101
|
**Scalars are always complete**, so `post.title` is typed present and *is*
|
package/dist/app/client.d.ts
CHANGED
|
@@ -103,6 +103,14 @@ export interface AppInstallConfig {
|
|
|
103
103
|
* (`appCustomerEmailUnavailableOf`), so say so before one is.
|
|
104
104
|
*/
|
|
105
105
|
customerEmailUnavailable: AppCustomerEmailUnavailableReason | null;
|
|
106
|
+
/**
|
|
107
|
+
* The customer sign-in this install pairs with the app, by key, for an app
|
|
108
|
+
* whose own system has customers; null when it pairs none. Every identity
|
|
109
|
+
* the app names is resolved through it, so while it is null nothing finds a
|
|
110
|
+
* customer by identity: `customers.byIdentity` and `customers.identities`
|
|
111
|
+
* are refused, and an operation that needs them is not registered.
|
|
112
|
+
*/
|
|
113
|
+
customerSignIn: string | null;
|
|
106
114
|
}
|
|
107
115
|
/**
|
|
108
116
|
* The status an install reports. Leaving out `subscriptionHealth`,
|
|
@@ -178,8 +186,24 @@ export interface AppNotification {
|
|
|
178
186
|
/** Makes the send safe to repeat: the same key from the same operation tells the customer once. */
|
|
179
187
|
idempotencyKey?: string;
|
|
180
188
|
}
|
|
181
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Who a consent change is for: a customer whose id the app holds, or an
|
|
191
|
+
* address.
|
|
192
|
+
*
|
|
193
|
+
* By `email`, the customer holding the address at the operation's level is
|
|
194
|
+
* found, or made, together with the change, so a change the platform refuses
|
|
195
|
+
* leaves no customer behind. It is for an opt-in from somebody who may have no
|
|
196
|
+
* customer yet (a sign-up form), and the operation must declare
|
|
197
|
+
* `customers:write` as well as `notifications:consent`.
|
|
198
|
+
*/
|
|
199
|
+
export type AppConsentSubject = {
|
|
182
200
|
customerId: string;
|
|
201
|
+
email?: undefined;
|
|
202
|
+
} | {
|
|
203
|
+
email: string;
|
|
204
|
+
customerId?: undefined;
|
|
205
|
+
};
|
|
206
|
+
export type AppConsentChange = AppConsentSubject & {
|
|
183
207
|
/** A notification category the project makes opt-in. */
|
|
184
208
|
category: string;
|
|
185
209
|
/** The channel the consent is for, as the project's preferences name it (for example `email`). */
|
|
@@ -207,7 +231,7 @@ export interface AppConsentChange {
|
|
|
207
231
|
* email is written in it when the customer has stated none of their own.
|
|
208
232
|
*/
|
|
209
233
|
locale?: string;
|
|
210
|
-
}
|
|
234
|
+
};
|
|
211
235
|
/** What the calling token is, and what it is served. Read from the platform, never off the token. */
|
|
212
236
|
export interface AppWhoami {
|
|
213
237
|
/** The app the token acts for, when it acts for one. */
|
|
@@ -378,12 +402,6 @@ export interface AppClient {
|
|
|
378
402
|
* else is attached to them. Needs `customers:write` as well.
|
|
379
403
|
*/
|
|
380
404
|
erase(model: string, identity: string): Promise<AppCustomerEraseResult>;
|
|
381
|
-
/**
|
|
382
|
-
* The other direction: the identities a customer the app names by id
|
|
383
|
-
* signs in with there, most recently seen first. Empty when they sign in
|
|
384
|
-
* with none. For an app acting on an event that names the Foir customer.
|
|
385
|
-
*/
|
|
386
|
-
identities(model: string, customerId: string): Promise<string[]>;
|
|
387
405
|
};
|
|
388
406
|
};
|
|
389
407
|
customers: {
|
|
@@ -415,6 +433,23 @@ export interface AppClient {
|
|
|
415
433
|
* `customers:read`.
|
|
416
434
|
*/
|
|
417
435
|
byEmail(email: string): Promise<AppCustomer | null>;
|
|
436
|
+
/**
|
|
437
|
+
* The id of the customer who holds this identity at the app's own system,
|
|
438
|
+
* or null when nobody does: for an app hearing from its system about one
|
|
439
|
+
* of its customers. The identity is the subject of the customer's sign-in
|
|
440
|
+
* with that system, and it is resolved through the customer sign-in the
|
|
441
|
+
* install pairs (`install.config().customerSignIn`) and no other. Never
|
|
442
|
+
* matched by email. Needs `customers:read:identity`, or `customers:read`.
|
|
443
|
+
*/
|
|
444
|
+
byIdentity(identity: string): Promise<string | null>;
|
|
445
|
+
/**
|
|
446
|
+
* The other direction: the identities a customer the app names by id
|
|
447
|
+
* holds at the app's own system, most recently seen first, through the
|
|
448
|
+
* same pairing. Empty when they have never signed in that way. For an app
|
|
449
|
+
* acting on an event that names the Foir customer. Needs
|
|
450
|
+
* `customers:read:identity`, or `customers:read`.
|
|
451
|
+
*/
|
|
452
|
+
identities(customerId: string): Promise<string[]>;
|
|
418
453
|
/**
|
|
419
454
|
* The customers at the install's level, a page at a time, newest first.
|
|
420
455
|
* Needs `customers:read`: an operation declaring only
|
|
@@ -439,6 +474,10 @@ export interface AppClient {
|
|
|
439
474
|
* been sent a link, or was sent one recently, and the preference turns on
|
|
440
475
|
* when they follow it. The answer is the same whether or not the address
|
|
441
476
|
* already had a customer.
|
|
477
|
+
*
|
|
478
|
+
* Name the customer by `customerId`, or by `email` for somebody who may
|
|
479
|
+
* have no customer yet: the platform finds or makes them with the change,
|
|
480
|
+
* and that also needs `customers:write`.
|
|
442
481
|
*/
|
|
443
482
|
setConsent(change: AppConsentChange): Promise<{
|
|
444
483
|
stale: boolean;
|
|
@@ -117,6 +117,14 @@ export declare type AppConfig = Message<"apps.v1.AppConfig"> & {
|
|
|
117
117
|
* @generated from field: repeated apps.v1.Feature features = 13;
|
|
118
118
|
*/
|
|
119
119
|
features: Feature[];
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* OPTIONAL. The app's own system has customers who sign in to it. See
|
|
123
|
+
* CustomerSignIn.
|
|
124
|
+
*
|
|
125
|
+
* @generated from field: apps.v1.CustomerSignIn customer_sign_in = 14;
|
|
126
|
+
*/
|
|
127
|
+
customerSignIn?: CustomerSignIn | undefined;
|
|
120
128
|
};
|
|
121
129
|
|
|
122
130
|
/**
|
|
@@ -125,6 +133,42 @@ export declare type AppConfig = Message<"apps.v1.AppConfig"> & {
|
|
|
125
133
|
*/
|
|
126
134
|
export declare const AppConfigSchema: GenMessage<AppConfig>;
|
|
127
135
|
|
|
136
|
+
/**
|
|
137
|
+
* The app's own system has customers, and the app names each by the subject of
|
|
138
|
+
* their sign-in with that system: a store's customer number, which is also who
|
|
139
|
+
* they are when they sign in to the project through the store's sign-in.
|
|
140
|
+
*
|
|
141
|
+
* Declaring it lets an install pair ONE of the project's customer sign-ins with
|
|
142
|
+
* the app (ConfirmInstallAppRequest.customer_sign_in): the operator says which
|
|
143
|
+
* sign-in the app's customers come through, and the platform resolves every
|
|
144
|
+
* identity the app names through that sign-in and no other. The pairing is the
|
|
145
|
+
* operator's, never inferred: the install knows the external account it
|
|
146
|
+
* connects, not which of the project's sign-ins its customers use, and a wrong
|
|
147
|
+
* pairing would attach one person's data to another.
|
|
148
|
+
*
|
|
149
|
+
* The pairing is optional. What needs it says so, and cannot be on without it:
|
|
150
|
+
* a source that names its customers by identity (SourceType.identity), a
|
|
151
|
+
* feature that finds customers through it (Feature.customer_sign_in), and an
|
|
152
|
+
* operation that asks who holds an identity (customers:read:identity).
|
|
153
|
+
*
|
|
154
|
+
* @generated from message apps.v1.CustomerSignIn
|
|
155
|
+
*/
|
|
156
|
+
export declare type CustomerSignIn = Message<"apps.v1.CustomerSignIn"> & {
|
|
157
|
+
/**
|
|
158
|
+
* What the setup tells the operator they are choosing, in a sentence: which
|
|
159
|
+
* customers these are and what identifies them.
|
|
160
|
+
*
|
|
161
|
+
* @generated from field: string description = 1;
|
|
162
|
+
*/
|
|
163
|
+
description: string;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Describes the message apps.v1.CustomerSignIn.
|
|
168
|
+
* Use `create(CustomerSignInSchema)` to create a new message.
|
|
169
|
+
*/
|
|
170
|
+
export declare const CustomerSignInSchema: GenMessage<CustomerSignIn>;
|
|
171
|
+
|
|
128
172
|
/**
|
|
129
173
|
* A feature of the app: something the operator turns on or off as a whole at
|
|
130
174
|
* setup, such as a store's preorders or its back-in-stock alerts.
|
|
@@ -180,6 +224,16 @@ export declare type Feature = Message<"apps.v1.Feature"> & {
|
|
|
180
224
|
* @generated from field: repeated string sources = 5;
|
|
181
225
|
*/
|
|
182
226
|
sources: string[];
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The feature finds customers through the install's customer sign-in
|
|
230
|
+
* (AppConfig.customer_sign_in), which the manifest must declare. It cannot
|
|
231
|
+
* be on at an install that pairs none: the setup asks for the sign-in at the
|
|
232
|
+
* switch, and the platform refuses the switch without one.
|
|
233
|
+
*
|
|
234
|
+
* @generated from field: bool customer_sign_in = 6;
|
|
235
|
+
*/
|
|
236
|
+
customerSignIn: boolean;
|
|
183
237
|
};
|
|
184
238
|
|
|
185
239
|
/**
|
|
@@ -586,10 +640,11 @@ export declare type SourceType = Message<"apps.v1.SourceType"> & {
|
|
|
586
640
|
*
|
|
587
641
|
* It is what lets a sync write customer-owned rows. The platform resolves the
|
|
588
642
|
* value to a Foir customer through the customer sign-in the install pairs
|
|
589
|
-
* with the
|
|
590
|
-
* the customer who signs in with that
|
|
591
|
-
* customer holds one row of the source,
|
|
592
|
-
* natural key; the customer is the key.
|
|
643
|
+
* with the app (AppConfig.customer_sign_in, which the manifest must then
|
|
644
|
+
* declare), at the install's level: the customer who signs in with that
|
|
645
|
+
* identity, or one made for it. Each customer holds one row of the source,
|
|
646
|
+
* their profile, so the source has no natural key; the customer is the key.
|
|
647
|
+
* Never matched by email.
|
|
593
648
|
*
|
|
594
649
|
* @generated from field: string identity = 10;
|
|
595
650
|
*/
|
|
@@ -1429,6 +1484,18 @@ export enum ManifestCapability {
|
|
|
1429
1484
|
*/
|
|
1430
1485
|
CUSTOMERS_READ_NAMED = 15,
|
|
1431
1486
|
|
|
1487
|
+
/**
|
|
1488
|
+
* The link between a customer and who they are at the app's own system, and
|
|
1489
|
+
* nothing else about them: the customer who holds an identity, and the
|
|
1490
|
+
* identities a customer the operation names by id holds, both through the
|
|
1491
|
+
* customer sign-in the install pairs with the app and no other. An operation
|
|
1492
|
+
* that declares customers:read holds it too; customers:read:named does not
|
|
1493
|
+
* reach it, nor it what that reaches.
|
|
1494
|
+
*
|
|
1495
|
+
* @generated from enum value: MANIFEST_CAPABILITY_CUSTOMERS_READ_IDENTITY = 16;
|
|
1496
|
+
*/
|
|
1497
|
+
CUSTOMERS_READ_IDENTITY = 16,
|
|
1498
|
+
|
|
1432
1499
|
/**
|
|
1433
1500
|
* Reporting an ASYNC run's outcome.
|
|
1434
1501
|
*
|
package/dist/app/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* The manifest's own messages are on `@foir/sdk/app/contract`.
|
|
22
22
|
*/
|
|
23
23
|
export { createAppClient } from './client';
|
|
24
|
-
export type { AppBinding, AppClient, AppClientConfig, AppConsentChange, AppCustomer, AppCustomerEraseResult, AppCustomerList, AppCustomerSaveResult, AppFieldMapping, AppInstallConfig, AppNotification, AppOperationStart, AppRecord, AppRecordList, AppStartedOperation, AppStatusReport, AppSyncDirection, AppUpsertResult, AppWhoami, } from './client';
|
|
24
|
+
export type { AppBinding, AppClient, AppClientConfig, AppConsentChange, AppConsentSubject, AppCustomer, AppCustomerEraseResult, AppCustomerList, AppCustomerSaveResult, AppFieldMapping, AppInstallConfig, AppNotification, AppOperationStart, AppRecord, AppRecordList, AppStartedOperation, AppStatusReport, AppSyncDirection, AppUpsertResult, AppWhoami, } from './client';
|
|
25
25
|
export { appCustomerEmailUnavailableOf, appCustomerUnlinkedOf, appLookupConflictOf, isAppFieldWithheld } from './errors';
|
|
26
26
|
export type { AppCustomerEmailUnavailableReason, AppLookupConflict } from './errors';
|
|
27
27
|
export { appModelFieldName, appModelTypeName, appSelectorEnumValue } from './naming';
|
|
@@ -7,7 +7,7 @@ export declare const APP_HOOK_EVENTS: readonly ["RECORD_CREATED", "RECORD_UPDATE
|
|
|
7
7
|
/** An event a hook can name. */
|
|
8
8
|
export type AppHookEvent = (typeof APP_HOOK_EVENTS)[number];
|
|
9
9
|
/** The capabilities an operation declares as written. */
|
|
10
|
-
export declare const APP_CAPABILITIES: readonly ["config:read", "secrets:read.app", "secrets:put.app", "secrets:list.app", "secrets:delete.app", "status:write", "files:read", "files:list", "files:write", "customers:write", "notifications:write", "notifications:consent", "customers:read", "customers:read:named", "operations:complete"];
|
|
10
|
+
export declare const APP_CAPABILITIES: readonly ["config:read", "secrets:read.app", "secrets:put.app", "secrets:list.app", "secrets:delete.app", "status:write", "files:read", "files:list", "files:write", "customers:write", "notifications:write", "notifications:consent", "customers:read", "customers:read:named", "customers:read:identity", "operations:complete"];
|
|
11
11
|
/** A capability an operation declares as written. */
|
|
12
12
|
export type AppCapability = (typeof APP_CAPABILITIES)[number];
|
|
13
13
|
/**
|
package/dist/app-contract.js
CHANGED
|
@@ -8,30 +8,31 @@ var file_buf_validate_validate = /* @__PURE__ */ fileDesc("ChtidWYvdmFsaWRhdGUvd
|
|
|
8
8
|
|
|
9
9
|
// ../proto-ts/src/apps/v1/apps_pb.js
|
|
10
10
|
import { file_google_protobuf_descriptor as file_google_protobuf_descriptor2, file_google_protobuf_struct } from "@bufbuild/protobuf/wkt";
|
|
11
|
-
var file_apps_v1_apps = /* @__PURE__ */ fileDesc2("
|
|
11
|
+
var file_apps_v1_apps = /* @__PURE__ */ fileDesc2("ChJhcHBzL3YxL2FwcHMucHJvdG8SB2FwcHMudjEiyQUKCUFwcENvbmZpZxI0CgRuYW1lGAEgASgJQia6SCNyIRhAMh1eW2Etel1bYS16MC05XSooLVthLXowLTldKykqJBIYCgd2ZXJzaW9uGAIgASgJQge6SARyAhABEiMKCG1ldGFkYXRhGAMgASgLMhEuYXBwcy52MS5NZXRhZGF0YRInCgptaWRkbGV3YXJlGAQgASgLMhMuYXBwcy52MS5NaWRkbGV3YXJlEikKC2NyZWRlbnRpYWxzGAUgASgLMhQuYXBwcy52MS5DcmVkZW50aWFscxIwCg9zZXR0aW5nc19zY2hlbWEYBiABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EjAKB3NvdXJjZXMYByADKAsyHy5hcHBzLnYxLkFwcENvbmZpZy5Tb3VyY2VzRW50cnkSLAoFc2lua3MYCCADKAsyHS5hcHBzLnYxLkFwcENvbmZpZy5TaW5rc0VudHJ5EiYKCm9wZXJhdGlvbnMYCSADKAsyEi5hcHBzLnYxLk9wZXJhdGlvbhIcCgVob29rcxgKIAMoCzINLmFwcHMudjEuSG9vaxIXCgJ1aRgLIAEoCzILLmFwcHMudjEuVUkSIQoHaW5ib3VuZBgMIAEoCzIQLmFwcHMudjEuSW5ib3VuZBIiCghmZWF0dXJlcxgNIAMoCzIQLmFwcHMudjEuRmVhdHVyZRIxChBjdXN0b21lcl9zaWduX2luGA4gASgLMhcuYXBwcy52MS5DdXN0b21lclNpZ25JbhpDCgxTb3VyY2VzRW50cnkSCwoDa2V5GAEgASgJEiIKBXZhbHVlGAIgASgLMhMuYXBwcy52MS5Tb3VyY2VUeXBlOgI4ARpDCgpTaW5rc0VudHJ5EgsKA2tleRgBIAEoCRIkCgV2YWx1ZRgCIAEoCzIVLmFwcHMudjEuU2lua0NvbnRyYWN0OgI4ASIlCg5DdXN0b21lclNpZ25JbhITCgtkZXNjcmlwdGlvbhgBIAEoCSKnAQoHRmVhdHVyZRIzCgNrZXkYASABKAlCJrpII3IhGEAyHV5bYS16XVthLXowLTldKigtW2EtejAtOV0rKSokEhYKBXRpdGxlGAIgASgJQge6SARyAhABEhMKC2Rlc2NyaXB0aW9uGAMgASgJEg8KB3NldHRpbmcYBCABKAkSDwoHc291cmNlcxgFIAMoCRIYChBjdXN0b21lcl9zaWduX2luGAYgASgIImUKCE1ldGFkYXRhEhQKDGRpc3BsYXlfbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIMCgRpY29uGAMgASgJEg4KBmF1dGhvchgEIAEoCRIQCghob21lcGFnZRgFIAEoCSItCgpNaWRkbGV3YXJlEh8KA3VybBgBIAEoCUISukgPcg0yC15odHRwczovLy4rItcCCgtDcmVkZW50aWFscxIvCghzdHJhdGVneRgBIAEoDjIdLmFwcHMudjEuQ3JlZGVudGlhbHMuU3RyYXRlZ3kSLAoGc2NoZW1hGAIgASgLMhcuZ29vZ2xlLnByb3RvYnVmLlN0cnVjdEgAiAEBEigKBW9hdXRoGAMgASgLMhQuYXBwcy52MS5PQXV0aENvbmZpZ0gBiAEBIqkBCghTdHJhdGVneRIYChRTVFJBVEVHWV9VTlNQRUNJRklFRBAAEhIKDlNUUkFURUdZX09BVVRIEAESFAoQU1RSQVRFR1lfQVBJX0tFWRACEhoKFlNUUkFURUdZX1NIQVJFRF9TRUNSRVQQAxIUChBTVFJBVEVHWV9TU0hfS0VZEAQSEQoNU1RSQVRFR1lfTk9ORRAFEhQKEFNUUkFURUdZX01BTkFHRUQQBkIJCgdfc2NoZW1hQggKBl9vYXV0aCJMCgtPQXV0aENvbmZpZxIcChRhdXRob3JpemVfc3RhcnRfcGF0aBgBIAEoCRIfChdhdXRob3JpemVfY2FsbGJhY2tfcGF0aBgCIAEoCSJWCgdJbmJvdW5kEiEKB3dlYmhvb2sYASABKAsyEC5hcHBzLnYxLldlYmhvb2sSKAoHYWNjb3VudBgCIAEoCzIXLmFwcHMudjEuQWNjb3VudFdlYmhvb2siFwoHV2ViaG9vaxIMCgRwYXRoGAEgASgJIj8KDkFjY291bnRXZWJob29rEgwKBHBhdGgYASABKAkSDwoHc2V0dGluZxgCIAEoCRIOCgZzb3VyY2UYAyABKAki3QIKClNvdXJjZVR5cGUSDQoFbGFiZWwYASABKAkSLwoGZmllbGRzGAIgAygLMh8uYXBwcy52MS5Tb3VyY2VUeXBlLkZpZWxkc0VudHJ5EhMKC25hdHVyYWxfa2V5GAMgASgJEhAKCG9wdGlvbmFsGAQgASgIEhYKDnByb3Zpc2lvbl9vbmx5GAUgASgIEisKCXJlZmVyZW5jZRgGIAEoCzIYLmFwcHMudjEuU291cmNlUmVmZXJlbmNlEiQKBW93bmVyGAcgASgOMhUuYXBwcy52MS5TdG9yYWdlT3duZXISEwoLcmVjb21tZW5kZWQYCCABKAgSDgoGbG9va3VwGAkgAygJEhAKCGlkZW50aXR5GAogASgJGkYKC0ZpZWxkc0VudHJ5EgsKA2tleRgBIAEoCRImCgV2YWx1ZRgCIAEoCzIXLmFwcHMudjEuU3VnZ2VzdGVkRmllbGQ6AjgBIqoCCg9Tb3VyY2VSZWZlcmVuY2USEQoJa2V5X2ZpZWxkGAEgASgJEjQKBmZpZWxkcxgCIAMoCzIkLmFwcHMudjEuU291cmNlUmVmZXJlbmNlLkZpZWxkc0VudHJ5EhAKCGVuZHBvaW50GAMgASgJEg0KBWhvc3RzGAQgAygJEioKB3JlcXVlc3QYBSABKAsyGS5hcHBzLnYxLlJlZmVyZW5jZVJlcXVlc3QSGgoScmVzcG9uc2VfdHJhbnNmb3JtGAYgASgJEh0KFWNhY2hlX21heF9hZ2Vfc2Vjb25kcxgHIAEoBRpGCgtGaWVsZHNFbnRyeRILCgNrZXkYASABKAkSJgoFdmFsdWUYAiABKAsyFy5hcHBzLnYxLlN1Z2dlc3RlZEZpZWxkOgI4ASKzAgoQUmVmZXJlbmNlUmVxdWVzdBIOCgZtZXRob2QYASABKAkSFQoNcGF0aF90ZW1wbGF0ZRgCIAEoCRIzCgVxdWVyeRgDIAMoCzIkLmFwcHMudjEuUmVmZXJlbmNlUmVxdWVzdC5RdWVyeUVudHJ5EjcKB2hlYWRlcnMYBCADKAsyJi5hcHBzLnYxLlJlZmVyZW5jZVJlcXVlc3QuSGVhZGVyc0VudHJ5EhUKDWJvZHlfdGVtcGxhdGUYBSABKAkSFQoNYmF0Y2hfY2FwYWJsZRgGIAEoCBosCgpRdWVyeUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaLgoMSGVhZGVyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiugEKDFNpbmtDb250cmFjdBINCgVsYWJlbBgBIAEoCRIxCgZmaWVsZHMYAiADKAsyIS5hcHBzLnYxLlNpbmtDb250cmFjdC5GaWVsZHNFbnRyeRITCgtuYXR1cmFsX2tleRgDIAEoCRIQCghvcHRpb25hbBgEIAEoCBpBCgtGaWVsZHNFbnRyeRILCgNrZXkYASABKAkSIQoFdmFsdWUYAiABKAsyEi5hcHBzLnYxLlNpbmtGaWVsZDoCOAEilgIKDlN1Z2dlc3RlZEZpZWxkEgwKBHR5cGUYASABKAkSEgoFbGFiZWwYAiABKAlIAIgBARIVCghyZXF1aXJlZBgDIAEoCEgBiAEBEiwKBmNvbmZpZxgEIAEoCzIXLmdvb2dsZS5wcm90b2J1Zi5TdHJ1Y3RIAogBARI9ChNzdXBwb3J0ZWRfZGlyZWN0aW9uGAUgASgOMhsuYXBwcy52MS5GaWVsZFN5bmNEaXJlY3Rpb25IA4gBARIWCglxdWVyeWFibGUYBiABKAhIBIgBAUIICgZfbGFiZWxCCwoJX3JlcXVpcmVkQgkKB19jb25maWdCFgoUX3N1cHBvcnRlZF9kaXJlY3Rpb25CDAoKX3F1ZXJ5YWJsZSJbCglTaW5rRmllbGQSDAoEdHlwZRgBIAEoCRIQCghzZW1hbnRpYxgCIAEoCRIQCghyZXF1aXJlZBgDIAEoCBISCgVsYWJlbBgEIAEoCUgAiAEBQggKBl9sYWJlbCKYBAoJT3BlcmF0aW9uEjoKA2tleRgBIAEoCUItukgqcigYQDIkXihfXyk/W2Etel1bYS16MC05X10qKC1bYS16MC05X10rKSokEgwKBG5hbWUYAiABKAkSGAoLZGVzY3JpcHRpb24YAyABKAlIAIgBARIdCghlbmRwb2ludBgEIAEoCUILukgIcgYyBF4vLioSFAoMY2FwYWJpbGl0aWVzGAUgAygJEhcKCnRpbWVvdXRfbXMYBiABKAVIAYgBARIyCgxyZXRyeV9wb2xpY3kYByABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0SAKIAQESMgoMaW5wdXRfc2NoZW1hGAggASgLMhcuZ29vZ2xlLnByb3RvYnVmLlN0cnVjdEgDiAEBEjMKDW91dHB1dF9zY2hlbWEYCSABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0SASIAQESGwoOc3VwcG9ydHNfYXN5bmMYCiABKAhIBYgBARIZChFydW5fYWZ0ZXJfaW5zdGFsbBgLIAEoCBIeChZydW5zX29uX21hcHBlZF9zb3VyY2VzGAwgASgIQg4KDF9kZXNjcmlwdGlvbkINCgtfdGltZW91dF9tc0IPCg1fcmV0cnlfcG9saWN5Qg8KDV9pbnB1dF9zY2hlbWFCEAoOX291dHB1dF9zY2hlbWFCEQoPX3N1cHBvcnRzX2FzeW5jIj0KFk1hbmlmZXN0Q2FwYWJpbGl0eVNwZWMSEAoIZGVjbGFyZXMYASABKAkSEQoJcGVyX21vZGVsGAIgASgIIqYBCgRIb29rEjMKA2tleRgBIAEoCUImukgjciEYQDIdXlthLXpdW2EtejAtOV0qKC1bYS16MC05XSspKiQSDAoEbmFtZRgCIAEoCRIWCgVldmVudBgDIAEoCUIHukgEcgIQARInCgZmaWx0ZXIYBCABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EhoKCW9wZXJhdGlvbhgFIAEoCUIHukgEcgIQASI3CgJVSRIxCgpwbGFjZW1lbnRzGAEgAygLMh0uYXBwcy52MS5QbGFjZW1lbnREZWNsYXJhdGlvbiK2BAoUUGxhY2VtZW50RGVjbGFyYXRpb24SMwoDa2V5GAEgASgJQia6SCNyIRhAMh1eW2Etel1bYS16MC05XSooLVthLXowLTldKykqJBI0CgZ0YXJnZXQYAiABKA4yJC5hcHBzLnYxLlBsYWNlbWVudERlY2xhcmF0aW9uLlRhcmdldBISCgVtb2RlbBgDIAEoCUgAiAEBEhIKBWZpZWxkGAQgASgJSAGIAQESIQoZZmllbGRfc2VsZWN0ZWRfYXRfaW5zdGFsbBgFIAEoCBIQCgN0YWIYBiABKAlIAogBARIWCgV0aXRsZRgHIAEoCUIHukgEcgIQARIfCgN1cmwYCCABKAlCErpID3INMgteaHR0cHM6Ly8uKxIdChByZXBsYWNlc19kZWZhdWx0GAkgASgISAOIAQESKwoFaGludHMYCiABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0SASIAQESDgoGc2NvcGVzGAsgAygJEhAKCHNldHRpbmdzGAwgAygJInQKBlRhcmdldBIWChJUQVJHRVRfVU5TUEVDSUZJRUQQABIUChBUQVJHRVRfRlVMTF9QQUdFEAESFgoSVEFSR0VUX01BSU5fRURJVE9SEAISEgoOVEFSR0VUX1NJREVCQVIQAxIQCgxUQVJHRVRfRklFTEQQBEIICgZfbW9kZWxCCAoGX2ZpZWxkQgYKBF90YWJCEwoRX3JlcGxhY2VzX2RlZmF1bHRCCAoGX2hpbnRzKqwBChJGaWVsZFN5bmNEaXJlY3Rpb24SJAogRklFTERfU1lOQ19ESVJFQ1RJT05fVU5TUEVDSUZJRUQQABIkCiBGSUVMRF9TWU5DX0RJUkVDVElPTl9TT1VSQ0VfT05MWRABEiIKHkZJRUxEX1NZTkNfRElSRUNUSU9OX1NJTktfT05MWRACEiYKIkZJRUxEX1NZTkNfRElSRUNUSU9OX0JJRElSRUNUSU9OQUwQAyqEAQoMU3RvcmFnZU93bmVyEh0KGVNUT1JBR0VfT1dORVJfVU5TUEVDSUZJRUQQABIZChVTVE9SQUdFX09XTkVSX1BST0pFQ1QQARIeChpTVE9SQUdFX09XTkVSX09SR0FOSVpBVElPThACEhoKFlNUT1JBR0VfT1dORVJfQ1VTVE9NRVIQAyqUCwoSTWFuaWZlc3RDYXBhYmlsaXR5EiMKH01BTklGRVNUX0NBUEFCSUxJVFlfVU5TUEVDSUZJRUQQABI2Ch9NQU5JRkVTVF9DQVBBQklMSVRZX0NPTkZJR19SRUFEEAEaEZqmHQ0KC2NvbmZpZzpyZWFkEkAKJE1BTklGRVNUX0NBUEFCSUxJVFlfU0VDUkVUU19SRUFEX0FQUBACGhaaph0SChBzZWNyZXRzOnJlYWQuYXBwEj4KI01BTklGRVNUX0NBUEFCSUxJVFlfU0VDUkVUU19QVVRfQVBQEAMaFZqmHREKD3NlY3JldHM6cHV0LmFwcBJACiRNQU5JRkVTVF9DQVBBQklMSVRZX1NFQ1JFVFNfTElTVF9BUFAQBBoWmqYdEgoQc2VjcmV0czpsaXN0LmFwcBJECiZNQU5JRkVTVF9DQVBBQklMSVRZX1NFQ1JFVFNfREVMRVRFX0FQUBAFGhiaph0UChJzZWNyZXRzOmRlbGV0ZS5hcHASOAogTUFOSUZFU1RfQ0FQQUJJTElUWV9TVEFUVVNfV1JJVEUQBhoSmqYdDgoMc3RhdHVzOndyaXRlEjQKHk1BTklGRVNUX0NBUEFCSUxJVFlfRklMRVNfUkVBRBAHGhCaph0MCgpmaWxlczpyZWFkEjQKHk1BTklGRVNUX0NBUEFCSUxJVFlfRklMRVNfTElTVBAIGhCaph0MCgpmaWxlczpsaXN0EjYKH01BTklGRVNUX0NBUEFCSUxJVFlfRklMRVNfV1JJVEUQCRoRmqYdDQoLZmlsZXM6d3JpdGUSPgojTUFOSUZFU1RfQ0FQQUJJTElUWV9DVVNUT01FUlNfV1JJVEUQChoVmqYdEQoPY3VzdG9tZXJzOndyaXRlEkYKJ01BTklGRVNUX0NBUEFCSUxJVFlfTk9USUZJQ0FUSU9OU19XUklURRALGhmaph0VChNub3RpZmljYXRpb25zOndyaXRlEkoKKU1BTklGRVNUX0NBUEFCSUxJVFlfTk9USUZJQ0FUSU9OU19DT05TRU5UEAwaG5qmHRcKFW5vdGlmaWNhdGlvbnM6Y29uc2VudBI8CiJNQU5JRkVTVF9DQVBBQklMSVRZX0NVU1RPTUVSU19SRUFEEA4aFJqmHRAKDmN1c3RvbWVyczpyZWFkEkgKKE1BTklGRVNUX0NBUEFCSUxJVFlfQ1VTVE9NRVJTX1JFQURfTkFNRUQQDxoamqYdFgoUY3VzdG9tZXJzOnJlYWQ6bmFtZWQSTgorTUFOSUZFU1RfQ0FQQUJJTElUWV9DVVNUT01FUlNfUkVBRF9JREVOVElUWRAQGh2aph0ZChdjdXN0b21lcnM6cmVhZDppZGVudGl0eRJGCidNQU5JRkVTVF9DQVBBQklMSVRZX09QRVJBVElPTlNfQ09NUExFVEUQDRoZmqYdFQoTb3BlcmF0aW9uczpjb21wbGV0ZRI7CiBNQU5JRkVTVF9DQVBBQklMSVRZX1JFQ09SRFNfUkVBRBAUGhWaph0RCg1yZWNvcmRzOnJlYWQ6EAESPQohTUFOSUZFU1RfQ0FQQUJJTElUWV9SRUNPUkRTX1dSSVRFEBUaFpqmHRIKDnJlY29yZHM6d3JpdGU6EAESPwoiTUFOSUZFU1RfQ0FQQUJJTElUWV9SRUNPUkRTX0RFTEVURRAWGheaph0TCg9yZWNvcmRzOmRlbGV0ZToQARJBCiNNQU5JRkVTVF9DQVBBQklMSVRZX1JFQ09SRFNfUFVCTElTSBAXGhiaph0UChByZWNvcmRzOnB1Ymxpc2g6EAESRQolTUFOSUZFU1RfQ0FQQUJJTElUWV9SRUNPUkRTX0FUVFJJQlVURRAYGhqaph0WChJyZWNvcmRzOmF0dHJpYnV0ZToQATp1ChNtYW5pZmVzdF9jYXBhYmlsaXR5EiEuZ29vZ2xlLnByb3RvYnVmLkVudW1WYWx1ZU9wdGlvbnMY49QDIAEoCzIfLmFwcHMudjEuTWFuaWZlc3RDYXBhYmlsaXR5U3BlY1ISbWFuaWZlc3RDYXBhYmlsaXR5QjVaM2dpdGh1Yi5jb20vZWlkZXN0dWRpby9mb2lyL2dlbi9wcm90by9hcHBzL3YxO2FwcHN2MWIGcHJvdG8z", [file_buf_validate_validate, file_google_protobuf_descriptor2, file_google_protobuf_struct]);
|
|
12
12
|
var AppConfigSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 0);
|
|
13
|
-
var
|
|
14
|
-
var
|
|
15
|
-
var
|
|
16
|
-
var
|
|
17
|
-
var
|
|
13
|
+
var CustomerSignInSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 1);
|
|
14
|
+
var FeatureSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 2);
|
|
15
|
+
var MetadataSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 3);
|
|
16
|
+
var MiddlewareSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 4);
|
|
17
|
+
var CredentialsSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 5);
|
|
18
|
+
var Credentials_StrategySchema = /* @__PURE__ */ enumDesc2(file_apps_v1_apps, 5, 0);
|
|
18
19
|
var Credentials_Strategy = /* @__PURE__ */ tsEnum2(Credentials_StrategySchema);
|
|
19
|
-
var OAuthConfigSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps,
|
|
20
|
-
var InboundSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps,
|
|
21
|
-
var WebhookSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps,
|
|
22
|
-
var AccountWebhookSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps,
|
|
23
|
-
var SourceTypeSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps,
|
|
24
|
-
var SourceReferenceSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps,
|
|
25
|
-
var ReferenceRequestSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps,
|
|
26
|
-
var SinkContractSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps,
|
|
27
|
-
var SuggestedFieldSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps,
|
|
28
|
-
var SinkFieldSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps,
|
|
29
|
-
var OperationSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps,
|
|
30
|
-
var ManifestCapabilitySpecSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps,
|
|
31
|
-
var HookSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps,
|
|
32
|
-
var UISchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps,
|
|
33
|
-
var PlacementDeclarationSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps,
|
|
34
|
-
var PlacementDeclaration_TargetSchema = /* @__PURE__ */ enumDesc2(file_apps_v1_apps,
|
|
20
|
+
var OAuthConfigSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 6);
|
|
21
|
+
var InboundSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 7);
|
|
22
|
+
var WebhookSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 8);
|
|
23
|
+
var AccountWebhookSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 9);
|
|
24
|
+
var SourceTypeSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 10);
|
|
25
|
+
var SourceReferenceSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 11);
|
|
26
|
+
var ReferenceRequestSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 12);
|
|
27
|
+
var SinkContractSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 13);
|
|
28
|
+
var SuggestedFieldSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 14);
|
|
29
|
+
var SinkFieldSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 15);
|
|
30
|
+
var OperationSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 16);
|
|
31
|
+
var ManifestCapabilitySpecSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 17);
|
|
32
|
+
var HookSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 18);
|
|
33
|
+
var UISchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 19);
|
|
34
|
+
var PlacementDeclarationSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 20);
|
|
35
|
+
var PlacementDeclaration_TargetSchema = /* @__PURE__ */ enumDesc2(file_apps_v1_apps, 20, 0);
|
|
35
36
|
var PlacementDeclaration_Target = /* @__PURE__ */ tsEnum2(PlacementDeclaration_TargetSchema);
|
|
36
37
|
var FieldSyncDirectionSchema = /* @__PURE__ */ enumDesc2(file_apps_v1_apps, 0);
|
|
37
38
|
var FieldSyncDirection = /* @__PURE__ */ tsEnum2(FieldSyncDirectionSchema);
|
|
@@ -75,6 +76,7 @@ export {
|
|
|
75
76
|
CredentialsSchema,
|
|
76
77
|
Credentials_Strategy,
|
|
77
78
|
Credentials_StrategySchema,
|
|
79
|
+
CustomerSignInSchema,
|
|
78
80
|
DiscoveredAccountSchema,
|
|
79
81
|
DiscoveredCountSchema,
|
|
80
82
|
DiscoveredFieldGroupSchema,
|
package/dist/app.js
CHANGED
|
@@ -497,7 +497,7 @@ function createAppClient(config) {
|
|
|
497
497
|
"integrationConfig",
|
|
498
498
|
`query AppInstallConfig {
|
|
499
499
|
integrationConfig {
|
|
500
|
-
name enabled middlewareUrl targetModels webhookUrl config customerEmailUnavailable
|
|
500
|
+
name enabled middlewareUrl targetModels webhookUrl config customerEmailUnavailable customerSignIn
|
|
501
501
|
sources { key model naturalKey mode fields { appField modelField direction } }
|
|
502
502
|
sinks { key model naturalKey fields { appField modelField direction } }
|
|
503
503
|
}
|
|
@@ -514,7 +514,8 @@ function createAppClient(config) {
|
|
|
514
514
|
settings: got.config ?? {},
|
|
515
515
|
sources: bindingsOf(got.sources),
|
|
516
516
|
sinks: bindingsOf(got.sinks),
|
|
517
|
-
customerEmailUnavailable: customerEmailUnavailableReasonOf(got.customerEmailUnavailable)
|
|
517
|
+
customerEmailUnavailable: customerEmailUnavailableReasonOf(got.customerEmailUnavailable),
|
|
518
|
+
customerSignIn: typeof got.customerSignIn === "string" && got.customerSignIn !== "" ? got.customerSignIn : null
|
|
518
519
|
};
|
|
519
520
|
},
|
|
520
521
|
async credentials() {
|
|
@@ -732,16 +733,6 @@ function createAppClient(config) {
|
|
|
732
733
|
customerDeleted: answer.customerDeleted === true,
|
|
733
734
|
customerKeptBecause: answer.customerKeptBecause ?? null
|
|
734
735
|
};
|
|
735
|
-
},
|
|
736
|
-
async identities(model, customerId) {
|
|
737
|
-
if (customerId === "") refuse(`a customer's identities on ${model} were asked for with no customer`);
|
|
738
|
-
const answer = await field(
|
|
739
|
-
"customerIdentitiesById",
|
|
740
|
-
`query AppCustomerIdentities($modelKey: String!, $customerId: ID!) { customerIdentitiesById(modelKey: $modelKey, customerId: $customerId) }`,
|
|
741
|
-
{ modelKey: model, customerId }
|
|
742
|
-
);
|
|
743
|
-
if (!Array.isArray(answer)) return refuse(`customerIdentitiesById reported no error and returned no list on ${model}`);
|
|
744
|
-
return answer.filter((identity) => typeof identity === "string");
|
|
745
736
|
}
|
|
746
737
|
}
|
|
747
738
|
},
|
|
@@ -776,6 +767,25 @@ function createAppClient(config) {
|
|
|
776
767
|
);
|
|
777
768
|
return customerOf(customer);
|
|
778
769
|
},
|
|
770
|
+
async byIdentity(identity) {
|
|
771
|
+
if (identity === "") refuse("a customer was asked for with no identity");
|
|
772
|
+
const answer = await field(
|
|
773
|
+
"customerByIdentity",
|
|
774
|
+
`query AppCustomerByIdentity($identity: String!) { customerByIdentity(identity: $identity) }`,
|
|
775
|
+
{ identity }
|
|
776
|
+
);
|
|
777
|
+
return typeof answer === "string" && answer !== "" ? answer : null;
|
|
778
|
+
},
|
|
779
|
+
async identities(customerId) {
|
|
780
|
+
if (customerId === "") refuse("a customer's identities were asked for with no customer");
|
|
781
|
+
const answer = await field(
|
|
782
|
+
"customerIdentitiesById",
|
|
783
|
+
`query AppCustomerIdentities($customerId: ID!) { customerIdentitiesById(customerId: $customerId) }`,
|
|
784
|
+
{ customerId }
|
|
785
|
+
);
|
|
786
|
+
if (!Array.isArray(answer)) return refuse("customerIdentitiesById reported no error and returned no list");
|
|
787
|
+
return answer.filter((identity) => typeof identity === "string");
|
|
788
|
+
},
|
|
779
789
|
async list(query = {}) {
|
|
780
790
|
const search = query.search?.trim() ?? "";
|
|
781
791
|
const page = await field(
|
|
@@ -825,9 +835,11 @@ function createAppClient(config) {
|
|
|
825
835
|
if (sent !== true) refuse("sendNotification reported no error and did not say the notification was sent");
|
|
826
836
|
},
|
|
827
837
|
async setConsent(change) {
|
|
828
|
-
|
|
838
|
+
const subject = consentSubject(change);
|
|
839
|
+
if (subject === null) return refuse("a consent change names its customer by customerId or by email, not both");
|
|
840
|
+
if (subject.value === "" || change.category === "") return refuse("a consent change needs a customer and a category");
|
|
829
841
|
const input = {
|
|
830
|
-
|
|
842
|
+
[subject.by]: subject.value,
|
|
831
843
|
category: change.category,
|
|
832
844
|
channel: change.channel,
|
|
833
845
|
enabled: change.enabled
|
|
@@ -876,6 +888,12 @@ function createAppClient(config) {
|
|
|
876
888
|
}
|
|
877
889
|
};
|
|
878
890
|
}
|
|
891
|
+
function consentSubject(change) {
|
|
892
|
+
const loose = change;
|
|
893
|
+
if (loose.email !== void 0 && loose.customerId !== void 0) return null;
|
|
894
|
+
if (loose.email !== void 0) return { by: "email", value: loose.email.trim() };
|
|
895
|
+
return { by: "customerId", value: loose.customerId ?? "" };
|
|
896
|
+
}
|
|
879
897
|
function directionOf(raw) {
|
|
880
898
|
const name = (raw ?? "").replace(/^FIELD_SYNC_DIRECTION_/, "");
|
|
881
899
|
return name === "SOURCE_ONLY" || name === "BIDIRECTIONAL" ? name : "SINK_ONLY";
|
|
@@ -1137,6 +1155,7 @@ var APP_CAPABILITIES = [
|
|
|
1137
1155
|
"notifications:consent",
|
|
1138
1156
|
"customers:read",
|
|
1139
1157
|
"customers:read:named",
|
|
1158
|
+
"customers:read:identity",
|
|
1140
1159
|
"operations:complete"
|
|
1141
1160
|
];
|
|
1142
1161
|
var APP_RECORD_CAPABILITY_PREFIXES = [
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the surface being served can still answer what a generated module
|
|
3
|
+
* prints.
|
|
4
|
+
*
|
|
5
|
+
* A generated module records the surface version it came from, and that version
|
|
6
|
+
* is a hash over everything `foir types` reads. So it moves when a model gains
|
|
7
|
+
* a field, when a namespace gains an op, and when the platform adds a mutation
|
|
8
|
+
* to every model at once. None of those changes a document this client prints:
|
|
9
|
+
* the document names the fields and arguments the module knew about, and the
|
|
10
|
+
* server still has every one of them. Treating a moved version as a reason to
|
|
11
|
+
* refuse calls made every deployed client stop working the moment the surface
|
|
12
|
+
* grew, with nothing in its own project having changed.
|
|
13
|
+
*
|
|
14
|
+
* So the two are kept apart. A moved version is DRIFT, and it is reported. A
|
|
15
|
+
* call is refused only for an INCOMPATIBILITY: something that call prints, or
|
|
16
|
+
* sends, that the server no longer serves the way the module describes. The
|
|
17
|
+
* client holds both sides of that question once the manifest has arrived, since
|
|
18
|
+
* the generated trees and the served manifest are the same shape, and this file
|
|
19
|
+
* compares them.
|
|
20
|
+
*
|
|
21
|
+
* ---------------------------------------------------------------------------
|
|
22
|
+
* What "the same" means
|
|
23
|
+
* ---------------------------------------------------------------------------
|
|
24
|
+
*
|
|
25
|
+
* What `document.ts` reads, because a property the printer never reads cannot
|
|
26
|
+
* change a document:
|
|
27
|
+
*
|
|
28
|
+
* - an op: its root `field`, whether it is a `mutation`, its `args` and
|
|
29
|
+
* `argTypes`, and its `payload` (`kind`, `model`, `union`, `fields`);
|
|
30
|
+
* - a selection field: its `key`, and which of the printer's branches it
|
|
31
|
+
* takes (a relation, a stopping point, a `model` reference, a `union`
|
|
32
|
+
* reference, an object with `fields`, or a leaf);
|
|
33
|
+
* - a relation an `include` resolves: `target`, `targets` and `connection`,
|
|
34
|
+
* and one property the printer does not read, `list`, because it decides
|
|
35
|
+
* whether the same document answers with one record or an array;
|
|
36
|
+
* - a model's `typeName`, which names the fragment a polymorphic relation
|
|
37
|
+
* prints for it;
|
|
38
|
+
* - a union's members, by `typeName`, and each member's fields.
|
|
39
|
+
*
|
|
40
|
+
* `cost`, `nullable`, `deprecated` and the buckets the generator does not emit
|
|
41
|
+
* are not compared. `alias` is not compared either, and that one is a decision:
|
|
42
|
+
* an alias is a name this client gives a field in its OWN document, so the
|
|
43
|
+
* server never sees the manifest's opinion of it. A new union member whose
|
|
44
|
+
* field collides with an existing one makes the server publish aliases for
|
|
45
|
+
* both, and the module's document, which selects only the members it knew,
|
|
46
|
+
* is as valid as it was.
|
|
47
|
+
*
|
|
48
|
+
* The comparison runs one way. Everything the module names must still be
|
|
49
|
+
* served; whatever the server has added is never looked at, with one
|
|
50
|
+
* exception: an argument the served op REQUIRES and the module does not know,
|
|
51
|
+
* since a call that cannot supply it cannot succeed.
|
|
52
|
+
*/
|
|
53
|
+
import { type FoirSelectionSource } from './document';
|
|
54
|
+
/** One thing the generated module describes that the server no longer serves that way. */
|
|
55
|
+
export interface FoirIncompatibility {
|
|
56
|
+
/**
|
|
57
|
+
* Where it sits, in the manifest's own keys:
|
|
58
|
+
* `models.order.selection.status`, `models.order.ops.update`,
|
|
59
|
+
* `namespaces.search.ops.searchRecords.args.query`.
|
|
60
|
+
*/
|
|
61
|
+
path: string;
|
|
62
|
+
/** What happened to it, as a clause a message can put in brackets. */
|
|
63
|
+
reason: string;
|
|
64
|
+
}
|
|
65
|
+
/** The comparison, held by a client for as long as it lives. */
|
|
66
|
+
export interface FoirSurfaceComparison {
|
|
67
|
+
/** Every incompatible path, sorted. Empty when the surface has only grown. */
|
|
68
|
+
readonly incompatible: readonly string[];
|
|
69
|
+
/** What `foir.<model>.<op>()` depends on that is incompatible. */
|
|
70
|
+
modelCall(model: string, op: string, include: unknown): readonly FoirIncompatibility[];
|
|
71
|
+
/** What `foir.ns.<namespace>.<op>()` depends on that is incompatible. */
|
|
72
|
+
namespaceCall(namespace: string, op: string, include: unknown): readonly FoirIncompatibility[];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Compare a generated module's trees with the manifest being served.
|
|
76
|
+
*
|
|
77
|
+
* Called once, when the manifest arrives, and off the response path. What it
|
|
78
|
+
* returns answers each later call from maps built here, so a call pays for a
|
|
79
|
+
* lookup and, when it has an `include`, a walk of the relations it names.
|
|
80
|
+
*
|
|
81
|
+
* How far a break reaches is decided per call rather than per model. A call
|
|
82
|
+
* depends on its op, on every tree its result prints (the model's own, the
|
|
83
|
+
* unions inside it, and so on down), and on the relations THIS call includes
|
|
84
|
+
* with the trees of their targets. A model whose relation points at a broken
|
|
85
|
+
* model is not refused for it until a call includes that relation, because
|
|
86
|
+
* until then the broken tree is not in the document. Refusing on the whole
|
|
87
|
+
* relation graph would let one field removed from one model stop every call in
|
|
88
|
+
* a project whose models all reach each other, which is most projects.
|
|
89
|
+
*
|
|
90
|
+
* It is not narrowed by `pick` or `omit`. A call that omits the one field that
|
|
91
|
+
* is gone is still refused: resolving a caller's paths against two trees, with
|
|
92
|
+
* aliases and union members in play, is the printer's work done a second time,
|
|
93
|
+
* and regenerating is the remedy either way.
|
|
94
|
+
*/
|
|
95
|
+
export declare function compareFoirSurfaces(generated: FoirSelectionSource, served: FoirSelectionSource): FoirSurfaceComparison;
|
package/dist/data/document.d.ts
CHANGED
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
* inferring a literal would reinstate the defect above under a condition no
|
|
52
52
|
* caller can see.
|
|
53
53
|
*/
|
|
54
|
-
import type { FoirManifestOp, FoirManifestRelation, FoirManifestSelection, FoirManifestSelectionField, FoirManifestSelectionUnion } from './manifest';
|
|
54
|
+
import type { FoirManifestOp, FoirManifestOpPayload, FoirManifestRelation, FoirManifestSelection, FoirManifestSelectionField, FoirManifestSelectionUnion } from './manifest';
|
|
55
55
|
/**
|
|
56
56
|
* One model, as the printer needs it.
|
|
57
57
|
*
|
|
@@ -145,6 +145,16 @@ export interface FoirRecordPath {
|
|
|
145
145
|
/** Model key whose published `selection` is printed at that position. */
|
|
146
146
|
modelKey: string;
|
|
147
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Every record position in a payload, in the order the tree declares them.
|
|
150
|
+
*
|
|
151
|
+
* ONE walk, and everything that needs to know where the records are reads it:
|
|
152
|
+
* `planCall` takes the model SET from it to decide where `pick`/`omit`/`include`
|
|
153
|
+
* land, and the plan carries the positions so the transport's caller can find
|
|
154
|
+
* the records in the response. A second walk would be correct on the day it was
|
|
155
|
+
* written and wrong on the day the payload format grew a case.
|
|
156
|
+
*/
|
|
157
|
+
export declare function payloadRecordPaths(payload: FoirManifestOpPayload): readonly FoirRecordPath[];
|
|
148
158
|
/** One printed operation, and what the transport needs to route it. */
|
|
149
159
|
export interface FoirDocumentPlan {
|
|
150
160
|
/** The document text. Also the thing that gets hashed for APQ. */
|
package/dist/data/transport.d.ts
CHANGED
|
@@ -419,19 +419,33 @@ export interface FoirKeyClientConfig<S extends FoirSchemaDefinition = FoirSchema
|
|
|
419
419
|
/** Called instead of `console.warn` when drift is detected. */
|
|
420
420
|
onSchemaDrift?: (drift: FoirSchemaDrift) => void;
|
|
421
421
|
/**
|
|
422
|
-
* Refuse typed
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
422
|
+
* Refuse a typed call that depends on something the server no longer serves
|
|
423
|
+
* the way the generated module describes. **On by default**; pass `false` to
|
|
424
|
+
* opt out.
|
|
425
|
+
*
|
|
426
|
+
* A drift by itself refuses nothing. The surface version moves whenever the
|
|
427
|
+
* surface grows (a field on a model, an op on a namespace, a mutation the
|
|
428
|
+
* platform adds to every model), and a document printed from the older trees
|
|
429
|
+
* is still answered in full. So once the manifest has arrived the client
|
|
430
|
+
* compares its generated trees with the served ones, and refuses only the
|
|
431
|
+
* calls whose own parts changed:
|
|
432
|
+
*
|
|
433
|
+
* - the op is gone, or its root field or result kind changed;
|
|
434
|
+
* - an argument the module declares is gone or has a type the generated
|
|
435
|
+
* one no longer fits, or the op now requires an argument the module does
|
|
436
|
+
* not know;
|
|
437
|
+
* - a field the call's selection names is gone or has another shape, in the
|
|
438
|
+
* model the op returns, in a union that model's tree embeds, or in the
|
|
439
|
+
* target of a relation the call includes.
|
|
440
|
+
*
|
|
441
|
+
* Anything ADDED refuses nothing: a model, a namespace, an op, an optional
|
|
442
|
+
* argument, a field, a relation, a union member, a subscription.
|
|
443
|
+
*
|
|
444
|
+
* It is the default because the alternative is worse for the caller. A call
|
|
445
|
+
* that depends on a removed field is answered with a server error naming the
|
|
446
|
+
* field, and one whose relation changed from a record to a list is answered
|
|
447
|
+
* in a shape the types do not describe. Neither says the generated module is
|
|
448
|
+
* the thing to fix. `FoirSchemaDriftError` does, and names the part.
|
|
435
449
|
*
|
|
436
450
|
* Two things it deliberately does not do.
|
|
437
451
|
*
|
|
@@ -441,20 +455,16 @@ export interface FoirKeyClientConfig<S extends FoirSchemaDefinition = FoirSchema
|
|
|
441
455
|
* block every first call on a second round trip, which is the cost the
|
|
442
456
|
* warning was designed to avoid.
|
|
443
457
|
*
|
|
444
|
-
* It does not touch `raw`. A hand-written document
|
|
445
|
-
*
|
|
446
|
-
* class of bug by construction, and refusing it would punish the escape
|
|
447
|
-
* hatch people reach for precisely because it is immune.
|
|
458
|
+
* It does not touch `raw`. A hand-written document is not printed from the
|
|
459
|
+
* generated trees, so nothing compared here says anything about it.
|
|
448
460
|
*
|
|
449
|
-
* Turning it off is never the remedy
|
|
450
|
-
* is
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
* knowingly ships a stale module for a while, and it says so at the call
|
|
454
|
-
* site, where a reader can see it:
|
|
461
|
+
* Turning it off is never the remedy. A refusal names something that really
|
|
462
|
+
* is no longer served as generated, and the fix is `foir types`. The opt-out
|
|
463
|
+
* exists for an app that would rather send the call and handle the server's
|
|
464
|
+
* answer, and it says so at the call site, where a reader can see it:
|
|
455
465
|
*
|
|
456
466
|
* ```ts
|
|
457
|
-
* createFoirClient({ strictSchemaDrift: false }) //
|
|
467
|
+
* createFoirClient({ strictSchemaDrift: false }) // never refuse, warn only
|
|
458
468
|
* ```
|
|
459
469
|
*/
|
|
460
470
|
strictSchemaDrift?: boolean;
|
|
@@ -578,9 +588,23 @@ export interface FoirSchemaDrift {
|
|
|
578
588
|
* too. Empty only when the module was wired without `surfaceHashes`.
|
|
579
589
|
*/
|
|
580
590
|
changed: readonly string[];
|
|
591
|
+
/**
|
|
592
|
+
* What the generated module describes that the server no longer serves that
|
|
593
|
+
* way, as paths into the manifest, sorted:
|
|
594
|
+
* `models.order.selection.status`, `models.order.ops.update`,
|
|
595
|
+
* `namespaces.search.ops.searchRecords.args.query`,
|
|
596
|
+
* `models.order.relations.customer`, `selectionUnions.Block.HeroBlock.title`.
|
|
597
|
+
*
|
|
598
|
+
* `changed` says what moved; this says what BROKE. Empty when the surface
|
|
599
|
+
* only grew, which is the usual drift, and then no typed call is refused.
|
|
600
|
+
* Also empty for a client that prints from the served manifest rather than
|
|
601
|
+
* from generated trees, since it has nothing older to compare.
|
|
602
|
+
*/
|
|
603
|
+
incompatible: readonly string[];
|
|
581
604
|
}
|
|
582
605
|
/**
|
|
583
|
-
* Thrown by a typed call under `strictSchemaDrift`
|
|
606
|
+
* Thrown by a typed call under `strictSchemaDrift` when the call depends on
|
|
607
|
+
* something in `drift.incompatible`. The message names which.
|
|
584
608
|
*
|
|
585
609
|
* A subclass rather than a plain `FoirRequestError` so an app can tell "your
|
|
586
610
|
* generated module is stale" from "this request failed", which want different
|
package/dist/data.js
CHANGED
|
@@ -1008,6 +1008,304 @@ function isFoirMutationDocument(documentText) {
|
|
|
1008
1008
|
return /\bmutation\b/.test(documentText);
|
|
1009
1009
|
}
|
|
1010
1010
|
|
|
1011
|
+
// src/data/compatibility.ts
|
|
1012
|
+
var newPart = () => ({ broken: [], prints: /* @__PURE__ */ new Set() });
|
|
1013
|
+
var GONE = "no longer served";
|
|
1014
|
+
function shapeOf(field) {
|
|
1015
|
+
if (field.relation) return "a relation";
|
|
1016
|
+
if (field.truncated) {
|
|
1017
|
+
return field.union ? `a stopping point before the union ${field.union}` : "a stopping point";
|
|
1018
|
+
}
|
|
1019
|
+
if (field.model) return `the model ${field.model}`;
|
|
1020
|
+
if (field.union) return `the union ${field.union}`;
|
|
1021
|
+
return field.fields?.length ? "an object" : "a leaf";
|
|
1022
|
+
}
|
|
1023
|
+
function compareFields(generated, served, path, part) {
|
|
1024
|
+
const byKey = new Map((served ?? []).map((field) => [field.key, field]));
|
|
1025
|
+
for (const field of generated) {
|
|
1026
|
+
if (field.relation) continue;
|
|
1027
|
+
const at = `${path}.${field.key}`;
|
|
1028
|
+
const now = byKey.get(field.key);
|
|
1029
|
+
if (!now) {
|
|
1030
|
+
part.broken.push({ path: at, reason: GONE });
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
const was = shapeOf(field);
|
|
1034
|
+
const is = shapeOf(now);
|
|
1035
|
+
if (field.truncated) {
|
|
1036
|
+
const composite = !now.relation && is !== "a leaf";
|
|
1037
|
+
if (!composite || field.union && now.union !== field.union) {
|
|
1038
|
+
part.broken.push({ path: at, reason: `generated as ${was}, served as ${is}` });
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
if (field.union) part.prints.add(`u:${field.union}`);
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
if (was !== is) {
|
|
1045
|
+
part.broken.push({ path: at, reason: `generated as ${was}, served as ${is}` });
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
if (field.model) part.prints.add(`m:${field.model}`);
|
|
1049
|
+
else if (field.union) part.prints.add(`u:${field.union}`);
|
|
1050
|
+
else if (field.fields?.length) compareFields(field.fields, now.fields, at, part);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
function variableFits(variable, position) {
|
|
1054
|
+
if (position.endsWith("!")) {
|
|
1055
|
+
return variable.endsWith("!") && variableFits(variable.slice(0, -1), position.slice(0, -1));
|
|
1056
|
+
}
|
|
1057
|
+
if (variable.endsWith("!")) return variableFits(variable.slice(0, -1), position);
|
|
1058
|
+
if (position.startsWith("[")) {
|
|
1059
|
+
return variable.startsWith("[") && variableFits(variable.slice(1, -1), position.slice(1, -1));
|
|
1060
|
+
}
|
|
1061
|
+
return variable === position;
|
|
1062
|
+
}
|
|
1063
|
+
function compareOp(generated, served, at) {
|
|
1064
|
+
const part = newPart();
|
|
1065
|
+
if (!served) {
|
|
1066
|
+
part.broken.push({ path: at, reason: GONE });
|
|
1067
|
+
return part;
|
|
1068
|
+
}
|
|
1069
|
+
if (generated.field !== served.field) {
|
|
1070
|
+
part.broken.push({
|
|
1071
|
+
path: `${at}.field`,
|
|
1072
|
+
reason: `root field ${generated.field}, served as ${served.field}`
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
if (Boolean(generated.mutation) !== Boolean(served.mutation)) {
|
|
1076
|
+
part.broken.push({
|
|
1077
|
+
path: `${at}.mutation`,
|
|
1078
|
+
reason: served.mutation ? "now a mutation" : "no longer a mutation"
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
const declared = generated.args ?? [];
|
|
1082
|
+
for (const name of declared) {
|
|
1083
|
+
const type = generated.argTypes?.[name];
|
|
1084
|
+
if (!type) continue;
|
|
1085
|
+
if (!served.args?.includes(name)) {
|
|
1086
|
+
part.broken.push({ path: `${at}.args.${name}`, reason: "no longer accepted" });
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
const now2 = served.argTypes?.[name];
|
|
1090
|
+
if (!now2 || !variableFits(type, now2)) {
|
|
1091
|
+
part.broken.push({
|
|
1092
|
+
path: `${at}.args.${name}`,
|
|
1093
|
+
reason: `type ${type}, served as ${now2 ?? "no published type"}`
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
for (const name of served.args ?? []) {
|
|
1098
|
+
if (declared.includes(name)) continue;
|
|
1099
|
+
if (served.argTypes?.[name]?.endsWith("!")) {
|
|
1100
|
+
part.broken.push({ path: `${at}.args.${name}`, reason: "a new required argument" });
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
const payload = generated.payload;
|
|
1104
|
+
if (!payload || payload.error) return part;
|
|
1105
|
+
const now = served.payload;
|
|
1106
|
+
if (!now || now.error) {
|
|
1107
|
+
part.broken.push({ path: `${at}.payload`, reason: "no result shape is served" });
|
|
1108
|
+
return part;
|
|
1109
|
+
}
|
|
1110
|
+
if (payload.kind !== now.kind) {
|
|
1111
|
+
part.broken.push({
|
|
1112
|
+
path: `${at}.payload`,
|
|
1113
|
+
reason: `result kind ${payload.kind}, served as ${String(now.kind)}`
|
|
1114
|
+
});
|
|
1115
|
+
return part;
|
|
1116
|
+
}
|
|
1117
|
+
if (payload.kind === "record" && payload.model) {
|
|
1118
|
+
if (payload.model !== now.model) {
|
|
1119
|
+
part.broken.push({
|
|
1120
|
+
path: `${at}.payload`,
|
|
1121
|
+
reason: `returns the model ${payload.model}, served as ${now.model ?? "none"}`
|
|
1122
|
+
});
|
|
1123
|
+
} else {
|
|
1124
|
+
part.prints.add(`m:${payload.model}`);
|
|
1125
|
+
}
|
|
1126
|
+
} else if (payload.kind === "union" && payload.union) {
|
|
1127
|
+
if (payload.union !== now.union) {
|
|
1128
|
+
part.broken.push({
|
|
1129
|
+
path: `${at}.payload`,
|
|
1130
|
+
reason: `returns the union ${payload.union}, served as ${now.union ?? "none"}`
|
|
1131
|
+
});
|
|
1132
|
+
} else {
|
|
1133
|
+
part.prints.add(`u:${payload.union}`);
|
|
1134
|
+
}
|
|
1135
|
+
} else if (payload.kind === "object") {
|
|
1136
|
+
compareFields(payload.fields ?? [], now.fields, `${at}.payload`, part);
|
|
1137
|
+
}
|
|
1138
|
+
return part;
|
|
1139
|
+
}
|
|
1140
|
+
function compareRelation(generated, served, at) {
|
|
1141
|
+
if (!served) return { path: at, reason: GONE };
|
|
1142
|
+
const targets = generated.targets ?? [];
|
|
1143
|
+
const nowTargets = served.targets ?? [];
|
|
1144
|
+
if (targets.length) {
|
|
1145
|
+
const dropped = targets.filter((target) => !nowTargets.includes(target));
|
|
1146
|
+
if (dropped.length) {
|
|
1147
|
+
return { path: at, reason: `no longer targets ${dropped.join(", ")}` };
|
|
1148
|
+
}
|
|
1149
|
+
} else if (nowTargets.length || (generated.target ?? "") !== (served.target ?? "")) {
|
|
1150
|
+
return {
|
|
1151
|
+
path: at,
|
|
1152
|
+
reason: `targets ${generated.target ?? "nothing"}, served as ${nowTargets.length ? nowTargets.join(" | ") : served.target ?? "nothing"}`
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
if (Boolean(generated.connection) !== Boolean(served.connection)) {
|
|
1156
|
+
return {
|
|
1157
|
+
path: at,
|
|
1158
|
+
reason: served.connection ? "now a connection" : "no longer a connection"
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
if (Boolean(generated.list) !== Boolean(served.list)) {
|
|
1162
|
+
return { path: at, reason: served.list ? "now a list" : "no longer a list" };
|
|
1163
|
+
}
|
|
1164
|
+
return void 0;
|
|
1165
|
+
}
|
|
1166
|
+
var NOTHING = [];
|
|
1167
|
+
function compareFoirSurfaces(generated, served) {
|
|
1168
|
+
const servedModels = served.models ?? {};
|
|
1169
|
+
const servedNamespaces = served.namespaces ?? {};
|
|
1170
|
+
const servedUnions = served.selectionUnions ?? {};
|
|
1171
|
+
const trees = /* @__PURE__ */ new Map();
|
|
1172
|
+
const ops = /* @__PURE__ */ new Map();
|
|
1173
|
+
const relations = /* @__PURE__ */ new Map();
|
|
1174
|
+
for (const [key, model] of Object.entries(generated.models ?? {})) {
|
|
1175
|
+
const at = `models.${key}`;
|
|
1176
|
+
const now = servedModels[key];
|
|
1177
|
+
const tree = newPart();
|
|
1178
|
+
trees.set(`m:${key}`, tree);
|
|
1179
|
+
if (!now) {
|
|
1180
|
+
const gone = { path: at, reason: GONE };
|
|
1181
|
+
tree.broken.push(gone);
|
|
1182
|
+
for (const op of Object.keys(model.ops ?? {})) {
|
|
1183
|
+
ops.set(`${at}.ops.${op}`, { broken: [gone], prints: /* @__PURE__ */ new Set() });
|
|
1184
|
+
}
|
|
1185
|
+
for (const relation of model.relations ?? []) {
|
|
1186
|
+
relations.set(`${key} ${relation.key}`, gone);
|
|
1187
|
+
}
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
if (model.typeName !== now.typeName) {
|
|
1191
|
+
tree.broken.push({
|
|
1192
|
+
path: `${at}.typeName`,
|
|
1193
|
+
reason: `type ${model.typeName}, served as ${now.typeName}`
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
if (model.selection) {
|
|
1197
|
+
if (!now.selection) tree.broken.push({ path: `${at}.selection`, reason: GONE });
|
|
1198
|
+
else compareFields(model.selection.fields, now.selection.fields, `${at}.selection`, tree);
|
|
1199
|
+
}
|
|
1200
|
+
for (const [op, def] of Object.entries(model.ops ?? {})) {
|
|
1201
|
+
ops.set(`${at}.ops.${op}`, compareOp(def, now.ops?.[op], `${at}.ops.${op}`));
|
|
1202
|
+
}
|
|
1203
|
+
for (const relation of model.relations ?? []) {
|
|
1204
|
+
const broken = compareRelation(
|
|
1205
|
+
relation,
|
|
1206
|
+
now.relations?.find((r) => r.key === relation.key),
|
|
1207
|
+
`${at}.relations.${relation.key}`
|
|
1208
|
+
);
|
|
1209
|
+
if (broken) relations.set(`${key} ${relation.key}`, broken);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
for (const [name, namespace] of Object.entries(generated.namespaces ?? {})) {
|
|
1213
|
+
const at = `namespaces.${name}`;
|
|
1214
|
+
const now = servedNamespaces[name];
|
|
1215
|
+
const gone = now ? void 0 : { path: at, reason: GONE };
|
|
1216
|
+
for (const [op, def] of Object.entries(namespace.ops ?? {})) {
|
|
1217
|
+
ops.set(
|
|
1218
|
+
`${at}.ops.${op}`,
|
|
1219
|
+
gone ? { broken: [gone], prints: /* @__PURE__ */ new Set() } : compareOp(def, now?.ops?.[op], `${at}.ops.${op}`)
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
for (const [name, union] of Object.entries(generated.selectionUnions ?? {})) {
|
|
1224
|
+
const at = `selectionUnions.${name}`;
|
|
1225
|
+
const now = servedUnions[name];
|
|
1226
|
+
const tree = newPart();
|
|
1227
|
+
trees.set(`u:${name}`, tree);
|
|
1228
|
+
if (!now) {
|
|
1229
|
+
tree.broken.push({ path: at, reason: GONE });
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
for (const member of union.members) {
|
|
1233
|
+
const nowMember = now.members?.find((m) => m.typeName === member.typeName);
|
|
1234
|
+
if (!nowMember) {
|
|
1235
|
+
tree.broken.push({ path: `${at}.${member.typeName}`, reason: "no longer a member" });
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
compareFields(member.fields, nowMember.fields, `${at}.${member.typeName}`, tree);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1242
|
+
for (const part of [...trees.values(), ...ops.values()]) {
|
|
1243
|
+
for (const broken of part.broken) paths.add(broken.path);
|
|
1244
|
+
}
|
|
1245
|
+
for (const broken of relations.values()) paths.add(broken.path);
|
|
1246
|
+
const incompatible = [...paths].sort();
|
|
1247
|
+
const reached = /* @__PURE__ */ new Map();
|
|
1248
|
+
const reach = (start) => {
|
|
1249
|
+
const held = reached.get(start);
|
|
1250
|
+
if (held) return held;
|
|
1251
|
+
const out = [];
|
|
1252
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1253
|
+
const pending = [start];
|
|
1254
|
+
while (pending.length) {
|
|
1255
|
+
const id = pending.pop();
|
|
1256
|
+
if (seen.has(id)) continue;
|
|
1257
|
+
seen.add(id);
|
|
1258
|
+
const tree = trees.get(id);
|
|
1259
|
+
if (!tree) continue;
|
|
1260
|
+
out.push(...tree.broken);
|
|
1261
|
+
pending.push(...tree.prints);
|
|
1262
|
+
}
|
|
1263
|
+
reached.set(start, out);
|
|
1264
|
+
return out;
|
|
1265
|
+
};
|
|
1266
|
+
const included = (modelKey, include, out) => {
|
|
1267
|
+
if (!include || typeof include !== "object") return;
|
|
1268
|
+
const model = generated.models[modelKey];
|
|
1269
|
+
if (!model) return;
|
|
1270
|
+
for (const [key, value] of Object.entries(include)) {
|
|
1271
|
+
if (value === void 0 || value === false) continue;
|
|
1272
|
+
const relation = model.relations?.find((r) => r.key === key);
|
|
1273
|
+
if (!relation) continue;
|
|
1274
|
+
const broken = relations.get(`${modelKey} ${key}`);
|
|
1275
|
+
if (broken) out.push(broken);
|
|
1276
|
+
const nested = value && typeof value === "object" ? value["include"] : void 0;
|
|
1277
|
+
const targets = relation.targets?.length ? relation.targets : [relation.target ?? ""];
|
|
1278
|
+
for (const target of targets) {
|
|
1279
|
+
out.push(...reach(`m:${target}`));
|
|
1280
|
+
included(target, nested, out);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
const call = (address, def, include) => {
|
|
1285
|
+
if (incompatible.length === 0) return NOTHING;
|
|
1286
|
+
const op = ops.get(address);
|
|
1287
|
+
if (!op || !def) return NOTHING;
|
|
1288
|
+
const out = [...op.broken];
|
|
1289
|
+
for (const id of op.prints) out.push(...reach(id));
|
|
1290
|
+
if (include && def.payload) {
|
|
1291
|
+
const records = new Set(payloadRecordPaths(def.payload).map((entry) => entry.modelKey));
|
|
1292
|
+
if (records.size === 1) included([...records][0], include, out);
|
|
1293
|
+
}
|
|
1294
|
+
if (out.length === 0) return NOTHING;
|
|
1295
|
+
const unique = new Map(out.map((broken) => [broken.path, broken]));
|
|
1296
|
+
return [...unique.values()].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
1297
|
+
};
|
|
1298
|
+
return {
|
|
1299
|
+
incompatible,
|
|
1300
|
+
modelCall: (model, op, include) => call(`models.${model}.ops.${op}`, generated.models[model]?.ops?.[op], include),
|
|
1301
|
+
namespaceCall: (namespace, op, include) => call(
|
|
1302
|
+
`namespaces.${namespace}.ops.${op}`,
|
|
1303
|
+
generated.namespaces?.[namespace]?.ops?.[op],
|
|
1304
|
+
include
|
|
1305
|
+
)
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1011
1309
|
// src/data/subscription-document.ts
|
|
1012
1310
|
function printSubscriptionDocument(input) {
|
|
1013
1311
|
const args = input.args ?? [];
|
|
@@ -1863,6 +2161,15 @@ function describeDrift(drift) {
|
|
|
1863
2161
|
const moved = describeChanged(drift.changed);
|
|
1864
2162
|
return `the types were generated from surface ${drift.generated}, and the server is serving ${drift.server} (${drift.channel}).` + (moved ? ` Changed since: ${moved}.` : "");
|
|
1865
2163
|
}
|
|
2164
|
+
var NAMED_INCOMPATIBLE_PARTS = 5;
|
|
2165
|
+
function nameParts(parts) {
|
|
2166
|
+
const named = parts.slice(0, NAMED_INCOMPATIBLE_PARTS).join(", ");
|
|
2167
|
+
const rest = parts.length - NAMED_INCOMPATIBLE_PARTS;
|
|
2168
|
+
return rest > 0 ? `${named} and ${rest} more` : named;
|
|
2169
|
+
}
|
|
2170
|
+
function describeIncompatible(parts) {
|
|
2171
|
+
return nameParts(parts.map((part) => `${part.path} (${part.reason})`));
|
|
2172
|
+
}
|
|
1866
2173
|
function announceFallback(config) {
|
|
1867
2174
|
if (config.generated !== void 0 || config.selections !== void 0) return;
|
|
1868
2175
|
if (config.announceManifestFallback === false) return;
|
|
@@ -1881,6 +2188,7 @@ function createFoirClient(clientConfig) {
|
|
|
1881
2188
|
announceFallback(config);
|
|
1882
2189
|
let driftChecked = false;
|
|
1883
2190
|
let detectedDrift;
|
|
2191
|
+
let servedComparison;
|
|
1884
2192
|
let manifestOnce;
|
|
1885
2193
|
const manifest = () => {
|
|
1886
2194
|
if (!manifestOnce) {
|
|
@@ -1909,32 +2217,51 @@ function createFoirClient(clientConfig) {
|
|
|
1909
2217
|
driftChecked = true;
|
|
1910
2218
|
void manifest().then((served) => {
|
|
1911
2219
|
if (!served.surfaceVersion || served.surfaceVersion === generated) return;
|
|
2220
|
+
const comparison = config.selections ? compareFoirSurfaces(
|
|
2221
|
+
{
|
|
2222
|
+
models: config.selections,
|
|
2223
|
+
namespaces: config.namespaceSelections,
|
|
2224
|
+
selectionUnions: config.selectionUnions
|
|
2225
|
+
},
|
|
2226
|
+
served
|
|
2227
|
+
) : void 0;
|
|
1912
2228
|
const drift = {
|
|
1913
2229
|
generated,
|
|
1914
2230
|
server: served.surfaceVersion,
|
|
1915
2231
|
channel: served.channel,
|
|
1916
|
-
changed: changedSurfaceParts(config.surfaceHashes, served.surfaceHashes)
|
|
2232
|
+
changed: changedSurfaceParts(config.surfaceHashes, served.surfaceHashes),
|
|
2233
|
+
incompatible: comparison?.incompatible ?? []
|
|
1917
2234
|
};
|
|
1918
2235
|
detectedDrift = drift;
|
|
2236
|
+
servedComparison = comparison;
|
|
1919
2237
|
if (config.onSchemaDrift) {
|
|
1920
2238
|
config.onSchemaDrift(drift);
|
|
1921
2239
|
return;
|
|
1922
2240
|
}
|
|
2241
|
+
if (drift.incompatible.length === 0) {
|
|
2242
|
+
console.warn(
|
|
2243
|
+
`[foir] schema drift: ${describeDrift(drift)} Everything the generated module describes is still served as generated, so every typed call keeps working. The surface has grown: re-run \`foir types\` to pick up what is new, because a field added since is not merely untyped, it is not requested.`
|
|
2244
|
+
);
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
const those = drift.incompatible.length === 1 ? "it" : "them";
|
|
1923
2248
|
console.warn(
|
|
1924
|
-
`[foir] schema drift: ${describeDrift(drift)}
|
|
2249
|
+
`[foir] schema drift: ${describeDrift(drift)} No longer served as the generated module describes: ${nameParts(drift.incompatible)}. ` + (config.strictSchemaDrift === false ? `Typed calls that depend on ${those} are still sent, and can fail or answer in another shape. ` : `Typed calls that depend on ${those} are refused. `) + `Re-run \`foir types\`.`
|
|
1925
2250
|
);
|
|
1926
2251
|
}).catch(() => {
|
|
1927
2252
|
});
|
|
1928
2253
|
};
|
|
1929
|
-
const
|
|
1930
|
-
if (config.strictSchemaDrift === false || !detectedDrift) return;
|
|
2254
|
+
const assertStillServed = (what, dependsOn) => {
|
|
2255
|
+
if (config.strictSchemaDrift === false || !detectedDrift || !servedComparison) return;
|
|
2256
|
+
const broken = dependsOn(servedComparison);
|
|
2257
|
+
if (broken.length === 0) return;
|
|
1931
2258
|
throw new FoirSchemaDriftError(
|
|
1932
|
-
`[foir] refusing ${what}: ${describeDrift(detectedDrift)}
|
|
2259
|
+
`[foir] refusing ${what}: ${describeDrift(detectedDrift)} This call depends on ${describeIncompatible(broken)}, which the server no longer serves as the generated module describes. Re-run \`foir types\`.`,
|
|
1933
2260
|
detectedDrift
|
|
1934
2261
|
);
|
|
1935
2262
|
};
|
|
1936
2263
|
const runModel = async (model, op, query) => {
|
|
1937
|
-
|
|
2264
|
+
assertStillServed(`${model}.${op}`, (served) => served.modelCall(model, op, query?.include));
|
|
1938
2265
|
const { args, include, omit, pick, depth } = splitQuery(query);
|
|
1939
2266
|
const source = await selectionSource();
|
|
1940
2267
|
const plan = planFoirModelCall(source, {
|
|
@@ -1952,7 +2279,10 @@ function createFoirClient(clientConfig) {
|
|
|
1952
2279
|
return result;
|
|
1953
2280
|
};
|
|
1954
2281
|
const runNamespace = async (namespace, op, query) => {
|
|
1955
|
-
|
|
2282
|
+
assertStillServed(
|
|
2283
|
+
`ns.${namespace}.${op}`,
|
|
2284
|
+
(served) => served.namespaceCall(namespace, op, query?.include)
|
|
2285
|
+
);
|
|
1956
2286
|
const { args, include, omit, pick, depth } = splitQuery(query);
|
|
1957
2287
|
const plan = planFoirNamespaceCall(await selectionSource(), {
|
|
1958
2288
|
namespace,
|