@graph8/sdk 0.13.0 → 0.13.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/README.md CHANGED
@@ -251,6 +251,28 @@ export const CTA = () => {
251
251
  | `g8.webhooks.constructEvent(body, sig, ts, secret, opts?)` | Verify a delivery's HMAC signature and return the parsed event (throws `WebhookSignatureError`) |
252
252
  | `g8.webhooks.knownEvents` | The known event-type catalog |
253
253
 
254
+ ## Native custom objects
255
+
256
+ Use an organization API key with explicit `objects:read` for schema/record/history
257
+ reads, `objects:write` for create/PATCH, and `objects:delete` for archive. Unscoped
258
+ keys receive 403. `objects:*` grants all three; grant only the operations the
259
+ integration needs.
260
+
261
+ ```typescript
262
+ import { g8 } from '@graph8/sdk';
263
+
264
+ g8.init({ apiKey: process.env.G8_API_KEY! });
265
+ const { data: objects } = await g8.objects.list();
266
+ const { data: attributes } = await g8.objects.listAttributes('invoices');
267
+ // Assumes this workspace defines an invoices object with a reference attribute.
268
+ const invoice = await g8.objects.createRecord('invoices', { reference: 'INV-1042' });
269
+ const history = await g8.objects.history('invoices', invoice.id);
270
+ ```
271
+
272
+ PATCH preserves omitted attributes. Enabled defaults apply only on create;
273
+ unknown fields return 422 and unique-value collisions return 409. Standard
274
+ contacts, companies, and deals continue using their existing SDK resources.
275
+
254
276
  ## App Platform
255
277
 
256
278
  Hosted apps do not carry a permanent org API key. Instead they **exchange** a
@@ -275,8 +297,8 @@ const appClient = createGraph8AppClient({
275
297
  });
276
298
 
277
299
  // Reach the existing resources with the app token via request():
278
- const { data: contacts } = await appClient.request<{ data: unknown[] }>(
279
- '/api/v1/contacts',
300
+ const { data: objects } = await appClient.request<{ data: unknown[] }>(
301
+ '/api/v1/app/objects',
280
302
  { query: { limit: 10 } },
281
303
  );
282
304
  ```
@@ -295,12 +317,12 @@ const service = createGraph8ServiceClient({
295
317
  scopes: ['objects:read', 'objects:write'],
296
318
  });
297
319
 
298
- const { data: rows } = await service.request<{ data: unknown[] }>('/api/v1/contacts');
320
+ const { data: objects } = await service.request<{ data: unknown[] }>('/api/v1/app/objects');
299
321
  ```
300
322
 
301
- > The typed `apps` (control-plane) and `objects` (custom-object CRUD) resource
302
- > modules are deferred until their backend routes ship (M6-3 / M6-5). Until then,
303
- > reach the Developer API through the token-bound `request()` shown above.
323
+ > App-token clients use `request()` for their consented app-owned objects.
324
+ > The `g8.objects` resource above uses an API key for native workspace objects;
325
+ > it does not substitute for app-token authorization.
304
326
 
305
327
  ## Auth Modes
306
328
 
@@ -315,3 +337,37 @@ Get your API key at [app.graph8.com/settings](https://app.graph8.com/settings) u
315
337
  ## License
316
338
 
317
339
  MIT
340
+
341
+
342
+ ### Conditional custom-record updates
343
+
344
+ Custom-record responses expose `revision`. Pass it when saving an interactive edit:
345
+
346
+ ```ts
347
+ import { g8 } from "@graph8/sdk";
348
+
349
+ g8.init({ apiKey: "YOUR_API_KEY" });
350
+ const projectId = "your-project-record-id";
351
+ const record = await g8.objects.getRecord("projects", projectId);
352
+ const updated = await g8.objects.updateRecord(
353
+ "projects", record.id, { name: "Updated project" },
354
+ { expectedRevision: record.revision },
355
+ );
356
+ ```
357
+
358
+ A stale revision returns HTTP 409 with code `revision_conflict`; reload the record
359
+ and reconcile the edit before retrying. Native and app APIs accept
360
+ `expected_revision` in the PATCH body, and the MCP update tool accepts the same
361
+ argument. Omitting it retains unconditional PATCH behavior. Older servers may
362
+ omit `revision`; deploy the revision-enabled backend before relying on this
363
+ precondition. This applies to custom-object records.
364
+
365
+
366
+ Restore a custom record with `g8.objects.restoreRecord(objectSlug, recordId)`.
367
+ The native API uses `POST /api/v1/objects/{objectSlug}/records/{recordId}/restore`;
368
+ MCP exposes `g8_object_record_restore`. Restoration preserves the original ID and
369
+ old history, validates the current schema and references, and returns 409 if a
370
+ unique value has been reused. Validation failures leave the record archived.
371
+ Ambiguous legacy history returns 422 with `archive_snapshot_unavailable` rather
372
+ than guessing its prior values. Repeating a successful restore adds no revision
373
+ or history entry.
package/dist/index.d.mts CHANGED
@@ -1765,6 +1765,10 @@ interface CustomObjectAttribute {
1765
1765
  /** No two ACTIVE records may hold the same value. A collision returns 409. */
1766
1766
  is_unique: boolean;
1767
1767
  is_multiselect: boolean;
1768
+ /** Apply a non-null default to omitted create fields, never PATCH. Absent on older servers. */
1769
+ is_default_value_enabled?: boolean;
1770
+ /** Configured default; validated like an explicit value when enabled. */
1771
+ default_value?: unknown;
1768
1772
  /** Type-specific configuration, e.g. the allowed options for a `select`. */
1769
1773
  config: Record<string, unknown>;
1770
1774
  }
@@ -1780,6 +1784,8 @@ interface CustomObjectRecord {
1780
1784
  */
1781
1785
  values: Record<string, unknown>;
1782
1786
  is_archived: boolean;
1787
+ /** Revision for conditional updates; absent on older servers. */
1788
+ revision?: number;
1783
1789
  created_at: string | null;
1784
1790
  updated_at: string | null;
1785
1791
  }
@@ -1824,10 +1830,7 @@ interface ObjectPagination {
1824
1830
  * Requires an API key (server-side). On the hardened HTTP core: throws a typed
1825
1831
  * `G8Error` on failure and retries transient errors.
1826
1832
  *
1827
- * PREVIEW AND GATED. Every endpoint returns 403 `app_not_enabled` until the
1828
- * custom-objects surface is switched on for the platform. It is off by default,
1829
- * so a call fails fast rather than returning an empty list that reads as "you
1830
- * have no objects".
1833
+ * Access requires an authenticated credential with the applicable object scope.
1831
1834
  *
1832
1835
  * HOW THIS DIFFERS FROM `g8.fields`. A FIELD adds a column to an existing
1833
1836
  * contact or company. A CUSTOM OBJECT is a whole new record type with its own
@@ -1840,10 +1843,16 @@ interface ObjectPagination {
1840
1843
  * lose your data, and the response lists every problem at once so a payload
1841
1844
  * with three mistakes takes one round trip to fix.
1842
1845
  * 2. `update` is a PARTIAL write. Attributes you omit are left alone; sending
1843
- * an explicit `null` CLEARS one. The two are deliberately different.
1846
+ * an explicit `null` clears an optional attribute. Required attributes
1847
+ * cannot be cleared with `null` or an empty multivalue list (422).
1844
1848
  * 3. `archive` does not destroy anything. The record leaves listings, stays
1845
1849
  * readable by id, and keeps its history.
1846
1850
  *
1851
+ * Record references must resolve to an active record of `config.target_object`
1852
+ * within the same organization and app. Unavailable targets return a 422 with
1853
+ * `invalid_reference` in the field error's `reason`, without disclosing whether
1854
+ * a target exists outside the caller's scope.
1855
+ *
1847
1856
  * Backed by:
1848
1857
  * GET /api/v1/objects
1849
1858
  * GET /api/v1/objects/{slug}
@@ -1887,12 +1896,16 @@ declare const createObjectsClient: (apiKey: string, apiUrl?: string) => {
1887
1896
  * Values are versioned rather than overwritten, so the previous value stays
1888
1897
  * readable through `history`.
1889
1898
  */
1890
- updateRecord(objectSlug: string, recordId: string, values: Record<string, unknown>): Promise<CustomObjectRecord>;
1899
+ updateRecord(objectSlug: string, recordId: string, values: Record<string, unknown>, options?: {
1900
+ expectedRevision?: number;
1901
+ }): Promise<CustomObjectRecord>;
1891
1902
  /**
1892
1903
  * Archive a record. It leaves listings, stays readable by id, and keeps its
1893
1904
  * history and associations. Nothing is destroyed.
1894
1905
  */
1895
1906
  archiveRecord(objectSlug: string, recordId: string): Promise<CustomObjectRecord>;
1907
+ /** Restore archived values under current constraints. Conflicts leave the record archived. */
1908
+ restoreRecord(objectSlug: string, recordId: string): Promise<CustomObjectRecord>;
1896
1909
  /**
1897
1910
  * A record's value timeline, newest first. An entry whose `active_until` is
1898
1911
  * null is the value currently in force.
@@ -3809,8 +3822,11 @@ declare class G8 {
3809
3822
  }>;
3810
3823
  createRecord(objectSlug: string, values: Record<string, unknown>): Promise<CustomObjectRecord>;
3811
3824
  getRecord(objectSlug: string, recordId: string): Promise<CustomObjectRecord>;
3812
- updateRecord(objectSlug: string, recordId: string, values: Record<string, unknown>): Promise<CustomObjectRecord>;
3825
+ updateRecord(objectSlug: string, recordId: string, values: Record<string, unknown>, options?: {
3826
+ expectedRevision?: number;
3827
+ }): Promise<CustomObjectRecord>;
3813
3828
  archiveRecord(objectSlug: string, recordId: string): Promise<CustomObjectRecord>;
3829
+ restoreRecord(objectSlug: string, recordId: string): Promise<CustomObjectRecord>;
3814
3830
  history(objectSlug: string, recordId: string, limit?: number): Promise<CustomObjectHistory>;
3815
3831
  };
3816
3832
  /** Deals and pipelines (requires API key). */
package/dist/index.d.ts CHANGED
@@ -1765,6 +1765,10 @@ interface CustomObjectAttribute {
1765
1765
  /** No two ACTIVE records may hold the same value. A collision returns 409. */
1766
1766
  is_unique: boolean;
1767
1767
  is_multiselect: boolean;
1768
+ /** Apply a non-null default to omitted create fields, never PATCH. Absent on older servers. */
1769
+ is_default_value_enabled?: boolean;
1770
+ /** Configured default; validated like an explicit value when enabled. */
1771
+ default_value?: unknown;
1768
1772
  /** Type-specific configuration, e.g. the allowed options for a `select`. */
1769
1773
  config: Record<string, unknown>;
1770
1774
  }
@@ -1780,6 +1784,8 @@ interface CustomObjectRecord {
1780
1784
  */
1781
1785
  values: Record<string, unknown>;
1782
1786
  is_archived: boolean;
1787
+ /** Revision for conditional updates; absent on older servers. */
1788
+ revision?: number;
1783
1789
  created_at: string | null;
1784
1790
  updated_at: string | null;
1785
1791
  }
@@ -1824,10 +1830,7 @@ interface ObjectPagination {
1824
1830
  * Requires an API key (server-side). On the hardened HTTP core: throws a typed
1825
1831
  * `G8Error` on failure and retries transient errors.
1826
1832
  *
1827
- * PREVIEW AND GATED. Every endpoint returns 403 `app_not_enabled` until the
1828
- * custom-objects surface is switched on for the platform. It is off by default,
1829
- * so a call fails fast rather than returning an empty list that reads as "you
1830
- * have no objects".
1833
+ * Access requires an authenticated credential with the applicable object scope.
1831
1834
  *
1832
1835
  * HOW THIS DIFFERS FROM `g8.fields`. A FIELD adds a column to an existing
1833
1836
  * contact or company. A CUSTOM OBJECT is a whole new record type with its own
@@ -1840,10 +1843,16 @@ interface ObjectPagination {
1840
1843
  * lose your data, and the response lists every problem at once so a payload
1841
1844
  * with three mistakes takes one round trip to fix.
1842
1845
  * 2. `update` is a PARTIAL write. Attributes you omit are left alone; sending
1843
- * an explicit `null` CLEARS one. The two are deliberately different.
1846
+ * an explicit `null` clears an optional attribute. Required attributes
1847
+ * cannot be cleared with `null` or an empty multivalue list (422).
1844
1848
  * 3. `archive` does not destroy anything. The record leaves listings, stays
1845
1849
  * readable by id, and keeps its history.
1846
1850
  *
1851
+ * Record references must resolve to an active record of `config.target_object`
1852
+ * within the same organization and app. Unavailable targets return a 422 with
1853
+ * `invalid_reference` in the field error's `reason`, without disclosing whether
1854
+ * a target exists outside the caller's scope.
1855
+ *
1847
1856
  * Backed by:
1848
1857
  * GET /api/v1/objects
1849
1858
  * GET /api/v1/objects/{slug}
@@ -1887,12 +1896,16 @@ declare const createObjectsClient: (apiKey: string, apiUrl?: string) => {
1887
1896
  * Values are versioned rather than overwritten, so the previous value stays
1888
1897
  * readable through `history`.
1889
1898
  */
1890
- updateRecord(objectSlug: string, recordId: string, values: Record<string, unknown>): Promise<CustomObjectRecord>;
1899
+ updateRecord(objectSlug: string, recordId: string, values: Record<string, unknown>, options?: {
1900
+ expectedRevision?: number;
1901
+ }): Promise<CustomObjectRecord>;
1891
1902
  /**
1892
1903
  * Archive a record. It leaves listings, stays readable by id, and keeps its
1893
1904
  * history and associations. Nothing is destroyed.
1894
1905
  */
1895
1906
  archiveRecord(objectSlug: string, recordId: string): Promise<CustomObjectRecord>;
1907
+ /** Restore archived values under current constraints. Conflicts leave the record archived. */
1908
+ restoreRecord(objectSlug: string, recordId: string): Promise<CustomObjectRecord>;
1896
1909
  /**
1897
1910
  * A record's value timeline, newest first. An entry whose `active_until` is
1898
1911
  * null is the value currently in force.
@@ -3809,8 +3822,11 @@ declare class G8 {
3809
3822
  }>;
3810
3823
  createRecord(objectSlug: string, values: Record<string, unknown>): Promise<CustomObjectRecord>;
3811
3824
  getRecord(objectSlug: string, recordId: string): Promise<CustomObjectRecord>;
3812
- updateRecord(objectSlug: string, recordId: string, values: Record<string, unknown>): Promise<CustomObjectRecord>;
3825
+ updateRecord(objectSlug: string, recordId: string, values: Record<string, unknown>, options?: {
3826
+ expectedRevision?: number;
3827
+ }): Promise<CustomObjectRecord>;
3813
3828
  archiveRecord(objectSlug: string, recordId: string): Promise<CustomObjectRecord>;
3829
+ restoreRecord(objectSlug: string, recordId: string): Promise<CustomObjectRecord>;
3814
3830
  history(objectSlug: string, recordId: string, limit?: number): Promise<CustomObjectHistory>;
3815
3831
  };
3816
3832
  /** Deals and pipelines (requires API key). */
package/dist/index.js CHANGED
@@ -1366,12 +1366,12 @@ var createObjectsClient = (apiKey, apiUrl) => {
1366
1366
  * Values are versioned rather than overwritten, so the previous value stays
1367
1367
  * readable through `history`.
1368
1368
  */
1369
- async updateRecord(objectSlug, recordId, values) {
1369
+ async updateRecord(objectSlug, recordId, values, options) {
1370
1370
  const resp = await request(
1371
1371
  baseUrl,
1372
1372
  `/api/v1/objects/${encode(objectSlug)}/records/${encode(recordId)}`,
1373
1373
  apiKey,
1374
- { method: "PATCH", body: { values } }
1374
+ { method: "PATCH", body: { values, ...options?.expectedRevision === void 0 ? {} : { expected_revision: options.expectedRevision } } }
1375
1375
  );
1376
1376
  return resp.data ?? resp;
1377
1377
  },
@@ -1388,6 +1388,16 @@ var createObjectsClient = (apiKey, apiUrl) => {
1388
1388
  );
1389
1389
  return resp.data ?? resp;
1390
1390
  },
1391
+ /** Restore archived values under current constraints. Conflicts leave the record archived. */
1392
+ async restoreRecord(objectSlug, recordId) {
1393
+ const resp = await request(
1394
+ baseUrl,
1395
+ `/api/v1/objects/${encode(objectSlug)}/records/${encode(recordId)}/restore`,
1396
+ apiKey,
1397
+ { method: "POST" }
1398
+ );
1399
+ return resp.data ?? resp;
1400
+ },
1391
1401
  /**
1392
1402
  * A record's value timeline, newest first. An entry whose `active_until` is
1393
1403
  * null is the value currently in force.