Files
simstudioai--sim/apps/sim/lib/table/column-keys.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

188 lines
6.9 KiB
TypeScript

/**
* Column id ↔ name translation helpers.
*
* Stored row data (`user_table_rows.data`), table metadata, workflow-group
* refs, and filter/sort all key on a column's stable **id**. `name` is a
* display label that changes on rename. The two name-translating boundaries
* (public v1 API, mothership tool) and CSV convert between the two with the
* map builders here.
*/
import { generateId } from '@sim/utils/id'
import type {
ColumnDefinition,
Filter,
RowData,
Sort,
TableSchema,
WorkflowGroup,
} from '@/lib/table/types'
/**
* Resolves a column's stable storage key. Falls back to `name` for legacy
* columns that predate the id backfill — those rows were written keyed by name,
* which is exactly the key the column still uses, so the fallback is correct.
*/
export function getColumnId(col: Pick<ColumnDefinition, 'id' | 'name'>): string {
return col.id ?? col.name
}
/**
* Mints a fresh column id. Generated ids are opaque (`col_<uuid>`) and
* deliberately distinct from display names so renames never disturb them. The
* `col_` prefix is required: the id is validated against `NAME_PATTERN` (it's a
* JSONB key and a filter/sort field) which must start with a letter/underscore,
* and a bare UUID can start with a digit. Dashes are stripped for the same
* reason. A v4 UUID's 122 random bits make a collision within a table's columns
* effectively impossible, so no uniqueness check is needed.
*/
export function generateColumnId(): string {
return `col_${generateId().replace(/-/g, '')}`
}
/**
* Matches a column against a reference that may be a stable id (first-party
* callers) or a display name (legacy / mothership / public API). Id match is
* exact; name match is case-insensitive (names are unique case-insensitively per
* schema validation). The single predicate behind every column-op resolver — use
* with `.find` / `.findIndex` so id-or-name resolution can't drift between sites.
*/
export function columnMatchesRef(col: ColumnDefinition, ref: string): boolean {
return getColumnId(col) === ref || col.name.toLowerCase() === ref.toLowerCase()
}
/**
* Returns a schema copy with a generated id stamped onto every column that
* lacks one, remapping any workflow-group refs that still hold a column **name**
* to the assigned id. Used at creation time (`createTable`) so a freshly created
* table is fully id-keyed from its first row write. Idempotent for columns that
* already carry an id.
*/
export function withGeneratedColumnIds(schema: TableSchema): TableSchema {
const idByName = new Map<string, string>()
const columns = schema.columns.map((col) => {
if (col.id) {
idByName.set(col.name, col.id)
return col
}
const id = generateColumnId()
idByName.set(col.name, id)
return { ...col, id }
})
const remap = (ref: string) => idByName.get(ref) ?? ref
const workflowGroups = schema.workflowGroups?.map((group) => ({
...group,
outputs: group.outputs.map((o) => ({ ...o, columnName: remap(o.columnName) })),
...(group.dependencies?.columns
? { dependencies: { columns: group.dependencies.columns.map(remap) } }
: {}),
...(group.inputMappings
? {
inputMappings: group.inputMappings.map((m) => ({
...m,
columnName: remap(m.columnName),
})),
}
: {}),
}))
return { ...schema, columns, ...(workflowGroups ? { workflowGroups } : {}) }
}
/**
* Rewrites a workflow group's column references (output `columnName`,
* `dependencies.columns`, `inputMapping.columnName`) from display name to stable
* id using `idByName`. A ref that is already an id (not a known column name) is
* left as-is, so this is safe whether the caller authored refs by name
* (mothership) or by id (first-party UI).
*/
export function remapGroupColumnRefs(
group: WorkflowGroup,
idByName: ReadonlyMap<string, string>
): WorkflowGroup {
const remap = (ref: string) => idByName.get(ref) ?? ref
return {
...group,
outputs: group.outputs.map((o) => ({ ...o, columnName: remap(o.columnName) })),
...(group.dependencies?.columns
? { dependencies: { columns: group.dependencies.columns.map(remap) } }
: {}),
...(group.inputMappings
? {
inputMappings: group.inputMappings.map((m) => ({
...m,
columnName: remap(m.columnName),
})),
}
: {}),
}
}
/** `name → id` for translating inbound wire data (v1 / mothership / CSV import). */
export function buildIdByName(schema: TableSchema): Map<string, string> {
const map = new Map<string, string>()
for (const col of schema.columns) map.set(col.name, getColumnId(col))
return map
}
/** `id → name` for translating outbound wire data (v1 / mothership / CSV export). */
export function buildNameById(schema: TableSchema): Map<string, string> {
const map = new Map<string, string>()
for (const col of schema.columns) map.set(getColumnId(col), col.name)
return map
}
/**
* Remaps a wire row keyed by column **name** to the stored **id** keying. Used
* at the name-translating boundaries on the way in. Keys not matching a known
* column are dropped (validation has already run against the schema).
*/
export function rowDataNameToId(data: RowData, idByName: Map<string, string>): RowData {
const out: RowData = {}
for (const [name, value] of Object.entries(data)) {
const id = idByName.get(name)
if (id !== undefined) out[id] = value
}
return out
}
/**
* Translates a filter's field names → column ids (recursing into `$or`/`$and`).
* Fields with no matching column (e.g. `createdAt`) pass through unchanged. Used
* at the name-translating boundaries before handing a filter to the query layer.
*/
export function filterNamesToIds(filter: Filter, idByName: ReadonlyMap<string, string>): Filter {
const out: Filter = {}
for (const [key, value] of Object.entries(filter)) {
if ((key === '$or' || key === '$and') && Array.isArray(value)) {
out[key] = (value as Filter[]).map((f) => filterNamesToIds(f, idByName))
} else {
out[idByName.get(key) ?? key] = value
}
}
return out
}
/** Translates a sort's field names → column ids. Unknown fields pass through. */
export function sortNamesToIds(sort: Sort, idByName: ReadonlyMap<string, string>): Sort {
const out: Sort = {}
for (const [field, dir] of Object.entries(sort)) out[idByName.get(field) ?? field] = dir
return out
}
/**
* Remaps a stored row keyed by column **id** back to **name** keying for the
* wire. Used at the name-translating boundaries on the way out. Ids with no
* current column (e.g. a column deleted by a not-yet-finished background strip)
* are dropped, so orphaned keys never surface.
*/
export function rowDataIdToName(data: RowData, nameById: Map<string, string>): RowData {
const out: RowData = {}
for (const [id, value] of Object.entries(data)) {
const name = nameById.get(id)
if (name !== undefined) out[name] = value
}
return out
}