zitejs 0.10.0-start.7 → 0.10.0-start.8

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.
Files changed (32) hide show
  1. package/dist/cjs/sync/lib.js +4 -7
  2. package/dist/cjs/vite/index.js +8 -3
  3. package/dist/cjs/vite/index.test.js +10 -2
  4. package/dist/esm/start/airtable/arrayFields.d.ts +10 -0
  5. package/dist/esm/start/airtable/arrayFields.js +30 -0
  6. package/dist/esm/start/airtable/dispatch.d.ts +54 -0
  7. package/dist/esm/start/airtable/dispatch.js +465 -0
  8. package/dist/esm/start/airtable/formulas.d.ts +69 -0
  9. package/dist/esm/start/airtable/formulas.js +235 -0
  10. package/dist/esm/start/airtable/index.d.ts +11 -0
  11. package/dist/esm/start/airtable/index.js +70 -0
  12. package/dist/esm/start/data/baseRunner.d.ts +92 -0
  13. package/dist/esm/start/data/baseRunner.js +83 -0
  14. package/dist/esm/start/data/dispatch.d.ts +16 -0
  15. package/dist/esm/start/data/dispatch.js +198 -0
  16. package/dist/esm/start/data/index.d.ts +18 -0
  17. package/dist/esm/start/data/index.js +76 -0
  18. package/dist/esm/start/data/sqlNameMappings.d.ts +58 -0
  19. package/dist/esm/start/data/sqlNameMappings.js +92 -0
  20. package/dist/esm/start/data/transformations.d.ts +128 -0
  21. package/dist/esm/start/data/transformations.js +318 -0
  22. package/dist/esm/start/data/types.d.ts +91 -0
  23. package/dist/esm/start/data/types.js +14 -0
  24. package/dist/esm/start/server.js +54 -5
  25. package/dist/esm/start/types.d.ts +40 -0
  26. package/dist/esm/start/version.d.ts +1 -1
  27. package/dist/esm/start/version.js +1 -1
  28. package/dist/esm/start/vite.js +2 -1
  29. package/dist/esm/sync/lib.js +4 -7
  30. package/dist/esm/vite/index.js +8 -3
  31. package/dist/esm/vite/index.test.js +10 -2
  32. package/package.json +1 -1
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Database Record Transformation Utilities
3
+ *
4
+ * Shared transformation functions for converting between:
5
+ * - SDK field names ↔ database field IDs
6
+ * - LLM-friendly formats ↔ database formats
7
+ *
8
+ * Used by both workflow-runner and cloudflare-lambda.
9
+ */
10
+ // =============================================================================
11
+ // Reverse Lookup Helpers
12
+ // =============================================================================
13
+ /**
14
+ * Build reverse lookup map: sdkName → fieldId
15
+ */
16
+ export function buildSdkNameToFieldIdMap(fieldMappings) {
17
+ const map = {};
18
+ if (!fieldMappings)
19
+ return map;
20
+ for (const [fieldId, fieldInfo] of Object.entries(fieldMappings)) {
21
+ if (fieldInfo?.mapping?.sdkName) {
22
+ map[fieldInfo.mapping.sdkName] = fieldId;
23
+ // Also add lowercase version for case-insensitive lookup
24
+ map[fieldInfo.mapping.sdkName.toLowerCase()] = fieldId;
25
+ }
26
+ }
27
+ return map;
28
+ }
29
+ // =============================================================================
30
+ // Filter Transformations
31
+ // =============================================================================
32
+ /**
33
+ * Transform SDK field names to database field IDs in filters
34
+ */
35
+ export function transformFiltersToFieldIds(filters, fieldMappings) {
36
+ if (!fieldMappings)
37
+ return filters;
38
+ const sdkNameToFieldId = buildSdkNameToFieldIdMap(fieldMappings);
39
+ const transformed = {};
40
+ for (const [key, value] of Object.entries(filters)) {
41
+ const fieldId = sdkNameToFieldId[key] || sdkNameToFieldId[key.toLowerCase()] || key;
42
+ transformed[fieldId] = value;
43
+ }
44
+ return transformed;
45
+ }
46
+ // =============================================================================
47
+ // Formula Expression Transformations
48
+ // =============================================================================
49
+ /**
50
+ * Transform SDK field names to database field IDs inside a formula expression.
51
+ *
52
+ * The base-runner formula evaluator only accepts display names or field IDs
53
+ * in `{fieldRef}` placeholders — never SDK names. The LLM, however, sees the
54
+ * codebase through the SDK (camelCase fields like `count`), so it naturally
55
+ * writes `{count}` in expressions. This rewrite normalizes those references
56
+ * to field IDs at the SDK boundary, mirroring how `transformFiltersToFieldIds`
57
+ * normalizes filter keys.
58
+ *
59
+ * Behavior:
60
+ * - `{sdkName}` → `{fieldId}` when sdkName matches a field in `fieldMappings`
61
+ * - `{fieldId}` → unchanged (already a valid id)
62
+ * - `{anything else}` → unchanged (let the formula validator reject with a
63
+ * clear, table-specific error message)
64
+ * - `{linkField.targetField}` (cross-table) → unchanged (target field lives
65
+ * in another table; resolving it requires a separate per-link mapping
66
+ * that the SDK boundary doesn't have)
67
+ */
68
+ export function transformFormulaExpressionToFieldIds(expression, fieldMappings) {
69
+ if (!fieldMappings)
70
+ return expression;
71
+ const sdkNameToFieldId = buildSdkNameToFieldIdMap(fieldMappings);
72
+ const knownFieldIds = new Set(Object.keys(fieldMappings));
73
+ return expression.replace(/\{([^}]+)\}/g, (match, ref) => {
74
+ if (ref.includes('.'))
75
+ return match;
76
+ if (knownFieldIds.has(ref))
77
+ return match;
78
+ const fieldId = sdkNameToFieldId[ref] ?? sdkNameToFieldId[ref.toLowerCase()];
79
+ return fieldId ? `{${fieldId}}` : match;
80
+ });
81
+ }
82
+ // =============================================================================
83
+ // Record Input Transformations (SDK → Database)
84
+ // =============================================================================
85
+ /**
86
+ * Transform record data to use field IDs instead of field names.
87
+ *
88
+ * LLMs often use human-readable field names, but the database expects field IDs.
89
+ * Uses a three-step fallback:
90
+ * 1. Check if key is already a valid field ID
91
+ * 2. Match by field name (case-insensitive)
92
+ * 3. Match by SDK name from field mappings
93
+ *
94
+ * @param recordData - The record data with potentially mixed keys
95
+ * @param table - Table definition for field name lookups (optional)
96
+ * @param fieldMappings - SDK name mappings (optional)
97
+ */
98
+ export function transformFieldNamesToIds(recordData, table, fieldMappings) {
99
+ if (!recordData)
100
+ return recordData;
101
+ // If no table definition, fall back to simple SDK name mapping
102
+ if (!table) {
103
+ return transformRecordToFieldIds(recordData, fieldMappings);
104
+ }
105
+ const transformedRecord = {};
106
+ for (const [key, value] of Object.entries(recordData)) {
107
+ // Step 1: Check if it's already a field ID
108
+ const existsAsFieldId = table.fields.some(f => f.id === key);
109
+ if (existsAsFieldId) {
110
+ transformedRecord[key] = value;
111
+ continue;
112
+ }
113
+ // Step 2: Try to find field by name (case-insensitive)
114
+ const fieldByName = table.fields.find(f => f.name.toLowerCase() === key.toLowerCase());
115
+ if (fieldByName) {
116
+ transformedRecord[fieldByName.id] = value;
117
+ continue;
118
+ }
119
+ // Step 3: Try using field mappings (SDK name to field ID)
120
+ if (fieldMappings) {
121
+ const fieldId = Object.keys(fieldMappings).find(id => fieldMappings[id]?.mapping?.sdkName?.toLowerCase() ===
122
+ key.toLowerCase());
123
+ if (fieldId) {
124
+ transformedRecord[fieldId] = value;
125
+ continue;
126
+ }
127
+ }
128
+ // If no mapping found, keep the original key (might be a valid field ID)
129
+ transformedRecord[key] = value;
130
+ }
131
+ return transformedRecord;
132
+ }
133
+ /**
134
+ * Simple transformation using only field mappings (no table definition).
135
+ * Used when table metadata is not available.
136
+ */
137
+ export function transformRecordToFieldIds(recordData, fieldMappings) {
138
+ if (!recordData || !fieldMappings)
139
+ return recordData;
140
+ const sdkNameToFieldId = buildSdkNameToFieldIdMap(fieldMappings);
141
+ const result = {};
142
+ for (const [key, value] of Object.entries(recordData)) {
143
+ // Skip id field
144
+ if (key === 'id' || key === 'recordID') {
145
+ continue;
146
+ }
147
+ const fieldId = sdkNameToFieldId[key] || sdkNameToFieldId[key.toLowerCase()] || key;
148
+ result[fieldId] = value;
149
+ }
150
+ return result;
151
+ }
152
+ /**
153
+ * Transform linked record fields to ensure they are arrays.
154
+ *
155
+ * Handles common LLM mistakes:
156
+ * - Passing a string instead of an array of record IDs
157
+ * - Passing null/undefined instead of an empty array
158
+ * - Passing literal strings like "null" or "undefined"
159
+ *
160
+ * @param recordData - Record data (should already have field IDs as keys)
161
+ * @param table - Table definition for field type lookups
162
+ */
163
+ export function transformLinkedRecordFields(recordData, table) {
164
+ if (!recordData || !table)
165
+ return recordData;
166
+ const transformedRecord = {};
167
+ for (const [key, value] of Object.entries(recordData)) {
168
+ const field = table.fields.find(f => f.id === key);
169
+ if (field?.type === 'linked_record') {
170
+ if (typeof value === 'string') {
171
+ // Filter out LLM-generated placeholder strings
172
+ const cleanValue = value.trim();
173
+ if (cleanValue === '' ||
174
+ cleanValue === 'null' ||
175
+ cleanValue === 'undefined') {
176
+ transformedRecord[key] = [];
177
+ }
178
+ else {
179
+ transformedRecord[key] = [cleanValue];
180
+ }
181
+ }
182
+ else if (value == null) {
183
+ // Convert null/undefined to empty array
184
+ transformedRecord[key] = [];
185
+ }
186
+ else if (Array.isArray(value)) {
187
+ // Filter out any null/undefined/"null"/"undefined" entries
188
+ transformedRecord[key] = value.filter(v => v != null && v !== 'null' && v !== 'undefined');
189
+ }
190
+ else {
191
+ transformedRecord[key] = value;
192
+ }
193
+ }
194
+ else {
195
+ transformedRecord[key] = value;
196
+ }
197
+ }
198
+ return transformedRecord;
199
+ }
200
+ // =============================================================================
201
+ // Record Output Transformations (Database → SDK)
202
+ // =============================================================================
203
+ /**
204
+ * Transform database record to use SDK field names
205
+ *
206
+ * @param record - Raw database record with field IDs
207
+ * @param fieldMappings - SDK name mappings
208
+ */
209
+ export function transformRecordToSdkNames(record, fieldMappings) {
210
+ if (!record)
211
+ return undefined;
212
+ const result = { id: record.id };
213
+ if (!fieldMappings) {
214
+ const { id: _ignoredId, ...dataWithoutId } = record.data;
215
+ return { ...result, ...dataWithoutId };
216
+ }
217
+ for (const [fieldId, value] of Object.entries(record.data)) {
218
+ const mapping = fieldMappings[fieldId];
219
+ const sdkName = mapping?.mapping?.sdkName || fieldId;
220
+ if (sdkName === 'id') {
221
+ continue;
222
+ }
223
+ result[sdkName] = value;
224
+ }
225
+ return result;
226
+ }
227
+ // =============================================================================
228
+ // Batch Transformations
229
+ // =============================================================================
230
+ /**
231
+ * Transform multiple records for input (SDK → Database)
232
+ */
233
+ export function transformRecordsForInput(records, table, fieldMappings) {
234
+ return records.map(record => {
235
+ const withFieldIds = transformFieldNamesToIds(record, table, fieldMappings);
236
+ return transformLinkedRecordFields(withFieldIds, table);
237
+ });
238
+ }
239
+ /**
240
+ * Transform multiple records for output (Database → SDK)
241
+ */
242
+ export function transformRecordsForOutput(records, fieldMappings) {
243
+ return records
244
+ .map(r => transformRecordToSdkNames(r, fieldMappings))
245
+ .filter((r) => r !== undefined);
246
+ }
247
+ /**
248
+ * Transform an array of SDK field names to field IDs.
249
+ *
250
+ * Used for the `fields` parameter in findOne/findAll to select which fields to return.
251
+ * Uses the same three-step fallback as transformFieldNamesToIds:
252
+ * 1. Check if name is already a valid field ID
253
+ * 2. Match by field name (case-insensitive)
254
+ * 3. Match by SDK name from field mappings
255
+ *
256
+ * @param fields - Array of SDK field names (e.g. ['customerName', 'email'])
257
+ * @param table - Table definition for field name lookups (optional)
258
+ * @param fieldMappings - SDK name mappings (optional)
259
+ * @returns Array of field IDs, or undefined if input is undefined
260
+ */
261
+ export function transformFieldsToFieldIds(fields, table, fieldMappings) {
262
+ if (!fields)
263
+ return undefined;
264
+ const sdkNameToFieldId = buildSdkNameToFieldIdMap(fieldMappings);
265
+ return fields.map(name => {
266
+ // Step 1: Check if it's already a field ID
267
+ if (table?.fields.some(f => f.id === name)) {
268
+ return name;
269
+ }
270
+ // Step 2: Match by human-readable field name (case-insensitive)
271
+ if (table) {
272
+ const fieldByName = table.fields.find(f => f.name.toLowerCase() === name.toLowerCase());
273
+ if (fieldByName)
274
+ return fieldByName.id;
275
+ }
276
+ // Step 3: Match by SDK name from field mappings
277
+ return (sdkNameToFieldId[name] || sdkNameToFieldId[name.toLowerCase()] || name);
278
+ });
279
+ }
280
+ /**
281
+ * Resolve the field references in a sort spec to field IDs.
282
+ *
283
+ * Sort arrives from app code using SDK names (`closeDate`), but base-runner
284
+ * resolves sort fields by field ID or display name only, and silently skips one
285
+ * it cannot match. Running the references through the same resolution order as
286
+ * `fields` is what makes a sort on a multi-word field ("Close date" ->
287
+ * `closeDate`) actually apply instead of being dropped without an error.
288
+ *
289
+ * System sort fields (`id`, `createdAt`, `updatedAt`) have no mapping and fall
290
+ * through unchanged, which is what base-runner expects.
291
+ */
292
+ export function transformSortToFieldIds(sort, table, fieldMappings) {
293
+ if (!sort)
294
+ return undefined;
295
+ const entries = typeof sort === 'string'
296
+ ? [{ field: sort, direction: 'asc' }]
297
+ : sort;
298
+ if (entries.length === 0)
299
+ return undefined;
300
+ const resolved = transformFieldsToFieldIds(entries.map(entry => entry.field), table, fieldMappings);
301
+ return entries.map((entry, index) => ({
302
+ field: resolved?.[index] ?? entry.field,
303
+ direction: entry.direction ?? 'asc',
304
+ }));
305
+ }
306
+ /**
307
+ * Transform matchOn field names to field IDs
308
+ */
309
+ export function transformMatchOnFields(matchOn, fieldMappings) {
310
+ if (!matchOn || !fieldMappings)
311
+ return matchOn;
312
+ const sdkNameToFieldId = buildSdkNameToFieldIdMap(fieldMappings);
313
+ return matchOn.map(fieldName => {
314
+ return (sdkNameToFieldId[fieldName] ||
315
+ sdkNameToFieldId[fieldName.toLowerCase()] ||
316
+ fieldName);
317
+ });
318
+ }
@@ -0,0 +1,91 @@
1
+ export type BaseTable = {
2
+ id: string;
3
+ name: string;
4
+ deletedAt?: string | null;
5
+ fields: Array<{
6
+ id: string;
7
+ name: string;
8
+ type: string;
9
+ deletedAt?: string | null;
10
+ template?: {
11
+ tableId?: string;
12
+ isInverse?: boolean;
13
+ } & Record<string, unknown>;
14
+ }>;
15
+ };
16
+ export type GetBaseResponseType = {
17
+ id: string;
18
+ name: string;
19
+ tables: BaseTable[];
20
+ };
21
+ export interface FindOneResponseType {
22
+ id: string;
23
+ data: Record<string, unknown>;
24
+ fields?: Record<string, unknown>;
25
+ }
26
+ export interface FindAllResponseType {
27
+ records: FindOneResponseType[];
28
+ offset?: string;
29
+ hasMore: boolean;
30
+ total?: number;
31
+ }
32
+ export interface BulkCreateResponseType {
33
+ success?: boolean;
34
+ records: FindOneResponseType[];
35
+ }
36
+ export type LegacyRecordsFilterClauseType = Record<string, unknown>;
37
+ /** Simplified table definition for SDK transformations. */
38
+ export interface SimpleTableDefinition {
39
+ id: string;
40
+ name: string;
41
+ fields: Array<{
42
+ id: string;
43
+ name: string;
44
+ type: string;
45
+ template?: unknown;
46
+ }>;
47
+ }
48
+ /** Field mappings for transforming between SDK names and field IDs */
49
+ export type FieldMappings = Record<string, {
50
+ mapping: {
51
+ sdkName: string;
52
+ };
53
+ }>;
54
+ /** A record after transformation. */
55
+ export interface DatabaseRecord {
56
+ id: string;
57
+ [key: string]: unknown;
58
+ }
59
+ /** The build's tables' and fields' SDK names, by id (`projectConfig.nameMappings`). */
60
+ export type DataNameMappings = {
61
+ tables: Record<string, {
62
+ mapping: {
63
+ sdkName: string;
64
+ };
65
+ fields: Record<string, {
66
+ mapping: {
67
+ sdkName: string;
68
+ };
69
+ }>;
70
+ }>;
71
+ };
72
+ /** base-runner's rename map for SQL, which `buildSdkNameMappingsForBase` builds. */
73
+ export interface NameMappings {
74
+ tables: Record<string, {
75
+ displayName: string;
76
+ fields: Record<string, string>;
77
+ }>;
78
+ linkTables: Record<string, {
79
+ displayName: string;
80
+ sourceIdColumnAlias: string;
81
+ targetIdColumnAlias: string;
82
+ }>;
83
+ }
84
+ /** Marks an error as our fault rather than the app's. */
85
+ export declare const ZITE_PLATFORM_FAULT_FLAG = "zitePlatformFault";
86
+ /** base-runner's refusal, with its status. */
87
+ export declare class DatabasesApiError extends Error {
88
+ readonly statusCode: number;
89
+ readonly statusText: string;
90
+ constructor(message: string, statusCode: number, statusText: string);
91
+ }
@@ -0,0 +1,14 @@
1
+ // Ported from restly's packages/workflow-edge/src/databases/types.ts: the shapes base-runner answers with.
2
+ /** Marks an error as our fault rather than the app's. */
3
+ export const ZITE_PLATFORM_FAULT_FLAG = "zitePlatformFault";
4
+ /** base-runner's refusal, with its status. */
5
+ export class DatabasesApiError extends Error {
6
+ statusCode;
7
+ statusText;
8
+ constructor(message, statusCode, statusText) {
9
+ super(message);
10
+ this.name = "DatabasesApiError";
11
+ this.statusCode = statusCode;
12
+ this.statusText = statusText;
13
+ }
14
+ }
@@ -3,6 +3,8 @@ import { describeApp } from "./describe.js";
3
3
  import { runRegistration } from "./registration.js";
