@graph8/sdk 0.13.1 → 0.15.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 CHANGED
@@ -251,11 +251,41 @@ 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
+ The unreleased CRM increment adds `crm.record.created`, `crm.record.updated`,
255
+ `crm.record.archived`, and `crm.record.restored`. Their `data` contains
256
+ `object_slug`, `record_id`, and `revision`, rather than private field values.
257
+ The envelope `id` is a stable event ID; `X-Studio-Delivery-Id` identifies a
258
+ subscription's delivery. Verify the original request bytes before processing,
259
+ and deduplicate by the stable identifiers because delivery is at least once.
260
+ CRM subscription creation, update, deletion and secret rotation through the API
261
+ require explicit `webhooks:write` and `objects:read` scopes. Removing CRM events
262
+ also requires these grants. Reading CRM subscriptions or their delivery history
263
+ requires explicit `webhooks:read` and `objects:read` scopes; a subscription list
264
+ containing CRM subscriptions enforces the same read grants before returning data.
265
+ These checks refresh credentials and apply even during compatibility scope soak.
266
+ The backend checks current credential, membership and record access again before
267
+ sending. Native custom
268
+ records currently follow workspace-level access; app-owned/delegated authority
269
+ and configurable Enterprise object grants remain incomplete.
270
+
254
271
  ## Native custom objects
255
272
 
256
273
  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
274
+ reads, `objects:write` for record create/PATCH, and `objects:delete` for record archive.
275
+ Schema create/update/archive require `objects:manage` and current Admin access through
276
+ a personal API key. `objects.update(slug, { is_archived: false })` restores an object.
277
+ Object create accepts `description` and a typed `icon`. Update also accepts
278
+ `display_attribute_slug` for an active, single-value label field. Omit presentation
279
+ properties to preserve them or pass `null` to clear them. The current display field
280
+ must be replaced or cleared before archiving it or making it multivalue.
281
+ Attribute administration in the unreleased increment uses `objects.createAttribute`,
282
+ `objects.updateAttribute`, and `objects.archiveAttribute` with the same scope and Admin requirement.
283
+ Restore with `objects.updateAttribute(objectSlug, attributeSlug, { is_archived: false })`.
284
+ Use `objects.listAttributes(objectSlug, { include_archived: true })` to find archived fields;
285
+ normal discovery and record validation use active fields only. Attribute PATCH preserves
286
+ omitted fields; `default_value: null` explicitly clears a default. Requires the matching backend.
287
+
288
+ Unscoped keys receive 403. `objects:*` grants all object scopes; grant only the operations the
259
289
  integration needs.
260
290
 
