Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (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 / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

208 lines
7.7 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,
PredicateNode,
RowData,
Sort,
SortSpec,
TablePredicate,
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
}
/**
* Translates a v2 predicate's leaf `field` names → column ids (recursing through
* `all`/`any` groups). Fields with no matching column pass through unchanged.
* The v2 analogue of {@link filterNamesToIds}.
*/
export function predicateNamesToIds(
predicate: TablePredicate,
idByName: ReadonlyMap<string, string>
): TablePredicate {
const remap = (node: PredicateNode): PredicateNode => {
// Group-first, matching isPredicateGroup/validateNode/buildPredicateNode.
if ('all' in node) return { all: node.all.map(remap) }
if ('any' in node) return { any: node.any.map(remap) }
return { ...node, field: idByName.get(node.field) ?? node.field }
}
return remap(predicate) as TablePredicate
}
/** Translates a v2 sort spec's field names → column ids. Unknown fields pass through. */
export function sortSpecNamesToIds(
sort: SortSpec,
idByName: ReadonlyMap<string, string>
): SortSpec {
return sort.map((s) => ({ field: idByName.get(s.field) ?? s.field, direction: s.direction }))
}
// The outbound direction (stored id-keyed row → name-keyed) deliberately does
// NOT live here: a `select` cell's value also needs translating, and keeping the
// key half separately callable is what let boundaries translate the keys and
// forget the values. Use `namedRowMapper` from `@/lib/table/cell-format`, which
// does both in one pass.