4
4
  import { isAuthHookName, loadAuthHooks } from "./authHooks.js";
5
5
  import { errorOf, preview } from "./runLog.js";
6
+ import { DATA_INTEGRATION_ID, dataContextOf, runDataCall } from "./data/index.js";
7
+ import { airtableSettingsOf, runAirtableCall } from "./airtable/index.js";
6
8
  import { getHost, requireScope, runInScope, runWithHost } from "./scope.js";
7
9
  import { ZITEJS_VERSION } from "./version.js";
8
10
  import { loadWorkflows, loggedInput, runWorkflow } from "./workflowRun.js";
@@ -22,7 +24,20 @@ export function installPlatformTransport() {
22
24
  if (!host?.platform)
23
25
  throw new Error("The Zite platform binding is missing");
24
26
  host.session ??= Promise.resolve(host.watch ? host.platform.session(host.watch) : host.platform.session());
25
- const result = await (await host.session).call({ integrationId, className, methodName, params, access: scope.kind });
27
+ const session = await host.session;
28
+ if (integrationId === DATA_INTEGRATION_ID) {
29
+ const found = await dataContextOf(session);
30
+ if (found) {
31
+ return recordCall(host, { integrationId, className, methodName, params }, () => runDataCall(session, found, host.mode, methodName, params));
32
+ }
33
+ }
34
+ else if (!integrationId.startsWith("__")) {
35
+ const airtable = await airtableSettingsOf(session, integrationId);
36
+ if (airtable) {
37
+ return recordCall(host, { integrationId, className, methodName, params }, () => runAirtableCall(host, session, integrationId, airtable, methodName, params, scope.kind));
38
+ }
39
+ }
40
+ const result = await session.call({ integrationId, className, methodName, params, access: scope.kind });
26
41
  if ("value" in result)
27
42
  return result.value;
28
43
  const { code, message, userFacingMessage } = result.error;
@@ -31,6 +46,27 @@ export function installPlatformTransport() {
31
46
  globalThis.__wrapSdkCall = sdkCall;
32
47
  watchOutboundFetches();
33
48
  }
49
+ /** A data call the app made itself, recorded as the host recorded the ones it made. */
50
+ const recordCall = async (host, call, run) => {
51
+ const at = Date.now();
52
+ const record = {
53
+ integrationId: call.integrationId,
54
+ className: call.className,
55
+ methodName: call.methodName,
56
+ at,
57
+ inputs: preview(call.params),
58
+ };
59
+ try {
60
+ const value = await run();
61
+ (host.calls ??= []).push({ ...record, durationMs: Date.now() - at, outputs: preview(value) });
62
+ return value;
63
+ }
64
+ catch (error) {
65
+ const { code, message } = errorOf(error);
66
+ (host.calls ??= []).push({ ...record, durationMs: Date.now() - at, error: { code, message } });
67
+ throw error;
68
+ }
69
+ };
34
70
  let fetchesWatched = false;
35
71
  const fetchedHost = (input) => {
36
72
  try {
@@ -69,13 +105,16 @@ export function hostContextFromProps(request, props) {
69
105
  appUrl: props.appUrl,
70
106
  baseId: props.baseId,
71
107
  previewClaim: props.previewClaim,
108
+ env: props.env,
72
109
  runs: [],
73
110
  platform: props.platform,
74
111
  };
75
112
  }
76
113
  // Classic sets these per run. Node and workerd both refuse getters on process.env, so each request writes what the
77
114
  // host sent, which is the same for every request a build serves: the live URL even on a demo.
78
- const setRequestEnv = ({ appUrl, baseId }) => {
115
+ // The connection vars the last request set, so one gone since (an integration removed) doesn't linger.
116
+ let connectionEnvKeys = new Set();
117
+ const setRequestEnv = ({ appUrl, baseId, env: connectionEnv }) => {
79
118
  const env = globalThis.process?.env;
80
119
  if (!env)
81
120
  return;
@@ -83,6 +122,16 @@ const setRequestEnv = ({ appUrl, baseId }) => {
83
122
  env.ZITE_APP_URL = appUrl;
84
123
  if (baseId)
85
124
  env.ZITE_BASE_ID = baseId;
125
+ if (!connectionEnv)
126
+ return;
127
+ for (const key of connectionEnvKeys)
128
+ if (!Object.hasOwn(connectionEnv, key))
129
+ delete env[key];
130
+ for (const [key, value] of Object.entries(connectionEnv)) {
131
+ if (key.startsWith("ZITE_"))
132
+ env[key] = value;
133
+ }
134
+ connectionEnvKeys = new Set(Object.keys(connectionEnv));
86
135
  };
87
136
  /**
88
137
  * A request the host didn't dispatch: the host's warm-up, `zitejs describe` under `vite preview`, and tests. Only the
@@ -241,14 +290,14 @@ export function createZiteHandlers(options) {
241
290
  response = new Response(response.body, { status, headers: response.headers });
242
291
  return onBodyEnd(response, () => {
243
292
  const { platform } = host;
244
- if ((!host.runs.length && !host.pending?.length) || !platform)
293
+ if ((!host.runs.length && !host.calls?.length && !host.pending?.length) || !platform)
245
294
  return;
246
295
  // On the request's session when it has one: one call, not another binding round trip.
247
296
  const flushed = (async () => {
248
297
  await Promise.all(host.pending ?? []);
249
- if (!host.runs.length)
298
+ if (!host.runs.length && !host.calls?.length)
250
299
  return;
251
- const batch = { runs: host.runs };
300
+ const batch = { runs: host.runs, ...(host.calls?.length && { calls: host.calls }) };
252
301
  const session = host.session ? await host.session : undefined;
253
302
  await (session?.runs ? session.runs(batch) : platform.runs(batch));
254
303
  })().catch((error) => {
@@ -53,12 +53,44 @@ export type PlatformCallResult = {
53
53
  userFacingMessage?: string;
54
54
  };
55
55
  };
56
+ /** One data call the app's own dispatch made, for its run's detail. */
57
+ export type CallRecord = {
58
+ integrationId: string;
59
+ className: string;
60
+ methodName: string;
61
+ at: number;
62
+ durationMs: number;
63
+ inputs?: string;
64
+ outputs?: string;
65
+ error?: {
66
+ code: string;
67
+ message: string;
68
+ };
69
+ };
56
70
  export type PlatformSession = {
57
71
  call(call: PlatformCallInput): Promise<PlatformCallResult>;
58
72
  /** As `PlatformBinding.runs`, on a session the request already holds. Hosts before 0.10.0-start.4 lack it. */
59
73
  runs?(batch: {
60
74
  runs: RunRecord[];
75
+ calls?: CallRecord[];
61
76
  }): Promise<void>;
77
+ /** The build's base and SDK names, which the app's data dispatch translates against. */
78
+ dataContext?(): Promise<{
79
+ baseId: string | null;
80
+ nameMappings: unknown;
81
+ }>;
82
+ /** One base-runner request; the host holds it to the build's base and adds the key and acting user. */
83
+ dataFetch?(request: {
84
+ method: string;
85
+ path: string;
86
+ body?: string;
87
+ }): Promise<Response>;
88
+ /** An integration's settings as the build declares them; null for one it doesn't, or that runs on the platform. */
89
+ integrationSettings?(integrationId: string): Promise<{
90
+ serviceType: string;
91
+ baseId?: string;
92
+ nameMappings?: unknown;
93
+ } | null>;
62
94
  };
63
95
  /**
64
96
  * The host's ear on a page render it may cache, from `render()`. Reports can
@@ -95,6 +127,8 @@ export type HostProps = {
95
127
  baseId?: string;
96
128
  /** An editor preview's access, which zitejs keeps on the app's links and calls. */
97
129
  previewClaim?: string;
130
+ /** `process.env` as classic sets it: connections' vars, secrets as placeholders egress swaps but S3's, then the user's own secrets. */
131
+ env?: Record<string, string>;
98
132
  };
99
133
  /** What the host resolved for this request. Set by the generated server entry, never by the browser. */
100
134
  export type ZiteRequestContext = {
@@ -106,8 +140,14 @@ export type ZiteRequestContext = {
106
140
  appUrl?: string;
107
141
  baseId?: string;
108
142
  previewClaim?: string;
143
+ /** Classic's connection env vars the host sent, set into `process.env` for the request. */
144
+ env?: Record<string, string>;
109
145
  /** Filled by run capture, flushed to the platform once the response ends. */
110
146
  runs: RunRecord[];
147
+ /** The data calls this request made, flushed with its runs. */
148
+ calls?: CallRecord[];
149
+ /** This request's Airtable reads, so the same one twice goes out once. */
150
+ airtableReads?: Map<string, Promise<unknown>>;
111
151
  /** Records still reading a copy of a body; the flush waits for them. */
112
152
  pending?: Promise<void>[];
113
153
  platform?: PlatformBinding;
@@ -1 +1 @@
1
- export declare const ZITEJS_VERSION = "0.10.0-start.7";
1
+ export declare const ZITEJS_VERSION = "0.10.0-start.8";
@@ -1,2 +1,2 @@
1
1
  // Written by scripts/write-version.mjs from package.json.
2
- export const ZITEJS_VERSION = "0.10.0-start.7";
2
+ export const ZITEJS_VERSION = "0.10.0-start.8";
@@ -131,6 +131,8 @@ export function zite(options = {}) {
131
131
  },
132
132
  };
133
133
  return [
134
+ // First, so ids name lines in the source, before TanStack splits a route's component out of it.
135
+ ziteId(),
134
136
  tanstackStart({
135
137
  // TanStack resolves these from `src/`.
136
138
  server: { entry: `../${serverEntry.replace(/\.ts$/, "")}` },
@@ -163,7 +165,6 @@ export function zite(options = {}) {
163
165
  }),
164
166
  options.cloudflare !== false ? loadCloudflare(serverEntry) : null,
165
167
  viteReact(),
166
- ziteId(),
167
168
  compileZitejs,
168
169
  functionIds,
169
170
  workflowRegistry,
@@ -71,6 +71,7 @@ const FIELD_TYPE_MAP = {
71
71
  formula: "any",
72
72
  created_at: "string",
73
73
  updated_at: "string",
74
+ created_by: "string",
74
75
  updated_by: "string",
75
76
  };
76
77
  /**
@@ -96,7 +97,7 @@ const FIELD_INPUT_TYPE_MAP = {
96
97
  * rather than typed and then refused at runtime.
97
98
  *
98
99
  * Mirrors `COMPUTED_FIELD_TYPES` in the product's own `shared/bases/fields`.
99
- * The first six are rejected by `createRecordDataSchema` — each has a
100
+ * The first seven are rejected by `createRecordDataSchema` — each has a
100
101
  * `refine(val => val == null)`, so a non-null write errors (null is a no-op
101
102
  * clear). `created_at` / `updated_at` are refused differently: they have no
102
103
  * column at all, so a write is silently dropped, which is the worse outcome to
@@ -108,6 +109,7 @@ const READ_ONLY_FIELD_TYPES = new Set([
108
109
  "source",
109
110
  "formula",
110
111
  "autonumber",
112
+ "created_by",
111
113
  "updated_by",
112
114
  "created_at",
113
115
  "updated_at",
@@ -245,13 +247,8 @@ function fieldJsdoc(schemaField, table, schema) {
245
247
  const parts = [];
246
248
  if (table.primaryFieldId === schemaField.id)
247
249
  parts.push("Primary field");
248
- if (def.type === "created_at" ||
249
- def.type === "updated_at" ||
250
- def.type === "lookup" ||
251
- def.type === "formula" ||
252
- def.type === "autonumber") {
250
+ if (READ_ONLY_FIELD_TYPES.has(def.type))
253
251
  parts.push("Read-only");
254
- }
255
252
  if (def.type === "linked_record") {
256
253
  const tpl = def.template;
257
254
  if (tpl.tableId) {