@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,137 @@
|
|
|
1
|
+
# Debugging
|
|
2
|
+
|
|
3
|
+
Tracing HTTP traffic through the SDK is opt-in via the
|
|
4
|
+
`ApiDebugConfig` callback interface. Wire it at client
|
|
5
|
+
construction time and the SDK's HTTP interceptor will log
|
|
6
|
+
every request + response through your callback.
|
|
7
|
+
|
|
8
|
+
**Safety first**: enabling debug logging without redaction
|
|
9
|
+
will leak credentials in the request bodies the SDK sends to
|
|
10
|
+
the platform. The `ApiDebugConfig.redactStrings` array is the
|
|
11
|
+
load-bearing safety feature — pass every active secret literal
|
|
12
|
+
you've installed BEFORE the SDK fires its first request.
|
|
13
|
+
|
|
14
|
+
## Wiring the debug interceptor
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import {
|
|
18
|
+
createIsolatedPlatformApi,
|
|
19
|
+
type ApiDebugConfig,
|
|
20
|
+
} from '@alvera-ai/platform-sdk'
|
|
21
|
+
|
|
22
|
+
const debugConfig: ApiDebugConfig = {
|
|
23
|
+
log: (msg: string) => process.stderr.write(msg + '\n'),
|
|
24
|
+
redactStrings: getActiveSecretLiterals(),
|
|
25
|
+
// e.g. ['AKIA...real-key', 'twilio-real-token', ...]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const api = createIsolatedPlatformApi({
|
|
29
|
+
baseUrl,
|
|
30
|
+
sessionToken,
|
|
31
|
+
debug: debugConfig,
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
When `debug` is wired:
|
|
36
|
+
- The SDK's HTTP interceptor logs each request + response
|
|
37
|
+
through the `log` callback. Consumer chooses the sink:
|
|
38
|
+
stderr, Pino, OpenTelemetry, etc.
|
|
39
|
+
- BEFORE logging, the SDK substitutes every string in
|
|
40
|
+
`redactStrings` with a fixed-length placeholder
|
|
41
|
+
(`********`).
|
|
42
|
+
|
|
43
|
+
When `debug` is omitted (the default), the SDK doesn't log
|
|
44
|
+
HTTP traffic at all — nothing to redact.
|
|
45
|
+
|
|
46
|
+
## Safety: the load-bearing piece
|
|
47
|
+
|
|
48
|
+
The SDK never sees secret REFERENCES (`<%= name %>` Eta
|
|
49
|
+
syntax is a consumer-side convention used by some — CLI
|
|
50
|
+
manifests, for instance). By the time a request body reaches the SDK,
|
|
51
|
+
credentials are already substituted to their literal values.
|
|
52
|
+
If you enable debug logging without populating
|
|
53
|
+
`redactStrings`, those literals appear in plaintext in your
|
|
54
|
+
log output — including production logs if `debug` is wired
|
|
55
|
+
in non-dev environments.
|
|
56
|
+
|
|
57
|
+
The redaction is fixed-length (`********`), NOT length-
|
|
58
|
+
preserving. A length-preserving mask would still leak
|
|
59
|
+
length-as-signal (an AWS access key is 20 characters;
|
|
60
|
+
a 20-char placeholder says "this WAS a 20-char string").
|
|
61
|
+
|
|
62
|
+
## The ApiDebugConfig contract
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
type ApiDebugConfig = {
|
|
66
|
+
log: (message: string) => void
|
|
67
|
+
redactStrings?: readonly string[]
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- `log` is the sink. The SDK emits formatted log messages
|
|
72
|
+
(request method + URL + body, response status + body) as
|
|
73
|
+
a single string per HTTP exchange.
|
|
74
|
+
- `redactStrings` is an array of LITERAL values the SDK
|
|
75
|
+
should scrub from log output. Empty / absent = no
|
|
76
|
+
redaction (use only when no secrets are in the request
|
|
77
|
+
bodies — rare).
|
|
78
|
+
|
|
79
|
+
The SDK's log output is plain strings, not structured. For
|
|
80
|
+
structured logging (per-field JSON, OTel spans, etc.),
|
|
81
|
+
consumers wrap the `log` callback with their own emitter.
|
|
82
|
+
|
|
83
|
+
## What the SDK does NOT redact
|
|
84
|
+
|
|
85
|
+
- **Exception bodies thrown by the SDK** (the 422 envelope,
|
|
86
|
+
etc.) are NOT filtered through `redactStrings`. If your
|
|
87
|
+
error-rendering path includes the offending request body,
|
|
88
|
+
redact it yourself before rendering. See `errors.md` for
|
|
89
|
+
the throw mechanism.
|
|
90
|
+
- **Response bodies that legitimately contain secrets**.
|
|
91
|
+
Server-derived fields that are credentials (rare but
|
|
92
|
+
possible) come back un-redacted in the response; the
|
|
93
|
+
consumer is responsible for not logging them.
|
|
94
|
+
- **Anything OUTSIDE the SDK's HTTP path**. `redactStrings`
|
|
95
|
+
only filters what the SDK itself writes to `log`. Your own
|
|
96
|
+
log statements that touch request/response data must
|
|
97
|
+
redact themselves.
|
|
98
|
+
|
|
99
|
+
## Defense in depth
|
|
100
|
+
|
|
101
|
+
`redactStrings` is one layer. Belt-and-suspenders practice:
|
|
102
|
+
- Avoid log statements that interpolate request bodies
|
|
103
|
+
OUTSIDE the SDK's debug pipeline.
|
|
104
|
+
- Use fixed-length placeholders in your own surfaces (CLI
|
|
105
|
+
tables, browser consoles, error pages, exception traces).
|
|
106
|
+
- Treat exception bodies as untrusted for log purposes —
|
|
107
|
+
parse what you need, don't dump the whole thing.
|
|
108
|
+
- Don't ship `debug` enabled in production unless the
|
|
109
|
+
redact list is provably complete.
|
|
110
|
+
|
|
111
|
+
## Pattern: snapshotting the redact list
|
|
112
|
+
|
|
113
|
+
`redactStrings` is read at client construction time
|
|
114
|
+
(`createIsolatedPlatformApi(...)`). If your consumer's
|
|
115
|
+
secret store mutates during the process's lifetime (e.g. a
|
|
116
|
+
new credential gets installed mid-run), you need to
|
|
117
|
+
construct a NEW client to pick up the new redaction string —
|
|
118
|
+
the SDK doesn't observe the source array after construction.
|
|
119
|
+
|
|
120
|
+
Consumers that hold secrets in a mutable store typically
|
|
121
|
+
build the client lazily (per-request or per-handler) to
|
|
122
|
+
guarantee a fresh snapshot.
|
|
123
|
+
|
|
124
|
+
## What's in the log line
|
|
125
|
+
|
|
126
|
+
A typical log message the SDK emits looks roughly like:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
[platform-sdk] POST /api/v1/tenants/acme/datalakes/main/tools
|
|
130
|
+
request body: {"name":"...","intent":"data_exchange",
|
|
131
|
+
"body":{"tool_body_type":"s3","access_key_id":"********",
|
|
132
|
+
"secret_access_key":"********",...}}
|
|
133
|
+
response 201: {"id":"...","slug":"...","name":"...",...}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
(Format is plain-string; exact prefix and field ordering are
|
|
137
|
+
SDK-internal and may evolve.)
|
package/.agent/errors.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Errors
|
|
2
|
+
|
|
3
|
+
When you call an SDK method, two things can happen:
|
|
4
|
+
|
|
5
|
+
1. **Success** — `{ data, meta? }` returned (typed).
|
|
6
|
+
2. **Throw** — the SDK throws the server's response body verbatim after
|
|
7
|
+
the HTTP call returned a non-2xx status.
|
|
8
|
+
|
|
9
|
+
The SDK does **not** validate request bodies on the client. Validation is
|
|
10
|
+
server-authoritative: a malformed body is rejected by the platform and
|
|
11
|
+
surfaced to you as a thrown **422** body. There is no synchronous
|
|
12
|
+
client-side validation error to catch — every validation failure comes back
|
|
13
|
+
from the server in the standardized envelope below.
|
|
14
|
+
|
|
15
|
+
> Driving the platform through the `alvera` **CLI** instead of the SDK? Then
|
|
16
|
+
> `alvera plan` runs a local pre-flight check against the spec and reports
|
|
17
|
+
> errors *before* any HTTP call. That's a CLI feature (it validates the spec
|
|
18
|
+
> locally); it is not SDK runtime behavior. SDK callers get errors from the
|
|
19
|
+
> server.
|
|
20
|
+
|
|
21
|
+
## The thrown value (post-HTTP)
|
|
22
|
+
|
|
23
|
+
When the platform returns a non-2xx status, **the SDK throws the parsed
|
|
24
|
+
response body verbatim** as the thrown value — not an `Error` subclass, just
|
|
25
|
+
the body object (decorated with an `_httpStatus` field so you can branch on
|
|
26
|
+
the status). Narrow it with a type guard:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
try {
|
|
30
|
+
const { data } = await api.tools.create(
|
|
31
|
+
tenantSlug, datalakeSlug, body,
|
|
32
|
+
)
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (
|
|
35
|
+
typeof err === 'object' && err !== null &&
|
|
36
|
+
'errors' in err && Array.isArray((err as any).errors)
|
|
37
|
+
) {
|
|
38
|
+
const { errors } = err as {
|
|
39
|
+
errors: Array<{
|
|
40
|
+
source: { pointer: string }
|
|
41
|
+
title?: string
|
|
42
|
+
detail?: string
|
|
43
|
+
}>
|
|
44
|
+
}
|
|
45
|
+
for (const e of errors) {
|
|
46
|
+
// e.source.pointer is "/field_name" or "/embed/field"
|
|
47
|
+
}
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
throw err
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The thrown body is typed as `AlveraApiError` (exported from the package root)
|
|
55
|
+
for 422s, and the simpler `ErrorResponse` for some other statuses.
|
|
56
|
+
|
|
57
|
+
## 422 envelope shape
|
|
58
|
+
|
|
59
|
+
The platform standardizes ALL 422 response bodies on a single shape —
|
|
60
|
+
regardless of which validation layer fired:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"errors": [
|
|
65
|
+
{
|
|
66
|
+
"source": { "pointer": "/<field_path>" },
|
|
67
|
+
"title": "<human-readable summary>",
|
|
68
|
+
"detail": "<specific reason this field failed>"
|
|
69
|
+
},
|
|
70
|
+
...
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- `errors` is always an ARRAY of error objects (never a field-keyed map).
|
|
76
|
+
- `source.pointer` is a JSON Pointer (`/field_name` for top-level fields;
|
|
77
|
+
`/nested/field` for embeds) naming the offending field on the request body.
|
|
78
|
+
- `detail` is the specific reason; `title` is a brief summary.
|
|
79
|
+
- Multiple errors may share the same `source.pointer` if more than one check
|
|
80
|
+
fired on the same field.
|
|
81
|
+
|
|
82
|
+
## Two layers behind the standardized 422
|
|
83
|
+
|
|
84
|
+
The platform validates a request in two layers; both surface through the
|
|
85
|
+
same envelope above:
|
|
86
|
+
|
|
87
|
+
**Layer 1 — OpenAPI cast (structural)**
|
|
88
|
+
|
|
89
|
+
Runs at the plug layer (`OpenApiSpex`) before the controller. Catches:
|
|
90
|
+
missing required fields, wrong types, invalid enums, unknown keys, format
|
|
91
|
+
violations (UUID patterns, etc.). This is the *same* OpenAPI schema the SDK's
|
|
92
|
+
types are generated from, so a body that satisfies the TypeScript types
|
|
93
|
+
almost always clears layer 1.
|
|
94
|
+
|
|
95
|
+
**Layer 2 — server-side semantic validator (post-cast)**
|
|
96
|
+
|
|
97
|
+
Runs after the structural cast succeeds. Catches: uniqueness, cross-field
|
|
98
|
+
rules (e.g. `auth_method = "iam_role"` combined with conflicting credential
|
|
99
|
+
fields), foreign-key constraints, custom platform validations. Layer-2
|
|
100
|
+
errors are normalized into the same envelope shape as layer 1 — consumers
|
|
101
|
+
don't distinguish layers from the JSON.
|
|
102
|
+
|
|
103
|
+
Practical consequence: every validation error reaches you as a thrown 422
|
|
104
|
+
body. Read `source.pointer`, fix that field, resubmit.
|
|
105
|
+
|
|
106
|
+
## Concrete examples
|
|
107
|
+
|
|
108
|
+
**Layer 1 — missing required fields** (POST `/agentic-workflows` with empty
|
|
109
|
+
body):
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"errors": [
|
|
114
|
+
{ "source": { "pointer": "/name" },
|
|
115
|
+
"detail": "Missing field: name" },
|
|
116
|
+
{ "source": { "pointer": "/dataset_type" },
|
|
117
|
+
"detail": "Missing field: dataset_type" }
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Layer 1 — invalid enum value** (`dataset_type: "nonexistent_type"`):
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"errors": [
|
|
127
|
+
{ "source": { "pointer": "/dataset_type" },
|
|
128
|
+
"detail": "Invalid value: not in enum" }
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Layer 2 — semantic rule** (DELETE on a default-flagged data activation
|
|
134
|
+
client):
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"errors": [
|
|
139
|
+
{ "source": { "pointer": "/is_default" },
|
|
140
|
+
"detail": "Default data activation clients cannot be deleted" }
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Layer 2 — uniqueness** (POST `/admin/invites` for an already-invited
|
|
146
|
+
email):
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"errors": [
|
|
151
|
+
{ "source": { "pointer": "/email" },
|
|
152
|
+
"detail": "is already invited" }
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## HTTP status codes (under the hood)
|
|
158
|
+
|
|
159
|
+
The SDK abstracts HTTP responses behind typed methods. Consumers mostly see
|
|
160
|
+
typed data or thrown bodies; the thrown body carries `_httpStatus`, and raw
|
|
161
|
+
status is also visible via the SDK's `--debug`-style interceptor. The
|
|
162
|
+
platform maps:
|
|
163
|
+
|
|
164
|
+
| Code | When it fires |
|
|
165
|
+
|------|----------------------------------------------------------------|
|
|
166
|
+
| 200 | GET / PUT / DELETE successful |
|
|
167
|
+
| 201 | POST successful |
|
|
168
|
+
| 400 | Request malformed (rare; the SDK serializes before send) |
|
|
169
|
+
| 401 | Missing or invalid Bearer / `x-api-key` |
|
|
170
|
+
| 403 | Authenticated but role lacks permission |
|
|
171
|
+
| 404 | Resource doesn't exist OR cross-tenant access (404, not 403) |
|
|
172
|
+
| 422 | Validation failed; envelope above is the thrown body |
|
|
173
|
+
| 5xx | Platform-internal failure; safe to retry with backoff |
|
|
174
|
+
|
|
175
|
+
## Retry semantics
|
|
176
|
+
|
|
177
|
+
Treat 5xx as infra noise — retry with exponential backoff. Treat 422 as a
|
|
178
|
+
real consumer-correctable error — fix the body and resubmit. **Never retry a
|
|
179
|
+
422 with the same body.**
|
|
180
|
+
|
|
181
|
+
## Mapping `source.pointer` back to your TypeScript type
|
|
182
|
+
|
|
183
|
+
A `source.pointer` like `/regulated_data_db_reader_user` maps directly to the
|
|
184
|
+
`regulated_data_db_reader_user` field on `DatalakeRequestWritable`. For
|
|
185
|
+
nested embeds, the pointer carries the embed path:
|
|
186
|
+
`/unregulated_cloud_storage/access_key_id`.
|
|
187
|
+
|
|
188
|
+
For polymorphic embeds, check the discriminator field's pointer first — if
|
|
189
|
+
`/tool_body_type` (or `/cloud_storage_type`, etc.) is in the errors array,
|
|
190
|
+
fix that BEFORE chasing other fields on the embed. The wrong discriminator
|
|
191
|
+
value cascades into spurious required-list failures on the wrong branch's
|
|
192
|
+
fields. See `mutations.md` for the discriminator naming convention.
|
|
193
|
+
|
|
194
|
+
Each per-resource MD names its `<Resource>RequestWritable` TypeScript type —
|
|
195
|
+
the corpus's resource pages are the source of truth for which fields exist on
|
|
196
|
+
which embed.
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# Generic tables
|
|
2
|
+
|
|
3
|
+
A **generic table** is an operator-defined table whose schema the
|
|
4
|
+
caller authors at create time, distinct from the platform's industry-
|
|
5
|
+
built **system datasets** (see `datalakes.md` §5 `.systemDatasets`).
|
|
6
|
+
The SDK namespace is `api.genericTables`.
|
|
7
|
+
|
|
8
|
+
Generic tables are **Datalake-DB-resident**: creation triggers an
|
|
9
|
+
async schema-migration job that builds a physical Postgres table
|
|
10
|
+
in the datalake's schema. The parent datalake MUST be
|
|
11
|
+
`status: 'ready'` before the job can run end-to-end — see
|
|
12
|
+
`datalakes.md` §5 Lifecycle.
|
|
13
|
+
|
|
14
|
+
## 1. Wire shape
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import type {
|
|
18
|
+
GenericTableColumn,
|
|
19
|
+
GenericTableResponse,
|
|
20
|
+
} from '@alvera-ai/platform-sdk'
|
|
21
|
+
|
|
22
|
+
const { data: created } = await api.genericTables.create(
|
|
23
|
+
tenantSlug,
|
|
24
|
+
datalakeSlug,
|
|
25
|
+
{
|
|
26
|
+
title: 'Contact Us Submissions',
|
|
27
|
+
description: 'Inbound contact-form rows',
|
|
28
|
+
columns: [
|
|
29
|
+
{
|
|
30
|
+
name: 'submission_id',
|
|
31
|
+
title: 'Submission ID',
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: 'Unique submission identifier',
|
|
34
|
+
is_unique: true,
|
|
35
|
+
privacy_requirement: 'none',
|
|
36
|
+
},
|
|
37
|
+
// ... more columns
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
)
|
|
41
|
+
// created.id — server-derived UUID
|
|
42
|
+
// created.title === body.title
|
|
43
|
+
// created.name — server-derived: `alvera_custom_<slug>`
|
|
44
|
+
// created.status === 'new' | 'processing' (migration not yet done)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The response returns immediately — the row exists in Platform-DB,
|
|
48
|
+
but the *physical* Postgres table does not yet. Consumers MUST
|
|
49
|
+
poll `.get(name)` until `status === 'deployed'` before treating
|
|
50
|
+
the table as queryable. See §5 Lifecycle.
|
|
51
|
+
|
|
52
|
+
## 2. Rules the type cannot encode
|
|
53
|
+
|
|
54
|
+
### `name` is server-derived from `title`
|
|
55
|
+
|
|
56
|
+
Caller supplies `title` (free-form, human-readable);
|
|
57
|
+
server slugifies it and prefixes `alvera_custom_`:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
title: 'Contact Us Submissions'
|
|
61
|
+
↓ slugify + prefix
|
|
62
|
+
name: 'alvera_custom_contact_us_submissions'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The `name` is what every downstream resource references —
|
|
66
|
+
`.get(name)`, `generic_table_id` foreign keys via the resolved
|
|
67
|
+
row, `datasets.metadataDetails(tenantSlug, datalakeSlug, 'generic_table', { genericTableId })`,
|
|
68
|
+
etc. Never pre-compute `name` client-side; read it back from
|
|
69
|
+
the create response. A server-side slugifier change would flip
|
|
70
|
+
every downstream binding, so specs assert the derived `name`
|
|
71
|
+
directly.
|
|
72
|
+
|
|
73
|
+
### `columns` is operator-authored schema
|
|
74
|
+
|
|
75
|
+
Each column declares storage + semantics in one shot:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
name required string — snake_case identifier
|
|
79
|
+
(physical Postgres column name)
|
|
80
|
+
title required string — human label (UI / docs)
|
|
81
|
+
type required enum — see "Column types" below
|
|
82
|
+
is_array optional bool — column holds an array of `type` values
|
|
83
|
+
description required string — narrative; surfaces in catalogs
|
|
84
|
+
is_unique required bool — Postgres UNIQUE constraint at migration time
|
|
85
|
+
privacy_requirement required enum — 'none' | 'tokenize' | 'redact_only'
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
#### Column types
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
string free-form text
|
|
92
|
+
integer whole numbers
|
|
93
|
+
float decimal numbers
|
|
94
|
+
boolean true / false
|
|
95
|
+
date calendar date (no time component)
|
|
96
|
+
datetime timestamp with timezone
|
|
97
|
+
time time of day (no date component)
|
|
98
|
+
jsonb arbitrary JSON document
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Each type can be paired with `is_array: true` to model an array
|
|
102
|
+
column (`string[]`, `integer[]`, etc.). The wire enum lives in
|
|
103
|
+
the generated SDK as `GenericTableColumnTypeEnum` — consult it for
|
|
104
|
+
the live set since new types land independently of corpus
|
|
105
|
+
revisions.
|
|
106
|
+
|
|
107
|
+
#### Privacy × type interactions
|
|
108
|
+
|
|
109
|
+
`privacy_requirement` selects how the column flows between the
|
|
110
|
+
regulated and unregulated storage tiers (see `datalakes.md` §1
|
|
111
|
+
for the tier split):
|
|
112
|
+
|
|
113
|
+
- `none` — keeps the raw value in the regulated tier only;
|
|
114
|
+
the unregulated tier has no column.
|
|
115
|
+
- `tokenize` — stores the raw value in the regulated tier and an
|
|
116
|
+
opaque stable token in the unregulated tier. AI tools working
|
|
117
|
+
off the unregulated tier see the token, not the underlying
|
|
118
|
+
identity.
|
|
119
|
+
- `redact_only` — keeps the raw value in the regulated tier and
|
|
120
|
+
strips it from the unregulated tier entirely.
|
|
121
|
+
|
|
122
|
+
Two type-level restrictions the typed validator does not encode:
|
|
123
|
+
|
|
124
|
+
- **`type: 'jsonb'` rejects `privacy_requirement: 'tokenize'`.**
|
|
125
|
+
The platform can't tokenize an arbitrary JSON document — there
|
|
126
|
+
is no canonical "identity" to tokenize. Valid privacy values
|
|
127
|
+
for jsonb are `'none'` and `'redact_only'` only. Submitting
|
|
128
|
+
`{ type: 'jsonb', privacy_requirement: 'tokenize' }` returns
|
|
129
|
+
422 on `/columns/N/privacy_requirement`.
|
|
130
|
+
- **Tokenize widens primitive types at the schema layer.** A
|
|
131
|
+
`{ type: 'integer', privacy_requirement: 'tokenize' }` column
|
|
132
|
+
surfaces in the unregulated tier as a string-typed token, not
|
|
133
|
+
an integer. This is a quirk of how the platform stores tokens
|
|
134
|
+
(one opaque string format across all source types) — your
|
|
135
|
+
unregulated-tier queries must treat the column as a string.
|
|
136
|
+
This affects `integer`, `float`, `boolean`, `is_array string`,
|
|
137
|
+
`date`, `datetime`, and `time`.
|
|
138
|
+
|
|
139
|
+
The privacy choice is **immutable post-create** — there is no
|
|
140
|
+
SDK mutation surface for column-level privacy changes.
|
|
141
|
+
|
|
142
|
+
**Privacy is operator-decided per column, not implied by name.**
|
|
143
|
+
A field called `message` can be `redact_only` in one tenant and
|
|
144
|
+
`none` in another; the SDK doesn't pattern-match column names
|
|
145
|
+
to default privacy levels. Pick deliberately at design time —
|
|
146
|
+
recreating the table is the only way to flip a column's
|
|
147
|
+
treatment later.
|
|
148
|
+
|
|
149
|
+
### At least one column MUST be `is_unique: true`
|
|
150
|
+
|
|
151
|
+
The platform requires at least one unique column to identify
|
|
152
|
+
rows for the data activation upsert path. A body with zero
|
|
153
|
+
unique columns returns 422 on `/columns` with a message naming
|
|
154
|
+
the missing-unique-column rule. The unique column is what bound
|
|
155
|
+
data activation clients use as the upsert key.
|
|
156
|
+
|
|
157
|
+
### Reserved column names
|
|
158
|
+
|
|
159
|
+
The platform reserves certain column names you cannot redeclare
|
|
160
|
+
in `columns[N].name`:
|
|
161
|
+
|
|
162
|
+
- **Infrastructure columns** (always reserved, every industry):
|
|
163
|
+
`id`, `inserted_at`, `updated_at`, `datalake_id`, plus the
|
|
164
|
+
unregulated/regulated tier-link fields the platform manages.
|
|
165
|
+
- **Industry-specific MDM FK columns** (reserved only inside
|
|
166
|
+
their owning industry's datalake):
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
healthcare patient_id
|
|
170
|
+
core_banking party_id
|
|
171
|
+
accounts_receivable customer_id
|
|
172
|
+
(other domains as they land)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
A `patient_id` column is REJECTED in a healthcare datalake's
|
|
176
|
+
generic table — the platform's MDM machinery owns that name —
|
|
177
|
+
but is ACCEPTED in a `core_banking` datalake's generic table
|
|
178
|
+
(it's just an opaque user-defined column there). Reserved-name
|
|
179
|
+
rejections surface as 422 on `/columns/N/name`.
|
|
180
|
+
|
|
181
|
+
### Title cannot start with `alvera_system`
|
|
182
|
+
|
|
183
|
+
The `alvera_system_` prefix is reserved for system-curated
|
|
184
|
+
generic tables the platform manages internally. A `title` whose
|
|
185
|
+
slugified form would start with `alvera_system` is rejected at
|
|
186
|
+
create with 422 on `/title`. Pick a title that slugifies to
|
|
187
|
+
something else (e.g. avoid leading "Alvera System" in the title
|
|
188
|
+
text).
|
|
189
|
+
|
|
190
|
+
### No update surface
|
|
191
|
+
|
|
192
|
+
`PUT /generic-tables/:name` is **not** an SDK affordance.
|
|
193
|
+
Generic tables are schema-immutable post-create — to change
|
|
194
|
+
columns, delete + recreate. This is a deliberate constraint:
|
|
195
|
+
mutating a deployed schema mid-ingestion would race the
|
|
196
|
+
data activation client.
|
|
197
|
+
|
|
198
|
+
## 3. Field ownership
|
|
199
|
+
|
|
200
|
+
**Server-derived (Response-only).** Universal set from
|
|
201
|
+
`type_naming.md`, plus:
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
name string — `alvera_custom_<slugified-title>`
|
|
205
|
+
status enum — 'new' | 'processing' | 'deployed' | 'failed'
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Caller-supplied (round-trip).**
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
title required string — human-readable label
|
|
212
|
+
description optional string — narrative
|
|
213
|
+
columns required array — column definitions (see §2)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Write-only (Request-only).** None.
|
|
217
|
+
|
|
218
|
+
## 4. Error envelopes
|
|
219
|
+
|
|
220
|
+
Standard JSON:API envelopes per `errors.md`. Common rejections:
|
|
221
|
+
|
|
222
|
+
| `source.pointer` | Cause |
|
|
223
|
+
|----------------------------------|----------------------------------------------------|
|
|
224
|
+
| `/title` | empty; generates a name colliding with existing; or starts with `alvera_system` prefix |
|
|
225
|
+
| `/columns` | empty array; duplicate `column.name`; or zero columns marked `is_unique: true` |
|
|
226
|
+
| `/columns/0/name` | reserved column name (infrastructure column or the industry's MDM FK; see §2) |
|
|
227
|
+
| `/columns/0/type` | value not in supported type enum (see §2 column types) |
|
|
228
|
+
| `/columns/0/privacy_requirement` | value not in enum, OR `tokenize` on a `jsonb` column |
|
|
229
|
+
|
|
230
|
+
All of these come back as a 422 `AlveraApiError` thrown from the
|
|
231
|
+
SDK on submit — structural cases (Layer 1) and semantic conflicts
|
|
232
|
+
like name collision / deployed-name reuse (Layer 2). The SDK does
|
|
233
|
+
not validate client-side; see `errors.md`.
|
|
234
|
+
|
|
235
|
+
## 5. Lifecycle
|
|
236
|
+
|
|
237
|
+
Two-phase create, parallel to datalakes (see `datalakes.md` §5):
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
.create ──→ status: 'new' or 'processing'
|
|
241
|
+
│
|
|
242
|
+
│ (async migration job — see async.md)
|
|
243
|
+
▼
|
|
244
|
+
status: 'deployed' ◄── safe to bind / query
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Create + poll
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
const { data: created } = await api.genericTables.create(
|
|
251
|
+
tenantSlug, datalakeSlug, body,
|
|
252
|
+
)
|
|
253
|
+
// created.status === 'new' (or 'processing' if worker raced)
|
|
254
|
+
|
|
255
|
+
while (true) {
|
|
256
|
+
const { data: row } = await api.genericTables.get(
|
|
257
|
+
tenantSlug, datalakeSlug, created.name,
|
|
258
|
+
)
|
|
259
|
+
if (row.status === 'deployed') break
|
|
260
|
+
if (row.status === 'failed') throw new Error('migration failed')
|
|
261
|
+
await new Promise((r) => setTimeout(r, 1_000))
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`.get(name)` distinguishes "row never created" (404) from
|
|
266
|
+
"deployment in progress" (200 with status sub-`deployed`) —
|
|
267
|
+
two distinct signals on the same endpoint. Use the 404 to
|
|
268
|
+
short-circuit retry loops; use the 200+status to drive the
|
|
269
|
+
poll above.
|
|
270
|
+
|
|
271
|
+
### Read shapes
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
.list(tenantSlug, datalakeSlug) Paged: { data, meta }
|
|
275
|
+
.get(tenantSlug, datalakeSlug, name) One row, by name
|
|
276
|
+
(NOT slug — generic
|
|
277
|
+
tables expose `name`)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Plus the polymorphic dataset surface for per-table markdown:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
const { data: md } = await api.datasets.metadataDetails(
|
|
284
|
+
tenantSlug,
|
|
285
|
+
datalakeSlug,
|
|
286
|
+
'generic_table',
|
|
287
|
+
{ genericTableId },
|
|
288
|
+
)
|
|
289
|
+
// md: string — agent-facing markdown describing the table's schema
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
For the whole-domain catalog (every system + generic table type
|
|
293
|
+
registered to this datalake) use the sibling
|
|
294
|
+
`api.datasets.metadata(tenantSlug, datalakeSlug)` — same shape as
|
|
295
|
+
`tools.metadata` / `dataSources.metadata`.
|
|
296
|
+
|
|
297
|
+
See `datalakes.md` §5 `.systemDatasets` for the system-dataset
|
|
298
|
+
sibling of this verb; the `'generic_table'` kind is the entry
|
|
299
|
+
point for generic-table metadata.
|
|
300
|
+
|
|
301
|
+
### Delete
|
|
302
|
+
|
|
303
|
+
`DELETE` removes the row and drops the physical table. Tables
|
|
304
|
+
bound to active data activation clients, workflows, or
|
|
305
|
+
interoperability contracts reject with a 422 naming the
|
|
306
|
+
dependency. Detach dependents first.
|
|
307
|
+
|
|
308
|
+
## 6. Gotchas
|
|
309
|
+
|
|
310
|
+
1. **`.get` takes `name`, not `slug`.** Generic tables expose
|
|
311
|
+
`name` as the canonical reference; the `slug` field — if
|
|
312
|
+
present on `GenericTableResponse` — is incidental and
|
|
313
|
+
currently unused by downstream resources. Stash `data.name`
|
|
314
|
+
from create; pass it to `.get` and to every downstream
|
|
315
|
+
`generic_table` binding.
|
|
316
|
+
|
|
317
|
+
2. **`status` reaches `deployed` asynchronously.** A bare
|
|
318
|
+
`.create` response with `status: 'new'` is NOT yet bindable.
|
|
319
|
+
The poll loop above is non-optional for any spec that
|
|
320
|
+
creates a generic table and immediately uses it. A 60-second
|
|
321
|
+
timeout floor survives a saturated worker queue on a busy box;
|
|
322
|
+
bump for automated test environments with slower migrations.
|
|
323
|
+
|
|
324
|
+
3. **Column `privacy_requirement` is immutable post-create.**
|
|
325
|
+
`tokenize` columns surface as an opaque stable token in the
|
|
326
|
+
unregulated tier; `redact_only` strips the value from that
|
|
327
|
+
tier entirely; `none` keeps the raw value only in the
|
|
328
|
+
regulated tier. There is no update surface to flip this —
|
|
329
|
+
recreate the table to change.
|
|
330
|
+
|
|
331
|
+
4. **System-dataset enumeration EXCLUDES generic tables.**
|
|
332
|
+
`api.datalakes.systemDatasets(...)` returns only the
|
|
333
|
+
industry-built name list; operator-defined generic
|
|
334
|
+
tables surface only via `api.genericTables.list(...)`.
|
|
335
|
+
The literal string `"generic_table"` is also absent from
|
|
336
|
+
the system-datasets enum — see `datalakes.md` §5.
|
|
337
|
+
|
|
338
|
+
5. **Title-collision returns at create time.** Two generic tables
|
|
339
|
+
cannot share a `title` that slugifies to the same `name`
|
|
340
|
+
within one datalake. The 422 surfaces on POST, well before
|
|
341
|
+
the migration job runs.
|
|
342
|
+
|
|
343
|
+
6. **Generic tables can carry agentic write-back columns.**
|
|
344
|
+
They are not exclusively ingestion targets. Columns the
|
|
345
|
+
data activation client fills at ingestion (e.g.
|
|
346
|
+
`submission_id`, `name`, `email`) sit alongside columns
|
|
347
|
+
reserved for downstream resources to update (e.g. a reply
|
|
348
|
+
column written by a workflow, or a scoring column written
|
|
349
|
+
by an agent). Plan the column set for the full lifecycle,
|
|
350
|
+
not just the inbound shape — adding columns later requires
|
|
351
|
+
delete + recreate.
|