Files
simstudioai--sim/apps/sim/lib/table/constants.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

366 lines
8.1 KiB
TypeScript

/**
* Limits and constants for user-defined tables.
*/
import { randomInt, randomItem } from '@sim/utils/random'
import { env, envNumber } from '@/lib/core/config/env'
export const TABLE_LIMITS = {
MAX_TABLES_PER_WORKSPACE: 100,
MAX_ROWS_PER_TABLE: 10000,
MAX_ROW_SIZE_BYTES: 400 * 1024, // 400KB
MAX_COLUMNS_PER_TABLE: 50,
MAX_TABLE_NAME_LENGTH: 128,
MAX_COLUMN_NAME_LENGTH: 50,
MAX_DESCRIPTION_LENGTH: 500,
DEFAULT_QUERY_LIMIT: 100,
MAX_QUERY_LIMIT: 1000,
/** Batch size for bulk update operations */
UPDATE_BATCH_SIZE: 100,
/** Batch size for bulk delete operations */
DELETE_BATCH_SIZE: 1000,
/** Maximum rows per batch insert */
MAX_BATCH_INSERT_SIZE: 1000,
/** Maximum rows per bulk update/delete operation */
MAX_BULK_OPERATION_SIZE: 1000,
/** Maximum rows a single clipboard copy/cut serializes; beyond this the user is steered to Export. */
MAX_COPY_ROWS: 50000,
/** Rows selected + deleted per page in the async background delete-job loop. Each
* DELETE_BATCH_SIZE chunk inside the page commits in its own transaction; the page is the
* keyset-select and cancel/ownership-check granularity. */
DELETE_PAGE_SIZE: 10000,
/** Row count above which an export runs as a background job instead of a synchronous stream.
* Tables at or under this stream instantly; larger ones fall back to an async export job. */
EXPORT_ASYNC_THRESHOLD_ROWS: 10000,
/** Cap on the exclusion set ("select all, minus these") sent to an async delete job. */
MAX_EXCLUDE_ROW_IDS: 10000,
} as const
/**
* Default plan-based table limits. Each value can be overridden via env vars
* (see `getTablePlanLimits`) so self-hosted deployments can raise the free-tier
* caps that apply when billing is disabled.
*/
export const DEFAULT_TABLE_PLAN_LIMITS = {
free: {
maxTables: 5,
maxRowsPerTable: 50000,
},
pro: {
maxTables: 100,
maxRowsPerTable: 100000,
},
team: {
maxTables: 1000,
maxRowsPerTable: 500000,
},
enterprise: {
maxTables: 10000,
maxRowsPerTable: 1000000,
},
} as const
/**
* Byte budget for one page of row reads, or null when disabled (the default).
* Dev-preview of the byte-bounded pagination follow-up: set `TABLE_MAX_PAGE_BYTES`
* to cut pages early once their serialized row data exceeds the budget. The
* production version moves the cut into SQL — see the pagination-hardening plan.
*/
export function getMaxPageBytes(): number | null {
const value = envNumber(env.TABLE_MAX_PAGE_BYTES, 0, { min: 0, integer: true })
return value > 0 ? value : null
}
/**
* Maximum serialized size in bytes of a single row. Defaults to
* `TABLE_LIMITS.MAX_ROW_SIZE_BYTES`; overridable via the
* `TABLE_MAX_ROW_SIZE_BYTES` env var (server-only, read at call time).
*/
export function getMaxRowSizeBytes(): number {
return envNumber(env.TABLE_MAX_ROW_SIZE_BYTES, TABLE_LIMITS.MAX_ROW_SIZE_BYTES, {
min: 1,
integer: true,
})
}
export type PlanName = keyof typeof DEFAULT_TABLE_PLAN_LIMITS
export interface TablePlanLimits {
maxTables: number
maxRowsPerTable: number
}
export type TablePlanLimitsByPlan = Record<PlanName, TablePlanLimits>
/**
* Returns plan-based table limits, applying env var overrides on top of the
* defaults. When no override is set the value falls back to the hosted-default
* constant so behavior is unchanged for the hosted product.
*/
export function getTablePlanLimits(): TablePlanLimitsByPlan {
return {
free: {
maxTables: envNumber(env.FREE_TABLES_LIMIT, DEFAULT_TABLE_PLAN_LIMITS.free.maxTables),
maxRowsPerTable: envNumber(
env.FREE_TABLE_ROWS_LIMIT,
DEFAULT_TABLE_PLAN_LIMITS.free.maxRowsPerTable
),
},
pro: {
maxTables: envNumber(env.PRO_TABLES_LIMIT, DEFAULT_TABLE_PLAN_LIMITS.pro.maxTables),
maxRowsPerTable: envNumber(
env.PRO_TABLE_ROWS_LIMIT,
DEFAULT_TABLE_PLAN_LIMITS.pro.maxRowsPerTable
),
},
team: {
maxTables: envNumber(env.TEAM_TABLES_LIMIT, DEFAULT_TABLE_PLAN_LIMITS.team.maxTables),
maxRowsPerTable: envNumber(
env.TEAM_TABLE_ROWS_LIMIT,
DEFAULT_TABLE_PLAN_LIMITS.team.maxRowsPerTable
),
},
enterprise: {
maxTables: envNumber(
env.ENTERPRISE_TABLES_LIMIT,
DEFAULT_TABLE_PLAN_LIMITS.enterprise.maxTables
),
maxRowsPerTable: envNumber(
env.ENTERPRISE_TABLE_ROWS_LIMIT,
DEFAULT_TABLE_PLAN_LIMITS.enterprise.maxRowsPerTable
),
},
}
}
export const COLUMN_TYPES = ['string', 'number', 'boolean', 'date', 'json'] as const
export const NAME_PATTERN = /^[a-z_][a-z0-9_]*$/i
export const USER_TABLE_ROWS_SQL_NAME = 'user_table_rows'
/**
* CSV/TSV uploads at or above this size import in the background (direct-to-storage
* upload + async worker) instead of being POSTed through the server. Kept safely under
* the Next.js proxy request-body cap (10MB) so a synchronous upload is never truncated.
*/
export const CSV_ASYNC_IMPORT_THRESHOLD_BYTES = 8 * 1024 * 1024
const TABLE_NAME_ADJECTIVES = [
'Radiant',
'Luminous',
'Blazing',
'Glowing',
'Bright',
'Gleaming',
'Shining',
'Lustrous',
'Vivid',
'Dazzling',
'Stellar',
'Cosmic',
'Astral',
'Galactic',
'Nebular',
'Orbital',
'Lunar',
'Solar',
'Starlit',
'Celestial',
'Infinite',
'Vast',
'Boundless',
'Immense',
'Colossal',
'Titanic',
'Grand',
'Supreme',
'Eternal',
'Ancient',
'Timeless',
'Primal',
'Nascent',
'Elder',
'Swift',
'Drifting',
'Surging',
'Pulsing',
'Soaring',
'Rising',
'Spiraling',
'Crimson',
'Azure',
'Violet',
'Indigo',
'Amber',
'Sapphire',
'Obsidian',
'Silver',
'Golden',
'Scarlet',
'Cobalt',
'Emerald',
'Magnetic',
'Quantum',
'Photonic',
'Spectral',
'Charged',
'Atomic',
'Electric',
'Kinetic',
'Ethereal',
'Mystic',
'Phantom',
'Silent',
'Distant',
'Hidden',
'Arcane',
'Frozen',
'Burning',
'Molten',
'Volatile',
'Fiery',
'Searing',
'Frigid',
'Mighty',
'Fierce',
'Serene',
'Tranquil',
'Harmonic',
'Resonant',
'Bold',
'Noble',
'Pure',
'Rare',
'Pristine',
'Exotic',
'Divine',
] as const
const TABLE_NAME_NOUNS = [
'Star',
'Pulsar',
'Quasar',
'Magnetar',
'Nova',
'Supernova',
'Neutron',
'Protostar',
'Blazar',
'Cepheid',
'Galaxy',
'Nebula',
'Cluster',
'Void',
'Filament',
'Halo',
'Spiral',
'Remnant',
'Cloud',
'Planet',
'Moon',
'World',
'Exoplanet',
'Titan',
'Europa',
'Triton',
'Enceladus',
'Comet',
'Meteor',
'Asteroid',
'Fireball',
'Shard',
'Fragment',
'Orion',
'Andromeda',
'Perseus',
'Pegasus',
'Phoenix',
'Draco',
'Cygnus',
'Aquila',
'Lyra',
'Vega',
'Hydra',
'Sirius',
'Polaris',
'Altair',
'Eclipse',
'Aurora',
'Corona',
'Flare',
'Vortex',
'Pulse',
'Wave',
'Ripple',
'Shimmer',
'Spark',
'Horizon',
'Zenith',
'Apex',
'Meridian',
'Equinox',
'Solstice',
'Transit',
'Orbit',
'Cosmos',
'Dimension',
'Realm',
'Expanse',
'Infinity',
'Continuum',
'Abyss',
'Ether',
'Photon',
'Neutrino',
'Tachyon',
'Graviton',
'Sector',
'Quadrant',
'Belt',
'Ring',
'Field',
'Stream',
'Frontier',
'Beacon',
'Signal',
'Probe',
'Voyager',
'Pioneer',
'Sentinel',
'Gateway',
'Portal',
'Nexus',
'Conduit',
'Rift',
'Core',
'Matrix',
'Lattice',
'Array',
'Reactor',
'Engine',
'Forge',
'Crucible',
] as const
/**
* Generates a unique space-themed table name that doesn't collide with existing names.
* Uses lowercase with underscores to satisfy NAME_PATTERN validation.
*/
export function generateUniqueTableName(existingNames: string[]): string {
const taken = new Set(existingNames.map((n) => n.toLowerCase()))
const maxAttempts = 50
for (let i = 0; i < maxAttempts; i++) {
const adj = randomItem(TABLE_NAME_ADJECTIVES)
const noun = randomItem(TABLE_NAME_NOUNS)
const name = `${adj.toLowerCase()}_${noun.toLowerCase()}`
if (!taken.has(name)) return name
}
const adj = randomItem(TABLE_NAME_ADJECTIVES)
const noun = randomItem(TABLE_NAME_NOUNS)
const suffix = randomInt(100, 1000)
return `${adj.toLowerCase()}_${noun.toLowerCase()}_${suffix}`
}