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
438 lines
11 KiB
TypeScript
438 lines
11 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: 1000,
|
|
MAX_TABLE_NAME_LENGTH: 128,
|
|
MAX_COLUMN_NAME_LENGTH: 50,
|
|
MAX_DESCRIPTION_LENGTH: 500,
|
|
DEFAULT_QUERY_LIMIT: 100,
|
|
MAX_QUERY_LIMIT: 1000,
|
|
/**
|
|
* Byte budget for one query response. `queryRows` drains rows in bounded
|
|
* batches; an UNBOUNDED query (no limit) that outgrows the budget fails fast
|
|
* (the whole result was promised — a partial page would be silent truncation),
|
|
* while a BOUNDED page cuts early and returns the partial list with
|
|
* `nextCursor` set. At least one row is always returned on a bounded page.
|
|
* Measured against serialized row `data` only, not the full HTTP envelope.
|
|
*/
|
|
MAX_QUERY_RESULT_BYTES: 5 * 1024 * 1024, // 5MB
|
|
/** Hard row cap per internal fetch batch inside queryRows' bounded drain loop. */
|
|
QUERY_BATCH_MAX_ROWS: 10000,
|
|
/** 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`). Billing-disabled deployments are unlimited
|
|
* unless the free-tier env vars are explicitly set (see
|
|
* `getBillingDisabledTableLimits`).
|
|
*/
|
|
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 at which a **bounded** page (one with an explicit `limit`) is cut
|
|
* short, or `null` when disabled — the default. Opt in with `TABLE_MAX_PAGE_BYTES`.
|
|
*
|
|
* Off by default because a short page is only safe for a client that terminates
|
|
* on `nextCursor === null`; a pre-existing v1 pager terminating on
|
|
* `rows.length < limit` would read the cut as end-of-data and silently truncate.
|
|
* Unbounded queries (no `limit`) are unaffected — they always fail fast at
|
|
* `TABLE_LIMITS.MAX_QUERY_RESULT_BYTES` rather than return a partial result.
|
|
*/
|
|
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
|
|
}
|
|
|
|
/**
|
|
* Table limits for billing-disabled deployments: unlimited by default, with
|
|
* each cap opting back in only when its free-tier env var is explicitly set
|
|
* to a valid positive integer.
|
|
*/
|
|
export function getBillingDisabledTableLimits(): TablePlanLimits {
|
|
const tablesOverride = envNumber(env.FREE_TABLES_LIMIT, 0, { min: 1, integer: true })
|
|
const rowsOverride = envNumber(env.FREE_TABLE_ROWS_LIMIT, 0, { min: 1, integer: true })
|
|
return {
|
|
maxTables: tablesOverride > 0 ? tablesOverride : Number.MAX_SAFE_INTEGER,
|
|
maxRowsPerTable: rowsOverride > 0 ? rowsOverride : Number.MAX_SAFE_INTEGER,
|
|
}
|
|
}
|
|
|
|
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
|
|
),
|
|
},
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Re-exported from the column-type module, which is the single source of truth.
|
|
* Kept here because this is where callers already import it from — restating
|
|
* the list would let the two drift, which is the class of bug the registry
|
|
* exists to remove.
|
|
*
|
|
* Points at `types` rather than `registry` on purpose: this module is re-
|
|
* exported by the `@/lib/table` barrel that 44 server modules import, and the
|
|
* registry pulls in `@sim/emcn/icons`.
|
|
*/
|
|
export { COLUMN_TYPES } from '@/lib/table/column-types/types'
|
|
|
|
/** Maximum number of options a `select`/`multiselect` column may declare. */
|
|
export const MAX_SELECT_OPTIONS = 100
|
|
|
|
/**
|
|
* The v2 filter operators, as a runtime tuple so both the `FilterOp` type and
|
|
* the boundary Zod enum derive from one source. Order is not significant.
|
|
*/
|
|
export const FILTER_OPS = [
|
|
'eq',
|
|
'ne',
|
|
'gt',
|
|
'gte',
|
|
'lt',
|
|
'lte',
|
|
'in',
|
|
'nin',
|
|
'contains',
|
|
'ncontains',
|
|
'startsWith',
|
|
'endsWith',
|
|
'like',
|
|
'ilike',
|
|
'nlike',
|
|
'nilike',
|
|
'isEmpty',
|
|
'isNotEmpty',
|
|
'isNull',
|
|
'isNotNull',
|
|
] as const
|
|
|
|
export const SORT_DIRECTIONS = ['asc', 'desc'] 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}`
|
|
}
|