@mymehq/sdk 1.0.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 +184 -0
- package/dist/index.js +576 -0
- package/package.json +33 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { ConflictSnapshot, ItemState, CreateItemInput, Item, PaginatedResult, ItemWithMetadata, Version, Metadata, SearchResult, Thread, TypeSchema, CreateKeyInput, ApiKey, CreateWebhookInput, Webhook, UpdateWebhookInput, WebhookDelivery } from '@mymehq/shared';
|
|
2
|
+
export { ApiKey, ConflictSnapshot, CreateItemInput, CreateKeyInput, Item, ItemState, Metadata, PaginatedResult, SearchResult, Thread, TypeSchema, Version } from '@mymehq/shared';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Conflict resolution strategy for item updates.
|
|
6
|
+
*
|
|
7
|
+
* - `"auto"` (default): Non-conflicting field changes merge automatically.
|
|
8
|
+
* Conflicting fields use the server's current value. Retries up to 3 times.
|
|
9
|
+
* - `"manual"`: Throws a `ConflictError` with both versions and the list of
|
|
10
|
+
* conflicting fields. The caller decides how to resolve.
|
|
11
|
+
* - `"callback"`: Calls a custom `ConflictResolver` function with the conflict
|
|
12
|
+
* data. The resolver returns the merged properties to submit.
|
|
13
|
+
*/
|
|
14
|
+
type ConflictStrategy = "auto" | "manual" | "callback";
|
|
15
|
+
interface ConflictData {
|
|
16
|
+
current: ConflictSnapshot;
|
|
17
|
+
ancestor: ConflictSnapshot;
|
|
18
|
+
conflictingFields: string[];
|
|
19
|
+
clientPatch: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
type ConflictResolver = (conflict: ConflictData) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
22
|
+
|
|
23
|
+
interface ClientConfig {
|
|
24
|
+
url: string;
|
|
25
|
+
apiKey: string;
|
|
26
|
+
fetch?: typeof globalThis.fetch;
|
|
27
|
+
/**
|
|
28
|
+
* Default conflict resolution strategy for all updates.
|
|
29
|
+
* Can be overridden per-call via `UpdateOptions.conflict`.
|
|
30
|
+
* Defaults to `"auto"` — non-conflicting fields merge, conflicting fields
|
|
31
|
+
* use the server's value.
|
|
32
|
+
*/
|
|
33
|
+
conflictStrategy?: ConflictStrategy;
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
cdnBaseUrl?: string;
|
|
36
|
+
}
|
|
37
|
+
interface UpdateOptions {
|
|
38
|
+
version?: number;
|
|
39
|
+
thread_id?: string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Override the client's default conflict strategy for this update.
|
|
42
|
+
* - `"auto"`: auto-merge non-conflicting fields (default)
|
|
43
|
+
* - `"manual"`: throw `ConflictError` with both versions
|
|
44
|
+
* - `"callback"`: call the `resolve` function to handle the conflict
|
|
45
|
+
*/
|
|
46
|
+
conflict?: ConflictStrategy;
|
|
47
|
+
/** Custom conflict resolver (required when `conflict` is `"callback"`). */
|
|
48
|
+
resolve?: ConflictResolver;
|
|
49
|
+
}
|
|
50
|
+
interface ListFilters {
|
|
51
|
+
type?: string;
|
|
52
|
+
state?: ItemState;
|
|
53
|
+
source?: string;
|
|
54
|
+
parent_id?: string;
|
|
55
|
+
thread_id?: string;
|
|
56
|
+
root_only?: boolean;
|
|
57
|
+
tags?: string[];
|
|
58
|
+
filter?: string;
|
|
59
|
+
sort?: "created_at" | "updated_at" | "timestamp";
|
|
60
|
+
direction?: "asc" | "desc";
|
|
61
|
+
since?: string;
|
|
62
|
+
until?: string;
|
|
63
|
+
limit?: number;
|
|
64
|
+
cursor?: string;
|
|
65
|
+
}
|
|
66
|
+
interface SearchFilters {
|
|
67
|
+
type?: string;
|
|
68
|
+
state?: ItemState;
|
|
69
|
+
filter?: string;
|
|
70
|
+
limit?: number;
|
|
71
|
+
}
|
|
72
|
+
interface MetadataInput {
|
|
73
|
+
tags?: string[];
|
|
74
|
+
about?: string[];
|
|
75
|
+
}
|
|
76
|
+
declare class MymeClient {
|
|
77
|
+
private readonly transport;
|
|
78
|
+
private readonly defaultConflictStrategy;
|
|
79
|
+
private readonly apiBaseUrl;
|
|
80
|
+
private readonly cdnBaseUrl?;
|
|
81
|
+
constructor(config: ClientConfig);
|
|
82
|
+
readonly items: {
|
|
83
|
+
create: (input: CreateItemInput) => Promise<Item>;
|
|
84
|
+
get: (id: string) => Promise<Item>;
|
|
85
|
+
list: (filters?: ListFilters) => Promise<PaginatedResult<Item>>;
|
|
86
|
+
listWithMetadata: (filters?: Omit<ListFilters, "include">) => Promise<PaginatedResult<ItemWithMetadata>>;
|
|
87
|
+
update: (id: string, properties: Record<string, unknown>, options?: UpdateOptions) => Promise<Item>;
|
|
88
|
+
delete: (id: string) => Promise<void>;
|
|
89
|
+
restore: (id: string) => Promise<Item>;
|
|
90
|
+
transition: (id: string, state: string) => Promise<Item>;
|
|
91
|
+
versions: (id: string) => Promise<Version[]>;
|
|
92
|
+
stats: () => Promise<Record<string, number>>;
|
|
93
|
+
};
|
|
94
|
+
readonly metadata: {
|
|
95
|
+
get: (itemId: string) => Promise<Metadata>;
|
|
96
|
+
set: (itemId: string, input: MetadataInput) => Promise<Metadata>;
|
|
97
|
+
merge: (itemId: string, input: Partial<MetadataInput>) => Promise<Metadata>;
|
|
98
|
+
addTags: (itemId: string, tags: string[]) => Promise<Metadata>;
|
|
99
|
+
removeTag: (itemId: string, tag: string) => Promise<void>;
|
|
100
|
+
getExtensions: (itemId: string, namespace?: string) => Promise<Record<string, Record<string, unknown>>>;
|
|
101
|
+
setExtension: (itemId: string, namespace: string, data: Record<string, unknown>) => Promise<Record<string, Record<string, unknown>>>;
|
|
102
|
+
deleteExtension: (itemId: string, namespace: string) => Promise<void>;
|
|
103
|
+
};
|
|
104
|
+
search(query: string, filters?: SearchFilters): Promise<SearchResult[]>;
|
|
105
|
+
readonly threads: {
|
|
106
|
+
create: () => Promise<Thread>;
|
|
107
|
+
list: (filters?: {
|
|
108
|
+
limit?: number;
|
|
109
|
+
cursor?: string;
|
|
110
|
+
}) => Promise<PaginatedResult<Thread>>;
|
|
111
|
+
get: (id: string) => Promise<{
|
|
112
|
+
thread: Thread;
|
|
113
|
+
items: Item[];
|
|
114
|
+
}>;
|
|
115
|
+
addItem: (threadId: string, itemId: string) => Promise<Item>;
|
|
116
|
+
removeItem: (threadId: string, itemId: string) => Promise<Item>;
|
|
117
|
+
};
|
|
118
|
+
readonly blobs: {
|
|
119
|
+
upload: (data: Uint8Array | ArrayBuffer, mimeType: string) => Promise<{
|
|
120
|
+
hash: string;
|
|
121
|
+
}>;
|
|
122
|
+
download: (hash: string) => Promise<ArrayBuffer>;
|
|
123
|
+
/** Check whether a blob exists without downloading it. */
|
|
124
|
+
exists: (hash: string) => Promise<boolean>;
|
|
125
|
+
/** Returns a direct CDN URL if configured, otherwise the API proxy URL. */
|
|
126
|
+
url: (hash: string) => string;
|
|
127
|
+
};
|
|
128
|
+
readonly types: {
|
|
129
|
+
list: () => Promise<TypeSchema[]>;
|
|
130
|
+
get: (id: string) => Promise<TypeSchema>;
|
|
131
|
+
register: (schema: TypeSchema) => Promise<TypeSchema>;
|
|
132
|
+
update: (id: string, schema: Omit<TypeSchema, "id">) => Promise<TypeSchema>;
|
|
133
|
+
delete: (id: string, options?: {
|
|
134
|
+
force?: boolean;
|
|
135
|
+
}) => Promise<void>;
|
|
136
|
+
};
|
|
137
|
+
readonly keys: {
|
|
138
|
+
create: (input: CreateKeyInput) => Promise<{
|
|
139
|
+
id: string;
|
|
140
|
+
key: string;
|
|
141
|
+
}>;
|
|
142
|
+
list: () => Promise<ApiKey[]>;
|
|
143
|
+
revoke: (id: string) => Promise<void>;
|
|
144
|
+
};
|
|
145
|
+
readonly webhooks: {
|
|
146
|
+
create: (input: CreateWebhookInput) => Promise<Webhook>;
|
|
147
|
+
list: () => Promise<Webhook[]>;
|
|
148
|
+
get: (id: string) => Promise<Webhook>;
|
|
149
|
+
update: (id: string, input: UpdateWebhookInput) => Promise<Webhook>;
|
|
150
|
+
delete: (id: string) => Promise<void>;
|
|
151
|
+
deliveries: (id: string, options?: {
|
|
152
|
+
limit?: number;
|
|
153
|
+
}) => Promise<WebhookDelivery[]>;
|
|
154
|
+
};
|
|
155
|
+
private throwRawError;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
declare class MymeError extends Error {
|
|
159
|
+
readonly code: string;
|
|
160
|
+
readonly status: number;
|
|
161
|
+
readonly details?: Record<string, unknown>;
|
|
162
|
+
constructor(code: string, message: string, status: number, details?: Record<string, unknown>);
|
|
163
|
+
}
|
|
164
|
+
declare class NotFoundError extends MymeError {
|
|
165
|
+
constructor(message: string, details?: Record<string, unknown>);
|
|
166
|
+
}
|
|
167
|
+
declare class ValidationError extends MymeError {
|
|
168
|
+
constructor(message: string, details?: Record<string, unknown>);
|
|
169
|
+
}
|
|
170
|
+
declare class UnauthorizedError extends MymeError {
|
|
171
|
+
constructor(message: string, details?: Record<string, unknown>);
|
|
172
|
+
}
|
|
173
|
+
declare class ForbiddenError extends MymeError {
|
|
174
|
+
constructor(message: string, details?: Record<string, unknown>);
|
|
175
|
+
}
|
|
176
|
+
declare class ConflictError extends MymeError {
|
|
177
|
+
readonly current: ConflictSnapshot;
|
|
178
|
+
readonly ancestor: ConflictSnapshot;
|
|
179
|
+
readonly conflictingFields: string[];
|
|
180
|
+
readonly clientPatch: Record<string, unknown>;
|
|
181
|
+
constructor(current: ConflictSnapshot, ancestor: ConflictSnapshot, conflictingFields: string[], clientPatch: Record<string, unknown>);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export { type ClientConfig, type ConflictData, ConflictError, type ConflictResolver, type ConflictStrategy, ForbiddenError, type ListFilters, type MetadataInput, MymeClient, MymeError, NotFoundError, type SearchFilters, UnauthorizedError, type UpdateOptions, ValidationError };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var MymeError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
status;
|
|
5
|
+
details;
|
|
6
|
+
constructor(code, message, status, details) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "MymeError";
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.details = details;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var NotFoundError = class extends MymeError {
|
|
15
|
+
constructor(message, details) {
|
|
16
|
+
super("not_found", message, 404, details);
|
|
17
|
+
this.name = "NotFoundError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var ValidationError = class extends MymeError {
|
|
21
|
+
constructor(message, details) {
|
|
22
|
+
super("validation_error", message, 400, details);
|
|
23
|
+
this.name = "ValidationError";
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var UnauthorizedError = class extends MymeError {
|
|
27
|
+
constructor(message, details) {
|
|
28
|
+
super("unauthorized", message, 401, details);
|
|
29
|
+
this.name = "UnauthorizedError";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var ForbiddenError = class extends MymeError {
|
|
33
|
+
constructor(message, details) {
|
|
34
|
+
super("forbidden", message, 403, details);
|
|
35
|
+
this.name = "ForbiddenError";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var ConflictError = class extends MymeError {
|
|
39
|
+
current;
|
|
40
|
+
ancestor;
|
|
41
|
+
conflictingFields;
|
|
42
|
+
clientPatch;
|
|
43
|
+
constructor(current, ancestor, conflictingFields, clientPatch) {
|
|
44
|
+
super("version_conflict", "Version conflict", 409);
|
|
45
|
+
this.name = "ConflictError";
|
|
46
|
+
this.current = current;
|
|
47
|
+
this.ancestor = ancestor;
|
|
48
|
+
this.conflictingFields = conflictingFields;
|
|
49
|
+
this.clientPatch = clientPatch;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// src/transport.ts
|
|
54
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
55
|
+
var HttpTransport = class {
|
|
56
|
+
baseUrl;
|
|
57
|
+
apiKey;
|
|
58
|
+
fetch;
|
|
59
|
+
timeoutMs;
|
|
60
|
+
constructor(config) {
|
|
61
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
62
|
+
this.apiKey = config.apiKey;
|
|
63
|
+
this.fetch = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
64
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
65
|
+
}
|
|
66
|
+
async request(method, path, options) {
|
|
67
|
+
const response = await this.rawRequest(method, path, options);
|
|
68
|
+
if (response.status === 204) {
|
|
69
|
+
return void 0;
|
|
70
|
+
}
|
|
71
|
+
const body = await response.json();
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
this.throwForError(response.status, body);
|
|
74
|
+
}
|
|
75
|
+
return body;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Like request(), but returns the conflict response instead of throwing
|
|
79
|
+
* when the server responds with 409. Returns either the success body or
|
|
80
|
+
* the ConflictResponse for the caller to handle.
|
|
81
|
+
*/
|
|
82
|
+
async requestWithConflict(method, path, options) {
|
|
83
|
+
const response = await this.rawRequest(method, path, options);
|
|
84
|
+
const body = await response.json();
|
|
85
|
+
if (response.status === 409) {
|
|
86
|
+
return body;
|
|
87
|
+
}
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
this.throwForError(response.status, body);
|
|
90
|
+
}
|
|
91
|
+
return body;
|
|
92
|
+
}
|
|
93
|
+
async rawRequest(method, path, options) {
|
|
94
|
+
const url = this.buildUrl(path, options?.query);
|
|
95
|
+
const headers = {
|
|
96
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
97
|
+
...options?.headers
|
|
98
|
+
};
|
|
99
|
+
const controller = new AbortController();
|
|
100
|
+
const timeout = setTimeout(() => {
|
|
101
|
+
controller.abort();
|
|
102
|
+
}, this.timeoutMs);
|
|
103
|
+
const init = { method, headers, signal: controller.signal };
|
|
104
|
+
if (options?.rawBody !== void 0) {
|
|
105
|
+
init.body = options.rawBody;
|
|
106
|
+
} else if (options?.body !== void 0) {
|
|
107
|
+
headers["Content-Type"] = "application/json";
|
|
108
|
+
init.body = JSON.stringify(options.body);
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
return await this.fetch(url, init);
|
|
112
|
+
} finally {
|
|
113
|
+
clearTimeout(timeout);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
buildUrl(path, query) {
|
|
117
|
+
const url = `${this.baseUrl}${path}`;
|
|
118
|
+
if (!query) return url;
|
|
119
|
+
const params = new URLSearchParams();
|
|
120
|
+
for (const [key, value] of Object.entries(query)) {
|
|
121
|
+
if (value !== void 0) {
|
|
122
|
+
params.set(key, String(value));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const qs = params.toString();
|
|
126
|
+
return qs ? `${url}?${qs}` : url;
|
|
127
|
+
}
|
|
128
|
+
throwForError(status, body) {
|
|
129
|
+
const parsed = body;
|
|
130
|
+
const errObj = parsed?.error;
|
|
131
|
+
const message = errObj?.message ?? `HTTP ${String(status)}`;
|
|
132
|
+
const details = errObj?.details;
|
|
133
|
+
const code = errObj?.code ?? "unknown";
|
|
134
|
+
switch (status) {
|
|
135
|
+
case 400:
|
|
136
|
+
throw new ValidationError(message, details);
|
|
137
|
+
case 401:
|
|
138
|
+
throw new UnauthorizedError(message, details);
|
|
139
|
+
case 403:
|
|
140
|
+
throw new ForbiddenError(message, details);
|
|
141
|
+
case 404:
|
|
142
|
+
throw new NotFoundError(message, details);
|
|
143
|
+
default:
|
|
144
|
+
throw new MymeError(code, message, status, details);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// src/conflict.ts
|
|
150
|
+
var MAX_RETRIES = 3;
|
|
151
|
+
function isConflictResponse(body) {
|
|
152
|
+
return typeof body === "object" && body !== null && "error" in body && "current" in body && "conflicting_fields" in body;
|
|
153
|
+
}
|
|
154
|
+
function autoMerge(conflict) {
|
|
155
|
+
const merged = { ...conflict.current.properties };
|
|
156
|
+
for (const [key, value] of Object.entries(conflict.clientPatch)) {
|
|
157
|
+
if (!conflict.conflictingFields.includes(key)) {
|
|
158
|
+
merged[key] = value;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return merged;
|
|
162
|
+
}
|
|
163
|
+
function toConflictError(response, clientPatch) {
|
|
164
|
+
return new ConflictError(
|
|
165
|
+
response.current,
|
|
166
|
+
response.ancestor,
|
|
167
|
+
response.conflicting_fields,
|
|
168
|
+
clientPatch
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
async function handleConflictUpdate(transport, itemId, clientPatch, version, strategy, resolver, threadId) {
|
|
172
|
+
let properties = clientPatch;
|
|
173
|
+
let currentVersion = version;
|
|
174
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
175
|
+
const result = await transport.requestWithConflict("PATCH", `/items/${itemId}`, {
|
|
176
|
+
body: {
|
|
177
|
+
properties,
|
|
178
|
+
version: currentVersion,
|
|
179
|
+
...threadId !== void 0 && { thread_id: threadId }
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
if (!isConflictResponse(result)) {
|
|
183
|
+
return result.item;
|
|
184
|
+
}
|
|
185
|
+
if (strategy === "manual") {
|
|
186
|
+
throw toConflictError(result, clientPatch);
|
|
187
|
+
}
|
|
188
|
+
if (attempt === MAX_RETRIES) {
|
|
189
|
+
throw toConflictError(result, clientPatch);
|
|
190
|
+
}
|
|
191
|
+
const conflict = {
|
|
192
|
+
current: result.current,
|
|
193
|
+
ancestor: result.ancestor,
|
|
194
|
+
conflictingFields: result.conflicting_fields,
|
|
195
|
+
clientPatch
|
|
196
|
+
};
|
|
197
|
+
if (strategy === "auto") {
|
|
198
|
+
properties = autoMerge(conflict);
|
|
199
|
+
} else {
|
|
200
|
+
if (!resolver) {
|
|
201
|
+
throw toConflictError(result, clientPatch);
|
|
202
|
+
}
|
|
203
|
+
properties = await resolver(conflict);
|
|
204
|
+
}
|
|
205
|
+
currentVersion = result.current.version;
|
|
206
|
+
}
|
|
207
|
+
throw new ConflictError(
|
|
208
|
+
{ version: 0, properties: {} },
|
|
209
|
+
{ version: 0, properties: {} },
|
|
210
|
+
[],
|
|
211
|
+
clientPatch
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/client.ts
|
|
216
|
+
var MymeClient = class {
|
|
217
|
+
transport;
|
|
218
|
+
defaultConflictStrategy;
|
|
219
|
+
apiBaseUrl;
|
|
220
|
+
cdnBaseUrl;
|
|
221
|
+
constructor(config) {
|
|
222
|
+
this.apiBaseUrl = config.url.replace(/\/+$/, "");
|
|
223
|
+
this.transport = new HttpTransport({
|
|
224
|
+
baseUrl: config.url,
|
|
225
|
+
apiKey: config.apiKey,
|
|
226
|
+
fetch: config.fetch,
|
|
227
|
+
timeoutMs: config.timeoutMs
|
|
228
|
+
});
|
|
229
|
+
this.defaultConflictStrategy = config.conflictStrategy ?? "auto";
|
|
230
|
+
this.cdnBaseUrl = config.cdnBaseUrl;
|
|
231
|
+
}
|
|
232
|
+
// ---- Items ----
|
|
233
|
+
items = {
|
|
234
|
+
create: async (input) => {
|
|
235
|
+
const res = await this.transport.request(
|
|
236
|
+
"POST",
|
|
237
|
+
"/items",
|
|
238
|
+
{ body: input }
|
|
239
|
+
);
|
|
240
|
+
return res.item;
|
|
241
|
+
},
|
|
242
|
+
get: async (id) => {
|
|
243
|
+
const res = await this.transport.request(
|
|
244
|
+
"GET",
|
|
245
|
+
`/items/${id}`
|
|
246
|
+
);
|
|
247
|
+
return res.item;
|
|
248
|
+
},
|
|
249
|
+
list: async (filters) => {
|
|
250
|
+
return this.transport.request("GET", "/items", {
|
|
251
|
+
query: filters
|
|
252
|
+
});
|
|
253
|
+
},
|
|
254
|
+
listWithMetadata: async (filters) => {
|
|
255
|
+
return this.transport.request(
|
|
256
|
+
"GET",
|
|
257
|
+
"/items",
|
|
258
|
+
{
|
|
259
|
+
query: {
|
|
260
|
+
...filters,
|
|
261
|
+
include: "metadata"
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
},
|
|
266
|
+
update: async (id, properties, options) => {
|
|
267
|
+
let version = options?.version;
|
|
268
|
+
if (version === void 0) {
|
|
269
|
+
const item = await this.items.get(id);
|
|
270
|
+
version = item.version;
|
|
271
|
+
}
|
|
272
|
+
const strategy = options?.conflict ?? this.defaultConflictStrategy;
|
|
273
|
+
return handleConflictUpdate(
|
|
274
|
+
this.transport,
|
|
275
|
+
id,
|
|
276
|
+
properties,
|
|
277
|
+
version,
|
|
278
|
+
strategy,
|
|
279
|
+
options?.resolve,
|
|
280
|
+
options?.thread_id
|
|
281
|
+
);
|
|
282
|
+
},
|
|
283
|
+
delete: async (id) => {
|
|
284
|
+
await this.transport.request("DELETE", `/items/${id}`);
|
|
285
|
+
},
|
|
286
|
+
restore: async (id) => {
|
|
287
|
+
const res = await this.transport.request(
|
|
288
|
+
"POST",
|
|
289
|
+
`/items/${id}/restore`
|
|
290
|
+
);
|
|
291
|
+
return res.item;
|
|
292
|
+
},
|
|
293
|
+
transition: async (id, state) => {
|
|
294
|
+
const res = await this.transport.request(
|
|
295
|
+
"POST",
|
|
296
|
+
`/items/${id}/transition`,
|
|
297
|
+
{ body: { state } }
|
|
298
|
+
);
|
|
299
|
+
return res.item;
|
|
300
|
+
},
|
|
301
|
+
versions: async (id) => {
|
|
302
|
+
const res = await this.transport.request(
|
|
303
|
+
"GET",
|
|
304
|
+
`/items/${id}/versions`
|
|
305
|
+
);
|
|
306
|
+
return res.versions;
|
|
307
|
+
},
|
|
308
|
+
stats: async () => {
|
|
309
|
+
return this.transport.request(
|
|
310
|
+
"GET",
|
|
311
|
+
"/items/stats"
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
// ---- Metadata ----
|
|
316
|
+
metadata = {
|
|
317
|
+
get: async (itemId) => {
|
|
318
|
+
const res = await this.transport.request(
|
|
319
|
+
"GET",
|
|
320
|
+
`/items/${itemId}/metadata`
|
|
321
|
+
);
|
|
322
|
+
return res.metadata;
|
|
323
|
+
},
|
|
324
|
+
set: async (itemId, input) => {
|
|
325
|
+
const res = await this.transport.request(
|
|
326
|
+
"PUT",
|
|
327
|
+
`/items/${itemId}/metadata`,
|
|
328
|
+
{ body: input }
|
|
329
|
+
);
|
|
330
|
+
return res.metadata;
|
|
331
|
+
},
|
|
332
|
+
merge: async (itemId, input) => {
|
|
333
|
+
const res = await this.transport.request(
|
|
334
|
+
"PATCH",
|
|
335
|
+
`/items/${itemId}/metadata`,
|
|
336
|
+
{ body: input }
|
|
337
|
+
);
|
|
338
|
+
return res.metadata;
|
|
339
|
+
},
|
|
340
|
+
addTags: async (itemId, tags) => {
|
|
341
|
+
const res = await this.transport.request(
|
|
342
|
+
"POST",
|
|
343
|
+
`/items/${itemId}/tags`,
|
|
344
|
+
{ body: { tags } }
|
|
345
|
+
);
|
|
346
|
+
return res.metadata;
|
|
347
|
+
},
|
|
348
|
+
removeTag: async (itemId, tag) => {
|
|
349
|
+
await this.transport.request(
|
|
350
|
+
"DELETE",
|
|
351
|
+
`/items/${itemId}/tags/${encodeURIComponent(tag)}`
|
|
352
|
+
);
|
|
353
|
+
},
|
|
354
|
+
getExtensions: async (itemId, namespace) => {
|
|
355
|
+
if (namespace) {
|
|
356
|
+
const res2 = await this.transport.request(
|
|
357
|
+
"GET",
|
|
358
|
+
`/items/${itemId}/extensions/${encodeURIComponent(namespace)}`
|
|
359
|
+
);
|
|
360
|
+
return res2.data ? { [namespace]: res2.data } : {};
|
|
361
|
+
}
|
|
362
|
+
const res = await this.transport.request("GET", `/items/${itemId}/extensions`);
|
|
363
|
+
return res.extensions;
|
|
364
|
+
},
|
|
365
|
+
setExtension: async (itemId, namespace, data) => {
|
|
366
|
+
const res = await this.transport.request(
|
|
367
|
+
"PUT",
|
|
368
|
+
`/items/${itemId}/extensions/${encodeURIComponent(namespace)}`,
|
|
369
|
+
{ body: data }
|
|
370
|
+
);
|
|
371
|
+
return res.extensions;
|
|
372
|
+
},
|
|
373
|
+
deleteExtension: async (itemId, namespace) => {
|
|
374
|
+
await this.transport.request(
|
|
375
|
+
"DELETE",
|
|
376
|
+
`/items/${itemId}/extensions/${encodeURIComponent(namespace)}`
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
// ---- Search ----
|
|
381
|
+
async search(query, filters) {
|
|
382
|
+
const res = await this.transport.request(
|
|
383
|
+
"GET",
|
|
384
|
+
"/search",
|
|
385
|
+
{ query: { q: query, ...filters } }
|
|
386
|
+
);
|
|
387
|
+
return res.results;
|
|
388
|
+
}
|
|
389
|
+
// ---- Threads ----
|
|
390
|
+
threads = {
|
|
391
|
+
create: async () => {
|
|
392
|
+
const res = await this.transport.request(
|
|
393
|
+
"POST",
|
|
394
|
+
"/threads"
|
|
395
|
+
);
|
|
396
|
+
return res.thread;
|
|
397
|
+
},
|
|
398
|
+
list: async (filters) => {
|
|
399
|
+
return this.transport.request(
|
|
400
|
+
"GET",
|
|
401
|
+
"/threads",
|
|
402
|
+
{ query: filters }
|
|
403
|
+
);
|
|
404
|
+
},
|
|
405
|
+
get: async (id) => {
|
|
406
|
+
return this.transport.request(
|
|
407
|
+
"GET",
|
|
408
|
+
`/threads/${id}`
|
|
409
|
+
);
|
|
410
|
+
},
|
|
411
|
+
addItem: async (threadId, itemId) => {
|
|
412
|
+
const res = await this.transport.request(
|
|
413
|
+
"POST",
|
|
414
|
+
`/threads/${threadId}/items`,
|
|
415
|
+
{ body: { item_id: itemId } }
|
|
416
|
+
);
|
|
417
|
+
return res.item;
|
|
418
|
+
},
|
|
419
|
+
removeItem: async (threadId, itemId) => {
|
|
420
|
+
const res = await this.transport.request(
|
|
421
|
+
"DELETE",
|
|
422
|
+
`/threads/${threadId}/items/${itemId}`
|
|
423
|
+
);
|
|
424
|
+
return res.item;
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
// ---- Blobs ----
|
|
428
|
+
blobs = {
|
|
429
|
+
upload: async (data, mimeType) => {
|
|
430
|
+
const response = await this.transport.rawRequest("POST", "/blobs", {
|
|
431
|
+
rawBody: data,
|
|
432
|
+
headers: { "Content-Type": mimeType }
|
|
433
|
+
});
|
|
434
|
+
const result = await response.json();
|
|
435
|
+
if (!response.ok) {
|
|
436
|
+
this.throwRawError(response.status, result);
|
|
437
|
+
}
|
|
438
|
+
return { hash: result.hash };
|
|
439
|
+
},
|
|
440
|
+
download: async (hash) => {
|
|
441
|
+
const response = await this.transport.rawRequest("GET", `/blobs/${hash}`);
|
|
442
|
+
if (!response.ok) {
|
|
443
|
+
const body = await response.json();
|
|
444
|
+
this.throwRawError(response.status, body);
|
|
445
|
+
}
|
|
446
|
+
return response.arrayBuffer();
|
|
447
|
+
},
|
|
448
|
+
/** Check whether a blob exists without downloading it. */
|
|
449
|
+
exists: async (hash) => {
|
|
450
|
+
const cleanHash = hash.startsWith("sha256:") ? hash : `sha256:${hash}`;
|
|
451
|
+
const response = await this.transport.rawRequest(
|
|
452
|
+
"HEAD",
|
|
453
|
+
`/blobs/${cleanHash}`
|
|
454
|
+
);
|
|
455
|
+
return response.status === 200;
|
|
456
|
+
},
|
|
457
|
+
/** Returns a direct CDN URL if configured, otherwise the API proxy URL. */
|
|
458
|
+
url: (hash) => {
|
|
459
|
+
const cleanHash = hash.startsWith("sha256:") ? hash : `sha256:${hash}`;
|
|
460
|
+
if (this.cdnBaseUrl) {
|
|
461
|
+
return `${this.cdnBaseUrl}/blobs/${cleanHash}`;
|
|
462
|
+
}
|
|
463
|
+
return `${this.apiBaseUrl}/blobs/${cleanHash}`;
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
// ---- Types ----
|
|
467
|
+
types = {
|
|
468
|
+
list: async () => {
|
|
469
|
+
return this.transport.request("GET", "/types");
|
|
470
|
+
},
|
|
471
|
+
get: async (id) => {
|
|
472
|
+
return this.transport.request("GET", `/types/${id}`);
|
|
473
|
+
},
|
|
474
|
+
register: async (schema) => {
|
|
475
|
+
return this.transport.request("POST", "/types", {
|
|
476
|
+
body: schema
|
|
477
|
+
});
|
|
478
|
+
},
|
|
479
|
+
update: async (id, schema) => {
|
|
480
|
+
return this.transport.request("PUT", `/types/${id}`, {
|
|
481
|
+
body: schema
|
|
482
|
+
});
|
|
483
|
+
},
|
|
484
|
+
delete: async (id, options) => {
|
|
485
|
+
const query = options?.force ? "?force=true" : "";
|
|
486
|
+
await this.transport.request(
|
|
487
|
+
"DELETE",
|
|
488
|
+
`/types/${id}${query}`
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
// ---- Keys ----
|
|
493
|
+
keys = {
|
|
494
|
+
create: async (input) => {
|
|
495
|
+
const res = await this.transport.request("POST", "/keys", { body: input });
|
|
496
|
+
return { id: res.id, key: res.key };
|
|
497
|
+
},
|
|
498
|
+
list: async () => {
|
|
499
|
+
const res = await this.transport.request(
|
|
500
|
+
"GET",
|
|
501
|
+
"/keys"
|
|
502
|
+
);
|
|
503
|
+
return res.keys;
|
|
504
|
+
},
|
|
505
|
+
revoke: async (id) => {
|
|
506
|
+
await this.transport.request("DELETE", `/keys/${id}`);
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
// ---- Webhooks ----
|
|
510
|
+
webhooks = {
|
|
511
|
+
create: async (input) => {
|
|
512
|
+
return this.transport.request("POST", "/webhooks", {
|
|
513
|
+
body: input
|
|
514
|
+
});
|
|
515
|
+
},
|
|
516
|
+
list: async () => {
|
|
517
|
+
const res = await this.transport.request(
|
|
518
|
+
"GET",
|
|
519
|
+
"/webhooks"
|
|
520
|
+
);
|
|
521
|
+
return res.webhooks;
|
|
522
|
+
},
|
|
523
|
+
get: async (id) => {
|
|
524
|
+
return this.transport.request("GET", `/webhooks/${id}`);
|
|
525
|
+
},
|
|
526
|
+
update: async (id, input) => {
|
|
527
|
+
return this.transport.request("PATCH", `/webhooks/${id}`, {
|
|
528
|
+
body: input
|
|
529
|
+
});
|
|
530
|
+
},
|
|
531
|
+
delete: async (id) => {
|
|
532
|
+
await this.transport.request(
|
|
533
|
+
"DELETE",
|
|
534
|
+
`/webhooks/${id}`
|
|
535
|
+
);
|
|
536
|
+
},
|
|
537
|
+
deliveries: async (id, options) => {
|
|
538
|
+
const query = options?.limit ? { limit: String(options.limit) } : {};
|
|
539
|
+
const res = await this.transport.request("GET", `/webhooks/${id}/deliveries`, { query });
|
|
540
|
+
return res.deliveries;
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
// ---- Internal ----
|
|
544
|
+
throwRawError(status, body) {
|
|
545
|
+
const parsed = body;
|
|
546
|
+
const errObj = parsed?.error;
|
|
547
|
+
const message = errObj?.message ?? `HTTP ${String(status)}`;
|
|
548
|
+
const details = errObj?.details;
|
|
549
|
+
switch (status) {
|
|
550
|
+
case 400:
|
|
551
|
+
throw new ValidationError(message, details);
|
|
552
|
+
case 401:
|
|
553
|
+
throw new UnauthorizedError(message, details);
|
|
554
|
+
case 403:
|
|
555
|
+
throw new ForbiddenError(message, details);
|
|
556
|
+
case 404:
|
|
557
|
+
throw new NotFoundError(message, details);
|
|
558
|
+
default:
|
|
559
|
+
throw new MymeError(
|
|
560
|
+
errObj?.code ?? "unknown",
|
|
561
|
+
message,
|
|
562
|
+
status,
|
|
563
|
+
details
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
export {
|
|
569
|
+
ConflictError,
|
|
570
|
+
ForbiddenError,
|
|
571
|
+
MymeClient,
|
|
572
|
+
MymeError,
|
|
573
|
+
NotFoundError,
|
|
574
|
+
UnauthorizedError,
|
|
575
|
+
ValidationError
|
|
576
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mymehq/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"registry": "https://registry.npmjs.org",
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@mymehq/shared": "workspace:*"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@mymehq/server": "workspace:*",
|
|
29
|
+
"@types/node": "^22.0.0",
|
|
30
|
+
"tsup": "^8.5.1",
|
|
31
|
+
"typescript": "^6.0.2"
|
|
32
|
+
}
|
|
33
|
+
}
|