@mymehq/sdk 3.2.0 → 3.3.1
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/dist/index.d.ts +76 -5
- package/dist/index.js +86 -9
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { ConflictSnapshot, ItemState, CreateItemInput,
|
|
2
|
-
export { ApiKey, ConflictSnapshot, CreateItemInput, CreateKeyInput, Item, ItemState, Metadata, PaginatedResult, SearchResult, TypeSchema, UpdateKeyInput, Version } from '@mymehq/shared';
|
|
1
|
+
import { MergeStrategy, ConflictSnapshot, MergePolicy, Item, ItemState, CreateItemInput, PaginatedResult, ItemWithMetadata, Version, Edge, Metadata, SearchResult, CreateEdgeInput, EdgeTypeSchema, TypeSchema, CreateKeyInput, ApiKey, UpdateKeyInput, CreateWebhookInput, Webhook, UpdateWebhookInput, WebhookDelivery, TenantConfig } from '@mymehq/shared';
|
|
2
|
+
export { ApiKey, ConflictSnapshot, CreateItemInput, CreateKeyInput, Item, ItemState, MergePolicy, MergeStrategy, Metadata, PaginatedResult, SearchResult, TypeSchema, UpdateKeyInput, Version } from '@mymehq/shared';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Conflict resolution strategy for item updates.
|
|
6
6
|
*
|
|
7
|
-
* - `"auto"` (default):
|
|
8
|
-
*
|
|
7
|
+
* - `"auto"` (default): policy-aware. Per-field strategies declared on the
|
|
8
|
+
* type drive the merge. `last_writer_wins` fields take the server's value;
|
|
9
|
+
* `keep_both_copies` fields spawn a sibling item tagged `conflicted-copy`
|
|
10
|
+
* with the client's value, leaving the original at the server's value.
|
|
11
|
+
* The default for fields not listed in the policy is `last_writer_wins`.
|
|
9
12
|
* - `"manual"`: Throws a `ConflictError` with both versions and the list of
|
|
10
13
|
* conflicting fields. The caller decides how to resolve.
|
|
11
14
|
* - `"callback"`: Calls a custom `ConflictResolver` function with the conflict
|
|
@@ -17,8 +20,32 @@ interface ConflictData {
|
|
|
17
20
|
ancestor: ConflictSnapshot;
|
|
18
21
|
conflictingFields: string[];
|
|
19
22
|
clientPatch: Record<string, unknown>;
|
|
23
|
+
/** Resolved per-field merge policy for the conflicting item's type. */
|
|
24
|
+
mergePolicy: MergePolicy;
|
|
20
25
|
}
|
|
21
26
|
type ConflictResolver = (conflict: ConflictData) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
27
|
+
/**
|
|
28
|
+
* Notification payload emitted by the auto-merge path. Surfaces the per-field
|
|
29
|
+
* outcome so callers can present an accurate UI affordance (e.g. "your edit
|
|
30
|
+
* was saved as a conflicted copy").
|
|
31
|
+
*
|
|
32
|
+
* - `itemId` — the original item that was being updated.
|
|
33
|
+
* - `mergedItemId` — the same id; included so listeners that care about
|
|
34
|
+
* identity in event streams have a stable field.
|
|
35
|
+
* - `conflictedCopyId` — present when at least one `keep_both_copies` field
|
|
36
|
+
* conflicted; the id of the spawned sibling that carries the client's edit.
|
|
37
|
+
* - `fields` — the list of fields that conflicted.
|
|
38
|
+
* - `strategy` — keyed by field name, the strategy applied to each.
|
|
39
|
+
*/
|
|
40
|
+
interface ConflictAutoMergedEvent {
|
|
41
|
+
itemId: string;
|
|
42
|
+
mergedItemId: string;
|
|
43
|
+
conflictedCopyId?: string;
|
|
44
|
+
fields: string[];
|
|
45
|
+
strategy: Record<string, MergeStrategy>;
|
|
46
|
+
}
|
|
47
|
+
/** Optional callback fired when the auto-merge path completes successfully. */
|
|
48
|
+
type ConflictAutoMergeListener = (event: ConflictAutoMergedEvent) => void | Promise<void>;
|
|
22
49
|
|
|
23
50
|
interface ClientConfig {
|
|
24
51
|
url: string;
|
|
@@ -31,6 +58,13 @@ interface ClientConfig {
|
|
|
31
58
|
* use the server's value.
|
|
32
59
|
*/
|
|
33
60
|
conflictStrategy?: ConflictStrategy;
|
|
61
|
+
/**
|
|
62
|
+
* Default listener invoked after the auto-merge path completes successfully.
|
|
63
|
+
* Receives a `ConflictAutoMergedEvent` describing the per-field outcome and
|
|
64
|
+
* any spawned conflicted-copy id. Per-call overridable via
|
|
65
|
+
* `UpdateOptions.onAutoMerge`.
|
|
66
|
+
*/
|
|
67
|
+
onConflictAutoMerge?: ConflictAutoMergeListener;
|
|
34
68
|
timeoutMs?: number;
|
|
35
69
|
cdnBaseUrl?: string;
|
|
36
70
|
}
|
|
@@ -48,6 +82,16 @@ interface UpdateOptions {
|
|
|
48
82
|
/** Toggle library / ambient state. Independent of the version-merge path
|
|
49
83
|
* for `properties`; a library-only update never conflicts. */
|
|
50
84
|
library?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Item type. Required by the `auto` strategy when a `keep_both_copies`
|
|
87
|
+
* conflict spawns a sibling item. Omit to let the SDK pre-fetch it.
|
|
88
|
+
*/
|
|
89
|
+
type?: string;
|
|
90
|
+
/**
|
|
91
|
+
* Listener invoked after the auto-merge path completes successfully.
|
|
92
|
+
* Overrides the client's default `onConflictAutoMerge` for this call.
|
|
93
|
+
*/
|
|
94
|
+
onAutoMerge?: ConflictAutoMergeListener;
|
|
51
95
|
}
|
|
52
96
|
interface ListFilters {
|
|
53
97
|
type?: string;
|
|
@@ -68,7 +112,25 @@ interface ListFilters {
|
|
|
68
112
|
until?: string;
|
|
69
113
|
limit?: number;
|
|
70
114
|
cursor?: string;
|
|
115
|
+
/**
|
|
116
|
+
* Opt-in hydrations on the list response. Comma-separated values; each
|
|
117
|
+
* value widens the per-item shape. Prefer the typed helpers
|
|
118
|
+
* (`listWithMetadata`, `listWithExtensions`) over raw strings.
|
|
119
|
+
*
|
|
120
|
+
* - `metadata` — wraps each entry as `{ item, metadata }`.
|
|
121
|
+
* - `edges` — hydrates outbound edges per item grouped by type.
|
|
122
|
+
* - `extensions` — hydrates extension namespaces (filtered by caller
|
|
123
|
+
* permissions, same rule as `GET /items/:id/extensions`).
|
|
124
|
+
*
|
|
125
|
+
* Lists are lean by default; opt into extras when the UI needs them
|
|
126
|
+
* to avoid N+1 per-item round trips.
|
|
127
|
+
*/
|
|
128
|
+
include?: string;
|
|
71
129
|
}
|
|
130
|
+
/** Item paired with its hydrated extension namespaces. */
|
|
131
|
+
type ItemWithExtensions = Item & {
|
|
132
|
+
extensions: Record<string, Record<string, unknown>>;
|
|
133
|
+
};
|
|
72
134
|
interface SearchFilters {
|
|
73
135
|
type?: string;
|
|
74
136
|
state?: ItemState;
|
|
@@ -86,6 +148,7 @@ interface MetadataInput {
|
|
|
86
148
|
declare class MymeClient {
|
|
87
149
|
private readonly transport;
|
|
88
150
|
private readonly defaultConflictStrategy;
|
|
151
|
+
private readonly defaultOnConflictAutoMerge?;
|
|
89
152
|
private readonly apiBaseUrl;
|
|
90
153
|
private readonly cdnBaseUrl?;
|
|
91
154
|
constructor(config: ClientConfig);
|
|
@@ -98,6 +161,14 @@ declare class MymeClient {
|
|
|
98
161
|
get: (id: string) => Promise<Item>;
|
|
99
162
|
list: (filters?: ListFilters) => Promise<PaginatedResult<Item>>;
|
|
100
163
|
listWithMetadata: (filters?: Omit<ListFilters, "include">) => Promise<PaginatedResult<ItemWithMetadata>>;
|
|
164
|
+
/**
|
|
165
|
+
* List items with their extension namespaces hydrated inline. Avoids
|
|
166
|
+
* the N+1 pattern of listing then calling `GET /items/:id/extensions`
|
|
167
|
+
* per item. Extensions are filtered by the caller's permissions —
|
|
168
|
+
* admins see every namespace, members see what they can read or
|
|
169
|
+
* write.
|
|
170
|
+
*/
|
|
171
|
+
listWithExtensions: (filters?: Omit<ListFilters, "include">) => Promise<PaginatedResult<ItemWithExtensions>>;
|
|
101
172
|
update: (id: string, properties: Record<string, unknown>, options?: UpdateOptions) => Promise<Item>;
|
|
102
173
|
delete: (id: string) => Promise<void>;
|
|
103
174
|
/** Permanently delete a trashed item (admin only). Item must already
|
|
@@ -281,4 +352,4 @@ declare class ConflictError extends MymeError {
|
|
|
281
352
|
constructor(current: ConflictSnapshot, ancestor: ConflictSnapshot, conflictingFields: string[], clientPatch: Record<string, unknown>);
|
|
282
353
|
}
|
|
283
354
|
|
|
284
|
-
export { type ClientConfig, type ConflictData, ConflictError, type ConflictResolver, type ConflictStrategy, ForbiddenError, type ListFilters, type MetadataInput, MymeClient, MymeError, NotFoundError, type SearchFilters, UnauthorizedError, type UpdateOptions, ValidationError };
|
|
355
|
+
export { type ClientConfig, type ConflictAutoMergeListener, type ConflictAutoMergedEvent, type ConflictData, ConflictError, type ConflictResolver, type ConflictStrategy, ForbiddenError, type ItemWithExtensions, type ListFilters, type MetadataInput, MymeClient, MymeError, NotFoundError, type SearchFilters, UnauthorizedError, type UpdateOptions, ValidationError };
|
package/dist/index.js
CHANGED
|
@@ -156,14 +156,41 @@ var MAX_RETRIES = 3;
|
|
|
156
156
|
function isConflictResponse(body) {
|
|
157
157
|
return typeof body === "object" && body !== null && "error" in body && "current" in body && "conflicting_fields" in body;
|
|
158
158
|
}
|
|
159
|
-
function
|
|
159
|
+
function strategyFor(field, policy) {
|
|
160
|
+
return policy?.fields?.[field] ?? policy?.default ?? "last_writer_wins";
|
|
161
|
+
}
|
|
162
|
+
function planAutoMerge(conflict) {
|
|
160
163
|
const merged = { ...conflict.current.properties };
|
|
164
|
+
const keepBothFields = [];
|
|
165
|
+
const strategyByField = {};
|
|
161
166
|
for (const [key, value] of Object.entries(conflict.clientPatch)) {
|
|
162
|
-
|
|
167
|
+
const isConflicting = conflict.conflictingFields.includes(key);
|
|
168
|
+
if (!isConflicting) {
|
|
163
169
|
merged[key] = value;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const strategy = strategyFor(key, conflict.mergePolicy);
|
|
173
|
+
strategyByField[key] = strategy;
|
|
174
|
+
if (strategy === "keep_both_copies") {
|
|
175
|
+
keepBothFields.push(key);
|
|
164
176
|
}
|
|
165
177
|
}
|
|
166
|
-
return merged;
|
|
178
|
+
return { merged, keepBothFields, strategyByField };
|
|
179
|
+
}
|
|
180
|
+
async function keepBothFlow(transport, type, current, clientPatch, keepBothFields) {
|
|
181
|
+
const properties = { ...current };
|
|
182
|
+
for (const field of keepBothFields) {
|
|
183
|
+
properties[field] = clientPatch[field];
|
|
184
|
+
}
|
|
185
|
+
const input = {
|
|
186
|
+
type,
|
|
187
|
+
properties,
|
|
188
|
+
tags: ["conflicted-copy"]
|
|
189
|
+
};
|
|
190
|
+
const res = await transport.request("POST", "/items", {
|
|
191
|
+
body: input
|
|
192
|
+
});
|
|
193
|
+
return res.item;
|
|
167
194
|
}
|
|
168
195
|
function toConflictError(response, clientPatch) {
|
|
169
196
|
return new ConflictError(
|
|
@@ -173,9 +200,10 @@ function toConflictError(response, clientPatch) {
|
|
|
173
200
|
clientPatch
|
|
174
201
|
);
|
|
175
202
|
}
|
|
176
|
-
async function handleConflictUpdate(transport, itemId, clientPatch, version, strategy, resolver, library) {
|
|
203
|
+
async function handleConflictUpdate(transport, itemId, itemType, clientPatch, version, strategy, resolver, library, onAutoMerge) {
|
|
177
204
|
let properties = clientPatch;
|
|
178
205
|
let currentVersion = version;
|
|
206
|
+
let pendingAutoMergeEvent;
|
|
179
207
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
180
208
|
const result = await transport.requestWithConflict("PATCH", `/items/${itemId}`, {
|
|
181
209
|
body: {
|
|
@@ -185,6 +213,9 @@ async function handleConflictUpdate(transport, itemId, clientPatch, version, str
|
|
|
185
213
|
}
|
|
186
214
|
});
|
|
187
215
|
if (!isConflictResponse(result)) {
|
|
216
|
+
if (pendingAutoMergeEvent && onAutoMerge) {
|
|
217
|
+
await onAutoMerge(pendingAutoMergeEvent);
|
|
218
|
+
}
|
|
188
219
|
return result.item;
|
|
189
220
|
}
|
|
190
221
|
if (strategy === "manual") {
|
|
@@ -197,10 +228,30 @@ async function handleConflictUpdate(transport, itemId, clientPatch, version, str
|
|
|
197
228
|
current: result.current,
|
|
198
229
|
ancestor: result.ancestor,
|
|
199
230
|
conflictingFields: result.conflicting_fields,
|
|
200
|
-
clientPatch
|
|
231
|
+
clientPatch,
|
|
232
|
+
mergePolicy: result.merge_policy
|
|
201
233
|
};
|
|
202
234
|
if (strategy === "auto") {
|
|
203
|
-
|
|
235
|
+
const plan = planAutoMerge(conflict);
|
|
236
|
+
let conflictedCopyId;
|
|
237
|
+
if (plan.keepBothFields.length > 0) {
|
|
238
|
+
const sibling = await keepBothFlow(
|
|
239
|
+
transport,
|
|
240
|
+
itemType,
|
|
241
|
+
result.current.properties,
|
|
242
|
+
clientPatch,
|
|
243
|
+
plan.keepBothFields
|
|
244
|
+
);
|
|
245
|
+
conflictedCopyId = sibling.id;
|
|
246
|
+
}
|
|
247
|
+
properties = plan.merged;
|
|
248
|
+
pendingAutoMergeEvent = {
|
|
249
|
+
itemId,
|
|
250
|
+
mergedItemId: itemId,
|
|
251
|
+
conflictedCopyId,
|
|
252
|
+
fields: conflict.conflictingFields,
|
|
253
|
+
strategy: plan.strategyByField
|
|
254
|
+
};
|
|
204
255
|
} else {
|
|
205
256
|
if (!resolver) {
|
|
206
257
|
throw toConflictError(result, clientPatch);
|
|
@@ -221,6 +272,7 @@ async function handleConflictUpdate(transport, itemId, clientPatch, version, str
|
|
|
221
272
|
var MymeClient = class {
|
|
222
273
|
transport;
|
|
223
274
|
defaultConflictStrategy;
|
|
275
|
+
defaultOnConflictAutoMerge;
|
|
224
276
|
apiBaseUrl;
|
|
225
277
|
cdnBaseUrl;
|
|
226
278
|
constructor(config) {
|
|
@@ -232,6 +284,7 @@ var MymeClient = class {
|
|
|
232
284
|
timeoutMs: config.timeoutMs
|
|
233
285
|
});
|
|
234
286
|
this.defaultConflictStrategy = config.conflictStrategy ?? "auto";
|
|
287
|
+
this.defaultOnConflictAutoMerge = config.onConflictAutoMerge;
|
|
235
288
|
this.cdnBaseUrl = config.cdnBaseUrl;
|
|
236
289
|
}
|
|
237
290
|
// ---- Items ----
|
|
@@ -268,21 +321,45 @@ var MymeClient = class {
|
|
|
268
321
|
}
|
|
269
322
|
);
|
|
270
323
|
},
|
|
324
|
+
/**
|
|
325
|
+
* List items with their extension namespaces hydrated inline. Avoids
|
|
326
|
+
* the N+1 pattern of listing then calling `GET /items/:id/extensions`
|
|
327
|
+
* per item. Extensions are filtered by the caller's permissions —
|
|
328
|
+
* admins see every namespace, members see what they can read or
|
|
329
|
+
* write.
|
|
330
|
+
*/
|
|
331
|
+
listWithExtensions: async (filters) => {
|
|
332
|
+
return this.transport.request(
|
|
333
|
+
"GET",
|
|
334
|
+
"/items",
|
|
335
|
+
{
|
|
336
|
+
query: {
|
|
337
|
+
...filters,
|
|
338
|
+
include: "extensions"
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
);
|
|
342
|
+
},
|
|
271
343
|
update: async (id, properties, options) => {
|
|
272
344
|
let version = options?.version;
|
|
273
|
-
|
|
345
|
+
let type = options?.type;
|
|
346
|
+
if (version === void 0 || type === void 0) {
|
|
274
347
|
const item = await this.items.get(id);
|
|
275
|
-
version
|
|
348
|
+
version ??= item.version;
|
|
349
|
+
type ??= item.type;
|
|
276
350
|
}
|
|
277
351
|
const strategy = options?.conflict ?? this.defaultConflictStrategy;
|
|
352
|
+
const onAutoMerge = options?.onAutoMerge ?? this.defaultOnConflictAutoMerge;
|
|
278
353
|
return handleConflictUpdate(
|
|
279
354
|
this.transport,
|
|
280
355
|
id,
|
|
356
|
+
type,
|
|
281
357
|
properties,
|
|
282
358
|
version,
|
|
283
359
|
strategy,
|
|
284
360
|
options?.resolve,
|
|
285
|
-
options?.library
|
|
361
|
+
options?.library,
|
|
362
|
+
onAutoMerge
|
|
286
363
|
);
|
|
287
364
|
},
|
|
288
365
|
delete: async (id) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mymehq/sdk",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"dist"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@mymehq/shared": "3.
|
|
19
|
+
"@mymehq/shared": "3.3.1"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^22.0.0",
|