261
291
  ```typescript
@@ -273,6 +303,36 @@ PATCH preserves omitted attributes. Enabled defaults apply only on create;
273
303
  unknown fields return 422 and unique-value collisions return 409. Standard
274
304
  contacts, companies, and deals continue using their existing SDK resources.
275
305
 
306
+ ### Create or update by a unique key
307
+
308
+ Use an active, single-value unique attribute to match a native custom record.
309
+ Supply its nonempty value with the fields to write. Existing matches retain
310
+ their graph8 ID and omitted values; new records receive configured defaults.
311
+
312
+ ```typescript
313
+ import { g8 } from '@graph8/sdk';
314
+
315
+ g8.init({ apiKey: process.env.G8_API_KEY! });
316
+ // Assumes invoices.reference is a unique text attribute in this workspace.
317
+ const invoice = await g8.objects.upsertRecord('invoices', 'reference', {
318
+ reference: 'INV-1042',
319
+ });
320
+ // A conditional update fails with 409 if the record changed or no longer matches.
321
+ await g8.objects.upsertRecord('invoices', 'reference', {
322
+ reference: 'INV-1042',
323
+ }, { expectedRevision: invoice.revision });
324
+ ```
325
+
326
+ The REST operation is `POST /api/v1/objects/{object_slug}/records/upsert` with
327
+ `{ "matching_attribute": "reference", "values": { "reference": "INV-1042" } }`.
328
+ It requires `objects:write`, returns 201 for creation or 200 for an update, and
329
+ accepts optional `expected_revision`. MCP exposes `g8_object_record_upsert`
330
+ with the same fields. Archived records do not match. Invalid or nonunique
331
+ matching attributes return 422; another unique-field collision returns 409.
332
+ Legacy values requiring a uniqueness backfill also return 409 rather than
333
+ creating a duplicate. Repeated upserts can create additional update history;
334
+ unique-key matching is not a promise of exactly-once side effects.
335
+
276
336
  ## App Platform
277
337
 
278
338
  Hosted apps do not carry a permanent org API key. Instead they **exchange** a
@@ -371,3 +431,68 @@ unique value has been reused. Validation failures leave the record archived.
371
431
  Ambiguous legacy history returns 422 with `archive_snapshot_unavailable` rather
372
432
  than guessing its prior values. Repeating a successful restore adds no revision
373
433
  or history entry.
434
+
435
+ ### Append or remove multivalue items
436
+
437
+ ```ts
438
+ import { g8 } from "@graph8/sdk";
439
+
440
+ g8.init({ apiKey: "YOUR_API_KEY" });
441
+ const projectId = "your-project-record-id";
442
+ const record = await g8.objects.getRecord("projects", projectId);
443
+ await g8.objects.updateRecord("projects", projectId, {}, {
444
+ appendValues: { tags: ["priority"] },
445
+ removeValues: { reviewers: ["former-reviewer-id"] },
446
+ expectedRevision: record.revision,
447
+ });
448
+ ```
449
+
450
+ Native and app PATCH APIs use `append_values` and `remove_values`; the MCP
451
+ `g8_object_record_update` tool accepts those same names. Each field must occur
452
+ in exactly one of `values`, `append_values`, or `remove_values`. These operations
453
+ require a multivalue attribute; `values` still replaces the entire supplied field.
454
+ Append preserves existing order and adds only canonically distinct items. Remove
455
+ deletes all matching items. The resulting values must satisfy required, unique,
456
+ and reference constraints. Changes are atomic and read the current values under
457
+ a record lock, so concurrent appends do not discard each other.
458
+
459
+ Repeating an append can leave the values unchanged while still advancing the
460
+ record revision and history. This is not an idempotency guarantee for events.
461
+ Deploy the matching backend before using these optional request fields.
462
+
463
+ ### Record mutation history
464
+
465
+ ```ts
466
+ import { g8 } from "@graph8/sdk";
467
+
468
+ g8.init({ apiKey: "YOUR_API_KEY" });
469
+ const firstPage = await g8.objects.changes("projects", "your-project-record-id", { limit: 50 });
470
+ if (firstPage.next_cursor) {
471
+ const olderPage = await g8.objects.changes("projects", "your-project-record-id", {
472
+ limit: 50,
473
+ cursor: firstPage.next_cursor,
474
+ });
475
+ console.log(olderPage.entries);
476
+ }
477
+ ```
478
+
479
+ The matching native and app APIs expose `/objects/{object_slug}/records/{record_id}/changes`
480
+ and `/app/objects/{object_slug}/records/{record_id}/changes`. The MCP tool is
481
+ `g8_object_record_changes`. These additions require the matching backend deployment.
482
+ Entries include the record revision, action, actor identity/type, source, time,
483
+ and changed values. Presence flags distinguish a cleared field from a present null.
484
+ An empty page means no mutation entries were recorded, not that the record never
485
+ changed; the existing value-history endpoint remains available for older facts.
486
+ Use the returned opaque cursor for older pages. A storage failure is an error,
487
+ not an empty history. This interface does not establish broader object/record ACL parity.
488
+
489
+ ### Conditional deal updates (unreleased)
490
+
491
+ Read the canonical deal record through `GET /api/v1/objects/deals/records/{id}`
492
+ to obtain its CRM revision, then pass `expected_revision` to
493
+ `g8.deals.update(id, { name: "Updated", expected_revision: revision })`.
494
+ A stale edit returns HTTP 409; reload and reconcile before retrying. A successful
495
+ conditional update returns `revision`. If tenant revision tracking is unavailable,
496
+ the conditional write returns 503 before mutation. Omitting `expected_revision`
497
+ retains the existing PATCH contract. The MCP `g8_update_deal` tool accepts the same
498
+ field. Deal UI integration and deployed QA verification remain pending.