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.
- package/dist/cjs/sync/lib.js +4 -7
- package/dist/cjs/vite/index.js +8 -3
- package/dist/cjs/vite/index.test.js +10 -2
- package/dist/esm/start/airtable/arrayFields.d.ts +10 -0
- package/dist/esm/start/airtable/arrayFields.js +30 -0
- package/dist/esm/start/airtable/dispatch.d.ts +54 -0
- package/dist/esm/start/airtable/dispatch.js +465 -0
- package/dist/esm/start/airtable/formulas.d.ts +69 -0
- package/dist/esm/start/airtable/formulas.js +235 -0
- package/dist/esm/start/airtable/index.d.ts +11 -0
- package/dist/esm/start/airtable/index.js +70 -0
- package/dist/esm/start/data/baseRunner.d.ts +92 -0
- package/dist/esm/start/data/baseRunner.js +83 -0
- package/dist/esm/start/data/dispatch.d.ts +16 -0
- package/dist/esm/start/data/dispatch.js +198 -0
- package/dist/esm/start/data/index.d.ts +18 -0
- package/dist/esm/start/data/index.js +76 -0
- package/dist/esm/start/data/sqlNameMappings.d.ts +58 -0
- package/dist/esm/start/data/sqlNameMappings.js +92 -0
- package/dist/esm/start/data/transformations.d.ts +128 -0
- package/dist/esm/start/data/transformations.js +318 -0
- package/dist/esm/start/data/types.d.ts +91 -0
- package/dist/esm/start/data/types.js +14 -0
- package/dist/esm/start/server.js +54 -5
- package/dist/esm/start/types.d.ts +40 -0
- package/dist/esm/start/version.d.ts +1 -1
- package/dist/esm/start/version.js +1 -1
- package/dist/esm/start/vite.js +2 -1
- package/dist/esm/sync/lib.js +4 -7
- package/dist/esm/vite/index.js +8 -3
- package/dist/esm/vite/index.test.js +10 -2
- package/package.json +1 -1
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { buildSdkNameMappingsForBase } from "./sqlNameMappings.js";
|
|
2
|
+
import { transformFieldNamesToIds, transformFieldsToFieldIds, transformFiltersToFieldIds, transformLinkedRecordFields, transformMatchOnFields, transformRecordToSdkNames, transformSortToFieldIds, } from "./transformations.js";
|
|
3
|
+
import { ZITE_PLATFORM_FAULT_FLAG, } from "./types.js";
|
|
4
|
+
export const DEMO_UNAVAILABLE_MESSAGE = "This action is not available in demo mode";
|
|
5
|
+
// An allowlist, so a database method added later is refused until it's listed.
|
|
6
|
+
const DEMO_READ_DATABASE_METHODS = new Set([
|
|
7
|
+
"findOne",
|
|
8
|
+
"findAll",
|
|
9
|
+
"executeSql",
|
|
10
|
+
"findAllUsers",
|
|
11
|
+
"getDatabaseSchema",
|
|
12
|
+
]);
|
|
13
|
+
const STATUS_TO_ZITE_ERROR_CODE = {
|
|
14
|
+
400: "BAD_REQUEST",
|
|
15
|
+
401: "UNAUTHORIZED",
|
|
16
|
+
403: "FORBIDDEN",
|
|
17
|
+
404: "NOT_FOUND",
|
|
18
|
+
409: "CONFLICT",
|
|
19
|
+
429: "RATE_LIMITED",
|
|
20
|
+
};
|
|
21
|
+
// A 4xx answers with its status as a coded ZiteError; a 5xx stays as thrown, flagged as ours, not the app's.
|
|
22
|
+
export function asZiteClientError(error) {
|
|
23
|
+
const status = error?.statusCode;
|
|
24
|
+
if (typeof status !== "number" || status < 400)
|
|
25
|
+
return error;
|
|
26
|
+
if (status >= 500) {
|
|
27
|
+
if (error && typeof error === "object")
|
|
28
|
+
error[ZITE_PLATFORM_FAULT_FLAG] = true;
|
|
29
|
+
return error;
|
|
30
|
+
}
|
|
31
|
+
const tagged = new Error(error instanceof Error ? error.message : String(error));
|
|
32
|
+
tagged.name = "ZiteError";
|
|
33
|
+
tagged.code = STATUS_TO_ZITE_ERROR_CODE[status] ?? "BAD_REQUEST";
|
|
34
|
+
tagged.statusCode = status;
|
|
35
|
+
tagged.cause = error;
|
|
36
|
+
if (error instanceof Error)
|
|
37
|
+
tagged.stack = error.stack;
|
|
38
|
+
return tagged;
|
|
39
|
+
}
|
|
40
|
+
/** One `zite.*` data call, as classic's dispatch runs it. */
|
|
41
|
+
export async function executeDatabasesCall(context, methodName, params) {
|
|
42
|
+
try {
|
|
43
|
+
return await runDatabasesCall(context, methodName, params ?? {});
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
throw asZiteClientError(error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function runDatabasesCall(context, methodName, params) {
|
|
50
|
+
const { baseRunner: sdk, baseId, nameMappings, tables } = context;
|
|
51
|
+
if (context.readOnly && !DEMO_READ_DATABASE_METHODS.has(methodName)) {
|
|
52
|
+
throw Object.assign(new Error(DEMO_UNAVAILABLE_MESSAGE), { statusCode: 403 });
|
|
53
|
+
}
|
|
54
|
+
if (methodName === "findAllUsers") {
|
|
55
|
+
return sdk.auth.findAll({
|
|
56
|
+
baseId,
|
|
57
|
+
appIds: params.appIds,
|
|
58
|
+
filters: params.filters,
|
|
59
|
+
filter: params.filter,
|
|
60
|
+
offset: params.offset,
|
|
61
|
+
sort: params.sort,
|
|
62
|
+
limit: params.limit,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (methodName === "updateUserProfile") {
|
|
66
|
+
const userId = params.userId ?? params.id;
|
|
67
|
+
const profile = params.profile ?? params.data ?? params.record ?? params;
|
|
68
|
+
if (!userId || !profile || typeof profile !== "object") {
|
|
69
|
+
throw new Error("updateUserProfile requires a user id and profile data");
|
|
70
|
+
}
|
|
71
|
+
if ("email" in profile) {
|
|
72
|
+
throw new Error("A Zite user email address cannot be updated");
|
|
73
|
+
}
|
|
74
|
+
return sdk.auth.update({
|
|
75
|
+
baseId,
|
|
76
|
+
userId,
|
|
77
|
+
profile: { firstName: profile.firstName, lastName: profile.lastName, image: profile.image },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (methodName === "executeSql") {
|
|
81
|
+
const query = params?.query ?? params?.sql;
|
|
82
|
+
if (typeof query !== "string" || query.trim() === "") {
|
|
83
|
+
throw new Error("executeSql requires a non-empty `query` parameter");
|
|
84
|
+
}
|
|
85
|
+
return await sdk.sql.execute({
|
|
86
|
+
baseId,
|
|
87
|
+
sql: query,
|
|
88
|
+
params: params.params,
|
|
89
|
+
statementTimeoutMs: params.statementTimeoutMs,
|
|
90
|
+
nameMappings: nameMappings && tables
|
|
91
|
+
? buildSdkNameMappingsForBase({ metadata: { tables }, settingsNameMappings: nameMappings })
|
|
92
|
+
: undefined,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const tableId = params.tableId;
|
|
96
|
+
if (!tableId) {
|
|
97
|
+
throw new Error("Databases calls require tableId in params");
|
|
98
|
+
}
|
|
99
|
+
const fieldMappings = nameMappings?.tables?.[tableId]?.fields;
|
|
100
|
+
const table = tables?.find((t) => t.id === tableId);
|
|
101
|
+
switch (methodName) {
|
|
102
|
+
case "findOne": {
|
|
103
|
+
const { id: recordId, filters, fields } = params;
|
|
104
|
+
const transformedFields = transformFieldsToFieldIds(fields, table, fieldMappings);
|
|
105
|
+
if (recordId) {
|
|
106
|
+
const record = await sdk.records.get({ baseId, tableId, recordId, fields: transformedFields });
|
|
107
|
+
return transformRecordToSdkNames(record, fieldMappings);
|
|
108
|
+
}
|
|
109
|
+
const result = await sdk.records.findAll({
|
|
110
|
+
baseId,
|
|
111
|
+
tableId,
|
|
112
|
+
filters: filters ? transformFiltersToFieldIds(filters, fieldMappings) : undefined,
|
|
113
|
+
limit: 1,
|
|
114
|
+
fields: transformedFields,
|
|
115
|
+
});
|
|
116
|
+
if (result.records.length === 0)
|
|
117
|
+
return undefined;
|
|
118
|
+
return transformRecordToSdkNames(result.records[0], fieldMappings);
|
|
119
|
+
}
|
|
120
|
+
case "findAll": {
|
|
121
|
+
const { filters, sort, limit, offset, fields } = params;
|
|
122
|
+
const result = await sdk.records.findAll({
|
|
123
|
+
baseId,
|
|
124
|
+
tableId,
|
|
125
|
+
filters: filters ? transformFiltersToFieldIds(filters, fieldMappings) : undefined,
|
|
126
|
+
sort: transformSortToFieldIds(sort, table, fieldMappings),
|
|
127
|
+
limit: limit || 500,
|
|
128
|
+
offset,
|
|
129
|
+
fields: transformFieldsToFieldIds(fields, table, fieldMappings),
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
records: result.records
|
|
133
|
+
.map((r) => transformRecordToSdkNames(r, fieldMappings))
|
|
134
|
+
.filter((r) => r !== undefined),
|
|
135
|
+
hasMore: result.hasMore,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
case "create": {
|
|
139
|
+
const { record: recordData } = params;
|
|
140
|
+
if (!recordData)
|
|
141
|
+
throw new Error("Create requires record data");
|
|
142
|
+
const record = await sdk.records.create({
|
|
143
|
+
baseId,
|
|
144
|
+
tableId,
|
|
145
|
+
record: transformLinkedRecordFields(transformFieldNamesToIds(recordData, table, fieldMappings), table),
|
|
146
|
+
});
|
|
147
|
+
const transformed = transformRecordToSdkNames(record, fieldMappings);
|
|
148
|
+
if (!transformed)
|
|
149
|
+
throw new Error("Failed to create record");
|
|
150
|
+
return transformed;
|
|
151
|
+
}
|
|
152
|
+
case "update": {
|
|
153
|
+
const { id: recordId, record: recordData } = params;
|
|
154
|
+
if (!recordId || !recordData)
|
|
155
|
+
throw new Error("Update requires id and record data");
|
|
156
|
+
const record = await sdk.records.update({
|
|
157
|
+
baseId,
|
|
158
|
+
tableId,
|
|
159
|
+
recordId,
|
|
160
|
+
record: transformLinkedRecordFields(transformFieldNamesToIds(recordData, table, fieldMappings), table),
|
|
161
|
+
});
|
|
162
|
+
const transformed = transformRecordToSdkNames(record, fieldMappings);
|
|
163
|
+
if (!transformed)
|
|
164
|
+
throw new Error("Failed to update record");
|
|
165
|
+
// Both flat and `fields`, for compatibility.
|
|
166
|
+
return { ...transformed, fields: transformed };
|
|
167
|
+
}
|
|
168
|
+
case "delete": {
|
|
169
|
+
const { id: recordId } = params;
|
|
170
|
+
if (!recordId)
|
|
171
|
+
throw new Error("Delete requires id parameter");
|
|
172
|
+
await sdk.records.delete({ baseId, tableId, recordId });
|
|
173
|
+
return { success: true, id: recordId };
|
|
174
|
+
}
|
|
175
|
+
case "bulkCreate": {
|
|
176
|
+
const { records: recordsData, matchOn } = params;
|
|
177
|
+
if (!recordsData || recordsData.length === 0)
|
|
178
|
+
return { records: [] };
|
|
179
|
+
const result = await sdk.records.bulkCreate({
|
|
180
|
+
baseId,
|
|
181
|
+
tableId,
|
|
182
|
+
records: recordsData.map((r) => transformLinkedRecordFields(transformFieldNamesToIds(r, table, fieldMappings), table)),
|
|
183
|
+
matchOn: transformMatchOnFields(matchOn, fieldMappings),
|
|
184
|
+
});
|
|
185
|
+
const records = result.records
|
|
186
|
+
.map((r) => transformRecordToSdkNames(r, fieldMappings))
|
|
187
|
+
.filter((r) => r !== undefined)
|
|
188
|
+
.map((record) => {
|
|
189
|
+
// Both flat and nested fields, for backwards compatibility.
|
|
190
|
+
const { id, ...fields } = record;
|
|
191
|
+
return { id, ...fields, fields };
|
|
192
|
+
});
|
|
193
|
+
return { success: true, records };
|
|
194
|
+
}
|
|
195
|
+
default:
|
|
196
|
+
throw new Error(`Unknown databases method: ${methodName}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DataNameMappings } from "./types.js";
|
|
2
|
+
import type { HostMode, PlatformSession } from "../types.js";
|
|
3
|
+
/** What generated `zite.*` calls go out under. */
|
|
4
|
+
export declare const DATA_INTEGRATION_ID = "databases";
|
|
5
|
+
type Found = {
|
|
6
|
+
baseId: string | null;
|
|
7
|
+
nameMappings: DataNameMappings | null;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* The build's data context, or undefined from a host that predates the app's own
|
|
11
|
+
* dispatch, whose `databases` platform call still answers.
|
|
12
|
+
*/
|
|
13
|
+
export declare const dataContextOf: (session: PlatformSession) => Promise<Found | undefined>;
|
|
14
|
+
/** One `zite.*` call, translated here and sent to base-runner through the platform. */
|
|
15
|
+
export declare function runDataCall(session: PlatformSession, found: Found, mode: HostMode, methodName: string, sent: unknown): Promise<unknown>;
|
|
16
|
+
/** For tests: forget what this isolate learned. */
|
|
17
|
+
export declare const resetDataDispatch: () => void;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createBaseRunner } from "./baseRunner.js";
|
|
2
|
+
import { asZiteClientError, executeDatabasesCall } from "./dispatch.js";
|
|
3
|
+
/** What generated `zite.*` calls go out under. */
|
|
4
|
+
export const DATA_INTEGRATION_ID = "databases";
|
|
5
|
+
// Schema changes aren't redeployed, so a new table or field reaches calls within this, as on the host.
|
|
6
|
+
const TABLES_REUSE_MS = 60_000;
|
|
7
|
+
// One build per script, so per isolate: what the platform says the build's base is, and that base's tables.
|
|
8
|
+
// Only answers are kept, never a pending call: one request's RPC session mustn't hold up another's.
|
|
9
|
+
let context;
|
|
10
|
+
let tables;
|
|
11
|
+
let unsupported = false;
|
|
12
|
+
const tablesOf = async (baseRunner, baseId) => {
|
|
13
|
+
if (tables?.baseId === baseId && Date.now() - tables.at < TABLES_REUSE_MS)
|
|
14
|
+
return tables.tables;
|
|
15
|
+
const base = await baseRunner.bases.get({ baseId });
|
|
16
|
+
tables = { baseId, tables: base.tables, at: Date.now() };
|
|
17
|
+
return base.tables;
|
|
18
|
+
};
|
|
19
|
+
// What Workers RPC throws for a method the host doesn't have yet.
|
|
20
|
+
const NOT_IMPLEMENTED = /does not implement (?:the )?method/i;
|
|
21
|
+
/**
|
|
22
|
+
* The build's data context, or undefined from a host that predates the app's own
|
|
23
|
+
* dispatch, whose `databases` platform call still answers.
|
|
24
|
+
*/
|
|
25
|
+
export const dataContextOf = async (session) => {
|
|
26
|
+
if (unsupported || !session.dataContext || !session.dataFetch)
|
|
27
|
+
return undefined;
|
|
28
|
+
if (context)
|
|
29
|
+
return context;
|
|
30
|
+
try {
|
|
31
|
+
const answer = await session.dataContext();
|
|
32
|
+
context = { baseId: answer.baseId, nameMappings: answer.nameMappings ?? null };
|
|
33
|
+
return context;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error instanceof Error && NOT_IMPLEMENTED.test(error.message)) {
|
|
37
|
+
unsupported = true;
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* The table id classic's generated client sends: its SDK name mapped through the
|
|
45
|
+
* build's name mappings, which on classic the client does from the run's config.
|
|
46
|
+
*/
|
|
47
|
+
const tableIdOf = (nameMappings, sent) => {
|
|
48
|
+
for (const [id, table] of Object.entries(nameMappings?.tables ?? {})) {
|
|
49
|
+
if (table.mapping?.sdkName === sent)
|
|
50
|
+
return id;
|
|
51
|
+
}
|
|
52
|
+
return sent;
|
|
53
|
+
};
|
|
54
|
+
/** One `zite.*` call, translated here and sent to base-runner through the platform. */
|
|
55
|
+
export async function runDataCall(session, found, mode, methodName, sent) {
|
|
56
|
+
const params = sent && typeof sent === "object" && typeof sent.tableId === "string"
|
|
57
|
+
? { ...sent, tableId: tableIdOf(found.nameMappings, sent.tableId) }
|
|
58
|
+
: sent;
|
|
59
|
+
if (!found.baseId)
|
|
60
|
+
throw asZiteClientError(Object.assign(new Error("This app has no database"), { statusCode: 404 }));
|
|
61
|
+
const baseRunner = createBaseRunner((request) => session.dataFetch(request));
|
|
62
|
+
let baseTables;
|
|
63
|
+
try {
|
|
64
|
+
baseTables = await tablesOf(baseRunner, found.baseId);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
throw asZiteClientError(error);
|
|
68
|
+
}
|
|
69
|
+
return executeDatabasesCall({ baseRunner, baseId: found.baseId, nameMappings: found.nameMappings, tables: baseTables, readOnly: mode === "demo" }, methodName, params);
|
|
70
|
+
}
|
|
71
|
+
/** For tests: forget what this isolate learned. */
|
|
72
|
+
export const resetDataDispatch = () => {
|
|
73
|
+
context = undefined;
|
|
74
|
+
tables = undefined;
|
|
75
|
+
unsupported = false;
|
|
76
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { NameMappings } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Settings shape this helper reads — a subset of zite.lock's `nameMappings`
|
|
4
|
+
* carrying table and field SDK names keyed by tableId / fieldId.
|
|
5
|
+
*/
|
|
6
|
+
export interface SdkNameMappingsSourceSettings {
|
|
7
|
+
tables: Record<string, {
|
|
8
|
+
mapping: {
|
|
9
|
+
sdkName: string;
|
|
10
|
+
};
|
|
11
|
+
fields: Record<string, {
|
|
12
|
+
mapping: {
|
|
13
|
+
sdkName: string;
|
|
14
|
+
};
|
|
15
|
+
}>;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Minimum metadata shape this helper reads — tables, fields, and the
|
|
20
|
+
* `linked_record` edges between them. `template` is optional + loose-typed to
|
|
21
|
+
* accept both BaseMetadata and the runtime's SimpleTableDefinition shape.
|
|
22
|
+
*
|
|
23
|
+
* `deletedAt` accepts `Date` as well as the wire's ISO string so backend
|
|
24
|
+
* callers (whose metadata types carry model-shaped dates) can pass their
|
|
25
|
+
* tables straight through — only its truthiness is read.
|
|
26
|
+
*/
|
|
27
|
+
export interface SdkNameMappingsSourceMetadata {
|
|
28
|
+
tables: ReadonlyArray<{
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
deletedAt?: string | Date | null;
|
|
32
|
+
fields: ReadonlyArray<{
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
type: string;
|
|
36
|
+
deletedAt?: string | Date | null;
|
|
37
|
+
template?: {
|
|
38
|
+
tableId?: string;
|
|
39
|
+
isInverse?: boolean;
|
|
40
|
+
} & Record<string, unknown>;
|
|
41
|
+
}>;
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Build the complete `NameMappings` wire payload for a base in SDK namespace.
|
|
46
|
+
* Applies the link-table conventions (alphabetical PascalCase concat for the
|
|
47
|
+
* name; `<tableCamel>Id` for column aliases) so base-runner does no
|
|
48
|
+
* derivations of its own.
|
|
49
|
+
*
|
|
50
|
+
* Dedupe rules mirror base-runner's `linkTablesByHumanName` keys:
|
|
51
|
+
* - one entry per ordered pair of tables (forward + inverse collapse to one)
|
|
52
|
+
* - forward direction wins for source/target orientation when both observed
|
|
53
|
+
* - multiple `linked_record` fields between the same pair share one entry
|
|
54
|
+
*/
|
|
55
|
+
export declare const buildSdkNameMappingsForBase: ({ metadata, settingsNameMappings, }: {
|
|
56
|
+
metadata: SdkNameMappingsSourceMetadata;
|
|
57
|
+
settingsNameMappings: SdkNameMappingsSourceSettings;
|
|
58
|
+
}) => NameMappings;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// SDK-name `NameMappings` builder for the SQL endpoint. Lives next to the
|
|
2
|
+
// other SDK-name-aware transformations in this package (e.g.
|
|
3
|
+
// `transformRecordToSdkNames`) so cloudflare-lambda can import it without
|
|
4
|
+
// taking a runtime dep on `@fillout/zite`. Pure data transformation — walks
|
|
5
|
+
// BaseMetadata + the zite.lock-style nameMappings and applies the link-table
|
|
6
|
+
// conventions to produce a complete wire payload.
|
|
7
|
+
const getLinkTableSdkName = (a, b) => {
|
|
8
|
+
const [first, second] = [a, b].sort();
|
|
9
|
+
return `${first}${second}`;
|
|
10
|
+
};
|
|
11
|
+
const getLinkColumnSdkAlias = (sdkTableName, side, isSelfReference) => {
|
|
12
|
+
const camel = sdkTableName.charAt(0).toLowerCase() + sdkTableName.slice(1);
|
|
13
|
+
if (isSelfReference) {
|
|
14
|
+
return `${side}${camel.charAt(0).toUpperCase()}${camel.slice(1)}Id`;
|
|
15
|
+
}
|
|
16
|
+
return `${camel}Id`;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Build the complete `NameMappings` wire payload for a base in SDK namespace.
|
|
20
|
+
* Applies the link-table conventions (alphabetical PascalCase concat for the
|
|
21
|
+
* name; `<tableCamel>Id` for column aliases) so base-runner does no
|
|
22
|
+
* derivations of its own.
|
|
23
|
+
*
|
|
24
|
+
* Dedupe rules mirror base-runner's `linkTablesByHumanName` keys:
|
|
25
|
+
* - one entry per ordered pair of tables (forward + inverse collapse to one)
|
|
26
|
+
* - forward direction wins for source/target orientation when both observed
|
|
27
|
+
* - multiple `linked_record` fields between the same pair share one entry
|
|
28
|
+
*/
|
|
29
|
+
export const buildSdkNameMappingsForBase = ({ metadata, settingsNameMappings, }) => {
|
|
30
|
+
const tablesById = new Map();
|
|
31
|
+
for (const t of metadata.tables) {
|
|
32
|
+
if (!t.deletedAt)
|
|
33
|
+
tablesById.set(t.id, t);
|
|
34
|
+
}
|
|
35
|
+
const tables = {};
|
|
36
|
+
for (const table of metadata.tables) {
|
|
37
|
+
if (table.deletedAt)
|
|
38
|
+
continue;
|
|
39
|
+
const tableMapping = settingsNameMappings.tables[table.id];
|
|
40
|
+
if (!tableMapping)
|
|
41
|
+
continue;
|
|
42
|
+
const fields = {};
|
|
43
|
+
for (const field of table.fields) {
|
|
44
|
+
if (field.deletedAt)
|
|
45
|
+
continue;
|
|
46
|
+
const sdkName = tableMapping.fields[field.id]?.mapping.sdkName;
|
|
47
|
+
if (sdkName)
|
|
48
|
+
fields[field.name] = sdkName;
|
|
49
|
+
}
|
|
50
|
+
tables[table.name] = {
|
|
51
|
+
displayName: tableMapping.mapping.sdkName,
|
|
52
|
+
fields,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const linkTables = {};
|
|
56
|
+
for (const source of metadata.tables) {
|
|
57
|
+
if (source.deletedAt)
|
|
58
|
+
continue;
|
|
59
|
+
for (const field of source.fields) {
|
|
60
|
+
if (field.deletedAt)
|
|
61
|
+
continue;
|
|
62
|
+
if (field.type !== 'linked_record')
|
|
63
|
+
continue;
|
|
64
|
+
// Only forward fields drive the entry. Inverse fields are auto-generated
|
|
65
|
+
// mirrors of a forward field on the other side — using them would flip
|
|
66
|
+
// the source/target orientation. Multiple forward fields between the
|
|
67
|
+
// same pair (e.g. Orders.customer + Orders.shippingAddress → Customers)
|
|
68
|
+
// produce identical entries, so the repeated write is idempotent.
|
|
69
|
+
if (field.template?.isInverse === true)
|
|
70
|
+
continue;
|
|
71
|
+
const targetTableId = field.template?.tableId;
|
|
72
|
+
if (!targetTableId)
|
|
73
|
+
continue;
|
|
74
|
+
const target = tablesById.get(targetTableId);
|
|
75
|
+
if (!target)
|
|
76
|
+
continue;
|
|
77
|
+
const sourceSdkName = settingsNameMappings.tables[source.id]?.mapping.sdkName;
|
|
78
|
+
const targetSdkName = settingsNameMappings.tables[target.id]?.mapping.sdkName;
|
|
79
|
+
if (!sourceSdkName || !targetSdkName)
|
|
80
|
+
continue;
|
|
81
|
+
const [humanFirst, humanSecond] = [source.name, target.name].sort();
|
|
82
|
+
const humanLinkName = `${humanFirst}__${humanSecond}`;
|
|
83
|
+
const isSelfReference = source.id === target.id;
|
|
84
|
+
linkTables[humanLinkName] = {
|
|
85
|
+
displayName: getLinkTableSdkName(sourceSdkName, targetSdkName),
|
|
86
|
+
sourceIdColumnAlias: getLinkColumnSdkAlias(sourceSdkName, 'source', isSelfReference),
|
|
87
|
+
targetIdColumnAlias: getLinkColumnSdkAlias(targetSdkName, 'target', isSelfReference),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { tables, linkTables };
|
|
92
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
import type { SimpleTableDefinition, FieldMappings, DatabaseRecord } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Build reverse lookup map: sdkName → fieldId
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildSdkNameToFieldIdMap(fieldMappings: FieldMappings | undefined): Record<string, string>;
|
|
15
|
+
/**
|
|
16
|
+
* Transform SDK field names to database field IDs in filters
|
|
17
|
+
*/
|
|
18
|
+
export declare function transformFiltersToFieldIds(filters: Record<string, unknown>, fieldMappings: FieldMappings | undefined): Record<string, unknown>;
|
|
19
|
+
/**
|
|
20
|
+
* Transform SDK field names to database field IDs inside a formula expression.
|
|
21
|
+
*
|
|
22
|
+
* The base-runner formula evaluator only accepts display names or field IDs
|
|
23
|
+
* in `{fieldRef}` placeholders — never SDK names. The LLM, however, sees the
|
|
24
|
+
* codebase through the SDK (camelCase fields like `count`), so it naturally
|
|
25
|
+
* writes `{count}` in expressions. This rewrite normalizes those references
|
|
26
|
+
* to field IDs at the SDK boundary, mirroring how `transformFiltersToFieldIds`
|
|
27
|
+
* normalizes filter keys.
|
|
28
|
+
*
|
|
29
|
+
* Behavior:
|
|
30
|
+
* - `{sdkName}` → `{fieldId}` when sdkName matches a field in `fieldMappings`
|
|
31
|
+
* - `{fieldId}` → unchanged (already a valid id)
|
|
32
|
+
* - `{anything else}` → unchanged (let the formula validator reject with a
|
|
33
|
+
* clear, table-specific error message)
|
|
34
|
+
* - `{linkField.targetField}` (cross-table) → unchanged (target field lives
|
|
35
|
+
* in another table; resolving it requires a separate per-link mapping
|
|
36
|
+
* that the SDK boundary doesn't have)
|
|
37
|
+
*/
|
|
38
|
+
export declare function transformFormulaExpressionToFieldIds(expression: string, fieldMappings: FieldMappings | undefined): string;
|
|
39
|
+
/**
|
|
40
|
+
* Transform record data to use field IDs instead of field names.
|
|
41
|
+
*
|
|
42
|
+
* LLMs often use human-readable field names, but the database expects field IDs.
|
|
43
|
+
* Uses a three-step fallback:
|
|
44
|
+
* 1. Check if key is already a valid field ID
|
|
45
|
+
* 2. Match by field name (case-insensitive)
|
|
46
|
+
* 3. Match by SDK name from field mappings
|
|
47
|
+
*
|
|
48
|
+
* @param recordData - The record data with potentially mixed keys
|
|
49
|
+
* @param table - Table definition for field name lookups (optional)
|
|
50
|
+
* @param fieldMappings - SDK name mappings (optional)
|
|
51
|
+
*/
|
|
52
|
+
export declare function transformFieldNamesToIds(recordData: Record<string, unknown>, table: SimpleTableDefinition | undefined, fieldMappings: FieldMappings | undefined): Record<string, unknown>;
|
|
53
|
+
/**
|
|
54
|
+
* Simple transformation using only field mappings (no table definition).
|
|
55
|
+
* Used when table metadata is not available.
|
|
56
|
+
*/
|
|
57
|
+
export declare function transformRecordToFieldIds(recordData: Record<string, unknown>, fieldMappings: FieldMappings | undefined): Record<string, unknown>;
|
|
58
|
+
/**
|
|
59
|
+
* Transform linked record fields to ensure they are arrays.
|
|
60
|
+
*
|
|
61
|
+
* Handles common LLM mistakes:
|
|
62
|
+
* - Passing a string instead of an array of record IDs
|
|
63
|
+
* - Passing null/undefined instead of an empty array
|
|
64
|
+
* - Passing literal strings like "null" or "undefined"
|
|
65
|
+
*
|
|
66
|
+
* @param recordData - Record data (should already have field IDs as keys)
|
|
67
|
+
* @param table - Table definition for field type lookups
|
|
68
|
+
*/
|
|
69
|
+
export declare function transformLinkedRecordFields(recordData: Record<string, unknown>, table: SimpleTableDefinition | undefined): Record<string, unknown>;
|
|
70
|
+
/**
|
|
71
|
+
* Transform database record to use SDK field names
|
|
72
|
+
*
|
|
73
|
+
* @param record - Raw database record with field IDs
|
|
74
|
+
* @param fieldMappings - SDK name mappings
|
|
75
|
+
*/
|
|
76
|
+
export declare function transformRecordToSdkNames(record: {
|
|
77
|
+
id: string;
|
|
78
|
+
data: Record<string, unknown>;
|
|
79
|
+
} | undefined, fieldMappings: FieldMappings | undefined): DatabaseRecord | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* Transform multiple records for input (SDK → Database)
|
|
82
|
+
*/
|
|
83
|
+
export declare function transformRecordsForInput(records: Record<string, unknown>[], table: SimpleTableDefinition | undefined, fieldMappings: FieldMappings | undefined): Record<string, unknown>[];
|
|
84
|
+
/**
|
|
85
|
+
* Transform multiple records for output (Database → SDK)
|
|
86
|
+
*/
|
|
87
|
+
export declare function transformRecordsForOutput(records: Array<{
|
|
88
|
+
id: string;
|
|
89
|
+
data: Record<string, unknown>;
|
|
90
|
+
}>, fieldMappings: FieldMappings | undefined): DatabaseRecord[];
|
|
91
|
+
/**
|
|
92
|
+
* Transform an array of SDK field names to field IDs.
|
|
93
|
+
*
|
|
94
|
+
* Used for the `fields` parameter in findOne/findAll to select which fields to return.
|
|
95
|
+
* Uses the same three-step fallback as transformFieldNamesToIds:
|
|
96
|
+
* 1. Check if name is already a valid field ID
|
|
97
|
+
* 2. Match by field name (case-insensitive)
|
|
98
|
+
* 3. Match by SDK name from field mappings
|
|
99
|
+
*
|
|
100
|
+
* @param fields - Array of SDK field names (e.g. ['customerName', 'email'])
|
|
101
|
+
* @param table - Table definition for field name lookups (optional)
|
|
102
|
+
* @param fieldMappings - SDK name mappings (optional)
|
|
103
|
+
* @returns Array of field IDs, or undefined if input is undefined
|
|
104
|
+
*/
|
|
105
|
+
export declare function transformFieldsToFieldIds(fields: string[] | undefined, table: SimpleTableDefinition | undefined, fieldMappings: FieldMappings | undefined): string[] | undefined;
|
|
106
|
+
/**
|
|
107
|
+
* Resolve the field references in a sort spec to field IDs.
|
|
108
|
+
*
|
|
109
|
+
* Sort arrives from app code using SDK names (`closeDate`), but base-runner
|
|
110
|
+
* resolves sort fields by field ID or display name only, and silently skips one
|
|
111
|
+
* it cannot match. Running the references through the same resolution order as
|
|
112
|
+
* `fields` is what makes a sort on a multi-word field ("Close date" ->
|
|
113
|
+
* `closeDate`) actually apply instead of being dropped without an error.
|
|
114
|
+
*
|
|
115
|
+
* System sort fields (`id`, `createdAt`, `updatedAt`) have no mapping and fall
|
|
116
|
+
* through unchanged, which is what base-runner expects.
|
|
117
|
+
*/
|
|
118
|
+
export declare function transformSortToFieldIds(sort: string | Array<{
|
|
119
|
+
field: string;
|
|
120
|
+
direction?: 'asc' | 'desc';
|
|
121
|
+
}> | undefined, table: SimpleTableDefinition | undefined, fieldMappings: FieldMappings | undefined): Array<{
|
|
122
|
+
field: string;
|
|
123
|
+
direction: 'asc' | 'desc';
|
|
124
|
+
}> | undefined;
|
|
125
|
+
/**
|
|
126
|
+
* Transform matchOn field names to field IDs
|
|
127
|
+
*/
|
|
128
|
+
export declare function transformMatchOnFields(matchOn: string[] | undefined, fieldMappings: FieldMappings | undefined): string[] | undefined;
|