@alvera-ai/platform-sdk 0.17.0 → 0.18.0-next.g49897f5
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/.agent/AGENTS.md +91 -144
- package/.agent/account_management.md +2 -2
- package/.agent/action_logs.md +4 -4
- package/.agent/advanced_migrations.md +202 -0
- package/.agent/ai_agents.md +28 -21
- package/.agent/ai_sandbox.md +49 -39
- package/.agent/connected_apps.md +3 -3
- package/.agent/cookbook/_fixtures/README.md +1 -1
- package/.agent/cookbook/_fixtures/{foundation → organic-marketing}/_lead_submissions_foundation_generic_table.liquid +1 -1
- package/.agent/cookbook/_fixtures/organic-marketing/_lead_submissions_foundation_legal_entity.liquid +80 -0
- package/.agent/cookbook/_fixtures/{foundation → organic-marketing}/_lead_submissions_foundation_mdm.liquid +2 -1
- package/.agent/cookbook/_fixtures/payments-compliance/_compliance_screenings_generic_table.liquid +57 -0
- package/.agent/cookbook/_fixtures/payments-compliance/_compliance_screenings_legal_entity.liquid +30 -0
- package/.agent/cookbook/_fixtures/payments-compliance/_compliance_screenings_mdm.liquid +44 -0
- package/.agent/cookbook/_fixtures/payments-compliance/_payment_accounts_generic_table.liquid +57 -0
- package/.agent/cookbook/_fixtures/payments-compliance/_payment_accounts_legal_entity.liquid +36 -0
- package/.agent/cookbook/_fixtures/payments-compliance/_payment_accounts_mdm.liquid +41 -0
- package/.agent/cookbook/_fixtures/primary-care-feedback/_cahps_appointments_generic_table.liquid +70 -0
- package/.agent/cookbook/_fixtures/primary-care-feedback/_cahps_appointments_legal_entity.liquid +52 -0
- package/.agent/cookbook/_fixtures/primary-care-feedback/_cahps_appointments_mdm.liquid +42 -0
- package/.agent/cookbook/_fixtures/subscription-saas/_customers_subscription_generic_table.liquid +38 -0
- package/.agent/cookbook/_fixtures/subscription-saas/_customers_subscription_legal_entity.liquid +48 -0
- package/.agent/cookbook/_fixtures/subscription-saas/_customers_subscription_mdm.liquid +49 -0
- package/.agent/cookbook/organic-marketing.md +2801 -0
- package/.agent/cookbook/payments-compliance.md +2180 -0
- package/.agent/cookbook/primary-care.md +2175 -0
- package/.agent/cookbook/subscription-saas.md +2403 -0
- package/.agent/data_activation_clients.md +65 -52
- package/.agent/datalakes.md +407 -171
- package/.agent/errors.md +3 -3
- package/.agent/generic_tables.md +183 -62
- package/.agent/interoperability_contracts.md +57 -22
- package/.agent/mdm.md +136 -153
- package/.agent/messages.md +36 -34
- package/.agent/mock-services.md +13 -12
- package/.agent/mutations.md +2 -2
- package/.agent/templates.md +14 -13
- package/.agent/tools.md +63 -21
- package/.agent/type_naming.md +13 -13
- package/.agent/workflows.md +99 -53
- package/README.md +2 -2
- package/dist/bin/platform-sdk.mjs +33 -47
- package/dist/bin/platform-sdk.mjs.map +1 -1
- package/dist/index.d.mts +619 -385
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +535 -59
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_legal_entity.liquid +0 -88
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_appointment.liquid +0 -47
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_mdm.liquid +0 -24
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_patient.liquid +0 -38
- package/.agent/cookbook/_fixtures/payments/_compliance_screenings_payments_compliance_screening.liquid +0 -59
- package/.agent/cookbook/_fixtures/payments/_compliance_screenings_payments_mdm.liquid +0 -36
- package/.agent/cookbook/_fixtures/payments/_payment_accounts_payments_mdm.liquid +0 -30
- package/.agent/cookbook/_fixtures/payments/_payment_accounts_payments_payment_account.liquid +0 -55
- package/.agent/cookbook/_fixtures/subscription/_customers_subscription_mdm.liquid +0 -20
- package/.agent/cookbook/_setup/foundation.md +0 -359
- package/.agent/cookbook/_setup/healthcare.md +0 -361
- package/.agent/cookbook/_setup/payments.md +0 -365
- package/.agent/cookbook/_setup/subscription.md +0 -364
- package/.agent/cookbook/action-status-updaters.md +0 -278
- package/.agent/cookbook/ai-agent-invoke.md +0 -279
- package/.agent/cookbook/appointment-review-sms-workflow.md +0 -801
- package/.agent/cookbook/birthday-greeting-sms-trigger.md +0 -696
- package/.agent/cookbook/bulk-ingest.md +0 -302
- package/.agent/cookbook/contact-us-triage-with-llm.md +0 -663
- package/.agent/cookbook/dunning-sms-for-delinquent.md +0 -659
- package/.agent/cookbook/generic-tables.md +0 -244
- package/.agent/cookbook/invite-team.md +0 -200
- package/.agent/cookbook/kyc-notification-on-account-activation.md +0 -661
- package/.agent/cookbook/marketing-campaign-send.md +0 -1044
- package/.agent/cookbook/paginated-restapi-poller.md +0 -383
- package/.agent/cookbook/rest-fetch.md +0 -273
- package/.agent/cookbook/sanctions-screening-with-agent-review.md +0 -773
- package/.agent/cookbook/score-leads-with-llm-categorization.md +0 -665
- package/.agent/cookbook/system-templates.md +0 -165
- package/.agent/cookbook/talk-to-data.md +0 -178
- package/.agent/cookbook/triage-prospects-by-priority.md +0 -571
- package/.agent/cookbook/welcome-sms-for-customers.md +0 -647
- /package/.agent/cookbook/_fixtures/{healthcare → primary-care-feedback}/memorandum-of-association-01.png +0 -0
- /package/.agent/cookbook/_fixtures/{healthcare → primary-care-feedback}/sample_two_page.pdf +0 -0
- /package/.agent/cookbook/_fixtures/{subscription → subscription-saas}/_customers_subscription_customer.liquid +0 -0
- /package/.agent/cookbook/_fixtures/{subscription → subscription-saas}/stripe_customers_batch1.csv +0 -0
package/.agent/AGENTS.md
CHANGED
|
@@ -88,7 +88,7 @@ corpus page documents the surface you already know:
|
|
|
88
88
|
| Host | The platform itself | This corpus + `account_management.md` |
|
|
89
89
|
| Client | Data Activation Client | `data_activation_clients.md` |
|
|
90
90
|
| Server | Agentic Workflow | `workflows.md` |
|
|
91
|
-
| Resources | Datasets
|
|
91
|
+
| Resources | Datasets + generic tables | `generic_tables.md`, `mdm.md` |
|
|
92
92
|
| Prompts | Interoperability Contracts | `interoperability_contracts.md`, `templates.md` |
|
|
93
93
|
| Sampling | Dataset event triggers | `data_activation_clients.md` §6.4 logs |
|
|
94
94
|
| Tools | Tool Calls (polymorphic body)| `tools.md` |
|
|
@@ -231,17 +231,16 @@ What changes between them is only *where you put the calls* (a component
|
|
|
231
231
|
handler vs. a test body), never the calls themselves. The cookbooks under
|
|
232
232
|
`cookbook/` read as test-style sequences because that is how they are
|
|
233
233
|
validated, but every step is a plain `api.*` call you can lift into an app
|
|
234
|
-
unchanged.
|
|
235
|
-
|
|
236
|
-
business outcome.
|
|
234
|
+
unchanged. Capabilities are not documented separately — each lives as
|
|
235
|
+
numbered steps inside the cookbook whose use case motivates it.
|
|
237
236
|
|
|
238
237
|
## Resources
|
|
239
238
|
|
|
240
239
|
```
|
|
241
240
|
INFRASTRUCTURE (stood up before data flows)
|
|
242
241
|
──────────────
|
|
243
|
-
datalakes.md Storage layer;
|
|
244
|
-
|
|
242
|
+
datalakes.md Storage layer; the raw lake and
|
|
243
|
+
its tokenized + redacted copies.
|
|
245
244
|
data_sources.md External ingestion endpoints.
|
|
246
245
|
tools.md Authenticated connections for
|
|
247
246
|
action execution.
|
|
@@ -260,6 +259,10 @@ business outcome.
|
|
|
260
259
|
DATA ACTIVATION (how data flows in)
|
|
261
260
|
───────────────
|
|
262
261
|
generic_tables.md Schema-on-write tables.
|
|
262
|
+
advanced_migrations.md Hand-written DDL for one lake —
|
|
263
|
+
indexes, foreign keys, anything
|
|
264
|
+
past the CREATE TABLE / ADD COLUMN
|
|
265
|
+
the platform writes for itself.
|
|
263
266
|
interoperability_contracts.md Liquid template mappings.
|
|
264
267
|
data_activation_clients.md Ingestion pipelines — both the
|
|
265
268
|
binding row (CRUD) and the
|
|
@@ -299,6 +302,7 @@ wire name (snake_case):
|
|
|
299
302
|
api.connectedApps connected_apps.md
|
|
300
303
|
api.mdm mdm.md
|
|
301
304
|
api.genericTables generic_tables.md
|
|
305
|
+
api.advancedMigrations advanced_migrations.md
|
|
302
306
|
api.interoperabilityContracts interoperability_contracts.md
|
|
303
307
|
api.dataActivationClients data_activation_clients.md
|
|
304
308
|
api.workflows workflows.md
|
|
@@ -323,12 +327,17 @@ at runtime.
|
|
|
323
327
|
|
|
324
328
|
Each cookbook structure:
|
|
325
329
|
|
|
326
|
-
- **Front matter** — `title`, `summary`, `
|
|
330
|
+
- **Front matter** — `title`, `summary`, `use_case`, `slug`,
|
|
327
331
|
`vitest_source` (list of integration-test files the snippets
|
|
328
|
-
are lifted from, anchor file first), `status`.
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
+
are lifted from, anchor file first), `status`. `use_case` is one
|
|
333
|
+
of the four surfaces — `organic-marketing`,
|
|
334
|
+
`subscription-saas`, `payments-compliance`,
|
|
335
|
+
`primary-care-feedback` — and there is exactly one cookbook per use
|
|
336
|
+
case. Three are at `cookbook/<use-case>.md`; the fourth is at
|
|
337
|
+
`cookbook/primary-care.md`, because `use_case` tracks the integration
|
|
338
|
+
suite it is lifted from (`integration-tests/tests/primary-care-feedback/`)
|
|
339
|
+
and the filename does not. Read `use_case` from the front matter rather
|
|
340
|
+
than deriving it from the filename.
|
|
332
341
|
- **Problem** — the business-outcome statement in domain terms,
|
|
333
342
|
sourced from the anchor vitest's behaviour (not from
|
|
334
343
|
customer-narrative documentation).
|
|
@@ -349,44 +358,33 @@ Each cookbook structure:
|
|
|
349
358
|
- **See also** — links to relevant per-resource reference MDs
|
|
350
359
|
and to the anchor + ancillary vitest files.
|
|
351
360
|
|
|
352
|
-
###
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
prefixed with its source slug so failure output
|
|
380
|
-
unambiguously points at the file to open (e.g.
|
|
381
|
-
`_setup/foundation §001 — auth` versus
|
|
382
|
-
`birthday-greeting-sms-trigger §001 — create workflow`).
|
|
383
|
-
Cookbook authors keep clean local §001-§00N numbering
|
|
384
|
-
inside their own markdown.
|
|
385
|
-
- An agent reading a scenario cookbook discovers the matching
|
|
386
|
-
setup file at the predictable conventional path
|
|
387
|
-
`_setup/<industry>.md` — one extra file open at a
|
|
388
|
-
fixed-by-convention location, not via cookbook-specific
|
|
389
|
-
metadata the agent has to learn.
|
|
361
|
+
### Every cookbook is self-reliant, and repeats what it needs
|
|
362
|
+
|
|
363
|
+
A cookbook stands up **its own** tenant, datalake and resources in
|
|
364
|
+
its own numbered steps. There is no shared setup file, no setup
|
|
365
|
+
group and no bootstrap phase: everything the walk depends on is
|
|
366
|
+
in the walk.
|
|
367
|
+
|
|
368
|
+
That means the auth + tenant + datalake preamble is **duplicated
|
|
369
|
+
across all four cookbooks, deliberately.** This corpus is read by
|
|
370
|
+
someone — or something — that opens one file and works through it,
|
|
371
|
+
and for that reader a shared setup is not a saving. It is a second
|
|
372
|
+
file to find, a second convention to learn, and a hidden dependency
|
|
373
|
+
on steps they never see. Four documents repeating a preamble is the
|
|
374
|
+
cheaper trade, and it is the trade this corpus makes. Do not
|
|
375
|
+
re-factor it out.
|
|
376
|
+
|
|
377
|
+
Two consequences worth stating plainly:
|
|
378
|
+
|
|
379
|
+
- **Write for idempotence, not just for a clean run.** Use stable
|
|
380
|
+
names and create-or-reuse: look the resource up, create it only
|
|
381
|
+
if it is absent, and assert the end state either way. A step that
|
|
382
|
+
mints a fresh random name works once and litters forever —
|
|
383
|
+
nothing in the product reclaims an abandoned datalake. Ask of
|
|
384
|
+
every creating step: what does this do on run two?
|
|
385
|
+
- **Label numbering is local and clean.** Each cookbook numbers its
|
|
386
|
+
own steps `§001…§00N` from the top, and a failure names the file
|
|
387
|
+
and the step directly.
|
|
390
388
|
|
|
391
389
|
### Vendored fixtures (Liquid templates, CSV bodies)
|
|
392
390
|
|
|
@@ -417,7 +415,7 @@ repo's own integration tests use.
|
|
|
417
415
|
const LE_TEMPLATE = fs.readFileSync(
|
|
418
416
|
path.join(
|
|
419
417
|
process.env.COOKBOOK_FIXTURES_DIR!,
|
|
420
|
-
'
|
|
418
|
+
'organic-marketing/_lead_submissions_foundation_legal_entity.liquid',
|
|
421
419
|
),
|
|
422
420
|
'utf8',
|
|
423
421
|
)
|
|
@@ -439,109 +437,58 @@ repo's own integration tests use.
|
|
|
439
437
|
template plus the cookbooks that depend on it. No need to
|
|
440
438
|
read cookbook code first to know what fixtures exist.
|
|
441
439
|
|
|
442
|
-
###
|
|
440
|
+
### The four cookbooks
|
|
443
441
|
|
|
444
|
-
|
|
445
|
-
|
|
442
|
+
`cookbook/` holds **four files, one per use case**, and nothing else that
|
|
443
|
+
runs. Each is self-reliant: it stands up its own tenant, datalake and
|
|
444
|
+
resources in its own numbered steps and shares nothing with its siblings.
|
|
445
|
+
A reader reads one file end to end and needs no second one. The
|
|
446
|
+
duplicated bootstrap across them is deliberate — four repeated preambles
|
|
447
|
+
are cheaper than one shared setup every reader must go and understand
|
|
448
|
+
first.
|
|
446
449
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
shown as the minimal call sequence that proves it (e.g. `bulk-ingest`,
|
|
453
|
-
`ai-agent-invoke`, `generic-tables`, `invite-team`). Read these to learn one
|
|
454
|
-
capability in isolation; cookbooks weave them into outcomes.
|
|
450
|
+
There is no separate per-capability file. Bulk upload, REST fetch, team
|
|
451
|
+
invitations, action status updaters, paginated pollers, text-to-SQL and
|
|
452
|
+
agent file-invocation each live as numbered steps inside the cookbook
|
|
453
|
+
whose use case motivates them, because a capability shown without a
|
|
454
|
+
reason to use it is the thing readers skip.
|
|
455
455
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
The ten business-cookbook scenarios. Each is anchored to a green
|
|
459
|
-
end-to-end vitest scenario in the platform's integration-tests suite and is
|
|
460
|
-
verified at dev time by `make validate-cookbook` at the platform-sdk repo root.
|
|
461
|
-
This index is mirrored into the managed block `alvera llm-export` writes to
|
|
462
|
-
consumer `AGENTS.md` files (`buildManagedBlock` in both the SDK and CLI
|
|
463
|
-
packages — kept in sync by hand; the llm-export test suites pin the shipped
|
|
464
|
-
copy, so a dropped entry breaks a test).
|
|
456
|
+
All four are verified at dev time by `make validate-cookbook` at the
|
|
457
|
+
repo root; every numbered step is one test.
|
|
465
458
|
|
|
466
459
|
<!-- BEGIN:cookbook-index -->
|
|
467
460
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
(high / medium / low) via an LLM agent, route each band to a
|
|
490
|
-
tailored SMS action.
|
|
491
|
-
|
|
492
|
-
**Payments**
|
|
493
|
-
|
|
494
|
-
- [kyc-notification-on-account-activation](./cookbook/kyc-notification-on-account-activation.md)
|
|
495
|
-
— Send a KYC-notification SMS when a payment account
|
|
496
|
-
transitions to active status.
|
|
497
|
-
- [sanctions-screening-with-agent-review](./cookbook/sanctions-screening-with-agent-review.md)
|
|
498
|
-
— Disambiguate gray-zone sanctions screenings via an LLM
|
|
499
|
-
agent, route confirmed-clean and confirmed-block outcomes to
|
|
500
|
-
distinct SMS actions.
|
|
501
|
-
|
|
502
|
-
**Foundation**
|
|
503
|
-
|
|
504
|
-
- [birthday-greeting-sms-trigger](./cookbook/birthday-greeting-sms-trigger.md)
|
|
505
|
-
— Send a happy-birthday SMS on each contact's next birthday
|
|
506
|
-
using a pure-Liquid trigger (year-roll math).
|
|
507
|
-
- [score-leads-with-llm-categorization](./cookbook/score-leads-with-llm-categorization.md)
|
|
508
|
-
— Score inbound leads into four bands
|
|
509
|
-
(hot / warm / cold / spam) via an LLM agent, route each band
|
|
510
|
-
to a tailored SMS action.
|
|
511
|
-
- [marketing-campaign-send](./cookbook/marketing-campaign-send.md)
|
|
512
|
-
— Send an A/B marketing campaign across SMS and email: the
|
|
513
|
-
suppression and reachability gates and the A/B split all live
|
|
514
|
-
in the workflow, each send carries a per-recipient short link,
|
|
515
|
-
and the reply re-attaches to the customer who sent it.
|
|
461
|
+
- [organic-marketing](./cookbook/organic-marketing.md) — score inbound
|
|
462
|
+
leads into four bands with an LLM agent, schedule a birthday greeting a
|
|
463
|
+
year out with a pure-Liquid trigger, and run an A/B campaign whose
|
|
464
|
+
suppression gates and split live in the workflow. Ends with delivery
|
|
465
|
+
reconciliation and a natural-language read of the lake. One raw
|
|
466
|
+
datalake.
|
|
467
|
+
- [payments-compliance](./cookbook/payments-compliance.md) —
|
|
468
|
+
disambiguate gray-zone sanctions screenings with an LLM agent that
|
|
469
|
+
never sees who was screened, and fire a KYC notification on account
|
|
470
|
+
activation. Ends by reading one row through raw, tokenized and redacted
|
|
471
|
+
to show what each reader gets. Three datalakes.
|
|
472
|
+
- [subscription-saas](./cookbook/subscription-saas.md) — one customers
|
|
473
|
+
table and three workflows over it: a welcome SMS gated on
|
|
474
|
+
reachability, a dunning reminder gated on reachability AND KYC, and an
|
|
475
|
+
LLM agent banding accounts by priority. Then the same table filled
|
|
476
|
+
three more ways — inline, bulk CSV, REST fetch — and a teammate invited
|
|
477
|
+
onto the tenant. One raw datalake.
|
|
478
|
+
- [primary-care](./cookbook/primary-care.md) — a review request that
|
|
479
|
+
only reaches patients whose visit actually happened, an LLM agent
|
|
480
|
+
triaging inbound messages off the tokenized projection, and a vision
|
|
481
|
+
agent reading a scanned document. Three datalakes.
|
|
516
482
|
|
|
517
483
|
<!-- END:cookbook-index -->
|
|
518
484
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
one upload (presigned link → PUT → `ingestFile` → search by batch).
|
|
527
|
-
- [rest-fetch](./cookbook/rest-fetch.md) — pull records from a third-party REST
|
|
528
|
-
API on demand (Bearer + OAuth2), no file upload.
|
|
529
|
-
- [ai-agent-invoke](./cookbook/ai-agent-invoke.md) — read an uploaded
|
|
530
|
-
document/image and pull structured JSON out of it (`aiAgents.invoke` with
|
|
531
|
-
files). The correct file-vision path — the agent reads the file; the DAC
|
|
532
|
-
ingests it.
|
|
533
|
-
- [generic-tables](./cookbook/generic-tables.md) — stand up a generic table the
|
|
534
|
-
built-in datasets don't model; deploy → ingest → read back via executeSql.
|
|
535
|
-
- [action-status-updaters](./cookbook/action-status-updaters.md) — reconcile
|
|
536
|
-
the delivery status of messages you send, on a schedule.
|
|
537
|
-
- [system-templates](./cookbook/system-templates.md) — discover the platform's
|
|
538
|
-
built-in row-mapping Liquid templates.
|
|
539
|
-
- [invite-team](./cookbook/invite-team.md) — invite a teammate into your tenant
|
|
540
|
-
(root / tenantless / tenant-scoped sessions in one flow).
|
|
541
|
-
- [talk-to-data](./cookbook/talk-to-data.md) — turn a datalake conversational:
|
|
542
|
-
natural language → reviewable SQL (`datalakes.textToSql`, data-free by
|
|
543
|
-
construction) → read-only execution returning a `{ data, meta }` row page or a
|
|
544
|
-
CSV export (`datalakes.executeSql`).
|
|
485
|
+
This index is mirrored into the managed block `alvera llm-export` writes
|
|
486
|
+
to consumer `AGENTS.md` files — `buildManagedBlock`, duplicated in both
|
|
487
|
+
the SDK and CLI packages and kept in sync by hand. **Both suites now
|
|
488
|
+
assert every cookbook named in the block exists on disk, and that every
|
|
489
|
+
cookbook on disk is named.** They previously pinned names against the
|
|
490
|
+
block string alone, which is how the index went on advertising four
|
|
491
|
+
deleted files without a single test failing.
|
|
545
492
|
|
|
546
493
|
## Utility namespaces
|
|
547
494
|
|
|
@@ -86,7 +86,7 @@ platform-admin side door (a root Bearer):
|
|
|
86
86
|
```typescript
|
|
87
87
|
const { data: minted } = await rootApi.admin.createTenantApiKey(tenantSlug, {
|
|
88
88
|
name: 'Bootstrap Key',
|
|
89
|
-
data_access_mode: '
|
|
89
|
+
data_access_mode: 'raw',
|
|
90
90
|
})
|
|
91
91
|
// minted.api_key — the publishable key value (thread into createSession)
|
|
92
92
|
```
|
|
@@ -329,7 +329,7 @@ What you SUBMIT when creating an invitation. The
|
|
|
329
329
|
| Invitation `role` | Result on accept |
|
|
330
330
|
|-------------------|----------------------------------------|
|
|
331
331
|
| `'member'` | Standard tenant user |
|
|
332
|
-
| `'researcher'` |
|
|
332
|
+
| `'researcher'` | Ceiling of `tokenized`; cannot read the raw lake |
|
|
333
333
|
| `'admin'` | Full tenant management |
|
|
334
334
|
|
|
335
335
|
```typescript
|
package/.agent/action_logs.md
CHANGED
|
@@ -39,7 +39,7 @@ batch_id — the run this outcome belongs to; THE
|
|
|
39
39
|
decision_key — which action fired
|
|
40
40
|
context_key — companion string key; like decision_key, it is
|
|
41
41
|
SQL-lane only (neither is Flop-filterable, §5)
|
|
42
|
-
|
|
42
|
+
legal_entity_id, subject_name, session_id
|
|
43
43
|
action_type ('sms'|'email'|'voice'|'data_exchange'), channel,
|
|
44
44
|
classification
|
|
45
45
|
status — see §3
|
|
@@ -92,17 +92,17 @@ message.
|
|
|
92
92
|
// dataset type is SINGULAR 'action_log' (the table is plural)
|
|
93
93
|
const { data: search } = await api.datasets.createUserSearch(
|
|
94
94
|
tenantSlug, datalakeSlug, 'action_log',
|
|
95
|
-
{ search_query: `
|
|
95
|
+
{ search_query: `al.batch_id = '${batchId}'` },
|
|
96
96
|
)
|
|
97
97
|
const { data: page } = await api.datasets.search(
|
|
98
98
|
tenantSlug, datalakeSlug, 'action_log',
|
|
99
|
-
{ userSearchId: search.id, dataAccessMode: '
|
|
99
|
+
{ userSearchId: search.id, dataAccessMode: 'tokenized' },
|
|
100
100
|
)
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
Filterable columns (Flop, the full set): `id`, `inserted_at`,
|
|
104
104
|
`action_type`, `channel`, `status`, `batch_id`, `classification`,
|
|
105
|
-
`workflow_id`, `sent_at`, `external_id`, `
|
|
105
|
+
`workflow_id`, `sent_at`, `external_id`, `legal_entity_id`. Note
|
|
106
106
|
there is **no `global_search`** here (unlike the message dataset) —
|
|
107
107
|
`decision_key`/`context_key` are not in this set either; scope them
|
|
108
108
|
via the `search_query` SQL fragment above.
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Advanced migrations
|
|
2
|
+
|
|
3
|
+
SQL a person wrote by hand for one datalake.
|
|
4
|
+
|
|
5
|
+
The platform writes its own DDL when a generic table is created or gains a
|
|
6
|
+
column — `CREATE TABLE` and `ALTER TABLE … ADD COLUMN`, and nothing else.
|
|
7
|
+
Everything past that is yours to write here: an index, a foreign key, a column
|
|
8
|
+
rename, a type change, a drop. The same migrator runs both.
|
|
9
|
+
|
|
10
|
+
**One name, two spellings.** Outwardly these are *advanced migrations*, which is
|
|
11
|
+
what the API path and this guide call them. The platform stores them in
|
|
12
|
+
`datalake_migrations`, and its own logs and internals use that name. Both are
|
|
13
|
+
correct; do not coin a third.
|
|
14
|
+
|
|
15
|
+
api.advancedMigrations → this guide
|
|
16
|
+
/datalakes/{slug}/advanced-migrations
|
|
17
|
+
|
|
18
|
+
## 1. Wire shape
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
const { data: created } = await api.advancedMigrations.create(
|
|
22
|
+
tenantSlug,
|
|
23
|
+
datalakeSlug, // scope comes from the PATH — see below
|
|
24
|
+
{
|
|
25
|
+
name: 'AddEmailIndexToContactSubmissions',
|
|
26
|
+
up_statement:
|
|
27
|
+
'CREATE INDEX IF NOT EXISTS idx_contact_submissions_email ' +
|
|
28
|
+
'ON alvera_custom_contact_submissions (email)',
|
|
29
|
+
down_statement:
|
|
30
|
+
'DROP INDEX IF EXISTS idx_contact_submissions_email',
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
// created.id — server-derived UUID
|
|
34
|
+
// created.version — server-assigned from a sequence; orders execution
|
|
35
|
+
// created.datalake_id — server-derived from the path
|
|
36
|
+
// created.checksum — server-computed; the drift fingerprint
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Three fields, all required, all strings:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
name required string — capitalized identifier, unique per datalake
|
|
43
|
+
up_statement required string — the DDL to run
|
|
44
|
+
down_statement required string — the DDL that reverses it
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**`datalake_id` is NOT a body field.** This is the one place the shape departs
|
|
48
|
+
from every other datalake-scoped kind. Elsewhere — tools, workflows, generic
|
|
49
|
+
tables — you send `datalake_id` in the body. Here it is `readOnly`: the scope
|
|
50
|
+
comes from `datalakeSlug` in the path, and the server fills the field in. Sending
|
|
51
|
+
it is not how you say which lake this belongs to.
|
|
52
|
+
|
|
53
|
+
**`version` is server-assigned and orders execution.** It comes from a sequence,
|
|
54
|
+
not from you, and it is the thing that decides what runs first — not the name,
|
|
55
|
+
and not the order you declared them in. Generated migrations carry a year-style
|
|
56
|
+
prefix in their names to read well beside yours; that prefix is cosmetic.
|
|
57
|
+
|
|
58
|
+
## 2. Rules the type cannot encode
|
|
59
|
+
|
|
60
|
+
### An applied migration is frozen — update and delete both refuse
|
|
61
|
+
|
|
62
|
+
Once a migration has run successfully, `update()` and `delete()` each answer
|
|
63
|
+
**409 Conflict**. The reasons differ and both are worth knowing.
|
|
64
|
+
|
|
65
|
+
**Update** is refused because a statement that has already executed cannot be
|
|
66
|
+
rewritten after the fact: the new text would no longer describe what happened.
|
|
67
|
+
|
|
68
|
+
**Delete** is refused because the run log cascades on delete, so removing an
|
|
69
|
+
applied migration would also erase the record of it having run.
|
|
70
|
+
|
|
71
|
+
So a migration has exactly two lives. Before it runs it is ordinary and
|
|
72
|
+
editable. After it runs it is history. **Changing applied DDL means writing a
|
|
73
|
+
NEW migration that alters what the old one built** — the same shape as changing
|
|
74
|
+
a generic table column's `privacy_requirement`, which is delete-and-recreate
|
|
75
|
+
rather than an edit.
|
|
76
|
+
|
|
77
|
+
"Applied" means precisely: a run log row exists for this migration with
|
|
78
|
+
direction `up` and status `success`. A failed run does not freeze it.
|
|
79
|
+
|
|
80
|
+
### Write the statement to be safe on a second run
|
|
81
|
+
|
|
82
|
+
The platform tracks what it has applied and will not run an applied migration
|
|
83
|
+
twice, so you do not need `IF NOT EXISTS` for that. You need it for the other
|
|
84
|
+
case: a run that failed **partway**. The applied-migrations record stops a second
|
|
85
|
+
run; it does not undo a first one that got halfway.
|
|
86
|
+
|
|
87
|
+
```sql
|
|
88
|
+
-- safe to meet again
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_submissions_email ON … (email)
|
|
90
|
+
|
|
91
|
+
-- Postgres has no ADD CONSTRAINT IF NOT EXISTS, so guard it yourself
|
|
92
|
+
DO $$
|
|
93
|
+
BEGIN
|
|
94
|
+
IF NOT EXISTS (
|
|
95
|
+
SELECT 1 FROM pg_constraint WHERE conname = 'fk_submissions_lead'
|
|
96
|
+
) THEN
|
|
97
|
+
ALTER TABLE alvera_custom_contact_submissions
|
|
98
|
+
ADD CONSTRAINT fk_submissions_lead
|
|
99
|
+
FOREIGN KEY (lead_id) REFERENCES alvera_custom_leads (id);
|
|
100
|
+
END IF;
|
|
101
|
+
END $$;
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The same applies to `down_statement`, which is likelier to be run against a
|
|
105
|
+
database that is not in the state you assumed.
|
|
106
|
+
|
|
107
|
+
### The table names are the platform's, not yours
|
|
108
|
+
|
|
109
|
+
A generic table you declared as `contact_submissions` is physically
|
|
110
|
+
`alvera_custom_contact_submissions`. Read the name back from the created table
|
|
111
|
+
rather than composing it — see `generic_tables.md` — because your migration
|
|
112
|
+
statement has to name the physical table, and getting it wrong is a runtime
|
|
113
|
+
failure inside DDL rather than a validation error.
|
|
114
|
+
|
|
115
|
+
### Ordering is real, and it is the version's job
|
|
116
|
+
|
|
117
|
+
Adding an index and then a foreign key that depends on it are two migrations,
|
|
118
|
+
and the sequence decides which runs first. Two migrations created in one session
|
|
119
|
+
run in creation order because the sequence is monotonic — but if you need B
|
|
120
|
+
after A, that is a fact about your two `version` values, not about how you
|
|
121
|
+
listed them.
|
|
122
|
+
|
|
123
|
+
## 3. Field ownership
|
|
124
|
+
|
|
125
|
+
**Caller-authored (Request).** `name`, `up_statement`, `down_statement`. That is
|
|
126
|
+
the whole writable surface.
|
|
127
|
+
|
|
128
|
+
**Server-derived (Response-only).** `id`, `datalake_id`, `version`, `checksum`,
|
|
129
|
+
`inserted_at`, `updated_at`. None may be sent; see `type_naming.md` for why the
|
|
130
|
+
Writable types exclude them.
|
|
131
|
+
|
|
132
|
+
`name` is unique per datalake, so two lakes may each carry a
|
|
133
|
+
`AddEmailIndex` and neither collides.
|
|
134
|
+
|
|
135
|
+
## 4. Error envelopes
|
|
136
|
+
|
|
137
|
+
Standard JSON:API envelope throughout — see `errors.md`.
|
|
138
|
+
|
|
139
|
+
| Status | When |
|
|
140
|
+
|---|---|
|
|
141
|
+
| 422 | a required field is missing, or `name` collides within this datalake |
|
|
142
|
+
| 409 | the migration has already been applied — `update` and `delete` both |
|
|
143
|
+
| 404 | the id belongs to a different datalake (deliberately not "403": its existence is not leaked) |
|
|
144
|
+
|
|
145
|
+
A 409 here is not a retryable condition. It is telling you the migration is
|
|
146
|
+
history and you want a new one.
|
|
147
|
+
|
|
148
|
+
## 5. Lifecycle
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
create → the row exists; nothing has run yet
|
|
152
|
+
migrate → the datalake's migrator runs it, in version order
|
|
153
|
+
(POST /datalakes/{slug}/migrate — asynchronous, 202)
|
|
154
|
+
applied → a run log row: direction up, status success
|
|
155
|
+
from here: update 409, delete 409
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Creating a migration does **not** run it. The datalake's migrate call does, and
|
|
159
|
+
it is asynchronous — it answers 202 as soon as the job is enqueued. Poll the
|
|
160
|
+
datalake for `status: ready` the way you would after creating one; see
|
|
161
|
+
`datalakes.md`.
|
|
162
|
+
|
|
163
|
+
## 6. Gotchas
|
|
164
|
+
|
|
165
|
+
**A created migration that never ran is invisible in the database.** The row
|
|
166
|
+
exists, `list()` returns it, and nothing has happened to any table. Reading the
|
|
167
|
+
catalog is the only way to know whether it took effect — the migration record
|
|
168
|
+
says what you asked for, not what is true.
|
|
169
|
+
|
|
170
|
+
**Proving one worked means asking the database, not the API.** The response
|
|
171
|
+
carries `checksum`, `datalake_id`, `down_statement`, `id`, `inserted_at`,
|
|
172
|
+
`name`, `up_statement`, `updated_at` and `version` — no status, no applied
|
|
173
|
+
flag, and `version` is execution ORDER rather than proof of execution. So the
|
|
174
|
+
honest check is a query against the catalog for the object the statement was
|
|
175
|
+
supposed to create:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
// an index
|
|
179
|
+
const { data: indexRows } = await api.datalakes.executeSql(tenantSlug, datalakeSlug, {
|
|
180
|
+
sql: `select 1 from pg_indexes
|
|
181
|
+
where tablename = 'alvera_custom_contact_submissions'
|
|
182
|
+
and indexname = 'idx_contact_submissions_email'`,
|
|
183
|
+
mode: 'raw',
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// a foreign key
|
|
187
|
+
const { data: fkRows } = await api.datalakes.executeSql(tenantSlug, datalakeSlug, {
|
|
188
|
+
sql: `select 1 from information_schema.table_constraints
|
|
189
|
+
where constraint_type = 'FOREIGN KEY'
|
|
190
|
+
and table_name = 'alvera_custom_contact_submissions'
|
|
191
|
+
and constraint_name = 'fk_submissions_lead'`,
|
|
192
|
+
mode: 'raw',
|
|
193
|
+
})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**`down_statement` is required and is rarely exercised.** Nothing makes you
|
|
197
|
+
prove it works, and it is the statement you will want on your worst day. Write
|
|
198
|
+
it as carefully as the `up`.
|
|
199
|
+
|
|
200
|
+
**A migration belongs to exactly one datalake.** There is no shared or
|
|
201
|
+
tenant-level migration. Three lakes needing the same index need three
|
|
202
|
+
migrations — which is also why `name` only has to be unique per lake.
|