@kubun/connector 0.17.1 → 0.17.3
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/lib/authority.d.ts +15 -0
- package/lib/authority.js +19 -0
- package/lib/errors.d.ts +51 -0
- package/lib/errors.js +72 -0
- package/lib/index.d.ts +4 -1
- package/lib/index.js +3 -0
- package/lib/provider-utils.d.ts +13 -0
- package/lib/provider-utils.js +29 -0
- package/lib/types.d.ts +164 -8
- package/package.json +3 -3
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capability action a caller presents to execute a connector action
|
|
3
|
+
* (create/update) on a `did:kokuin:` owner's behalf. Gated BEFORE any provider
|
|
4
|
+
* credential I/O by the plugin-connector execution path — see
|
|
5
|
+
* `authorizeAction` in `@kubun/engine`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const CONNECTOR_EXECUTE_ACTION = "connector/execute";
|
|
8
|
+
/**
|
|
9
|
+
* Group-anchored resource specificity tiers for a `connector/execute`
|
|
10
|
+
* capability, most-specific first: the exact model, any model of the
|
|
11
|
+
* connector, then any connector in the group. Every tier is anchored to the
|
|
12
|
+
* group — there is no group-agnostic or global (`*`) tier, so a grant can
|
|
13
|
+
* never authorize a connector spend outside the group it was issued in.
|
|
14
|
+
*/
|
|
15
|
+
export declare function connectorExecuteResourceTiers(groupID: string, connector: string, model: string): Array<string>;
|
package/lib/authority.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capability action a caller presents to execute a connector action
|
|
3
|
+
* (create/update) on a `did:kokuin:` owner's behalf. Gated BEFORE any provider
|
|
4
|
+
* credential I/O by the plugin-connector execution path — see
|
|
5
|
+
* `authorizeAction` in `@kubun/engine`.
|
|
6
|
+
*/ export const CONNECTOR_EXECUTE_ACTION = 'connector/execute';
|
|
7
|
+
/**
|
|
8
|
+
* Group-anchored resource specificity tiers for a `connector/execute`
|
|
9
|
+
* capability, most-specific first: the exact model, any model of the
|
|
10
|
+
* connector, then any connector in the group. Every tier is anchored to the
|
|
11
|
+
* group — there is no group-agnostic or global (`*`) tier, so a grant can
|
|
12
|
+
* never authorize a connector spend outside the group it was issued in.
|
|
13
|
+
*/ export function connectorExecuteResourceTiers(groupID, connector, model) {
|
|
14
|
+
return [
|
|
15
|
+
`urn:kubun:connector:${groupID}/${connector}/${model}`,
|
|
16
|
+
`urn:kubun:connector:${groupID}/${connector}/*`,
|
|
17
|
+
`urn:kubun:connector:${groupID}/*`
|
|
18
|
+
];
|
|
19
|
+
}
|
package/lib/errors.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Machine-readable outcome classification for a failed connector action. */
|
|
2
|
+
export type ConnectorErrorKind = 'rejected' | 'unknown';
|
|
3
|
+
export type ConnectorErrorOptions = ErrorOptions & {
|
|
4
|
+
/** A machine-readable failure code. */
|
|
5
|
+
code?: string;
|
|
6
|
+
/** Whether the failure is a definite non-commit or outcome-unknown. */
|
|
7
|
+
kind?: ConnectorErrorKind;
|
|
8
|
+
/** The provider's HTTP status, when the failure came from a response. */
|
|
9
|
+
httpStatus?: number;
|
|
10
|
+
/** Set when the failure came after a provider write went out. */
|
|
11
|
+
sent?: boolean;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* A connector failure carrying the fields write-back recovery reads: an
|
|
15
|
+
* optional `code`, the outcome classification (`kind`), the provider HTTP
|
|
16
|
+
* status and whether the write already went out (`sent`). A field left unset
|
|
17
|
+
* reads `undefined`, which recovery treats as unclassified.
|
|
18
|
+
*/
|
|
19
|
+
export declare class ConnectorError extends Error {
|
|
20
|
+
#private;
|
|
21
|
+
constructor(message: string, options?: ConnectorErrorOptions);
|
|
22
|
+
get code(): string | undefined;
|
|
23
|
+
get kind(): ConnectorErrorKind | undefined;
|
|
24
|
+
get httpStatus(): number | undefined;
|
|
25
|
+
get sent(): boolean | undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Classify an HTTP failure by whether the write definitely did not commit.
|
|
29
|
+
* A 4xx (except 429 and 412) is a definite non-commit — the request was
|
|
30
|
+
* rejected and a retry will be rejected the same way. A 429 rate-limit, any
|
|
31
|
+
* 5xx, and any transport failure are outcome-unknown: the provider may have
|
|
32
|
+
* accepted the write before the failure surfaced, so a blind retry risks a
|
|
33
|
+
* duplicate. A 412 (If-Match precondition failed) is also outcome-unknown —
|
|
34
|
+
* it means the provider record changed under us, so recovery must reconcile
|
|
35
|
+
* rather than treat it as a terminal rejection.
|
|
36
|
+
*/
|
|
37
|
+
export declare function classifyHTTPStatus(status: number): ConnectorErrorKind;
|
|
38
|
+
/**
|
|
39
|
+
* The error a connector throws for a failed provider HTTP call, tagged with the
|
|
40
|
+
* HTTP status and its outcome classification (`kind`) so the caller can tell a
|
|
41
|
+
* definite rejection from an indeterminate failure.
|
|
42
|
+
*/
|
|
43
|
+
export declare function connectorHTTPError(message: string, status: number): ConnectorError;
|
|
44
|
+
/**
|
|
45
|
+
* Wraps a failure raised after a handler's provider write already went out
|
|
46
|
+
* (e.g. the read-back that follows a send). The write may have landed, so the
|
|
47
|
+
* failure is marked `sent` — never taken for a request that could not have
|
|
48
|
+
* reached the provider, and so never refunded — and classified `unknown`, never
|
|
49
|
+
* a definite rejection, whatever the later call's own status was.
|
|
50
|
+
*/
|
|
51
|
+
export declare function markWriteSent(cause: unknown): ConnectorError;
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Machine-readable outcome classification for a failed connector action. */ /**
|
|
2
|
+
* A connector failure carrying the fields write-back recovery reads: an
|
|
3
|
+
* optional `code`, the outcome classification (`kind`), the provider HTTP
|
|
4
|
+
* status and whether the write already went out (`sent`). A field left unset
|
|
5
|
+
* reads `undefined`, which recovery treats as unclassified.
|
|
6
|
+
*/ export class ConnectorError extends Error {
|
|
7
|
+
#code;
|
|
8
|
+
#kind;
|
|
9
|
+
#httpStatus;
|
|
10
|
+
#sent;
|
|
11
|
+
constructor(message, options = {}){
|
|
12
|
+
const { code, kind, httpStatus, sent, ...errorOptions } = options;
|
|
13
|
+
super(message, errorOptions);
|
|
14
|
+
this.name = 'ConnectorError';
|
|
15
|
+
this.#code = code;
|
|
16
|
+
this.#kind = kind;
|
|
17
|
+
this.#httpStatus = httpStatus;
|
|
18
|
+
this.#sent = sent;
|
|
19
|
+
}
|
|
20
|
+
get code() {
|
|
21
|
+
return this.#code;
|
|
22
|
+
}
|
|
23
|
+
get kind() {
|
|
24
|
+
return this.#kind;
|
|
25
|
+
}
|
|
26
|
+
get httpStatus() {
|
|
27
|
+
return this.#httpStatus;
|
|
28
|
+
}
|
|
29
|
+
get sent() {
|
|
30
|
+
return this.#sent;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Classify an HTTP failure by whether the write definitely did not commit.
|
|
35
|
+
* A 4xx (except 429 and 412) is a definite non-commit — the request was
|
|
36
|
+
* rejected and a retry will be rejected the same way. A 429 rate-limit, any
|
|
37
|
+
* 5xx, and any transport failure are outcome-unknown: the provider may have
|
|
38
|
+
* accepted the write before the failure surfaced, so a blind retry risks a
|
|
39
|
+
* duplicate. A 412 (If-Match precondition failed) is also outcome-unknown —
|
|
40
|
+
* it means the provider record changed under us, so recovery must reconcile
|
|
41
|
+
* rather than treat it as a terminal rejection.
|
|
42
|
+
*/ export function classifyHTTPStatus(status) {
|
|
43
|
+
return status >= 400 && status < 500 && status !== 429 && status !== 412 ? 'rejected' : 'unknown';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The error a connector throws for a failed provider HTTP call, tagged with the
|
|
47
|
+
* HTTP status and its outcome classification (`kind`) so the caller can tell a
|
|
48
|
+
* definite rejection from an indeterminate failure.
|
|
49
|
+
*/ export function connectorHTTPError(message, status) {
|
|
50
|
+
return new ConnectorError(message, {
|
|
51
|
+
httpStatus: status,
|
|
52
|
+
kind: classifyHTTPStatus(status)
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Wraps a failure raised after a handler's provider write already went out
|
|
57
|
+
* (e.g. the read-back that follows a send). The write may have landed, so the
|
|
58
|
+
* failure is marked `sent` — never taken for a request that could not have
|
|
59
|
+
* reached the provider, and so never refunded — and classified `unknown`, never
|
|
60
|
+
* a definite rejection, whatever the later call's own status was.
|
|
61
|
+
*/ export function markWriteSent(cause) {
|
|
62
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
63
|
+
const status = cause?.httpStatus;
|
|
64
|
+
return new ConnectorError(message, {
|
|
65
|
+
cause,
|
|
66
|
+
sent: true,
|
|
67
|
+
kind: 'unknown',
|
|
68
|
+
...typeof status === 'number' ? {
|
|
69
|
+
httpStatus: status
|
|
70
|
+
} : {}
|
|
71
|
+
});
|
|
72
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
export { CONNECTOR_EXECUTE_ACTION, connectorExecuteResourceTiers } from './authority.js';
|
|
1
2
|
export { clusterModelURNs, resolveModelID } from './cluster-utils.js';
|
|
3
|
+
export { ConnectorError, type ConnectorErrorKind, type ConnectorErrorOptions, classifyHTTPStatus, connectorHTTPError, markWriteSent, } from './errors.js';
|
|
2
4
|
export { connectorClusters, connectorModelIDs } from './models/cluster.js';
|
|
3
5
|
export { externalEntityDefinition } from './models/external-entity.js';
|
|
4
|
-
export
|
|
6
|
+
export { type FetchOrAbsentOptions, fetchOrAbsent, isNotFoundError, parseMaxAgeDays, type TargetedRead, } from './provider-utils.js';
|
|
7
|
+
export type { ActionDefinition, ActionHandler, ActionHandlerFactory, ActionResult, ActionTargetResolver, ActionTargetStorage, ActionWriteContext, ActionWriteResult, ClientDataProvider, ClientProviderFactory, ConnectorAuth, ConnectorDefinition, ConnectorSyncEventPayload, CreatedSourceIDPredictor, DataProvider, DeadLetter, DeviceConnectorAuth, DevicePermission, EntityBatch, EntityRecord, FetchChangesParams, FetchedAbsentEntity, FetchedEntity, FetchedPresentEntity, FetchParams, IntentCanonicalizer, IntentComparison, JSONValue, LocalAnnotationMerger, OAuthConnectorAuth, OAuthState, PendingOp, ReconcileResult, Recovery, ResolvedActionTarget, ServerDataProvider, ServerProviderFactory, SyncBoundary, SyncCompleteEvent, SyncErrorEvent, SyncEvent, SyncFailureSample, SyncProgressCASCurrent, SyncProgressCASResult, SyncProgressEvent, SyncProgressRecompute, SyncState, SyncStateEntry, SyncStateStore, SyncStatus, UserSyncPreferences, WebhookParams, WebhookRegistration, WriteAttachment, } from './types.js';
|
package/lib/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export { CONNECTOR_EXECUTE_ACTION, connectorExecuteResourceTiers } from './authority.js';
|
|
1
2
|
export { clusterModelURNs, resolveModelID } from './cluster-utils.js';
|
|
3
|
+
export { ConnectorError, classifyHTTPStatus, connectorHTTPError, markWriteSent } from './errors.js';
|
|
2
4
|
export { connectorClusters, connectorModelIDs } from './models/cluster.js';
|
|
3
5
|
export { externalEntityDefinition } from './models/external-entity.js';
|
|
6
|
+
export { fetchOrAbsent, isNotFoundError, parseMaxAgeDays } from './provider-utils.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EntityRecord, ReconcileResult, SyncBoundary } from './types.js';
|
|
2
|
+
export type TargetedRead = {
|
|
3
|
+
record: EntityRecord;
|
|
4
|
+
providerETag?: string;
|
|
5
|
+
};
|
|
6
|
+
export type FetchOrAbsentOptions = {
|
|
7
|
+
sourceID: string;
|
|
8
|
+
sourceFetchHLC: string;
|
|
9
|
+
read: () => Promise<TargetedRead>;
|
|
10
|
+
};
|
|
11
|
+
export declare function isNotFoundError(err: unknown): boolean;
|
|
12
|
+
export declare function fetchOrAbsent(options: FetchOrAbsentOptions): Promise<ReconcileResult>;
|
|
13
|
+
export declare function parseMaxAgeDays(boundary: SyncBoundary | undefined): number | null;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function isNotFoundError(err) {
|
|
2
|
+
return err?.httpStatus === 404;
|
|
3
|
+
}
|
|
4
|
+
export async function fetchOrAbsent(options) {
|
|
5
|
+
try {
|
|
6
|
+
const { record, providerETag } = await options.read();
|
|
7
|
+
return {
|
|
8
|
+
present: true,
|
|
9
|
+
record,
|
|
10
|
+
sourceFetchHLC: options.sourceFetchHLC,
|
|
11
|
+
...providerETag != null ? {
|
|
12
|
+
providerETag
|
|
13
|
+
} : {}
|
|
14
|
+
};
|
|
15
|
+
} catch (err) {
|
|
16
|
+
if (isNotFoundError(err)) {
|
|
17
|
+
return {
|
|
18
|
+
present: false,
|
|
19
|
+
sourceID: options.sourceID,
|
|
20
|
+
sourceFetchHLC: options.sourceFetchHLC
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function parseMaxAgeDays(boundary) {
|
|
27
|
+
const match = boundary?.maxAge != null ? /^P(\d+)D$/i.exec(boundary.maxAge) : null;
|
|
28
|
+
return match?.[1] != null ? Number.parseInt(match[1], 10) : null;
|
|
29
|
+
}
|
package/lib/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Credential } from '@kubun/credential-types';
|
|
1
|
+
import type { Credential, ProviderAccount } from '@kubun/credential-types';
|
|
2
2
|
import type { ClustersRecord } from '@kubun/protocol';
|
|
3
3
|
import type { JSONSchema } from 'json-schema-typed';
|
|
4
4
|
export type OAuthState = {
|
|
@@ -93,13 +93,25 @@ export type EntityRecord = Record<string, unknown> & {
|
|
|
93
93
|
sourceService: string;
|
|
94
94
|
sourceID: string;
|
|
95
95
|
};
|
|
96
|
+
export type FetchedEntity = {
|
|
97
|
+
present: true;
|
|
98
|
+
record: EntityRecord;
|
|
99
|
+
sourceFetchHLC: string;
|
|
100
|
+
} | {
|
|
101
|
+
present: false;
|
|
102
|
+
sourceID: string;
|
|
103
|
+
sourceFetchHLC: string;
|
|
104
|
+
};
|
|
105
|
+
export type FetchedPresentEntity = Extract<FetchedEntity, {
|
|
106
|
+
present: true;
|
|
107
|
+
}>;
|
|
108
|
+
export type FetchedAbsentEntity = Extract<FetchedEntity, {
|
|
109
|
+
present: false;
|
|
110
|
+
}>;
|
|
96
111
|
export type EntityBatch = {
|
|
97
112
|
modelID: string;
|
|
98
|
-
entities: Array<
|
|
99
|
-
deleted?: Array<
|
|
100
|
-
sourceService: string;
|
|
101
|
-
sourceID: string;
|
|
102
|
-
}>;
|
|
113
|
+
entities: Array<FetchedPresentEntity>;
|
|
114
|
+
deleted?: Array<FetchedAbsentEntity>;
|
|
103
115
|
checkpoint?: unknown;
|
|
104
116
|
hasMore: boolean;
|
|
105
117
|
};
|
|
@@ -111,6 +123,11 @@ export type FetchParams = {
|
|
|
111
123
|
export type FetchChangesParams = FetchParams & {
|
|
112
124
|
lastSyncedAt?: Date;
|
|
113
125
|
};
|
|
126
|
+
export type ReconcileResult = (FetchedPresentEntity & {
|
|
127
|
+
providerETag?: string;
|
|
128
|
+
}) | (FetchedAbsentEntity & {
|
|
129
|
+
providerETag?: string;
|
|
130
|
+
});
|
|
114
131
|
/**
|
|
115
132
|
* A source of external entities, driven by an opaque `checkpoint`.
|
|
116
133
|
*
|
|
@@ -128,6 +145,8 @@ export type FetchChangesParams = FetchParams & {
|
|
|
128
145
|
export type DataProvider = {
|
|
129
146
|
fetchAll(params: FetchParams): AsyncIterable<EntityBatch>;
|
|
130
147
|
fetchChanges(params: FetchChangesParams): AsyncIterable<EntityBatch>;
|
|
148
|
+
fetchEntity(model: string, sourceID: string, snapshot: unknown): Promise<FetchedEntity>;
|
|
149
|
+
reconcile(model: string, sourceID: string, snapshot: unknown): Promise<ReconcileResult>;
|
|
131
150
|
supportedStrategies: Array<'polling' | 'on-demand'>;
|
|
132
151
|
};
|
|
133
152
|
export type WebhookParams = {
|
|
@@ -166,6 +185,20 @@ export type ActionDefinition = {
|
|
|
166
185
|
output?: JSONSchema.Object;
|
|
167
186
|
entityModel?: string;
|
|
168
187
|
requiresCredential: boolean;
|
|
188
|
+
recoveryClass: 'reissue-safe' | 'reconcile-only';
|
|
189
|
+
/**
|
|
190
|
+
* The field of the target document holding the provider's conditional-write
|
|
191
|
+
* token (e.g. an ETag) as last fetched. An update pins it on the operation
|
|
192
|
+
* at claim — read from the local document, never re-fetched before the
|
|
193
|
+
* write — and hands it to the handler as `ActionWriteContext.providerETag`.
|
|
194
|
+
*/
|
|
195
|
+
providerETagField?: string;
|
|
196
|
+
/**
|
|
197
|
+
* The provider writes only against that token: an update whose target
|
|
198
|
+
* document holds none is refused before the operation is claimed (a sync
|
|
199
|
+
* first provides it), rather than claimed and then dead-lettered.
|
|
200
|
+
*/
|
|
201
|
+
providerETagRequired?: boolean;
|
|
169
202
|
};
|
|
170
203
|
export type ActionResult = {
|
|
171
204
|
raw?: Record<string, unknown>;
|
|
@@ -178,21 +211,123 @@ export type WriteAttachment = (input: ReadableStream<Uint8Array>, options?: {
|
|
|
178
211
|
encrypted?: boolean;
|
|
179
212
|
keyID?: string;
|
|
180
213
|
}) => Promise<string>;
|
|
214
|
+
export type ActionWriteResult = FetchedPresentEntity & {
|
|
215
|
+
providerETag?: string;
|
|
216
|
+
localChange?: boolean;
|
|
217
|
+
};
|
|
218
|
+
/** What the write-back layer knows about the operation a write belongs to. */
|
|
219
|
+
export type ActionWriteContext = {
|
|
220
|
+
/**
|
|
221
|
+
* The caller-derived deterministic identity of the intent the write carries
|
|
222
|
+
* (the operation envelope's `deterministicEventID`), the same on every
|
|
223
|
+
* attempt of the operation. A connector whose provider accepts a
|
|
224
|
+
* caller-chosen resource id derives that id from it, so a create re-issued
|
|
225
|
+
* after a lost response collides with its own earlier effect instead of
|
|
226
|
+
* duplicating it.
|
|
227
|
+
*/
|
|
228
|
+
deterministicEventID: string;
|
|
229
|
+
/**
|
|
230
|
+
* The provider's conditional-write token (e.g. an ETag) for the state the
|
|
231
|
+
* write is based on, as captured when that state was fetched and pinned on
|
|
232
|
+
* the operation at claim (see `ActionDefinition.providerETagField`). An
|
|
233
|
+
* update writes conditionally on it, so a provider change since then fails
|
|
234
|
+
* the write instead of being overwritten. Absent when unknown.
|
|
235
|
+
*/
|
|
236
|
+
providerETag?: string | null;
|
|
237
|
+
};
|
|
181
238
|
export type ActionHandler = {
|
|
182
|
-
create(model: string, params: Record<string, unknown
|
|
183
|
-
update(model: string, sourceID: string, params: Record<string, unknown>, existing?: Record<string, unknown
|
|
239
|
+
create(model: string, params: Record<string, unknown>, context?: ActionWriteContext): Promise<ActionWriteResult>;
|
|
240
|
+
update(model: string, sourceID: string, params: Record<string, unknown>, existing?: Record<string, unknown>, context?: ActionWriteContext): Promise<ActionWriteResult>;
|
|
184
241
|
};
|
|
185
242
|
export type ActionHandlerFactory = (config: {
|
|
186
243
|
credential: Credential;
|
|
187
244
|
writeAttachment?: WriteAttachment;
|
|
245
|
+
mintFetchHLC: () => string;
|
|
188
246
|
}) => ActionHandler;
|
|
189
247
|
export type ServerProviderFactory = (config: {
|
|
190
248
|
credential: unknown;
|
|
191
249
|
boundary: SyncBoundary;
|
|
250
|
+
mintFetchHLC: () => string;
|
|
192
251
|
}) => ServerDataProvider;
|
|
193
252
|
export type ClientProviderFactory = (config: {
|
|
194
253
|
boundary: SyncBoundary;
|
|
195
254
|
}) => ClientDataProvider;
|
|
255
|
+
/** A write's intent and a provider record, in the forms they are compared in. */
|
|
256
|
+
export type IntentComparison = {
|
|
257
|
+
intent: Record<string, unknown>;
|
|
258
|
+
record: Record<string, unknown>;
|
|
259
|
+
};
|
|
260
|
+
/**
|
|
261
|
+
* Brings a write's intent and a provider record into the forms the provider
|
|
262
|
+
* normalizes to, so a record that holds the intent compares equal field by
|
|
263
|
+
* field (e.g. a date-time the provider re-renders in another offset). Only an
|
|
264
|
+
* equivalence the provider guarantees may be folded in: anything looser would
|
|
265
|
+
* confirm an effect that never happened. `null` means the record cannot hold
|
|
266
|
+
* the intent at all.
|
|
267
|
+
*/
|
|
268
|
+
export type IntentCanonicalizer = (params: {
|
|
269
|
+
model: string;
|
|
270
|
+
op: 'create' | 'update';
|
|
271
|
+
intent: Record<string, unknown>;
|
|
272
|
+
record: EntityRecord;
|
|
273
|
+
}) => IntentComparison | null;
|
|
274
|
+
/**
|
|
275
|
+
* Carries the device-local annotations of an existing document into an
|
|
276
|
+
* incoming provider record that does not author them — e.g. an imported
|
|
277
|
+
* attachment's `blob`, nested in attachment metadata the provider does author
|
|
278
|
+
* — so a later provider write of the entity keeps them. Only local
|
|
279
|
+
* annotations may be carried over, never a provider-authored value.
|
|
280
|
+
*/
|
|
281
|
+
export type LocalAnnotationMerger = (params: {
|
|
282
|
+
model: string;
|
|
283
|
+
incoming: EntityRecord;
|
|
284
|
+
existing: Record<string, unknown>;
|
|
285
|
+
}) => EntityRecord;
|
|
286
|
+
/** Storage-only reads available to an {@link ActionTargetResolver}. */
|
|
287
|
+
export type ActionTargetStorage = {
|
|
288
|
+
/**
|
|
289
|
+
* The acting owner's live documents of one of this connector's models, as
|
|
290
|
+
* synced to this device (document data only). Never a provider call.
|
|
291
|
+
*/
|
|
292
|
+
documents(model: string): Promise<Array<Record<string, unknown>>>;
|
|
293
|
+
};
|
|
294
|
+
/** The canonical target of a write and the input it is written with. */
|
|
295
|
+
export type ResolvedActionTarget = {
|
|
296
|
+
/** The canonical provider target the write lands in (pinned on the operation). */
|
|
297
|
+
target: string;
|
|
298
|
+
/** The action input with any alias replaced by what it resolved to. */
|
|
299
|
+
input: Record<string, unknown>;
|
|
300
|
+
};
|
|
301
|
+
/**
|
|
302
|
+
* Derives the canonical target of a write from its own action input, before
|
|
303
|
+
* the operation is claimed and with storage-only access: an alias (a
|
|
304
|
+
* provider's "primary", say) is resolved from locally synced data, never by a
|
|
305
|
+
* provider call. The resolved input is what the operation pins and what the
|
|
306
|
+
* handler writes with. Throws a coded error when the target cannot be
|
|
307
|
+
* resolved locally.
|
|
308
|
+
*/
|
|
309
|
+
export type ActionTargetResolver = (params: {
|
|
310
|
+
model: string;
|
|
311
|
+
op: 'create' | 'update';
|
|
312
|
+
input: Record<string, unknown>;
|
|
313
|
+
sourceID?: string;
|
|
314
|
+
/** The provider account of the stored credential the write will spend. */
|
|
315
|
+
providerAccount: ProviderAccount;
|
|
316
|
+
storage: ActionTargetStorage;
|
|
317
|
+
}) => Promise<ResolvedActionTarget>;
|
|
318
|
+
/**
|
|
319
|
+
* Predicts, from a create's resolved input and its operation's deterministic
|
|
320
|
+
* event id alone (no storage, no provider call), the provider id the created
|
|
321
|
+
* entity will be stored under — for a provider whose create inserts under a
|
|
322
|
+
* caller-chosen id. `null` when the provider assigns the id itself, or when
|
|
323
|
+
* the input does not determine it. Every attempt of the operation must end
|
|
324
|
+
* up under the predicted id.
|
|
325
|
+
*/
|
|
326
|
+
export type CreatedSourceIDPredictor = (params: {
|
|
327
|
+
model: string;
|
|
328
|
+
input: Record<string, unknown>;
|
|
329
|
+
deterministicEventID: string;
|
|
330
|
+
}) => string | null;
|
|
196
331
|
export type ConnectorDefinition = {
|
|
197
332
|
name: string;
|
|
198
333
|
version: string;
|
|
@@ -200,6 +335,27 @@ export type ConnectorDefinition = {
|
|
|
200
335
|
syncStrategies: Array<'polling' | 'webhook' | 'on-demand'>;
|
|
201
336
|
actions?: Array<ActionDefinition>;
|
|
202
337
|
actionHandler?: ActionHandlerFactory;
|
|
338
|
+
/**
|
|
339
|
+
* How write-back recovery compares an operation's intent against the
|
|
340
|
+
* provider resource that should hold it. Absent, values compare exactly.
|
|
341
|
+
*/
|
|
342
|
+
canonicalizeIntent?: IntentCanonicalizer;
|
|
343
|
+
/**
|
|
344
|
+
* Applied to every provider record projected onto an existing document
|
|
345
|
+
* (sync, verification re-pull, an action's write result).
|
|
346
|
+
*/
|
|
347
|
+
mergeLocalAnnotations?: LocalAnnotationMerger;
|
|
348
|
+
/**
|
|
349
|
+
* How a write's canonical target is derived from its input. Absent, the
|
|
350
|
+
* caller must name it as a generic `input.target`.
|
|
351
|
+
*/
|
|
352
|
+
resolveActionTarget?: ActionTargetResolver;
|
|
353
|
+
/**
|
|
354
|
+
* The provider id a create will be stored under, when the connector chooses
|
|
355
|
+
* it (see {@link CreatedSourceIDPredictor}). Absent, a create's document is
|
|
356
|
+
* known only once the write commits.
|
|
357
|
+
*/
|
|
358
|
+
predictCreatedSourceID?: CreatedSourceIDPredictor;
|
|
203
359
|
serverProvider?: ServerProviderFactory;
|
|
204
360
|
clientProvider?: ClientProviderFactory;
|
|
205
361
|
graphql?: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/connector",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.3",
|
|
4
4
|
"keywords": [],
|
|
5
5
|
"license": "see LICENSE.md",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"LICENSE.md"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@kubun/credential-types": "^0.17.
|
|
19
|
-
"@kubun/protocol": "^0.17.
|
|
18
|
+
"@kubun/credential-types": "^0.17.2",
|
|
19
|
+
"@kubun/protocol": "^0.17.2",
|
|
20
20
|
"@kubun/scalars": "^0.17.0",
|
|
21
21
|
"json-schema-typed": "^8.0.2"
|
|
22
22
|
},
|