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
366 lines
8.1 KiB
TypeScript
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}`
|
|
}
|