/* eslint-disable @typescript-eslint/no-explicit-any */ import { Optional } from '@speckle/shared' import knex from '@/db/knex' import { BaseMetaRecord } from '@/modules/core/helpers/meta' import { Knex } from 'knex' import { reduce } from 'lodash-es' type BaseInnerSchemaConfig = { /** * Table name */ name: T /** * Get `knex(tableName)` QueryBuilder instance. Use the generic argument to type the results of the query. */ knex: (db?: Knex) => Knex.QueryBuilder /** * Get names of table columns. The names can be prefixed with the table name or not, depending * on whether `withoutTablePrefix` was set when accessing the helper. */ col: { [colName in C]: string } /** * Build a "col AS alias" definition that can be used in .select() calls and .where() clauses */ colAs(colName: C, alias: A): Knex.Raw /** * Use in .select() calls when selecting joined tables to ensure all table's rows get collected into a single * array and held in a key identified by name. * * Make sure the rows of this table are grouped, otherwise this aggregation won't work */ groupArray(name: string): Knex.Raw /** * All of the column names in an array */ cols: string[] } type BaseSchemaConfig> = BC & { /** * Return schema helper with custom configuration options */ with: (params?: SchemaConfigParams) => BC /** * Helper with withoutTablePrefix set to true */ withoutTablePrefix: BC } type InnerSchemaConfig< T extends string, C extends string, M extends Optional> > = BaseInnerSchemaConfig & { /** * Associated meta table helper, if any */ meta: M } export type SchemaConfig< T extends string, C extends string, M extends Optional> > = BaseSchemaConfig> type MetaInnerSchemaConfig< T extends string, C extends string, MK extends string > = BaseInnerSchemaConfig & { /** * Get meta keys individually */ metaKey: { [keyName in MK]: keyName } /** * Get all available meta keys */ metaKeys: string[] /** * Column in the meta table that identifies an entity from the associated parent table. * E.g. In the users_meta table this column is 'user_id' - it identifies the user for which the meta value is stored */ parentIdentityCol: string } export type MetaSchemaConfig< T extends string, C extends string, MK extends string > = BaseSchemaConfig> type SchemaConfigParams = { /** * Configure `col` properties to not have the table name prefixed. For the most part you want the prefix, * cause this helps in queries with JOINS (when multiple tables have a col with the same name), but you don't * want the prefix when triggering UPDATE queries, because the `SET = ` syntax doesn't support * column names with table prefixes. */ withoutTablePrefix?: boolean /** * Configure a custom table prefix that will be attached to column names. This will be relevant when you're * building subqueries or joining a table onto itself. */ withCustomTablePrefix?: string /** * Will surround with quotes for putting directly in knex.raw() queries */ quoted?: boolean } const createBaseInnerSchemaConfigBuilder = (tableName: T, columns: C[]) => (params: SchemaConfigParams = {}): BaseInnerSchemaConfig => { const quoted = params.quoted || false const aliasedTableName = params.withCustomTablePrefix ? `${tableName} as ${params.withCustomTablePrefix}` : tableName const prefix = params.withoutTablePrefix ? null : params.withCustomTablePrefix || tableName const colName = (col: string, options?: Partial<{ addQuotes: boolean }>) => { const { addQuotes } = options || {} return addQuotes ? (prefix?.length ? `"${prefix}".` : '') + `"${col}"` : (prefix?.length ? `${prefix}.` : '') + `${col}` } return { name: aliasedTableName as T, knex: (db?: Knex) => (db || knex)(aliasedTableName), col: reduce( columns, (prev, curr) => { prev[curr] = colName(curr, { addQuotes: quoted }) return prev }, {} as Record ), colAs: (col, alias) => knex.raw(`${colName(col, { addQuotes: true })} AS "${alias}"`), groupArray: (name) => knex.raw( `array_agg(row_to_json(${ (prefix?.length ? prefix + '.' : '') + '*' })) as "${name}"` ), cols: columns.map((c) => colName(c, { addQuotes: quoted })) } } /** * Create table schema helper * @param tableName * @param columns */ export function buildTableHelper< T extends string, C extends string, M extends Optional> >(tableName: T, columns: C[], metaTable?: M): SchemaConfig { const buildBaseConfig = createBaseInnerSchemaConfigBuilder(tableName, columns) const buildInnerConfig = ( params: SchemaConfigParams = {} ): InnerSchemaConfig => ({ ...buildBaseConfig(params), meta: metaTable as M }) return { ...buildInnerConfig(), with: buildInnerConfig, withoutTablePrefix: buildInnerConfig({ withoutTablePrefix: true }) } } /** * Create meta table schema helper */ export function buildMetaTableHelper< T extends string, C extends string, MK extends string >( tableName: T, extraColumns: C[], metaKeys: MK[], parentIdentityCol: C ): MetaSchemaConfig { const baseColumns: Array = [ 'key', 'value', 'createdAt', 'updatedAt' ] const buildBaseConfig = createBaseInnerSchemaConfigBuilder(tableName, [ ...extraColumns, ...baseColumns ]) const buildInnerMetaConfig = ( params: SchemaConfigParams = {} ): MetaInnerSchemaConfig => ({ ...buildBaseConfig(params), metaKeys, metaKey: reduce( [...metaKeys, ...baseColumns], (prev, curr) => { prev[curr] = curr return prev }, {} as Record ) as { [keyName in MK]: keyName }, parentIdentityCol }) return { ...buildInnerMetaConfig(), with: buildInnerMetaConfig, withoutTablePrefix: buildInnerMetaConfig({ withoutTablePrefix: true }) } } /* * TABLE HELPERS * The generated helpers are used like this: * * Streams.name - TableName * Streams.col.id - Get column names * Streams.knex() - Get knex() instance for this specific table * * Streams.with({...}) - configure helper, e.g. disable table name being prefixed to col names: * Streams.with({withoutTablePrefix: true}).col.id * * Streams.withoutTablePrefix.col.id - Shorthand for accessing columns without the table prefix * * META TABLE HELPERS * Largely the same, but also hold extra props like `metaKeys` that store allowed meta keys */ export const StreamsMeta = buildMetaTableHelper( 'streams_meta', ['streamId', 'key', 'value', 'createdAt', 'updatedAt'], ['onboardingBaseStream'], 'streamId' ) export const Streams = buildTableHelper( 'streams', [ 'id', 'name', 'description', 'clonedFrom', 'createdAt', 'updatedAt', 'allowPublicComments', 'workspaceId', 'regionKey', 'visibility' ], StreamsMeta ) export const StreamAcl = buildTableHelper('stream_acl', [ 'userId', 'resourceId', 'role' ]) export const StreamFavorites = buildTableHelper('stream_favorites', [ 'streamId', 'userId', 'createdAt', 'cursor' ]) export const UsersMeta = buildMetaTableHelper( 'users_meta', ['userId', 'key', 'value', 'createdAt', 'updatedAt'], [ 'isOnboardingFinished', 'onboardingStreamId', 'activeWorkspace', 'isProjectsActive', 'newWorkspaceExplainerDismissed', 'speckleConBannerDismissed', 'legacyProjectsExplainerCollapsed', // Used in tests 'foo', 'bar' ], 'userId' ) export const Users = buildTableHelper( 'users', [ 'id', 'suuid', 'createdAt', 'name', 'bio', 'company', 'email', 'verified', 'avatar', 'profiles', 'passwordDigest', 'ip' ], UsersMeta ) export const ServerAcl = buildTableHelper('server_acl', ['userId', 'role']) export const Comments = buildTableHelper('comments', [ 'id', 'streamId', 'authorId', 'createdAt', 'updatedAt', 'text', 'screenshot', 'data', 'archived', 'parentComment' ]) export const CommentLinks = buildTableHelper('comment_links', [ 'commentId', 'resourceId', 'resourceType' ]) export const CommentViews = buildTableHelper('comment_views', [ 'commentId', 'userId', 'viewedAt' ]) export const ServerInvites = buildTableHelper('server_invites', [ 'id', 'target', 'inviterId', 'createdAt', 'updatedAt', 'message', 'resource', 'token' ]) export const PasswordResetTokens = buildTableHelper('pwdreset_tokens', [ 'id', 'email', 'createdAt' ]) export const RefreshTokens = buildTableHelper('refresh_tokens', [ 'id', 'tokenDigest', 'appId', 'userId', 'createdAt', 'lifespan' ]) export const AuthorizationCodes = buildTableHelper('authorization_codes', [ 'id', 'appId', 'userId', 'challenge', 'createdAt', 'lifespan' ]) export const ApiTokens = buildTableHelper('api_tokens', [ 'id', 'tokenDigest', 'owner', 'name', 'lastChars', 'revoked', 'lifespan', 'createdAt', 'lastUsed' ]) export const PersonalApiTokens = buildTableHelper('personal_api_tokens', [ 'tokenId', 'userId' ]) export const EmbedApiTokens = buildTableHelper('embed_api_tokens', [ 'tokenId', 'projectId', 'userId', 'resourceIdString' ]) export const UserServerAppTokens = buildTableHelper('user_server_app_tokens', [ 'appId', 'userId', 'tokenId' ]) export const TokenScopes = buildTableHelper('token_scopes', ['tokenId', 'scopeName']) export const EmailVerifications = buildTableHelper('email_verifications', [ 'id', 'email', 'createdAt', 'used', 'code' ]) export const ServerAccessRequests = buildTableHelper('server_access_requests', [ 'id', 'requesterId', 'resourceType', 'resourceId', 'createdAt', 'updatedAt' ]) export const Activity = buildTableHelper('activity', [ 'id', 'contextResourceId', 'contextResourceType', 'eventType', 'userId', 'payload', 'createdAt' ]) export const StreamActivity = buildTableHelper('stream_activity', [ 'streamId', 'time', 'resourceType', 'resourceId', 'actionType', 'userId', 'info', 'message' ]) export const UserNotificationPreferences = buildTableHelper( 'user_notification_preferences', ['userId', 'preferences'] ) export const Commits = buildTableHelper('commits', [ 'id', 'referencedObject', 'author', 'message', 'createdAt', 'sourceApplication', 'totalChildrenCount', 'parents' ]) export const StreamCommits = buildTableHelper('stream_commits', [ 'streamId', 'commitId' ]) export const BranchCommits = buildTableHelper('branch_commits', [ 'branchId', 'commitId' ]) export const Branches = buildTableHelper('branches', [ 'id', 'streamId', 'authorId', 'name', 'description', 'createdAt', 'updatedAt' ]) export const ScheduledTasks = buildTableHelper('scheduled_tasks', [ 'taskName', 'lockExpiresAt' ]) export const Objects = buildTableHelper('objects', [ 'id', 'speckleType', 'totalChildrenCount', 'totalChildrenCountByDepth', 'createdAt', 'data', 'streamId' ]) export const FileUploads = buildTableHelper('file_uploads', [ 'id', 'streamId', 'branchName', 'userId', 'modelId', 'fileName', 'fileType', 'fileSize', 'uploadComplete', 'uploadDate', 'convertedStatus', 'convertedLastUpdate', 'convertedMessage', 'convertedCommitId' ]) export const ServerAppsScopes = buildTableHelper('server_apps_scopes', [ 'appId', 'scopeName' ]) export const ServerApps = buildTableHelper('server_apps', [ 'id', 'secret', 'name', 'description', 'termsAndConditionsLink', 'logo', 'public', 'trustByDefault', 'authorId', 'createdAt', 'redirectUrl' ]) export const Scopes = buildTableHelper('scopes', ['name', 'description', 'public']) export const TokenResourceAccess = buildTableHelper('token_resource_access', [ 'tokenId', 'resourceType', 'resourceId' ]) export const AutomationFunctionRuns = buildTableHelper('automation_function_runs', [ 'id', 'runId', 'functionReleaseId', 'functionId', 'elapsed', 'status', 'contextView', 'statusMessage', 'results', 'createdAt', 'updatedAt' ]) export const AutomationRevisionFunctions = buildTableHelper( 'automation_revision_functions', ['automationRevisionId', 'functionReleaseId', 'functionInputs', 'functionId'] ) export const AutomationRevisions = buildTableHelper('automation_revisions', [ 'id', 'automationId', 'active', 'createdAt', 'userId', 'publicKey' ]) export const AutomationTokens = buildTableHelper('automation_tokens', [ 'automationId', 'automateToken' ]) export const AutomationRuns = buildTableHelper('automation_runs', [ 'id', 'automationRevisionId', 'createdAt', 'updatedAt', 'status', 'executionEngineRunId' ]) export const AutomationTriggers = buildTableHelper('automation_triggers', [ 'automationRevisionId', 'triggerType', 'triggeringId' ]) export const AutomationRunTriggers = buildTableHelper('automation_run_triggers', [ 'automationRunId', 'triggerType', 'triggeringId' ]) export const Automations = buildTableHelper('automations', [ 'id', 'name', 'projectId', 'enabled', 'createdAt', 'updatedAt', 'userId', 'executionEngineAutomationId', 'isTestAutomation', 'isDeleted' ]) export const GendoAIRenders = buildTableHelper('gendo_ai_renders', [ 'id', 'userId', 'projectId', 'modelId', 'versionId', 'createdAt', 'updatedAt', 'gendoGenerationId', 'status', 'prompt', 'camera', 'baseImage', 'responseImage' ]) export const UserEmails = buildTableHelper('user_emails', [ 'id', 'email', 'primary', 'verified', 'userId', 'createdAt', 'updatedAt' ]) export const UserRoles = buildTableHelper('user_roles', [ 'name', 'description', 'resourceTarget', 'aclTableName', 'weight', 'public' ]) export { knex }