@mymehq/sdk 3.4.0 → 3.6.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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
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';
1
+ import { MergeStrategy, ConflictSnapshot, MergePolicy, ItemState, Item, CreateItemInput, PaginatedResult, ItemWithMetadata, Version, Edge, Metadata, SearchResult, CreateEdgeInput, EdgeTypeSchema, TypeSchema, CreateKeyInput, ApiKey, UpdateKeyInput, CreateWebhookInput, Webhook, UpdateWebhookInput, WebhookDelivery, TenantConfig } from '@mymehq/shared';
2
2
  export { ApiKey, ConflictSnapshot, CreateItemInput, CreateKeyInput, Item, ItemState, MergePolicy, MergeStrategy, Metadata, PaginatedResult, SearchResult, TypeSchema, UpdateKeyInput, Version } from '@mymehq/shared';
3
3
 
4
4
  /**
@@ -69,6 +69,21 @@ interface ClientConfig {
69
69
  cdnBaseUrl?: string;
70
70
  }
71
71
  interface UpdateOptions {
72
+ /**
73
+ * Version the caller expects the item to currently be at. When provided,
74
+ * the SDK skips the upfront `GET /items/:id` and PATCHes directly. If the
75
+ * server's actual version differs, the update 409s through the usual
76
+ * conflict path — the caller's retry policy is out of scope here.
77
+ *
78
+ * Prefer `expectedVersion` for bulk importers and sync loops that already
79
+ * know the version locally; one request instead of two.
80
+ */
81
+ expectedVersion?: number;
82
+ /**
83
+ * Legacy alias for `expectedVersion`. Kept for backwards compatibility;
84
+ * new code should use `expectedVersion`.
85
+ * @deprecated Use `expectedVersion`.
86
+ */
72
87
  version?: number;
73
88
  /**
74
89
  * Override the client's default conflict strategy for this update.
@@ -84,7 +99,10 @@ interface UpdateOptions {
84
99
  library?: boolean;
85
100
  /**
86
101
  * 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.
102
+ * conflict spawns a sibling item. Omit to let the SDK pre-fetch it
103
+ * when `expectedVersion` is also omitted the pre-fetch happens upfront;
104
+ * when `expectedVersion` is provided the fetch is deferred until a
105
+ * `keep_both_copies` conflict actually needs it.
88
106
  */
89
107
  type?: string;
