@myco-dev/sdk 0.1.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/README.md ADDED
@@ -0,0 +1,526 @@
1
+ # @myco/sdk
2
+
3
+ TypeScript SDK for building apps on the Myco platform.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @myco/sdk
9
+ ```
10
+
11
+ ### Peer Dependencies
12
+
13
+ ```bash
14
+ npm install react better-auth swr
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ```typescript
20
+ import { createMycoSDK } from "@myco/sdk";
21
+
22
+ // Optional: import generated types for type-safe entity access
23
+ import type { EntityTypes } from "./types/myco.generated";
24
+
25
+ const myco = createMycoSDK<EntityTypes>("your-app-key");
26
+
27
+ // Fetch records - automatically waits for SDK to be ready
28
+ const { records } = await myco.data.getRecords("property");
29
+ ```
30
+
31
+ ## Client SDK
32
+
33
+ ### Initialization
34
+
35
+ ```typescript
36
+ import { createMycoSDK } from "@myco/sdk";
37
+
38
+ const myco = createMycoSDK<EntityTypes>("your-app-key", {
39
+ // Optional: override the base URL
40
+ baseUrl: "https://api.myco.com",
41
+ // Optional: disable auto-redirect on unauthenticated (default: true)
42
+ redirectOnUnauth: false,
43
+ });
44
+
45
+ // Optional: await ready if you need to know when initialization completes
46
+ // All API calls automatically wait for ready internally
47
+ await myco.ready;
48
+ ```
49
+
50
+ The SDK automatically:
51
+ - Detects local vs production environment
52
+ - Handles workspace initialization via the `/api/app/join` endpoint
53
+ - Persists workspace ID in localStorage
54
+ - Redirects to login if unauthenticated (configurable)
55
+
56
+ ---
57
+
58
+ ### Auth Namespace
59
+
60
+ #### `myco.auth.login(returnTo?)`
61
+
62
+ Redirects to the federated login page.
63
+
64
+ ```typescript
65
+ // Redirect to login, return to current page after
66
+ myco.auth.login();
67
+
68
+ // Redirect to login, return to specific URL after
69
+ myco.auth.login("/dashboard");
70
+ ```
71
+
72
+ #### `myco.auth.logout()`
73
+
74
+ Signs out the current user and clears the session.
75
+
76
+ ```typescript
77
+ await myco.auth.logout();
78
+ ```
79
+
80
+ #### `myco.auth.useSession()`
81
+
82
+ React hook for session state.
83
+
84
+ ```typescript
85
+ function MyComponent() {
86
+ const { data: session, isPending, error } = myco.auth.useSession();
87
+
88
+ if (isPending) return <div>Loading...</div>;
89
+ if (!session) return <div>Not logged in</div>;
90
+
91
+ return <div>Hello, {session.user.name}</div>;
92
+ }
93
+ ```
94
+
95
+ #### `myco.auth.getSession()`
96
+
97
+ Get session data outside of React components.
98
+
99
+ ```typescript
100
+ const session = await myco.auth.getSession();
101
+ ```
102
+
103
+ ---
104
+
105
+ ### Workspaces Namespace
106
+
107
+ #### `myco.workspaces.ensure()`
108
+
109
+ Ensures a workspace is set up. Called automatically during initialization.
110
+
111
+ ```typescript
112
+ // Call manually after login if redirectOnUnauth was false
113
+ await myco.workspaces.ensure();
114
+ ```
115
+
116
+ #### `myco.workspaces.list()`
117
+
118
+ List all workspaces the user has access to.
119
+
120
+ ```typescript
121
+ const workspaces = await myco.workspaces.list();
122
+ ```
123
+
124
+ #### `myco.workspaces.current()`
125
+
126
+ Get the current workspace details.
127
+
128
+ ```typescript
129
+ const workspace = await myco.workspaces.current();
130
+ console.log(workspace.name);
131
+ ```
132
+
133
+ #### `myco.workspaces.switch(workspaceId)`
134
+
135
+ Switch to a different workspace.
136
+
137
+ ```typescript
138
+ myco.workspaces.switch("ws_abc123");
139
+ ```
140
+
141
+ #### `myco.workspaces.create(name)`
142
+
143
+ Create a new workspace.
144
+
145
+ ```typescript
146
+ const workspace = await myco.workspaces.create("My New Workspace");
147
+ ```
148
+
149
+ #### `myco.workspaces.update(data)`
150
+
151
+ Update the current workspace.
152
+
153
+ ```typescript
154
+ await myco.workspaces.update({ name: "Renamed Workspace" });
155
+ ```
156
+
157
+ #### `myco.workspaces.delete()`
158
+
159
+ Delete the current workspace.
160
+
161
+ ```typescript
162
+ await myco.workspaces.delete();
163
+ ```
164
+
165
+ #### `myco.workspaces.getMembers()`
166
+
167
+ Get members of the current workspace.
168
+
169
+ ```typescript
170
+ const members = await myco.workspaces.getMembers();
171
+ ```
172
+
173
+ #### `myco.workspaces.addMember(userId, role?)`
174
+
175
+ Add a member to the current workspace.
176
+
177
+ ```typescript
178
+ await myco.workspaces.addMember("user_123", "admin");
179
+ await myco.workspaces.addMember("user_456"); // defaults to "member"
180
+ ```
181
+
182
+ #### `myco.workspaces.removeMember(userId)`
183
+
184
+ Remove a member from the current workspace.
185
+
186
+ ```typescript
187
+ await myco.workspaces.removeMember("user_123");
188
+ ```
189
+
190
+ ---
191
+
192
+ ### Data Namespace
193
+
194
+ #### Entities
195
+
196
+ ##### `myco.data.getEntities()`
197
+
198
+ Get all entities for the current app.
199
+
200
+ ```typescript
201
+ const entities = await myco.data.getEntities();
202
+ ```
203
+
204
+ ##### `myco.data.getEntity(entitySlug)`
205
+
206
+ Get a single entity by slug.
207
+
208
+ ```typescript
209
+ const entity = await myco.data.getEntity("property");
210
+ console.log(entity.fields);
211
+ ```
212
+
213
+ #### Records (Async Methods)
214
+
215
+ ##### `myco.data.getRecords(entitySlug, options?)`
216
+
217
+ Get paginated records for an entity.
218
+
219
+ ```typescript
220
+ const { records, pagination } = await myco.data.getRecords("property", {
221
+ page: 1,
222
+ pageSize: 20,
223
+ sort: "createdAt",
224
+ order: "desc",
225
+ filter: { status: "active" },
226
+ });
227
+ ```
228
+
229
+ ##### `myco.data.getRecord(entitySlug, recordId)`
230
+
231
+ Get a single record by ID.
232
+
233
+ ```typescript
234
+ const record = await myco.data.getRecord("property", "rec_123");
235
+ ```
236
+
237
+ ##### `myco.data.createRecord(entitySlug, data)`
238
+
239
+ Create a new record.
240
+
241
+ ```typescript
242
+ const record = await myco.data.createRecord("property", {
243
+ title: "Beach House",
244
+ price: 500000,
245
+ });
246
+ ```
247
+
248
+ ##### `myco.data.updateRecord(entitySlug, recordId, data)`
249
+
250
+ Update an existing record.
251
+
252
+ ```typescript
253
+ const record = await myco.data.updateRecord("property", "rec_123", {
254
+ price: 550000,
255
+ });
256
+ ```
257
+
258
+ ##### `myco.data.deleteRecord(entitySlug, recordId)`
259
+
260
+ Delete a record.
261
+
262
+ ```typescript
263
+ await myco.data.deleteRecord("property", "rec_123");
264
+ ```
265
+
266
+ ##### `myco.data.batchGetRecords(entitySlug, ids)`
267
+
268
+ Batch get multiple records by IDs.
269
+
270
+ ```typescript
271
+ const records = await myco.data.batchGetRecords("property", [
272
+ "rec_123",
273
+ "rec_456",
274
+ ]);
275
+ ```
276
+
277
+ #### Records (React Hooks)
278
+
279
+ ##### `myco.data.useRecords(entitySlug, options?)`
280
+
281
+ SWR hook for fetching records with built-in pagination.
282
+
283
+ ```typescript
284
+ function PropertyList() {
285
+ const {
286
+ data: properties,
287
+ isLoading,
288
+ error,
289
+ // Pagination
290
+ page,
291
+ hasNextPage,
292
+ hasPreviousPage,
293
+ nextPage,
294
+ previousPage,
295
+ setPage,
296
+ } = myco.data.useRecords("property", {
297
+ pageSize: 10,
298
+ initialPage: 1,
299
+ sort: "createdAt",
300
+ order: "desc",
301
+ filter: { status: "active" },
302
+ // Called when page changes (for URL sync, analytics, etc.)
303
+ onPageChange: (page) => {
304
+ router.push(`?page=${page}`);
305
+ },
306
+ });
307
+
308
+ if (isLoading) return <div>Loading...</div>;
309
+ if (error) return <div>Error: {error.message}</div>;
310
+
311
+ return (
312
+ <div>
313
+ {properties?.map((property) => (
314
+ <div key={property.id}>{property.data.title}</div>
315
+ ))}
316
+ <button onClick={previousPage} disabled={!hasPreviousPage}>
317
+ Previous
318
+ </button>
319
+ <span>Page {page}</span>
320
+ <button onClick={nextPage} disabled={!hasNextPage}>
321
+ Next
322
+ </button>
323
+ </div>
324
+ );
325
+ }
326
+ ```
327
+
328
+ **Options:**
329
+
330
+ | Option | Type | Description |
331
+ |--------|------|-------------|
332
+ | `initialPage` | `number` | Starting page (default: 1) |
333
+ | `pageSize` | `number` | Records per page |
334
+ | `sort` | `string` | Field to sort by |
335
+ | `order` | `"asc" \| "desc"` | Sort direction |
336
+ | `filter` | `object` | Filter criteria |
337
+ | `onPageChange` | `(page: number) => void` | Callback when page changes |
338
+
339
+ **Return value extends SWR response with:**
340
+
341
+ | Property | Type | Description |
342
+ |----------|------|-------------|
343
+ | `page` | `number` | Current page number |
344
+ | `hasNextPage` | `boolean` | Whether there's a next page |
345
+ | `hasPreviousPage` | `boolean` | Whether there's a previous page |
346
+ | `nextPage` | `() => void` | Go to next page |
347
+ | `previousPage` | `() => void` | Go to previous page |
348
+ | `setPage` | `(page: number) => void` | Jump to specific page |
349
+
350
+ ##### `myco.data.useRecord(entitySlug, recordId)`
351
+
352
+ SWR hook for fetching a single record.
353
+
354
+ ```typescript
355
+ function PropertyDetail({ id }: { id: string }) {
356
+ const { data: property, isLoading } = myco.data.useRecord("property", id);
357
+
358
+ if (isLoading) return <div>Loading...</div>;
359
+
360
+ return <div>{property?.data.title}</div>;
361
+ }
362
+ ```
363
+
364
+ Supports conditional fetching by passing `null` or `undefined`:
365
+
366
+ ```typescript
367
+ // Only fetch when selectedId is set
368
+ const { data } = myco.data.useRecord("property", selectedId ?? null);
369
+ ```
370
+
371
+ ---
372
+
373
+ ## Server SDK
374
+
375
+ For server-side rendering and API routes (e.g., Remix loaders/actions).
376
+
377
+ ```typescript
378
+ import { createServerSDK } from "@myco/sdk/server";
379
+
380
+ export async function loader({ request }: LoaderArgs) {
381
+ const myco = createServerSDK(
382
+ { MYCO_APP_KEY: process.env.MYCO_APP_KEY! },
383
+ request
384
+ );
385
+
386
+ const { user } = await myco.auth.getVerifiedUser();
387
+
388
+ if (!user) {
389
+ return redirect(myco.auth.getLoginUrl(request.url));
390
+ }
391
+
392
+ const { records } = await myco.data.getRecords("property");
393
+ return json({ records });
394
+ }
395
+ ```
396
+
397
+ ### Server Auth
398
+
399
+ #### `myco.auth.getVerifiedUser()`
400
+
401
+ Verify the JWT from cookies and return the user.
402
+
403
+ ```typescript
404
+ const { user, jwt } = await myco.auth.getVerifiedUser();
405
+ if (!user) {
406
+ // Not authenticated
407
+ }
408
+ ```
409
+
410
+ #### `myco.auth.getLoginUrl(returnTo)`
411
+
412
+ Get the federated login URL.
413
+
414
+ ```typescript
415
+ const loginUrl = myco.auth.getLoginUrl("https://myapp.com/dashboard");
416
+ return redirect(loginUrl);
417
+ ```
418
+
419
+ ### Server Workspaces
420
+
421
+ Same API as client SDK:
422
+ - `myco.workspaces.list()`
423
+ - `myco.workspaces.current()`
424
+ - `myco.workspaces.create(name)`
425
+ - `myco.workspaces.update(data)`
426
+ - `myco.workspaces.delete()`
427
+ - `myco.workspaces.getMembers()`
428
+ - `myco.workspaces.addMember(userId, role?)`
429
+ - `myco.workspaces.removeMember(userId)`
430
+
431
+ ### Server Data
432
+
433
+ Same API as client SDK (async methods only, no hooks):
434
+ - `myco.data.getEntities()`
435
+ - `myco.data.getEntity(entitySlug)`
436
+ - `myco.data.getRecords(entitySlug, options?)`
437
+ - `myco.data.getRecord(entitySlug, recordId)`
438
+ - `myco.data.createRecord(entitySlug, data)`
439
+ - `myco.data.updateRecord(entitySlug, recordId, data)`
440
+ - `myco.data.deleteRecord(entitySlug, recordId)`
441
+ - `myco.data.batchGetRecords(recordIds)`
442
+
443
+ ### Server Fetch
444
+
445
+ For custom API calls:
446
+
447
+ ```typescript
448
+ const result = await myco.fetch<MyType>("/api/custom-endpoint", {
449
+ method: "POST",
450
+ body: JSON.stringify({ data }),
451
+ });
452
+ ```
453
+
454
+ ---
455
+
456
+ ## Error Handling
457
+
458
+ The SDK exports an `ApiError` class for handling API errors:
459
+
460
+ ```typescript
461
+ import { ApiError } from "@myco/sdk";
462
+
463
+ try {
464
+ await myco.data.getRecord("property", "invalid-id");
465
+ } catch (error) {
466
+ if (error instanceof ApiError) {
467
+ console.log(error.status); // HTTP status code
468
+ console.log(error.body); // Response body
469
+ console.log(error.path); // Request path
470
+ }
471
+ }
472
+ ```
473
+
474
+ ---
475
+
476
+ ## TypeScript
477
+
478
+ ### Generated Types
479
+
480
+ For type-safe entity access, generate types from your Myco schema:
481
+
482
+ ```typescript
483
+ // types/myco.generated.ts
484
+ export interface EntityTypes {
485
+ property: {
486
+ title: string;
487
+ price: number;
488
+ bedrooms: number;
489
+ status: "active" | "sold";
490
+ };
491
+ agent: {
492
+ name: string;
493
+ email: string;
494
+ };
495
+ }
496
+ ```
497
+
498
+ Then pass the type to `createMycoSDK`:
499
+
500
+ ```typescript
501
+ import type { EntityTypes } from "./types/myco.generated";
502
+
503
+ const myco = createMycoSDK<EntityTypes>("your-app-key");
504
+
505
+ // Now fully typed!
506
+ const { records } = await myco.data.getRecords("property");
507
+ // records is TypedEntityRecord<{ title: string; price: number; ... }>[]
508
+ ```
509
+
510
+ ### Exported Types
511
+
512
+ ```typescript
513
+ import type {
514
+ MycoSDKConfig,
515
+ JoinResponse,
516
+ VerifiedUser,
517
+ Workspace,
518
+ WorkspaceMember,
519
+ Entity,
520
+ EntityField,
521
+ EntityRecord,
522
+ PaginatedResponse,
523
+ GetRecordsOptions,
524
+ TypedEntityRecord,
525
+ } from "@myco/sdk";
526
+ ```