@alvera-ai/platform-sdk 0.10.0-rc.2 → 0.10.0-rc.21
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 +440 -0
- package/.agent/account_management.md +455 -0
- package/.agent/action_status_updaters.md +262 -0
- package/.agent/ai_agents.md +423 -0
- package/.agent/ai_sandbox.md +265 -0
- package/.agent/async.md +111 -0
- package/.agent/connected_apps.md +407 -0
- package/.agent/cookbook/_fixtures/README.md +99 -0
- package/.agent/cookbook/_fixtures/accounts_receivable/_customers_accounts_receivable_customer.liquid +32 -0
- package/.agent/cookbook/_fixtures/accounts_receivable/_customers_accounts_receivable_mdm.liquid +20 -0
- package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_generic_table.liquid +33 -0
- package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_legal_entity.liquid +88 -0
- package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_mdm.liquid +48 -0
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_appointment.liquid +47 -0
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_mdm.liquid +24 -0
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_patient.liquid +38 -0
- package/.agent/cookbook/_fixtures/payment_risk/_compliance_screenings_payment_risk_compliance_screening.liquid +59 -0
- package/.agent/cookbook/_fixtures/payment_risk/_compliance_screenings_payment_risk_mdm.liquid +36 -0
- package/.agent/cookbook/_fixtures/payment_risk/_payment_accounts_payment_risk_mdm.liquid +30 -0
- package/.agent/cookbook/_fixtures/payment_risk/_payment_accounts_payment_risk_payment_account.liquid +55 -0
- package/.agent/cookbook/_setup/accounts_receivable.md +282 -0
- package/.agent/cookbook/_setup/foundation.md +277 -0
- package/.agent/cookbook/_setup/healthcare.md +279 -0
- package/.agent/cookbook/_setup/payment_risk.md +283 -0
- package/.agent/cookbook/appointment-review-sms-workflow.md +761 -0
- package/.agent/cookbook/birthday-greeting-sms-trigger.md +656 -0
- package/.agent/cookbook/contact-us-triage-with-llm.md +603 -0
- package/.agent/cookbook/dunning-sms-for-delinquent.md +619 -0
- package/.agent/cookbook/kyc-notification-on-account-activation.md +619 -0
- package/.agent/cookbook/sanctions-screening-with-agent-review.md +711 -0
- package/.agent/cookbook/score-leads-with-llm-categorization.md +602 -0
- package/.agent/cookbook/welcome-sms-for-customers.md +607 -0
- package/.agent/data_activation_clients.md +557 -0
- package/.agent/data_sources.md +234 -0
- package/.agent/datalakes.md +712 -0
- package/.agent/debugging.md +137 -0
- package/.agent/errors.md +196 -0
- package/.agent/generic_tables.md +351 -0
- package/.agent/interoperability_contracts.md +351 -0
- package/.agent/mdm.md +293 -0
- package/.agent/mutations.md +152 -0
- package/.agent/templates.md +98 -0
- package/.agent/tool-call-configs.md +90 -0
- package/.agent/tools.md +546 -0
- package/.agent/type_naming.md +131 -0
- package/.agent/workflows.md +601 -0
- package/README.md +46 -0
- package/dist/bin/platform-sdk.d.mts +1 -0
- package/dist/bin/platform-sdk.mjs +106 -0
- package/dist/bin/platform-sdk.mjs.map +1 -0
- package/dist/index.d.mts +1200 -43201
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1859 -7319
- package/dist/index.mjs.map +1 -1
- package/package.json +19 -10
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Accounts-receivable industry bootstrap setup
|
|
3
|
+
summary: Auth as root, sign up + confirm a fresh industry-admin user, create an accounts-receivable tenant + datalake. Shared by every accounts-receivable scenario cookbook.
|
|
4
|
+
industry: accounts_receivable
|
|
5
|
+
slug: accounts_receivable
|
|
6
|
+
vitest_source:
|
|
7
|
+
- integration-tests/tests/accounts_receivable/bootstrap.test.ts
|
|
8
|
+
status: green
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Problem
|
|
12
|
+
|
|
13
|
+
Every accounts-receivable scenario cookbook needs the same
|
|
14
|
+
starting point: a fresh tenant on the platform with an
|
|
15
|
+
accounts-receivable-domain datalake attached, accessed by an
|
|
16
|
+
industry-admin user. Authoring this prelude inline inside every
|
|
17
|
+
scenario cookbook would duplicate the auth + tenant + datalake
|
|
18
|
+
chain across two cookbooks (and six more across the other
|
|
19
|
+
industries). The validator auto-discovers this file from each
|
|
20
|
+
cookbook's `industry: accounts_receivable` front-matter and
|
|
21
|
+
inlines its numbered steps ahead of the scenario's own, so the
|
|
22
|
+
generated bun:test spec is hermetic without the cookbook author
|
|
23
|
+
having to copy-paste this prelude.
|
|
24
|
+
|
|
25
|
+
# Composition
|
|
26
|
+
|
|
27
|
+
| Resource provisioned | Owner |
|
|
28
|
+
|-------------------------------|-------------|
|
|
29
|
+
| Root session | setup |
|
|
30
|
+
| Industry-admin user (`sarah`) | setup |
|
|
31
|
+
| Accounts-receivable tenant | setup |
|
|
32
|
+
| Accounts-receivable datalake | setup |
|
|
33
|
+
|
|
34
|
+
# Walkthrough
|
|
35
|
+
|
|
36
|
+
## 001 — root admin signs in and provisions a fresh industry-admin user
|
|
37
|
+
|
|
38
|
+
Authenticate as the platform's root admin (`admin@dev.local` /
|
|
39
|
+
`devpassword` in local dev). Then sign up a per-run industry-admin
|
|
40
|
+
user (`sarah`) under a unique email derived from `runSuffix`, and
|
|
41
|
+
confirm them so they can sign in. Both the signup and the confirm
|
|
42
|
+
endpoints require root scope.
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
ctx.rootSession = await createSession({
|
|
46
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
47
|
+
email: process.env.ALVERA_ROOT_EMAIL!,
|
|
48
|
+
password: process.env.ALVERA_ROOT_PASSWORD!,
|
|
49
|
+
})
|
|
50
|
+
ctx.rootApi = createIsolatedPlatformApi({
|
|
51
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
52
|
+
sessionToken: ctx.rootSession.sessionToken,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
ctx.sarahEmail = `cookbook-sarah-${runSuffix}@dev.local`
|
|
56
|
+
ctx.sarahPassword = 'CookbookPass1!'
|
|
57
|
+
|
|
58
|
+
const signUpResp = await ctx.rootApi.auth.signUp({
|
|
59
|
+
email: ctx.sarahEmail,
|
|
60
|
+
password: ctx.sarahPassword,
|
|
61
|
+
first_name: 'Cookbook',
|
|
62
|
+
last_name: 'Sarah',
|
|
63
|
+
})
|
|
64
|
+
ctx.sarahUserId = signUpResp.data.id
|
|
65
|
+
await ctx.rootApi.admin.confirmUser(ctx.sarahUserId)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 002 — sarah signs in (tenantless) and creates the accounts-receivable tenant
|
|
69
|
+
|
|
70
|
+
Sarah signs in for the first time without a tenant scope (no
|
|
71
|
+
tenant exists yet for her), then immediately creates a fresh
|
|
72
|
+
accounts-receivable tenant. The tenant's server-derived `slug` is
|
|
73
|
+
captured into the closure-scoped `tenantSlug` slot for downstream
|
|
74
|
+
steps.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
ctx.sarahTenantlessSession = await createSession({
|
|
78
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
79
|
+
email: ctx.sarahEmail,
|
|
80
|
+
password: ctx.sarahPassword,
|
|
81
|
+
})
|
|
82
|
+
ctx.sarahTenantlessApi = createIsolatedPlatformApi({
|
|
83
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
84
|
+
sessionToken: ctx.sarahTenantlessSession.sessionToken,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const tenantResp = await ctx.sarahTenantlessApi.tenants.create({
|
|
88
|
+
name: `Cookbook Accounts Receivable ${runSuffix}`,
|
|
89
|
+
})
|
|
90
|
+
tenantSlug = tenantResp.data.slug!
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## 003 — sarah signs in tenant-scoped — the canonical client
|
|
94
|
+
|
|
95
|
+
Re-mint sarah's session with the new tenant slug. This bearer is
|
|
96
|
+
the canonical tenant-scoped client every subsequent step uses; it
|
|
97
|
+
is assigned to the closure-scoped `api` slot so the scenario
|
|
98
|
+
cookbook's steps inherit it via inlining.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const sarahTenantSession = await createSession({
|
|
102
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
103
|
+
email: ctx.sarahEmail,
|
|
104
|
+
password: ctx.sarahPassword,
|
|
105
|
+
tenantSlug,
|
|
106
|
+
})
|
|
107
|
+
api = createIsolatedPlatformApi({
|
|
108
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
109
|
+
sessionToken: sarahTenantSession.sessionToken,
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## 004 — sarah creates the accounts-receivable datalake
|
|
114
|
+
|
|
115
|
+
Provision an accounts-receivable-domain datalake on the new
|
|
116
|
+
tenant. The DB schemas are scoped to the cookbook run via
|
|
117
|
+
`runSuffix` so parallel cookbook runs do not collide on schema
|
|
118
|
+
names. Local-dev defaults (`postgres` on `localhost:5432`,
|
|
119
|
+
`alvera_dev_accounts_receivable`) match the seeded `dev.exs`
|
|
120
|
+
setup; LocalStack S3 (`localhost:4566`) serves the unregulated +
|
|
121
|
+
regulated buckets. The datalake's server-derived `slug` is
|
|
122
|
+
captured into `datalakeSlug`.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
const DB_HOST = 'localhost'
|
|
126
|
+
const DB_PORT = 5432
|
|
127
|
+
const DB_USER = 'postgres'
|
|
128
|
+
const DB_PASS = 'postgres'
|
|
129
|
+
const DB_NAME = 'alvera_dev_accounts_receivable'
|
|
130
|
+
const UNREG_SCHEMA = `cookbook_${runSuffix}_unreg`
|
|
131
|
+
const REG_SCHEMA = `cookbook_${runSuffix}_reg`
|
|
132
|
+
|
|
133
|
+
const S3 = {
|
|
134
|
+
cloud_storage_type: 'aws' as const,
|
|
135
|
+
region: 'us-east-1',
|
|
136
|
+
access_key_id: 'test',
|
|
137
|
+
secret_access_key: 'test',
|
|
138
|
+
endpoint: 'http://localhost:4566',
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const datalakeResp = await api.datalakes.create(tenantSlug, {
|
|
142
|
+
name: `Cookbook Accounts Receivable Datalake ${runSuffix}`,
|
|
143
|
+
description: 'Accounts-receivable datalake provisioned by cookbook doctest.',
|
|
144
|
+
data_domain: 'accounts_receivable',
|
|
145
|
+
timezone: 'America/New_York',
|
|
146
|
+
pool_size: 5,
|
|
147
|
+
|
|
148
|
+
unregulated_db_writer_host: DB_HOST,
|
|
149
|
+
unregulated_db_writer_port: DB_PORT,
|
|
150
|
+
unregulated_db_writer_name: DB_NAME,
|
|
151
|
+
unregulated_db_writer_schema: UNREG_SCHEMA,
|
|
152
|
+
unregulated_db_writer_auth_method: 'password',
|
|
153
|
+
unregulated_db_writer_user: DB_USER,
|
|
154
|
+
unregulated_db_writer_pass: DB_PASS,
|
|
155
|
+
unregulated_db_writer_enable_ssl: false,
|
|
156
|
+
unregulated_db_reader_host: DB_HOST,
|
|
157
|
+
unregulated_db_reader_port: DB_PORT,
|
|
158
|
+
unregulated_db_reader_name: DB_NAME,
|
|
159
|
+
unregulated_db_reader_schema: UNREG_SCHEMA,
|
|
160
|
+
unregulated_db_reader_auth_method: 'password',
|
|
161
|
+
unregulated_db_reader_user: DB_USER,
|
|
162
|
+
unregulated_db_reader_pass: DB_PASS,
|
|
163
|
+
unregulated_db_reader_enable_ssl: false,
|
|
164
|
+
|
|
165
|
+
regulated_data_db_writer_host: DB_HOST,
|
|
166
|
+
regulated_data_db_writer_port: DB_PORT,
|
|
167
|
+
regulated_data_db_writer_name: DB_NAME,
|
|
168
|
+
regulated_data_db_writer_schema: REG_SCHEMA,
|
|
169
|
+
regulated_data_db_writer_auth_method: 'password',
|
|
170
|
+
regulated_data_db_writer_user: DB_USER,
|
|
171
|
+
regulated_data_db_writer_pass: DB_PASS,
|
|
172
|
+
regulated_data_db_writer_enable_ssl: false,
|
|
173
|
+
regulated_data_db_reader_host: DB_HOST,
|
|
174
|
+
regulated_data_db_reader_port: DB_PORT,
|
|
175
|
+
regulated_data_db_reader_name: DB_NAME,
|
|
176
|
+
regulated_data_db_reader_schema: REG_SCHEMA,
|
|
177
|
+
regulated_data_db_reader_auth_method: 'password',
|
|
178
|
+
regulated_data_db_reader_user: DB_USER,
|
|
179
|
+
regulated_data_db_reader_pass: DB_PASS,
|
|
180
|
+
regulated_data_db_reader_enable_ssl: false,
|
|
181
|
+
|
|
182
|
+
unregulated_cloud_storage: { ...S3, bucket: 'accounts-receivable-lake-unregulated' },
|
|
183
|
+
regulated_cloud_storage: { ...S3, bucket: 'accounts-receivable-lake-regulated' },
|
|
184
|
+
})
|
|
185
|
+
datalakeSlug = datalakeResp.data.slug!
|
|
186
|
+
// `tools.create` and a few other endpoints accept the datalake by
|
|
187
|
+
// UUID in the request body rather than only the slug in the URL,
|
|
188
|
+
// so keep it on `ctx` for downstream steps that need it.
|
|
189
|
+
ctx.datalakeId = datalakeResp.data.id!
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## 005 — enqueue the datalake migrations
|
|
193
|
+
|
|
194
|
+
`datalakes.create` persists the datalake row in `:new` status but
|
|
195
|
+
does not itself run the schema migrations. Migration is a
|
|
196
|
+
separately-triggered async job so the operator controls when the
|
|
197
|
+
(potentially slow) per-industry DDL runs. `datalakes.migrate`
|
|
198
|
+
enqueues the `DatalakeMigrationWorker` Oban job and returns
|
|
199
|
+
immediately with `status: 'enqueued'` plus the Oban `job_id`. The
|
|
200
|
+
poll in §006 then waits for that worker to finish — without this
|
|
201
|
+
call the datalake would sit at `:new` forever.
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
const migrateResp = await api.datalakes.migrate(tenantSlug, datalakeSlug)
|
|
205
|
+
if (migrateResp.data.status !== 'enqueued') {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`datalake migration not enqueued (status: ${migrateResp.data.status})`,
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## 006 — poll the datalake until status is ready
|
|
213
|
+
|
|
214
|
+
Datalake migrations are async — the §005 migrate call enqueued a
|
|
215
|
+
`DatalakeMigrationWorker` Oban job that runs the per-industry
|
|
216
|
+
schema migrations (the regulated customer + invoice tables and
|
|
217
|
+
their join tables), deploys PostgREST roles, and provisions the
|
|
218
|
+
service-account bot user. Downstream resource creates (tools,
|
|
219
|
+
workflows) only need `datalakeId` to be persisted (which it is
|
|
220
|
+
the moment §004 returns), but any scenario step that ingests into
|
|
221
|
+
the regulated/unregulated DBs or runs a dataset search requires
|
|
222
|
+
`status: 'ready'`. Polling here makes every accounts-receivable
|
|
223
|
+
scenario cookbook deterministic regardless of how long the cold
|
|
224
|
+
migration takes on a given host; the loop exits the moment the
|
|
225
|
+
datalake reaches `:ready`, so the 5-minute cap is paid only in
|
|
226
|
+
failure mode.
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
const READY_TIMEOUT_MS = 5 * 60_000
|
|
230
|
+
const READY_POLL_MS = 5_000
|
|
231
|
+
const deadline = Date.now() + READY_TIMEOUT_MS
|
|
232
|
+
let datalakeStatus: string | undefined
|
|
233
|
+
while (Date.now() < deadline) {
|
|
234
|
+
const { data } = await api.datalakes.get(tenantSlug, ctx.datalakeId)
|
|
235
|
+
datalakeStatus = data.status
|
|
236
|
+
if (datalakeStatus === 'ready') break
|
|
237
|
+
await new Promise((r) => setTimeout(r, READY_POLL_MS))
|
|
238
|
+
}
|
|
239
|
+
if (datalakeStatus !== 'ready') {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`datalake did not reach :ready within ${READY_TIMEOUT_MS}ms (last status: ${datalakeStatus})`,
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
# Rollback
|
|
247
|
+
|
|
248
|
+
The cookbook doctest harness does not currently tear down the
|
|
249
|
+
created tenant / datalake / user — each run mints fresh names via
|
|
250
|
+
`runSuffix` so reruns do not collide, and the seeded local DB is
|
|
251
|
+
cheap to reset (`mix ecto.reset` on the platform). When the
|
|
252
|
+
validator graduates to continuous integration at Milestone 18
|
|
253
|
+
(running against the mockoon-backed mock env at `ALVERA_ENV=mock`),
|
|
254
|
+
no real state is created at all.
|
|
255
|
+
|
|
256
|
+
# Outcome
|
|
257
|
+
|
|
258
|
+
After this setup runs, the closure-scoped slots are populated as:
|
|
259
|
+
|
|
260
|
+
- `api` — a tenant-scoped `PlatformApi` client authenticated as
|
|
261
|
+
the industry-admin user
|
|
262
|
+
- `tenantSlug` — server-derived slug of the fresh
|
|
263
|
+
accounts-receivable tenant
|
|
264
|
+
- `datalakeSlug` — server-derived slug of the fresh
|
|
265
|
+
accounts-receivable datalake
|
|
266
|
+
- `ctx.sarahEmail` / `ctx.sarahPassword` — the per-run admin
|
|
267
|
+
credentials (kept on `ctx` because no scenario cookbook needs
|
|
268
|
+
to re-authenticate by default)
|
|
269
|
+
|
|
270
|
+
Scenario cookbooks under `industry: accounts_receivable` start at
|
|
271
|
+
their own `§001` with these slots already populated.
|
|
272
|
+
|
|
273
|
+
# See also
|
|
274
|
+
|
|
275
|
+
- `.agent/datalakes.md` — datalake create body shape (regulated +
|
|
276
|
+
unregulated tier configuration)
|
|
277
|
+
- `.agent/AGENTS.md` § SDK auth + client construction — root vs
|
|
278
|
+
tenantless vs tenant-scoped session scopes
|
|
279
|
+
- `integration-tests/tests/accounts_receivable/bootstrap.test.ts` —
|
|
280
|
+
the green test these snippets are lifted from (the sibling
|
|
281
|
+
datalake creation and the sanity probes are out of scope for
|
|
282
|
+
this lean cookbook setup)
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Foundation industry bootstrap setup
|
|
3
|
+
summary: Auth as root, sign up + confirm a fresh industry-admin user, create a foundation tenant + datalake. Shared by every foundation scenario cookbook.
|
|
4
|
+
industry: foundation
|
|
5
|
+
slug: foundation
|
|
6
|
+
vitest_source:
|
|
7
|
+
- integration-tests/tests/foundation/bootstrap.test.ts
|
|
8
|
+
status: green
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Problem
|
|
12
|
+
|
|
13
|
+
Every foundation scenario cookbook needs the same starting point:
|
|
14
|
+
a fresh tenant on the platform with a foundation-domain datalake
|
|
15
|
+
attached, accessed by an industry-admin user. Authoring this
|
|
16
|
+
prelude inline inside every scenario cookbook would duplicate the
|
|
17
|
+
auth + tenant + datalake chain across two cookbooks (and four more
|
|
18
|
+
across the other industries). The validator auto-discovers this
|
|
19
|
+
file from each cookbook's `industry: foundation` front-matter and
|
|
20
|
+
inlines its numbered steps ahead of the scenario's own, so the
|
|
21
|
+
generated bun:test spec is hermetic without the cookbook author
|
|
22
|
+
having to copy-paste this prelude.
|
|
23
|
+
|
|
24
|
+
# Composition
|
|
25
|
+
|
|
26
|
+
| Resource provisioned | Owner |
|
|
27
|
+
|-------------------------------|-------------|
|
|
28
|
+
| Root session | setup |
|
|
29
|
+
| Industry-admin user (`sarah`) | setup |
|
|
30
|
+
| Foundation tenant | setup |
|
|
31
|
+
| Foundation datalake | setup |
|
|
32
|
+
|
|
33
|
+
# Walkthrough
|
|
34
|
+
|
|
35
|
+
## 001 — root admin signs in and provisions a fresh industry-admin user
|
|
36
|
+
|
|
37
|
+
Authenticate as the platform's root admin (`admin@dev.local` /
|
|
38
|
+
`devpassword` in local dev). Then sign up a per-run industry-admin
|
|
39
|
+
user (`sarah`) under a unique email derived from `runSuffix`, and
|
|
40
|
+
confirm them so they can sign in. Both the signup and the confirm
|
|
41
|
+
endpoints require root scope.
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
ctx.rootSession = await createSession({
|
|
45
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
46
|
+
email: process.env.ALVERA_ROOT_EMAIL!,
|
|
47
|
+
password: process.env.ALVERA_ROOT_PASSWORD!,
|
|
48
|
+
})
|
|
49
|
+
ctx.rootApi = createIsolatedPlatformApi({
|
|
50
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
51
|
+
sessionToken: ctx.rootSession.sessionToken,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
ctx.sarahEmail = `cookbook-sarah-${runSuffix}@dev.local`
|
|
55
|
+
ctx.sarahPassword = 'CookbookPass1!'
|
|
56
|
+
|
|
57
|
+
const signUpResp = await ctx.rootApi.auth.signUp({
|
|
58
|
+
email: ctx.sarahEmail,
|
|
59
|
+
password: ctx.sarahPassword,
|
|
60
|
+
first_name: 'Cookbook',
|
|
61
|
+
last_name: 'Sarah',
|
|
62
|
+
})
|
|
63
|
+
ctx.sarahUserId = signUpResp.data.id
|
|
64
|
+
await ctx.rootApi.admin.confirmUser(ctx.sarahUserId)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 002 — sarah signs in (tenantless) and creates the foundation tenant
|
|
68
|
+
|
|
69
|
+
Sarah signs in for the first time without a tenant scope (no
|
|
70
|
+
tenant exists yet for her), then immediately creates a fresh
|
|
71
|
+
foundation tenant. The tenant's server-derived `slug` is captured
|
|
72
|
+
into the closure-scoped `tenantSlug` slot for downstream steps.
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
ctx.sarahTenantlessSession = await createSession({
|
|
76
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
77
|
+
email: ctx.sarahEmail,
|
|
78
|
+
password: ctx.sarahPassword,
|
|
79
|
+
})
|
|
80
|
+
ctx.sarahTenantlessApi = createIsolatedPlatformApi({
|
|
81
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
82
|
+
sessionToken: ctx.sarahTenantlessSession.sessionToken,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const tenantResp = await ctx.sarahTenantlessApi.tenants.create({
|
|
86
|
+
name: `Cookbook Foundation ${runSuffix}`,
|
|
87
|
+
})
|
|
88
|
+
tenantSlug = tenantResp.data.slug!
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 003 — sarah signs in tenant-scoped — the canonical client
|
|
92
|
+
|
|
93
|
+
Re-mint sarah's session with the new tenant slug. This bearer is
|
|
94
|
+
the canonical tenant-scoped client every subsequent step uses; it
|
|
95
|
+
is assigned to the closure-scoped `api` slot so the scenario
|
|
96
|
+
cookbook's steps inherit it via inlining.
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
const sarahTenantSession = await createSession({
|
|
100
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
101
|
+
email: ctx.sarahEmail,
|
|
102
|
+
password: ctx.sarahPassword,
|
|
103
|
+
tenantSlug,
|
|
104
|
+
})
|
|
105
|
+
api = createIsolatedPlatformApi({
|
|
106
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
107
|
+
sessionToken: sarahTenantSession.sessionToken,
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## 004 — sarah creates the foundation datalake
|
|
112
|
+
|
|
113
|
+
Provision a foundation-domain datalake on the new tenant. The DB
|
|
114
|
+
schemas are scoped to the cookbook run via `runSuffix` so parallel
|
|
115
|
+
cookbook runs do not collide on schema names. Local-dev defaults
|
|
116
|
+
(`postgres` on `localhost:5432`, `alvera_dev_foundation`) match
|
|
117
|
+
the seeded `dev.exs` setup; LocalStack S3 (`localhost:4566`)
|
|
118
|
+
serves the unregulated + regulated buckets. The datalake's
|
|
119
|
+
server-derived `slug` is captured into `datalakeSlug`.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
const DB_HOST = 'localhost'
|
|
123
|
+
const DB_PORT = 5432
|
|
124
|
+
const DB_USER = 'postgres'
|
|
125
|
+
const DB_PASS = 'postgres'
|
|
126
|
+
const DB_NAME = 'alvera_dev_foundation'
|
|
127
|
+
const UNREG_SCHEMA = `cookbook_${runSuffix}_unreg`
|
|
128
|
+
const REG_SCHEMA = `cookbook_${runSuffix}_reg`
|
|
129
|
+
|
|
130
|
+
const S3 = {
|
|
131
|
+
cloud_storage_type: 'aws' as const,
|
|
132
|
+
region: 'us-east-1',
|
|
133
|
+
access_key_id: 'test',
|
|
134
|
+
secret_access_key: 'test',
|
|
135
|
+
endpoint: 'http://localhost:4566',
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const datalakeResp = await api.datalakes.create(tenantSlug, {
|
|
139
|
+
name: `Cookbook Foundation Datalake ${runSuffix}`,
|
|
140
|
+
description: 'Foundation datalake provisioned by cookbook doctest.',
|
|
141
|
+
data_domain: 'foundation',
|
|
142
|
+
timezone: 'America/New_York',
|
|
143
|
+
pool_size: 5,
|
|
144
|
+
|
|
145
|
+
unregulated_db_writer_host: DB_HOST,
|
|
146
|
+
unregulated_db_writer_port: DB_PORT,
|
|
147
|
+
unregulated_db_writer_name: DB_NAME,
|
|
148
|
+
unregulated_db_writer_schema: UNREG_SCHEMA,
|
|
149
|
+
unregulated_db_writer_auth_method: 'password',
|
|
150
|
+
unregulated_db_writer_user: DB_USER,
|
|
151
|
+
unregulated_db_writer_pass: DB_PASS,
|
|
152
|
+
unregulated_db_writer_enable_ssl: false,
|
|
153
|
+
unregulated_db_reader_host: DB_HOST,
|
|
154
|
+
unregulated_db_reader_port: DB_PORT,
|
|
155
|
+
unregulated_db_reader_name: DB_NAME,
|
|
156
|
+
unregulated_db_reader_schema: UNREG_SCHEMA,
|
|
157
|
+
unregulated_db_reader_auth_method: 'password',
|
|
158
|
+
unregulated_db_reader_user: DB_USER,
|
|
159
|
+
unregulated_db_reader_pass: DB_PASS,
|
|
160
|
+
unregulated_db_reader_enable_ssl: false,
|
|
161
|
+
|
|
162
|
+
regulated_data_db_writer_host: DB_HOST,
|
|
163
|
+
regulated_data_db_writer_port: DB_PORT,
|
|
164
|
+
regulated_data_db_writer_name: DB_NAME,
|
|
165
|
+
regulated_data_db_writer_schema: REG_SCHEMA,
|
|
166
|
+
regulated_data_db_writer_auth_method: 'password',
|
|
167
|
+
regulated_data_db_writer_user: DB_USER,
|
|
168
|
+
regulated_data_db_writer_pass: DB_PASS,
|
|
169
|
+
regulated_data_db_writer_enable_ssl: false,
|
|
170
|
+
regulated_data_db_reader_host: DB_HOST,
|
|
171
|
+
regulated_data_db_reader_port: DB_PORT,
|
|
172
|
+
regulated_data_db_reader_name: DB_NAME,
|
|
173
|
+
regulated_data_db_reader_schema: REG_SCHEMA,
|
|
174
|
+
regulated_data_db_reader_auth_method: 'password',
|
|
175
|
+
regulated_data_db_reader_user: DB_USER,
|
|
176
|
+
regulated_data_db_reader_pass: DB_PASS,
|
|
177
|
+
regulated_data_db_reader_enable_ssl: false,
|
|
178
|
+
|
|
179
|
+
unregulated_cloud_storage: { ...S3, bucket: 'foundation-lake-unregulated' },
|
|
180
|
+
regulated_cloud_storage: { ...S3, bucket: 'foundation-lake-regulated' },
|
|
181
|
+
})
|
|
182
|
+
datalakeSlug = datalakeResp.data.slug!
|
|
183
|
+
// `tools.create` and a few other endpoints accept the datalake by
|
|
184
|
+
// UUID in the request body rather than only the slug in the URL,
|
|
185
|
+
// so keep it on `ctx` for downstream steps that need it.
|
|
186
|
+
ctx.datalakeId = datalakeResp.data.id!
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## 005 — enqueue the datalake migrations
|
|
190
|
+
|
|
191
|
+
`datalakes.create` persists the datalake row in `:new` status but
|
|
192
|
+
does not itself run the schema migrations. Migration is a
|
|
193
|
+
separately-triggered async job so the operator controls when the
|
|
194
|
+
(potentially slow) per-industry DDL runs. `datalakes.migrate`
|
|
195
|
+
enqueues the `DatalakeMigrationWorker` Oban job and returns
|
|
196
|
+
immediately with `status: 'enqueued'` plus the Oban `job_id`. The
|
|
197
|
+
poll in §006 then waits for that worker to finish — without this
|
|
198
|
+
call the datalake would sit at `:new` forever.
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
const migrateResp = await api.datalakes.migrate(tenantSlug, datalakeSlug)
|
|
202
|
+
if (migrateResp.data.status !== 'enqueued') {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`datalake migration not enqueued (status: ${migrateResp.data.status})`,
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## 006 — poll the datalake until status is ready
|
|
210
|
+
|
|
211
|
+
Datalake migrations are async — the §005 migrate call enqueued a
|
|
212
|
+
`DatalakeMigrationWorker` Oban job that runs the per-industry
|
|
213
|
+
schema migrations, deploys PostgREST roles, and provisions the
|
|
214
|
+
service-account bot user. Downstream resource creates (tools,
|
|
215
|
+
workflows) only need `datalakeId` to be persisted (which it is
|
|
216
|
+
the moment §004 returns), but any scenario step that touches
|
|
217
|
+
the regulated/unregulated DBs or the dataset infrastructure
|
|
218
|
+
requires `status: 'ready'`. Polling here makes every foundation
|
|
219
|
+
scenario cookbook deterministic regardless of how long the cold
|
|
220
|
+
migration takes on a given host; the loop exits the moment the
|
|
221
|
+
datalake reaches `:ready`, so the 5-minute cap is paid only in
|
|
222
|
+
failure mode.
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
const READY_TIMEOUT_MS = 5 * 60_000
|
|
226
|
+
const READY_POLL_MS = 5_000
|
|
227
|
+
const deadline = Date.now() + READY_TIMEOUT_MS
|
|
228
|
+
let datalakeStatus: string | undefined
|
|
229
|
+
while (Date.now() < deadline) {
|
|
230
|
+
const { data } = await api.datalakes.get(tenantSlug, ctx.datalakeId)
|
|
231
|
+
datalakeStatus = data.status
|
|
232
|
+
if (datalakeStatus === 'ready') break
|
|
233
|
+
await new Promise((r) => setTimeout(r, READY_POLL_MS))
|
|
234
|
+
}
|
|
235
|
+
if (datalakeStatus !== 'ready') {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`datalake did not reach :ready within ${READY_TIMEOUT_MS}ms (last status: ${datalakeStatus})`,
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
# Rollback
|
|
243
|
+
|
|
244
|
+
The cookbook doctest harness does not currently tear down the
|
|
245
|
+
created tenant / datalake / user — each run mints fresh names via
|
|
246
|
+
`runSuffix` so reruns do not collide, and the seeded local DB is
|
|
247
|
+
cheap to reset (`mix ecto.reset` on the platform). When the
|
|
248
|
+
validator graduates to continuous integration at Milestone 18
|
|
249
|
+
(running against the mockoon-backed mock env at `ALVERA_ENV=mock`),
|
|
250
|
+
no real state is created at all.
|
|
251
|
+
|
|
252
|
+
# Outcome
|
|
253
|
+
|
|
254
|
+
After this setup runs, the closure-scoped slots are populated as:
|
|
255
|
+
|
|
256
|
+
- `api` — a tenant-scoped `PlatformApi` client authenticated as
|
|
257
|
+
the industry-admin user
|
|
258
|
+
- `tenantSlug` — server-derived slug of the fresh foundation
|
|
259
|
+
tenant
|
|
260
|
+
- `datalakeSlug` — server-derived slug of the fresh foundation
|
|
261
|
+
datalake
|
|
262
|
+
- `ctx.sarahEmail` / `ctx.sarahPassword` — the per-run admin
|
|
263
|
+
credentials (kept on `ctx` because no scenario cookbook needs
|
|
264
|
+
to re-authenticate by default)
|
|
265
|
+
|
|
266
|
+
Scenario cookbooks under `industry: foundation` start at their own
|
|
267
|
+
`§001` with these slots already populated.
|
|
268
|
+
|
|
269
|
+
# See also
|
|
270
|
+
|
|
271
|
+
- `.agent/datalakes.md` — datalake create body shape (regulated +
|
|
272
|
+
unregulated tier configuration)
|
|
273
|
+
- `.agent/AGENTS.md` § SDK auth + client construction — root vs
|
|
274
|
+
tenantless vs tenant-scoped session scopes
|
|
275
|
+
- `integration-tests/tests/foundation/bootstrap.test.ts` — the
|
|
276
|
+
green test these snippets are lifted from (the §7b sibling
|
|
277
|
+
datalake creation is out of scope for this lean cookbook setup)
|