90
108
  /**
@@ -145,6 +163,126 @@ interface SearchFilters {
145
163
  interface MetadataInput {
146
164
  tags?: string[];
147
165
  }
166
+ /** One item to create or upsert in a `POST /items/bulk` call. Shape is
167
+ * `CreateItemInput` plus compact outbound-only inline `edges`. */
168
+ interface BulkItemInput {
169
+ id?: string;
170
+ type: string;
171
+ properties?: Record<string, unknown>;
172
+ state?: ItemState;
173
+ library?: boolean;
174
+ timestamp?: string;
175
+ /** Ignored on the wire — server stamps `source` from the credential.
176
+ * Kept on the input shape for round-trip parity with /export output. */
177
+ source?: string;
178
+ source_id?: string;
179
+ origin?: "user" | "ai" | "worker";
180
+ device?: string;
181
+ tags?: string[];
182
+ /** Outbound edges to reconcile in the same transaction as the item
183
+ * write. Replace-all semantics per edge_type. Absent = untouched. */
184
+ edges?: Record<string, string[]>;
185
+ }
186
+ type BulkMode = "upsert" | "create_only";
187
+ interface BulkInput {
188
+ items: BulkItemInput[];
189
+ /** Default: `"upsert"`. */
190
+ mode?: BulkMode;
191
+ /** Default: `true`. When false, errors are collected per item and the
192
+ * batch continues past failures. */
193
+ atomic?: boolean;
194
+ /** Default: `false`. Per-item events are suppressed on bulk writes
195
+ * unless the caller opts in. */
196
+ emit_events?: boolean;
197
+ }
198
+ type BulkOutcome = "created" | "updated" | "skipped" | "errored";
199
+ interface BulkResultEntry {
200
+ index: number;
201
+ outcome: BulkOutcome;
202
+ id?: string;
203
+ reason?: string;
204
+ error?: {
205
+ code: string;
206
+ message: string;
207
+ };
208
+ }
209
+ interface BulkResult {
210
+ counts: {
211
+ created: number;
212
+ updated: number;
213
+ skipped: number;
214
+ errored: number;
215
+ };
216
+ results: BulkResultEntry[];
217
+ /** Present only on archive-restore paths. */
218
+ blobs_imported?: number;
219
+ }
220
+ /** Filter shape for `POST /items/bulk_action`. Mirrors the `GET /items`
221
+ * query grammar — every field is AND-composed, `filter` accepts the
222
+ * full filter-SQL DSL. */
223
+ interface BulkActionFilter {
224
+ type?: string;
225
+ state?: ItemState;
226
+ source?: string;
227
+ library?: boolean;
228
+ tags?: string[];
229
+ since?: string;
230
+ until?: string;
231
+ /** Same grammar as `GET /items?filter=`. `edge[type]=id` shorthand
232
+ * becomes `edge[type] eq "id"` here. */
233
+ filter?: string;
234
+ }
235
+ /** Shared knobs every bulk action accepts. */
236
+ interface BulkActionBase {
237
+ filter?: BulkActionFilter;
238
+ dry_run?: boolean;
239
+ max_items?: number;
240
+ emit_events?: boolean;
241
+ }
242
+ /** Discriminated union over the six bulk actions. The compiler pins the
243
+ * per-case parameters at the call site — no runtime string fiddling. */
244
+ type BulkActionInput = (BulkActionBase & {
245
+ action: "transition";
246
+ state: "active" | "archived" | "trashed";
247
+ }) | (BulkActionBase & {
248
+ action: "purge";
249
+ /** Required literal. The server returns `400 bulk_confirmation_required`
250
+ * without it; the SDK throws the same shape before sending. */
251
+ confirm: "PURGE";
252
+ }) | (BulkActionBase & {
253
+ action: "update_tags";
254
+ add?: string[];
255
+ remove?: string[];
256
+ }) | (BulkActionBase & {
257
+ action: "update_library";
258
+ library: boolean;
259
+ }) | (BulkActionBase & {
260
+ action: "update_properties";
261
+ patch: Record<string, unknown>;
262
+ }) | (BulkActionBase & {
263
+ action: "update_timestamp";
264
+ timestamp: string;
265
+ });
266
+ interface BulkActionErrorEntry {
267
+ id: string;
268
+ code: string;
269
+ message: string;
270
+ }
271
+ interface BulkActionResult {
272
+ action: string;
273
+ matched: number;
274
+ succeeded: number;
275
+ errored: number;
276
+ dry_run: boolean;
277
+ /** Present when matched / succeeded is small enough to be useful
278
+ * (dry-run always, otherwise when succeeded ≤ 100). */
279
+ ids?: string[];
280
+ errors?: BulkActionErrorEntry[];
281
+ /** Unique blob hashes referenced by items in a `purge` action. Not a
282
+ * strict orphan count — callers that need that should wait for blob
283
+ * GC to land. Omitted for non-purge actions. */
284
+ blob_hashes_referenced?: number;
285
+ }
148
286
  declare class MymeClient {
149
287
  private readonly transport;
150
288
  private readonly defaultConflictStrategy;
@@ -178,6 +316,25 @@ declare class MymeClient {
178
316
  transition: (id: string, state: string) => Promise<Item>;
179
317
  versions: (id: string) => Promise<Version[]>;
180
318
  stats: () => Promise<Record<string, number>>;
319
+ /**
320
+ * Create or upsert many items in one call (admin-only). Replaces the
321
+ * historical `/import` endpoint. Up to 5000 items per call.
322
+ *
323
+ * Modes: `"upsert"` (default) updates matching `(source, source_id)`
324
+ * rows in place; `"create_only"` surfaces matches as `skipped`.
325
+ * `atomic: true` (default) rolls back the whole batch on any failure.
326
+ */
327
+ bulk: (input: BulkInput) => Promise<BulkResult>;
328
+ /**
329
+ * Apply one action to every item matching a filter. Six actions
330
+ * discriminated on `action`. Purge is admin-only and requires
331
+ * `confirm: "PURGE"` — the SDK throws a `bulk_confirmation_required`
332
+ * `MymeError` client-side if you forget, matching the server's 400.
333
+ *
334
+ * Non-admin callers see their match set narrowed to writable types
335
+ * for every action except `purge`, which hard-403s.
336
+ */
337
+ bulkAction: (input: BulkActionInput) => Promise<BulkActionResult>;
181
338
  /** Outbound edges from this item. Shortcut for edges.listFromSource. */
182
339
  edges: (itemId: string, filters?: {
183
340
  edge_type?: string | string[];
@@ -330,7 +487,7 @@ declare class MymeError extends Error {
330
487
  readonly code: string;
331
488
  readonly status: number;
332
489
  readonly details?: Record<string, unknown>;
333
- constructor(code: string, message: string, status: number, details?: Record<string, unknown>);
490
+ constructor(code: string, message: string, status: number, details?: Record<string, unknown>, cause?: unknown);
334
491
  }
335
492
  declare class NotFoundError extends MymeError {
336
493
  constructor(message: string, details?: Record<string, unknown>);
@@ -404,4 +561,4 @@ interface VerifyWebhookSignatureInput {
404
561
  */
405
562
  declare function verifyWebhookSignature(input: VerifyWebhookSignatureInput): WebhookVerifyResult;
406
563
 
407
- 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, type VerifyWebhookSignatureInput, type WebhookVerifyReason, type WebhookVerifyResult, verifyWebhookSignature };
564
+ export { type BulkActionErrorEntry, type BulkActionFilter, type BulkActionInput, type BulkActionResult, type BulkInput, type BulkItemInput, type BulkMode, type BulkOutcome, type BulkResult, type BulkResultEntry, 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, type VerifyWebhookSignatureInput, type WebhookVerifyReason, type WebhookVerifyResult, verifyWebhookSignature };
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@ var MymeError = class extends Error {
3
3
  code;
4
4
  status;
5
5
  details;
6
- constructor(code, message, status, details) {
7
- super(message);
6
+ constructor(code, message, status, details, cause) {
7
+ super(message, cause !== void 0 ? { cause } : void 0);
8
8
  this.name = "MymeError";
9
9
  this.code = code;
10
10
  this.status = status;
@@ -68,7 +68,7 @@ var HttpTransport = class {
68
68
  if (response.status === 204) {
69
69
  return void 0;
70
70
  }
71
- const body = await response.json();
71
+ const body = await this.parseJson(response);
72
72
  if (!response.ok) {
73
73
  this.throwForError(response.status, body);
74
74
  }
@@ -81,7 +81,7 @@ var HttpTransport = class {
81
81
  */
82
82
  async requestWithConflict(method, path, options) {
83
83
  const response = await this.rawRequest(method, path, options);
84
- const body = await response.json();
84
+ const body = await this.parseJson(response);
85
85
  if (response.status === 409) {
86
86
  return body;
87
87
  }
@@ -109,10 +109,42 @@ var HttpTransport = class {
109
109
  }
110
110
  try {
111
111
  return await this.fetch(url, init);
112
+ } catch (err) {
113
+ if (err instanceof DOMException && err.name === "AbortError") {
114
+ throw new MymeError(
115
+ "timeout",
116
+ `Request to ${path} timed out after ${String(this.timeoutMs)}ms`,
117
+ 0,
118
+ { path, timeoutMs: this.timeoutMs },
119
+ err
120
+ );
121
+ }
122
+ const reason = err instanceof Error ? err.message : String(err);
123
+ throw new MymeError(
124
+ "network_error",
125
+ `Network request to ${path} failed: ${reason}`,
126
+ 0,
127
+ { path },
128
+ err
129
+ );
112
130
  } finally {
113
131
  clearTimeout(timeout);
114
132
  }
115
133
  }
134
+ async parseJson(response) {
135
+ try {
136
+ return await response.json();
137
+ } catch (err) {
138
+ const reason = err instanceof Error ? err.message : String(err);
139
+ throw new MymeError(
140
+ "parse_error",
141
+ `Failed to parse response body as JSON (HTTP ${String(response.status)}): ${reason}`,
142
+ 0,
143
+ { httpStatus: response.status },
144
+ err
145
+ );
146
+ }
147
+ }
116
148
  buildUrl(path, query) {
117
149
  const url = `${this.baseUrl}${path}`;
118
150
  if (!query) return url;
@@ -152,6 +184,13 @@ var HttpTransport = class {
152
184
  };
153
185
 
154
186
  // src/conflict.ts
187
+ async function fetchItemType(transport, itemId) {
188
+ const res = await transport.request(
189
+ "GET",
190
+ `/items/${itemId}`
191
+ );
192
+ return res.item.type;
193
+ }
155
194
  var MAX_RETRIES = 3;
156
195
  function isConflictResponse(body) {
157
196
  return typeof body === "object" && body !== null && "error" in body && "current" in body && "conflicting_fields" in body;
@@ -235,9 +274,10 @@ async function handleConflictUpdate(transport, itemId, itemType, clientPatch, ve
235
274
  const plan = planAutoMerge(conflict);
236
275
  let conflictedCopyId;
237
276
  if (plan.keepBothFields.length > 0) {
277
+ const effectiveType = itemType ?? await fetchItemType(transport, itemId);
238
278
  const sibling = await keepBothFlow(
239
279
  transport,
240
- itemType,
280
+ effectiveType,
241
281
  result.current.properties,
242
282
  clientPatch,
243
283
  plan.keepBothFields
@@ -341,11 +381,14 @@ var MymeClient = class {
341
381
  );
342
382
  },
343
383
  update: async (id, properties, options) => {
344
- let version = options?.version;
384
+ const expected = options?.expectedVersion ?? options?.version;
385
+ let version;
345
386
  let type = options?.type;
346
- if (version === void 0 || type === void 0) {
387
+ if (expected !== void 0) {
388
+ version = expected;
389
+ } else {
347
390
  const item = await this.items.get(id);
348
- version ??= item.version;
391
+ version = item.version;
349
392
  type ??= item.type;
350
393
  }
351
394
  const strategy = options?.conflict ?? this.defaultConflictStrategy;
@@ -401,6 +444,45 @@ var MymeClient = class {
401
444
  "/items/stats"
402
445
  );
403
446
  },
447
+ /**
448
+ * Create or upsert many items in one call (admin-only). Replaces the
449
+ * historical `/import` endpoint. Up to 5000 items per call.
450
+ *
451
+ * Modes: `"upsert"` (default) updates matching `(source, source_id)`
452
+ * rows in place; `"create_only"` surfaces matches as `skipped`.
453
+ * `atomic: true` (default) rolls back the whole batch on any failure.
454
+ */
455
+ bulk: async (input) => {
456
+ return this.transport.request("POST", "/items/bulk", {
457
+ body: input
458
+ });
459
+ },
460
+ /**
461
+ * Apply one action to every item matching a filter. Six actions
462
+ * discriminated on `action`. Purge is admin-only and requires
463
+ * `confirm: "PURGE"` — the SDK throws a `bulk_confirmation_required`
464
+ * `MymeError` client-side if you forget, matching the server's 400.
465
+ *
466
+ * Non-admin callers see their match set narrowed to writable types
467
+ * for every action except `purge`, which hard-403s.
468
+ */
469
+ bulkAction: async (input) => {
470
+ if (input.action === "purge") {
471
+ const confirm = input.confirm;
472
+ if (confirm !== "PURGE") {
473
+ throw new MymeError(
474
+ "bulk_confirmation_required",
475
+ "bulkAction({ action: 'purge' }) requires confirm: 'PURGE'",
476
+ 400
477
+ );
478
+ }
479
+ }
480
+ return this.transport.request(
481
+ "POST",
482
+ "/items/bulk_action",
483
+ { body: input }
484
+ );
485
+ },
404
486
  /** Outbound edges from this item. Shortcut for edges.listFromSource. */
405
487
  edges: (itemId, filters) => this.edges.listFromSource(itemId, filters),
406
488
  /** Inbound edges targeting this item. Shortcut for edges.listToTarget. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mymehq/sdk",
3
- "version": "3.4.0",
3
+ "version": "3.6.0",
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.4.0"
19
+ "@mymehq/shared": "3.5.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^22.0.0",