@granular-software/sdk 0.4.21 → 0.4.22
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 +241 -151
- package/dist/cli/index.js +452 -167
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,95 +61,107 @@ Projects created with `granular init` can include Markdown for coding agents (op
|
|
|
61
61
|
## Quick Start
|
|
62
62
|
|
|
63
63
|
```typescript
|
|
64
|
-
import { Granular, type ManifestContent } from
|
|
64
|
+
import { Granular, type ManifestContent } from "@granular-software/sdk";
|
|
65
65
|
|
|
66
66
|
const granular = new Granular({ apiKey: process.env.GRANULAR_API_KEY });
|
|
67
67
|
|
|
68
68
|
// 1. Connect to the default dev environment for one of your app users
|
|
69
69
|
const env = await granular.connect({
|
|
70
|
-
ontology:
|
|
71
|
-
environment:
|
|
72
|
-
userId:
|
|
73
|
-
permissions: [
|
|
74
|
-
name:
|
|
75
|
-
email:
|
|
70
|
+
ontology: "my-ontology",
|
|
71
|
+
environment: "dev",
|
|
72
|
+
userId: "user_123",
|
|
73
|
+
permissions: ["agent"],
|
|
74
|
+
name: "Jane Doe", // optional
|
|
75
|
+
email: "jane@example.com", // optional
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
// 2. Define your domain ontology
|
|
79
79
|
const manifest: ManifestContent = {
|
|
80
80
|
schemaVersion: 2,
|
|
81
|
-
name:
|
|
82
|
-
volumes: [
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
81
|
+
name: "my-app",
|
|
82
|
+
volumes: [
|
|
83
|
+
{
|
|
84
|
+
name: "schema",
|
|
85
|
+
scope: "sandbox",
|
|
86
|
+
imports: [{ alias: "@std", name: "standard_modules", label: "prod" }],
|
|
87
|
+
operations: [
|
|
88
|
+
// Define classes with typed properties
|
|
89
|
+
{
|
|
90
|
+
create: "customer",
|
|
91
|
+
extends: "@std/class",
|
|
92
|
+
has: {
|
|
93
|
+
name: { type: "string", description: "Customer name" },
|
|
94
|
+
email: { type: "string", description: "Email address" },
|
|
95
|
+
tier: { type: "string", description: "Subscription tier" },
|
|
96
|
+
},
|
|
95
97
|
},
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
{
|
|
99
|
+
create: "order",
|
|
100
|
+
extends: "@std/class",
|
|
101
|
+
has: {
|
|
102
|
+
total: { type: "number", description: "Order total" },
|
|
103
|
+
status: { type: "string", description: "Order status" },
|
|
104
|
+
},
|
|
103
105
|
},
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
// Define relationships
|
|
107
|
+
{
|
|
108
|
+
defineRelationship: {
|
|
109
|
+
left: "customer",
|
|
110
|
+
right: "order",
|
|
111
|
+
leftSubmodel: "orders",
|
|
112
|
+
rightSubmodel: "customer",
|
|
113
|
+
leftIsMany: true,
|
|
114
|
+
rightIsMany: false,
|
|
115
|
+
},
|
|
111
116
|
},
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
115
120
|
};
|
|
116
121
|
|
|
117
122
|
await env.applyManifest(manifest);
|
|
118
123
|
|
|
119
124
|
// 3. Record object instances
|
|
120
125
|
await env.recordObject({
|
|
121
|
-
className:
|
|
122
|
-
id:
|
|
123
|
-
label:
|
|
124
|
-
fields: { name:
|
|
126
|
+
className: "customer",
|
|
127
|
+
id: "cust_42",
|
|
128
|
+
label: "Acme Corp",
|
|
129
|
+
fields: { name: "Acme Corp", email: "billing@acme.com", tier: "enterprise" },
|
|
125
130
|
});
|
|
126
131
|
|
|
127
132
|
// 4. Register live effect handlers for effects already declared in the ontology manifest
|
|
128
133
|
await granular.registerEffects(env.sandboxId, [
|
|
129
134
|
{
|
|
130
|
-
name:
|
|
131
|
-
description:
|
|
132
|
-
className:
|
|
135
|
+
name: "get_billing_summary",
|
|
136
|
+
description: "Get billing summary for a customer",
|
|
137
|
+
className: "customer", // Attached to Customer class
|
|
133
138
|
// static omitted → instance method (receives object ID automatically)
|
|
134
139
|
inputSchema: {
|
|
135
|
-
type:
|
|
140
|
+
type: "object",
|
|
136
141
|
properties: {
|
|
137
|
-
period: {
|
|
142
|
+
period: {
|
|
143
|
+
type: "string",
|
|
144
|
+
description: 'Billing period (e.g. "2024-Q1")',
|
|
145
|
+
},
|
|
138
146
|
},
|
|
139
147
|
},
|
|
140
148
|
outputSchema: {
|
|
141
|
-
type:
|
|
149
|
+
type: "object",
|
|
142
150
|
properties: {
|
|
143
|
-
total:
|
|
144
|
-
invoices: { type:
|
|
145
|
-
period:
|
|
151
|
+
total: { type: "number", description: "Total billed amount" },
|
|
152
|
+
invoices: { type: "number", description: "Number of invoices" },
|
|
153
|
+
period: { type: "string", description: "The billing period" },
|
|
146
154
|
},
|
|
147
|
-
required: [
|
|
155
|
+
required: ["total", "invoices"],
|
|
148
156
|
},
|
|
149
157
|
handler: async (customerId: string, params: any, ctx: any) => {
|
|
150
158
|
// customerId comes from `this.id` in sandbox code
|
|
151
|
-
console.log(
|
|
152
|
-
return {
|
|
159
|
+
console.log("Running for subject:", ctx.user.subjectId);
|
|
160
|
+
return {
|
|
161
|
+
total: 4250.0,
|
|
162
|
+
invoices: 3,
|
|
163
|
+
period: params?.period || "current",
|
|
164
|
+
};
|
|
153
165
|
},
|
|
154
166
|
},
|
|
155
167
|
]);
|
|
@@ -183,13 +195,13 @@ Effects must be declared ahead of time in the ontology version manifest with `wi
|
|
|
183
195
|
## Core Flow
|
|
184
196
|
|
|
185
197
|
```
|
|
186
|
-
declare effects in the manifest → `granular build` creates or reuses a version → `connect({ ontology, environment, userId })` opens a named environment → `recordObject()` → `registerEffects()` → `submitJob()`
|
|
198
|
+
declare effects in the manifest → `granular build` creates or reuses a version → `connect({ ontology, environment, userId })` opens a named environment → `recordObject()` or `recordObjects()` → `registerEffects()` → `submitJob()`
|
|
187
199
|
```
|
|
188
200
|
|
|
189
201
|
1. **`connect()`** — Connect to an ontology environment for a given `userId`, returning an `Environment`
|
|
190
202
|
2. **`recordUser()`** — Optional explicit user upsert when you want the returned `granularId`
|
|
191
203
|
3. **`applyManifest()`** — Define your domain ontology (classes, properties, relationships)
|
|
192
|
-
4. **`recordObject()`** —
|
|
204
|
+
4. **`recordObject()` / `recordObjects()`** — Use `recordObject()` for one targeted upsert. Use `recordObjects()` for immediate multi-record writes with chunk progress.
|
|
193
205
|
5. **`granular.registerEffects()`** — Register sandbox-scoped live handlers for effects declared in the ontology manifest
|
|
194
206
|
6. **`submitJob()`** — Execute code in the sandbox that uses the auto-generated typed classes
|
|
195
207
|
|
|
@@ -200,42 +212,47 @@ Use `applyManifest()` to declare classes, typed properties, and relationships:
|
|
|
200
212
|
```typescript
|
|
201
213
|
const manifest: ManifestContent = {
|
|
202
214
|
schemaVersion: 2,
|
|
203
|
-
name:
|
|
204
|
-
volumes: [
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
215
|
+
name: "library-app",
|
|
216
|
+
volumes: [
|
|
217
|
+
{
|
|
218
|
+
name: "schema",
|
|
219
|
+
scope: "sandbox",
|
|
220
|
+
imports: [{ alias: "@std", name: "standard_modules", label: "prod" }],
|
|
221
|
+
operations: [
|
|
222
|
+
// Classes with typed properties
|
|
223
|
+
{
|
|
224
|
+
create: "author",
|
|
225
|
+
extends: "@std/class",
|
|
226
|
+
has: {
|
|
227
|
+
name: { type: "string", description: "Author full name" },
|
|
228
|
+
birth_year: { type: "number", description: "Year of birth" },
|
|
229
|
+
},
|
|
216
230
|
},
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
231
|
+
{
|
|
232
|
+
create: "book",
|
|
233
|
+
extends: "@std/class",
|
|
234
|
+
has: {
|
|
235
|
+
title: { type: "string", description: "Book title" },
|
|
236
|
+
isbn: { type: "string", description: "ISBN number" },
|
|
237
|
+
published_year: { type: "number", description: "Year published" },
|
|
238
|
+
},
|
|
225
239
|
},
|
|
226
|
-
},
|
|
227
240
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
241
|
+
// Relationships (bidirectional, with cardinality)
|
|
242
|
+
{
|
|
243
|
+
defineRelationship: {
|
|
244
|
+
left: "author",
|
|
245
|
+
right: "book",
|
|
246
|
+
leftSubmodel: "books",
|
|
247
|
+
rightSubmodel: "author",
|
|
248
|
+
leftIsMany: true,
|
|
249
|
+
rightIsMany: false,
|
|
250
|
+
// → author.books (one-to-many), book.author (many-to-one)
|
|
251
|
+
},
|
|
235
252
|
},
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
253
|
+
],
|
|
254
|
+
},
|
|
255
|
+
],
|
|
239
256
|
};
|
|
240
257
|
|
|
241
258
|
await env.applyManifest(manifest);
|
|
@@ -247,32 +264,47 @@ After defining the ontology, connect as a user and populate it with data:
|
|
|
247
264
|
|
|
248
265
|
```typescript
|
|
249
266
|
const env = await granular.connect({
|
|
250
|
-
ontology:
|
|
251
|
-
environment:
|
|
252
|
-
userId:
|
|
253
|
-
permissions: [
|
|
267
|
+
ontology: "library-app",
|
|
268
|
+
environment: "dev",
|
|
269
|
+
userId: "user_123",
|
|
270
|
+
permissions: ["agent"],
|
|
254
271
|
});
|
|
255
272
|
|
|
256
|
-
const tolkien = await env.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
273
|
+
const [tolkien, lotr] = await env.recordObjects(
|
|
274
|
+
[
|
|
275
|
+
{
|
|
276
|
+
className: "author",
|
|
277
|
+
id: "tolkien", // Real-world ID (unique per class)
|
|
278
|
+
label: "J.R.R. Tolkien",
|
|
279
|
+
fields: { name: "J.R.R. Tolkien", birth_year: 1892 },
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
className: "book",
|
|
283
|
+
id: "lotr",
|
|
284
|
+
label: "The Lord of the Rings",
|
|
285
|
+
fields: {
|
|
286
|
+
title: "The Lord of the Rings",
|
|
287
|
+
isbn: "978-0-618-64015-7",
|
|
288
|
+
published_year: 1954,
|
|
289
|
+
},
|
|
290
|
+
relationships: { author: "tolkien" }, // Real-world ID — SDK resolves it automatically
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
{
|
|
294
|
+
onChunkComplete(info) {
|
|
295
|
+
console.log(`Committed chunk ${info.chunkIndex + 1}/${info.totalChunks}`);
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
);
|
|
262
299
|
// tolkien.path → 'author_tolkien' (internal graph path, unique globally)
|
|
263
300
|
// tolkien.id → 'tolkien' (real-world ID)
|
|
264
|
-
|
|
265
|
-
const lotr = await env.recordObject({
|
|
266
|
-
className: 'book',
|
|
267
|
-
id: 'lotr',
|
|
268
|
-
label: 'The Lord of the Rings',
|
|
269
|
-
fields: { title: 'The Lord of the Rings', isbn: '978-0-618-64015-7', published_year: 1954 },
|
|
270
|
-
relationships: { author: 'tolkien' }, // Real-world ID — SDK resolves it automatically
|
|
271
|
-
});
|
|
301
|
+
// lotr.created → true (false means the record already existed and was updated)
|
|
272
302
|
```
|
|
273
303
|
|
|
274
304
|
> **Cross-class ID uniqueness**: Two objects of different classes can share the same real-world ID (e.g., an `author` "tolkien" and a `publisher` "tolkien"). The SDK derives unique graph paths internally (`author_tolkien`, `publisher_tolkien`) so they never collide.
|
|
275
305
|
|
|
306
|
+
For one-off writes, `recordObject(...)` is still the clearest choice. For large fire-and-forget imports, use `enqueueRecordImport(...)` and poll `getRecordImport(...)` / `getRecordImportSummary()` for aggregate progress.
|
|
307
|
+
|
|
276
308
|
## Effect Definitions
|
|
277
309
|
|
|
278
310
|
Effects are declared in the manifest with `withEffect`, then their live handlers are registered at sandbox scope. Effects can be **instance methods**, **static methods**, or **global functions**. Both `inputSchema` and `outputSchema` use JSON Schema:
|
|
@@ -282,44 +314,44 @@ await granular.registerEffects(env.sandboxId, [
|
|
|
282
314
|
// Instance method: called as `tolkien.get_bio({ detailed: true })`
|
|
283
315
|
// Handler receives (objectId, params)
|
|
284
316
|
{
|
|
285
|
-
name:
|
|
286
|
-
description:
|
|
287
|
-
className:
|
|
317
|
+
name: "get_bio",
|
|
318
|
+
description: "Get biography of an author",
|
|
319
|
+
className: "author", // Attached to Author class
|
|
288
320
|
// static: false (default) // Instance method
|
|
289
321
|
inputSchema: {
|
|
290
|
-
type:
|
|
322
|
+
type: "object",
|
|
291
323
|
properties: {
|
|
292
|
-
detailed: { type:
|
|
324
|
+
detailed: { type: "boolean", description: "Include full details" },
|
|
293
325
|
},
|
|
294
326
|
},
|
|
295
327
|
outputSchema: {
|
|
296
|
-
type:
|
|
328
|
+
type: "object",
|
|
297
329
|
properties: {
|
|
298
|
-
bio:
|
|
299
|
-
source: { type:
|
|
330
|
+
bio: { type: "string", description: "The biography text" },
|
|
331
|
+
source: { type: "string", description: "Source of the bio" },
|
|
300
332
|
},
|
|
301
|
-
required: [
|
|
333
|
+
required: ["bio"],
|
|
302
334
|
},
|
|
303
335
|
handler: async (id: string, params: any, ctx: any) => {
|
|
304
|
-
return { bio: `Biography of ${id}`, source:
|
|
336
|
+
return { bio: `Biography of ${id}`, source: "database" };
|
|
305
337
|
},
|
|
306
338
|
},
|
|
307
339
|
|
|
308
340
|
// Static method: called as `Author.search({ query: 'tolkien' })`
|
|
309
341
|
// Handler receives (params) — no object ID
|
|
310
342
|
{
|
|
311
|
-
name:
|
|
312
|
-
description:
|
|
313
|
-
className:
|
|
343
|
+
name: "search",
|
|
344
|
+
description: "Search for authors",
|
|
345
|
+
className: "author",
|
|
314
346
|
static: true,
|
|
315
347
|
inputSchema: {
|
|
316
|
-
type:
|
|
317
|
-
properties: { query: { type:
|
|
318
|
-
required: [
|
|
348
|
+
type: "object",
|
|
349
|
+
properties: { query: { type: "string" } },
|
|
350
|
+
required: ["query"],
|
|
319
351
|
},
|
|
320
352
|
outputSchema: {
|
|
321
|
-
type:
|
|
322
|
-
properties: { results: { type:
|
|
353
|
+
type: "object",
|
|
354
|
+
properties: { results: { type: "array" } },
|
|
323
355
|
},
|
|
324
356
|
handler: async (params: any, ctx: any) => {
|
|
325
357
|
return { results: [`Found: ${params.query}`] };
|
|
@@ -329,16 +361,16 @@ await granular.registerEffects(env.sandboxId, [
|
|
|
329
361
|
// Global tool: called as `global_search({ query: 'rings' })`
|
|
330
362
|
// No className → standalone exported function
|
|
331
363
|
{
|
|
332
|
-
name:
|
|
333
|
-
description:
|
|
364
|
+
name: "global_search",
|
|
365
|
+
description: "Search across everything",
|
|
334
366
|
inputSchema: {
|
|
335
|
-
type:
|
|
336
|
-
properties: { query: { type:
|
|
337
|
-
required: [
|
|
367
|
+
type: "object",
|
|
368
|
+
properties: { query: { type: "string" } },
|
|
369
|
+
required: ["query"],
|
|
338
370
|
},
|
|
339
371
|
outputSchema: {
|
|
340
|
-
type:
|
|
341
|
-
properties: { results: { type:
|
|
372
|
+
type: "object",
|
|
373
|
+
properties: { results: { type: "array" } },
|
|
342
374
|
},
|
|
343
375
|
handler: async (params: any, ctx: any) => {
|
|
344
376
|
return { results: [`Result for: ${params.query}`] };
|
|
@@ -369,22 +401,45 @@ export declare class Author {
|
|
|
369
401
|
constructor(id: string, fields?: Record<string, any>);
|
|
370
402
|
|
|
371
403
|
/** Get a cached Author by graph path, hydrating from the graph when needed */
|
|
372
|
-
static get(query: {
|
|
404
|
+
static get(query: {
|
|
405
|
+
path: string;
|
|
406
|
+
refresh?: boolean;
|
|
407
|
+
}): Promise<Author | null>;
|
|
373
408
|
|
|
374
409
|
/** Count Author instances without loading them into the heap */
|
|
375
410
|
static count(): Promise<number>;
|
|
376
411
|
|
|
377
412
|
/** Return one page of Author instances together with pagination metadata */
|
|
378
|
-
static page(query?: {
|
|
413
|
+
static page(query?: {
|
|
414
|
+
page?: number;
|
|
415
|
+
perPage?: number;
|
|
416
|
+
limit?: number;
|
|
417
|
+
saveAs?: string;
|
|
418
|
+
refresh?: boolean;
|
|
419
|
+
}): Promise<SandboxPageResult<Author>>;
|
|
379
420
|
|
|
380
421
|
/** List one page of Author instances */
|
|
381
|
-
static list(query?: {
|
|
422
|
+
static list(query?: {
|
|
423
|
+
page?: number;
|
|
424
|
+
perPage?: number;
|
|
425
|
+
limit?: number;
|
|
426
|
+
saveAs?: string;
|
|
427
|
+
refresh?: boolean;
|
|
428
|
+
}): Promise<Author[]>;
|
|
382
429
|
|
|
383
430
|
/** Stream Author instances page by page */
|
|
384
|
-
static iterate(query?: {
|
|
431
|
+
static iterate(query?: {
|
|
432
|
+
page?: number;
|
|
433
|
+
perPage?: number;
|
|
434
|
+
limit?: number;
|
|
435
|
+
maxItems?: number;
|
|
436
|
+
refresh?: boolean;
|
|
437
|
+
}): AsyncIterable<Author>;
|
|
385
438
|
|
|
386
439
|
/** Get biography of an author */
|
|
387
|
-
get_bio(input?: {
|
|
440
|
+
get_bio(input?: {
|
|
441
|
+
detailed?: boolean;
|
|
442
|
+
}): Promise<{ bio: string; source?: string }>;
|
|
388
443
|
|
|
389
444
|
/** Search for authors (static) */
|
|
390
445
|
static search(input: { query: string }): Promise<{ results?: any[] }>;
|
|
@@ -403,47 +458,67 @@ export declare class Book {
|
|
|
403
458
|
|
|
404
459
|
static count(): Promise<number>;
|
|
405
460
|
|
|
406
|
-
static page(query?: {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
461
|
+
static page(query?: {
|
|
462
|
+
page?: number;
|
|
463
|
+
perPage?: number;
|
|
464
|
+
limit?: number;
|
|
465
|
+
saveAs?: string;
|
|
466
|
+
refresh?: boolean;
|
|
467
|
+
}): Promise<SandboxPageResult<Book>>;
|
|
468
|
+
|
|
469
|
+
static list(query?: {
|
|
470
|
+
page?: number;
|
|
471
|
+
perPage?: number;
|
|
472
|
+
limit?: number;
|
|
473
|
+
saveAs?: string;
|
|
474
|
+
refresh?: boolean;
|
|
475
|
+
}): Promise<Book[]>;
|
|
476
|
+
|
|
477
|
+
static iterate(query?: {
|
|
478
|
+
page?: number;
|
|
479
|
+
perPage?: number;
|
|
480
|
+
limit?: number;
|
|
481
|
+
maxItems?: number;
|
|
482
|
+
refresh?: boolean;
|
|
483
|
+
}): AsyncIterable<Book>;
|
|
411
484
|
|
|
412
485
|
/** Navigate to author (many_to_one) */
|
|
413
486
|
get_author(): Promise<Author | null>;
|
|
414
487
|
}
|
|
415
488
|
|
|
416
489
|
/** Search across everything */
|
|
417
|
-
export declare function global_search(input: {
|
|
490
|
+
export declare function global_search(input: {
|
|
491
|
+
query: string;
|
|
492
|
+
}): Promise<{ results?: any[] }>;
|
|
418
493
|
```
|
|
419
494
|
|
|
420
495
|
The LLM or user writes code against these typed classes:
|
|
421
496
|
|
|
422
497
|
```typescript
|
|
423
|
-
import { Author, Book, global_search } from
|
|
498
|
+
import { Author, Book, global_search } from "./sandbox-tools";
|
|
424
499
|
|
|
425
500
|
const totalAuthors = await Author.count();
|
|
426
501
|
const firstPage = await Author.page({ page: 1, perPage: 25 });
|
|
427
502
|
const authors = firstPage.items;
|
|
428
|
-
const tolkien = authors.find((author) => author.name ===
|
|
429
|
-
if (!tolkien) throw new Error(
|
|
503
|
+
const tolkien = authors.find((author) => author.name === "J.R.R. Tolkien");
|
|
504
|
+
if (!tolkien) throw new Error("Author not found");
|
|
430
505
|
console.log(totalAuthors, firstPage.hasMore);
|
|
431
|
-
console.log(tolkien.name);
|
|
506
|
+
console.log(tolkien.name); // "J.R.R. Tolkien"
|
|
432
507
|
|
|
433
508
|
for await (const author of Author.iterate({ perPage: 100, maxItems: 200 })) {
|
|
434
509
|
console.log(author.id);
|
|
435
510
|
}
|
|
436
511
|
|
|
437
512
|
const bio = await tolkien.get_bio({ detailed: true });
|
|
438
|
-
console.log(bio.bio);
|
|
513
|
+
console.log(bio.bio); // typed as string
|
|
439
514
|
|
|
440
515
|
const books = await tolkien.get_books();
|
|
441
516
|
for (const book of books) {
|
|
442
|
-
console.log(book.title, book.isbn);
|
|
517
|
+
console.log(book.title, book.isbn); // typed properties
|
|
443
518
|
}
|
|
444
519
|
|
|
445
|
-
const results = await Author.search({ query:
|
|
446
|
-
const globalResults = await global_search({ query:
|
|
520
|
+
const results = await Author.search({ query: "tolkien" });
|
|
521
|
+
const globalResults = await global_search({ query: "rings" });
|
|
447
522
|
```
|
|
448
523
|
|
|
449
524
|
## GraphQL API
|
|
@@ -453,7 +528,7 @@ Every environment has a built-in GraphQL API for direct graph queries:
|
|
|
453
528
|
```typescript
|
|
454
529
|
// Authenticated automatically with your API key
|
|
455
530
|
const result = await env.graphql(
|
|
456
|
-
`query { model(path: "author") { path label submodels { path label } } }
|
|
531
|
+
`query { model(path: "author") { path label submodels { path label } } }`,
|
|
457
532
|
);
|
|
458
533
|
|
|
459
534
|
// The endpoint is also available as a URL
|
|
@@ -475,21 +550,27 @@ See [examples/](./examples/) for runnable code:
|
|
|
475
550
|
## API Reference
|
|
476
551
|
|
|
477
552
|
### `granular.recordUser(options)`
|
|
553
|
+
|
|
478
554
|
Registers a user identity and their assigned permission profiles.
|
|
479
555
|
|
|
480
556
|
### `granular.connect(options)`
|
|
557
|
+
|
|
481
558
|
Connects to an environment session for the specified user. Pass `ontology` and `environment` explicitly, for example `environment: 'dev'` or `environment: 'prod'`. Only pass `tagName` for advanced overrides.
|
|
482
559
|
|
|
483
560
|
### `environment.applyManifest(manifest)`
|
|
561
|
+
|
|
484
562
|
Defines domain ontology: classes (with typed properties), and relationships (with cardinality).
|
|
485
563
|
|
|
486
564
|
### `environment.recordObject(options)`
|
|
565
|
+
|
|
487
566
|
Creates or updates a class instance with fields and relationships. Returns `{ path, id, created }`.
|
|
488
567
|
|
|
489
568
|
### `environment.recordObjects(records, options?)`
|
|
569
|
+
|
|
490
570
|
Batch upsert for many instances. The SDK sends **chunks** (default **100** rows per HTTP `POST` to `/records/batch`) with **retries** on transient failures, so large arrays do not time out as a single oversized request.
|
|
491
571
|
|
|
492
572
|
Optional **`options`**:
|
|
573
|
+
|
|
493
574
|
- **`batchSize`** — max rows per request (default 100).
|
|
494
575
|
- **`concurrency`** — how many chunk requests may run in parallel (default 1, max 16); can reduce wall time when the server can overlap work.
|
|
495
576
|
- **`onChunkComplete`** — async-friendly hook after each chunk for progress UIs; the returned array is always ordered like `records`.
|
|
@@ -497,30 +578,39 @@ Optional **`options`**:
|
|
|
497
578
|
**Sync batch vs queued import:** use **`recordObjects`** when you need **synchronous** commits and/or per-chunk feedback. Use **`enqueueRecordImport`** + **`getRecordImport` / `getRecordImportSummary`** for **background** ingestion with aggregate counters when admission latency matters more than immediate row-by-row completion.
|
|
498
579
|
|
|
499
580
|
### `environment.getRelationships(modelPath)`
|
|
581
|
+
|
|
500
582
|
Returns relationship definitions for a given class.
|
|
501
583
|
|
|
502
584
|
### `environment.listRelated(modelPath, submodelPath)`
|
|
585
|
+
|
|
503
586
|
Lists related instances through a relationship.
|
|
504
587
|
|
|
505
588
|
### `granular.registerEffects(sandboxId, effects)`
|
|
589
|
+
|
|
506
590
|
Registers sandbox-scoped live handlers for effects declared in the ontology manifest. Effects can be instance methods (`className` set, `static` omitted), static methods (`static: true`), or global functions (no `className`).
|
|
507
591
|
|
|
508
592
|
### `environment.submitJob(code)`
|
|
593
|
+
|
|
509
594
|
Submits code to be executed in the sandbox. The code imports typed classes from `./sandbox-tools`.
|
|
510
595
|
|
|
511
596
|
### `environment.getDomainDocumentation()`
|
|
597
|
+
|
|
512
598
|
Get auto-generated TypeScript class declarations. Pass this to LLMs to help them write correct code.
|
|
513
599
|
|
|
514
600
|
### `environment.graphql(query, variables?)`
|
|
601
|
+
|
|
515
602
|
Execute a GraphQL query against the environment's graph. Authenticated automatically.
|
|
516
603
|
|
|
517
604
|
### `environment.on(event, handler)`
|
|
605
|
+
|
|
518
606
|
Listen for events: `'effect:invoke'`, `'effect:result'`, `'job:status'`, `'stdout'`, etc. Legacy `'tool:*'` aliases still exist internally but are no longer the primary model.
|
|
519
607
|
|
|
520
608
|
### `Environment.toGraphPath(className, id)`
|
|
609
|
+
|
|
521
610
|
Convert a class name + real-world ID to a unique graph path (`{className}_{id}`).
|
|
522
611
|
|
|
523
612
|
### `Environment.extractIdFromGraphPath(graphPath, className)`
|
|
613
|
+
|
|
524
614
|
Extract the real-world ID from a graph path by stripping the class prefix.
|
|
525
615
|
|
|
526
616
|
## License
|