UNPKG

@directus/api

Version:

Directus is a real-time API and App dashboard for managing SQL database content

812 lines (811 loc) 38.3 kB
import { DEFAULT_NUMERIC_PRECISION, DEFAULT_NUMERIC_SCALE, KNEX_TYPES, REGEX_BETWEEN_PARENS, } from '@directus/constants'; import { useEnv } from '@directus/env'; import { ForbiddenError, InvalidPayloadError } from '@directus/errors'; import { createInspector } from '@directus/schema'; import { isSystemField } from '@directus/system-data'; import { addFieldFlag, getRelations, toArray } from '@directus/utils'; import { isEqual, isNil, merge } from 'lodash-es'; import { z } from 'zod'; import { clearSystemCache, getCache, getCacheValue, setCacheValue } from '../cache.js'; import { ALIAS_TYPES, ALLOWED_DB_DEFAULT_FUNCTIONS } from '../constants.js'; import { translateDatabaseError } from '../database/errors/translate.js'; import { getHelpers } from '../database/helpers/index.js'; import getDatabase, { getSchemaInspector } from '../database/index.js'; import emitter from '../emitter.js'; import { fetchPermissions } from '../permissions/lib/fetch-permissions.js'; import { fetchPolicies } from '../permissions/lib/fetch-policies.js'; import { validateAccess } from '../permissions/modules/validate-access/validate-access.js'; import getDefaultValue from '../utils/get-default-value.js'; import { getSystemFieldRowsWithAuthProviders } from '../utils/get-field-system-rows.js'; import getLocalType from '../utils/get-local-type.js'; import { getSchema } from '../utils/get-schema.js'; import { sanitizeColumn } from '../utils/sanitize-schema.js'; import { shouldClearCache } from '../utils/should-clear-cache.js'; import { transaction } from '../utils/transaction.js'; import { buildCollectionAndFieldRelations } from './fields/build-collection-and-field-relations.js'; import { getCollectionMetaUpdates } from './fields/get-collection-meta-updates.js'; import { getCollectionRelationList } from './fields/get-collection-relation-list.js'; import { ItemsService } from './items.js'; import { PayloadService } from './payload.js'; import { RelationsService } from './relations.js'; const systemFieldRows = getSystemFieldRowsWithAuthProviders(); const env = useEnv(); export const systemFieldUpdateSchema = z .object({ collection: z.string().optional(), field: z.string().optional(), schema: z .object({ is_indexed: z.boolean().optional(), }) .strict(), }) .strict(); export class FieldsService { knex; helpers; accountability; itemsService; payloadService; schemaInspector; schema; cache; systemCache; schemaCache; constructor(options) { this.knex = options.knex || getDatabase(); this.helpers = getHelpers(this.knex); this.schemaInspector = options.knex ? createInspector(options.knex) : getSchemaInspector(); this.accountability = options.accountability || null; this.itemsService = new ItemsService('directus_fields', options); this.payloadService = new PayloadService('directus_fields', options); this.schema = options.schema; const { cache, systemCache, localSchemaCache } = getCache(); this.cache = cache; this.systemCache = systemCache; this.schemaCache = localSchemaCache; } async columnInfo(collection, field) { const schemaCacheIsEnabled = Boolean(env['CACHE_SCHEMA']); let columnInfo = null; if (schemaCacheIsEnabled) { columnInfo = await getCacheValue(this.schemaCache, 'columnInfo'); } if (!columnInfo) { columnInfo = await this.schemaInspector.columnInfo(); if (schemaCacheIsEnabled) { await setCacheValue(this.schemaCache, 'columnInfo', columnInfo); } } if (collection) { columnInfo = columnInfo.filter((column) => column.table === collection); } if (field) { return columnInfo.find((column) => column.name === field); } return columnInfo; } async readAll(collection) { let fields; if (this.accountability) { await validateAccess({ accountability: this.accountability, action: 'read', collection: 'directus_fields', }, { schema: this.schema, knex: this.knex, }); } const nonAuthorizedItemsService = new ItemsService('directus_fields', { knex: this.knex, schema: this.schema, }); if (collection) { fields = await nonAuthorizedItemsService.readByQuery({ filter: { collection: { _eq: collection } }, limit: -1, }); fields.push(...systemFieldRows.filter((fieldMeta) => fieldMeta.collection === collection)); } else { fields = await nonAuthorizedItemsService.readByQuery({ limit: -1 }); fields.push(...systemFieldRows); } const columns = (await this.columnInfo(collection)).map((column) => ({ ...column, default_value: getDefaultValue(column, fields.find((field) => field.collection === column.table && field.field === column.name)), })); const columnsWithSystem = columns.map((column) => { const field = fields.find((field) => { return field.field === column.name && field.collection === column.table; }); const type = getLocalType(column, field); const data = { collection: column.table, field: column.name, type: type, schema: column, meta: field || null, }; return data; }); const aliasQuery = this.knex.select('*').from('directus_fields'); if (collection) { aliasQuery.andWhere('collection', collection); } let aliasFields = [...(await this.payloadService.processValues('read', await aliasQuery))]; if (collection) { aliasFields.push(...systemFieldRows.filter((fieldMeta) => fieldMeta.collection === collection)); } else { aliasFields.push(...systemFieldRows); } aliasFields = aliasFields.filter((field) => { const specials = toArray(field.special); for (const type of ALIAS_TYPES) { if (specials.includes(type)) return true; } return false; }); const aliasFieldsAsField = aliasFields.map((field) => { const type = getLocalType(undefined, field); const data = { collection: field.collection, field: field.field, type, schema: null, meta: field, }; return data; }); const knownCollections = Object.keys(this.schema.collections); let result = [...columnsWithSystem, ...aliasFieldsAsField].filter((field) => knownCollections.includes(field.collection)); // Filter the result so we only return the fields you have read access to if (this.accountability && this.accountability.admin !== true) { const policies = await fetchPolicies(this.accountability, { knex: this.knex, schema: this.schema }); const permissions = await fetchPermissions(collection ? { action: 'read', policies, collections: [collection], accountability: this.accountability, } : { action: 'read', policies, accountability: this.accountability, }, { knex: this.knex, schema: this.schema }); const allowedFieldsInCollection = {}; permissions.forEach((permission) => { if (!allowedFieldsInCollection[permission.collection]) { allowedFieldsInCollection[permission.collection] = new Set(); } for (const field of permission.fields ?? []) { allowedFieldsInCollection[permission.collection].add(field); } }); if (collection && collection in allowedFieldsInCollection === false) { throw new ForbiddenError(); } result = result.filter((field) => { if (field.collection in allowedFieldsInCollection === false) return false; const allowedFields = allowedFieldsInCollection[field.collection]; if (allowedFields.has('*')) return true; return allowedFields.has(field.field); }); } // Update specific database type overrides for (const field of result) { field.type = this.helpers.schema.processFieldType(field); } return result; } async readOne(collection, field) { if (this.accountability && this.accountability.admin !== true) { await validateAccess({ accountability: this.accountability, action: 'read', collection, }, { schema: this.schema, knex: this.knex, }); const policies = await fetchPolicies(this.accountability, { knex: this.knex, schema: this.schema }); const permissions = await fetchPermissions({ action: 'read', policies, collections: [collection], accountability: this.accountability }, { knex: this.knex, schema: this.schema }); let hasAccess = false; for (const permission of permissions) { if (permission.fields) { if (permission.fields.includes('*') || permission.fields.includes(field)) { hasAccess = true; break; } } } if (!hasAccess) { throw new ForbiddenError(); } } let column = undefined; let fieldInfo = await this.knex.select('*').from('directus_fields').where({ collection, field }).first(); if (fieldInfo) { fieldInfo = (await this.payloadService.processValues('read', fieldInfo)); } fieldInfo = fieldInfo || systemFieldRows.find((fieldMeta) => fieldMeta.collection === collection && fieldMeta.field === field); try { column = await this.columnInfo(collection, field); } catch { // Do nothing } if (!column && !fieldInfo) throw new ForbiddenError(); const type = getLocalType(column, fieldInfo); const columnWithCastDefaultValue = column ? { ...column, default_value: getDefaultValue(column, fieldInfo), } : null; const data = { collection, field, type, meta: fieldInfo || null, schema: type === 'alias' ? null : columnWithCastDefaultValue, }; return data; } async createField(collection, field, table, // allows collection creation to opts) { if (this.accountability && this.accountability.admin !== true) { throw new ForbiddenError(); } const runPostColumnChange = await this.helpers.schema.preColumnChange(); const nestedActionEvents = []; try { const exists = field.field in this.schema.collections[collection].fields || isNil(await this.knex.select('id').from('directus_fields').where({ collection, field: field.field }).first()) === false; // Check if field already exists, either as a column, or as a row in directus_fields if (exists) { throw new InvalidPayloadError({ reason: `Field "${field.field}" already exists in collection "${collection}"`, }); } // Add flag for specific database type overrides const flagToAdd = this.helpers.date.fieldFlagForField(field.type); if (flagToAdd) { addFieldFlag(field, flagToAdd); } let hookAdjustedField = field; const attemptConcurrentIndex = Boolean(opts?.attemptConcurrentIndex); await transaction(this.knex, async (trx) => { const itemsService = new ItemsService('directus_fields', { knex: trx, accountability: this.accountability, schema: this.schema, }); hookAdjustedField = opts?.emitEvents !== false ? await emitter.emitFilter(`fields.create`, field, { collection: collection, }, { database: trx, schema: this.schema, accountability: this.accountability, }) : field; if (hookAdjustedField.type && ALIAS_TYPES.includes(hookAdjustedField.type) === false) { if (table) { this.addColumnToTable(table, collection, hookAdjustedField, { attemptConcurrentIndex, }); } else { await trx.schema.alterTable(collection, (table) => { this.addColumnToTable(table, collection, hookAdjustedField, { attemptConcurrentIndex, }); }); } } if (hookAdjustedField.meta) { const existingSortRecord = await trx .from('directus_fields') .where(hookAdjustedField.meta?.group ? { collection, group: hookAdjustedField.meta.group } : { collection }) .max('sort', { as: 'max' }) .first(); const newSortValue = existingSortRecord?.max ? existingSortRecord.max + 1 : 1; await itemsService.createOne({ ...merge({ sort: newSortValue }, hookAdjustedField.meta), collection: collection, field: hookAdjustedField.field, }, { emitEvents: false }); } const actionEvent = { event: 'fields.create', meta: { payload: hookAdjustedField, key: hookAdjustedField.field, collection: collection, }, context: { database: getDatabase(), schema: this.schema, accountability: this.accountability, }, }; if (opts?.bypassEmitAction) { opts.bypassEmitAction(actionEvent); } else { nestedActionEvents.push(actionEvent); } }); // concurrent index creation cannot be done inside the transaction if (attemptConcurrentIndex && hookAdjustedField.type && ALIAS_TYPES.includes(hookAdjustedField.type) === false) { await this.addColumnIndex(collection, hookAdjustedField, { attemptConcurrentIndex, }); } } finally { if (runPostColumnChange) { await this.helpers.schema.postColumnChange(); } if (shouldClearCache(this.cache, opts)) { await this.cache.clear(); } if (opts?.autoPurgeSystemCache !== false) { await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache }); } if (opts?.emitEvents !== false && nestedActionEvents.length > 0) { const updatedSchema = await getSchema({ database: this.knex }); for (const nestedActionEvent of nestedActionEvents) { nestedActionEvent.context.schema = updatedSchema; emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context); } } } } async updateField(collection, field, opts) { if (this.accountability && this.accountability.admin !== true) { throw new ForbiddenError(); } const runPostColumnChange = await this.helpers.schema.preColumnChange(); const nestedActionEvents = []; // 'type' is required for further checks on schema update if (field.schema && !field.type) { const existingType = this.schema.collections[collection]?.fields[field.field]?.type; if (existingType) field.type = existingType; } try { const hookAdjustedField = opts?.emitEvents !== false ? await emitter.emitFilter(`fields.update`, field, { keys: [field.field], collection: collection, }, { database: this.knex, schema: this.schema, accountability: this.accountability, }) : field; const record = field.meta ? await this.knex.select('id').from('directus_fields').where({ collection, field: field.field }).first() : null; if (hookAdjustedField.type && (hookAdjustedField.type === 'alias' || this.schema.collections[collection].fields[field.field]?.type === 'alias') && hookAdjustedField.type !== (this.schema.collections[collection].fields[field.field]?.type ?? 'alias')) { throw new InvalidPayloadError({ reason: 'Alias type cannot be changed' }); } if (hookAdjustedField.schema) { const existingColumn = await this.columnInfo(collection, hookAdjustedField.field); if (existingColumn.is_primary_key) { if (hookAdjustedField.schema?.is_nullable === true) { throw new InvalidPayloadError({ reason: 'Primary key cannot be null' }); } } // Sanitize column only when applying snapshot diff as opts is only passed from /utils/apply-diff.ts const columnToCompare = opts?.bypassLimits && opts.autoPurgeSystemCache === false ? sanitizeColumn(existingColumn) : existingColumn; if (!isEqual(columnToCompare, hookAdjustedField.schema)) { try { const attemptConcurrentIndex = Boolean(opts?.attemptConcurrentIndex); await transaction(this.knex, async (trx) => { await trx.schema.alterTable(collection, (table) => { if (!hookAdjustedField.schema) return; this.addColumnToTable(table, collection, field, { existing: existingColumn, attemptConcurrentIndex, }); }); }); // concurrent index creation cannot be done inside the transaction if (attemptConcurrentIndex) { await this.addColumnIndex(collection, field, { existing: existingColumn, attemptConcurrentIndex, }); } } catch (err) { throw await translateDatabaseError(err, field); } } } // Only create/update a database record if this is not a system field if (hookAdjustedField.meta && !isSystemField(collection, hookAdjustedField.field)) { if (record) { await this.itemsService.updateOne(record.id, { ...hookAdjustedField.meta, collection: collection, field: hookAdjustedField.field, }, { emitEvents: false }); } else { await this.itemsService.createOne({ ...hookAdjustedField.meta, collection: collection, field: hookAdjustedField.field, }, { emitEvents: false }); } } const actionEvent = { event: 'fields.update', meta: { payload: hookAdjustedField, keys: [hookAdjustedField.field], collection: collection, }, context: { database: getDatabase(), schema: this.schema, accountability: this.accountability, }, }; if (opts?.bypassEmitAction) { opts.bypassEmitAction(actionEvent); } else { nestedActionEvents.push(actionEvent); } return field.field; } finally { if (runPostColumnChange) { await this.helpers.schema.postColumnChange(); } if (shouldClearCache(this.cache, opts)) { await this.cache.clear(); } if (opts?.autoPurgeSystemCache !== false) { await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache }); } if (opts?.emitEvents !== false && nestedActionEvents.length > 0) { const updatedSchema = await getSchema({ database: this.knex }); for (const nestedActionEvent of nestedActionEvents) { nestedActionEvent.context.schema = updatedSchema; emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context); } } } } async updateFields(collection, fields, opts) { const nestedActionEvents = []; try { const fieldNames = []; const attemptConcurrentIndex = Boolean(opts?.attemptConcurrentIndex); for (const field of fields) { fieldNames.push(await this.updateField(collection, field, { autoPurgeCache: false, autoPurgeSystemCache: false, bypassEmitAction: (params) => nestedActionEvents.push(params), attemptConcurrentIndex, })); } return fieldNames; } finally { if (shouldClearCache(this.cache, opts)) { await this.cache.clear(); } if (opts?.autoPurgeSystemCache !== false) { await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache }); } if (opts?.emitEvents !== false && nestedActionEvents.length > 0) { const updatedSchema = await getSchema({ database: this.knex }); for (const nestedActionEvent of nestedActionEvents) { nestedActionEvent.context.schema = updatedSchema; emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context); } } } } async deleteField(collection, field, opts) { if (this.accountability && this.accountability.admin !== true) { throw new ForbiddenError(); } const runPostColumnChange = await this.helpers.schema.preColumnChange(); const nestedActionEvents = []; try { if (opts?.emitEvents !== false) { await emitter.emitFilter('fields.delete', [field], { collection: collection, }, { database: this.knex, schema: this.schema, accountability: this.accountability, }); } await transaction(this.knex, async (trx) => { const relations = getRelations(this.schema.relations, collection, field); const relationsService = new RelationsService({ knex: trx, accountability: this.accountability, schema: this.schema, }); const fieldsService = new FieldsService({ knex: trx, accountability: this.accountability, schema: this.schema, }); for (const relation of relations) { const isM2O = relation.collection === collection && relation.field === field; // If the current field is a m2o, delete the related o2m if it exists and remove the relationship if (isM2O) { await relationsService.deleteOne(collection, field, { autoPurgeSystemCache: false, bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params), }); if (relation.related_collection && relation.meta?.one_field && relation.related_collection !== collection && relation.meta.one_field !== field) { await fieldsService.deleteField(relation.related_collection, relation.meta.one_field, { autoPurgeCache: false, autoPurgeSystemCache: false, bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params), }); } } // If the current field is a o2m, just delete the one field config from the relation if (!isM2O && relation.meta?.one_field) { await trx('directus_relations') .update({ one_field: null }) .where({ many_collection: relation.collection, many_field: relation.field }); } } // Delete field only after foreign key constraints are removed if (this.schema.collections[collection] && field in this.schema.collections[collection].fields && this.schema.collections[collection].fields[field].alias === false) { await trx.schema.table(collection, (table) => { table.dropColumn(field); }); } const { collectionRelationTree, fieldToCollectionList } = await buildCollectionAndFieldRelations(this.schema.relations); const collectionRelationList = getCollectionRelationList(collection, collectionRelationTree); const collectionMetaQuery = trx .queryBuilder() .select('collection', 'archive_field', 'sort_field', 'item_duplication_fields') .from('directus_collections') .where({ collection }); if (collectionRelationList.size !== 0) { collectionMetaQuery.orWhere(function () { this.whereIn('collection', Array.from(collectionRelationList)).whereNotNull('item_duplication_fields'); }); } const collectionMetas = await collectionMetaQuery; const collectionMetaUpdates = getCollectionMetaUpdates(collection, field, collectionMetas, this.schema.collections, fieldToCollectionList); for (const meta of collectionMetaUpdates) { await trx('directus_collections').update(meta.updates).where({ collection: meta.collection }); } // Cleanup directus_fields const metaRow = await trx .select('collection', 'field') .from('directus_fields') .where({ collection, field }) .first(); if (metaRow) { // Handle recursive FK constraints await trx('directus_fields') .update({ group: null }) .where({ group: metaRow.field, collection: metaRow.collection }); } const itemsService = new ItemsService('directus_fields', { knex: trx, accountability: this.accountability, schema: this.schema, }); await itemsService.deleteByQuery({ filter: { collection: { _eq: collection }, field: { _eq: field }, }, }, { emitEvents: false }); // cleanup permissions for deleted field const permissionRows = await trx .select('id', 'collection', 'fields') .from('directus_permissions') .whereRaw('?? = ? AND ?? LIKE ?', ['collection', collection, 'fields', '%' + field + '%']); if (permissionRows.length > 0) { for (const permissionRow of permissionRows) { const newFields = permissionRow['fields'] .split(',') .filter((v) => v !== field) .join(','); await trx('directus_permissions') .update('fields', newFields.length > 0 ? newFields : null) .where('id', '=', permissionRow['id']); } } }); const actionEvent = { event: 'fields.delete', meta: { payload: [field], collection: collection, }, context: { database: this.knex, schema: this.schema, accountability: this.accountability, }, }; if (opts?.bypassEmitAction) { opts.bypassEmitAction(actionEvent); } else { nestedActionEvents.push(actionEvent); } } finally { if (runPostColumnChange) { await this.helpers.schema.postColumnChange(); } if (shouldClearCache(this.cache, opts)) { await this.cache.clear(); } if (opts?.autoPurgeSystemCache !== false) { await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache }); } if (opts?.emitEvents !== false && nestedActionEvents.length > 0) { const updatedSchema = await getSchema({ database: this.knex }); for (const nestedActionEvent of nestedActionEvents) { nestedActionEvent.context.schema = updatedSchema; emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context); } } } } addColumnToTable(table, collection, field, options) { let column; // Don't attempt to add a DB column for alias / corrupt fields if (field.type === 'alias' || field.type === 'unknown') return; const existing = options?.existing ?? null; if (field.schema?.has_auto_increment) { if (field.type === 'bigInteger') { column = table.bigIncrements(field.field); } else { column = table.increments(field.field); } } else if (field.type === 'string') { column = table.string(field.field, field.schema?.max_length ?? existing?.max_length ?? undefined); } else if (['float', 'decimal'].includes(field.type)) { const type = field.type; column = table[type](field.field, field.schema?.numeric_precision ?? existing?.numeric_precision ?? DEFAULT_NUMERIC_PRECISION, field.schema?.numeric_scale ?? existing?.numeric_scale ?? DEFAULT_NUMERIC_SCALE); } else if (field.type === 'csv') { column = table.text(field.field); } else if (field.type === 'hash') { column = table.string(field.field, 255); } else if (field.type === 'dateTime') { column = table.dateTime(field.field, { useTz: false }); } else if (field.type === 'timestamp') { column = table.timestamp(field.field, { useTz: true }); } else if (field.type.startsWith('geometry')) { column = this.helpers.st.createColumn(table, field); } else if (KNEX_TYPES.includes(field.type)) { column = table[field.type](field.field); } else { throw new InvalidPayloadError({ reason: `Illegal type passed: "${field.type}"` }); } /** * The column nullability must be set on every alter or it will be dropped * This is due to column.alter() not being incremental per https://knexjs.org/guide/schema-builder.html#alter */ this.helpers.schema.setNullable(column, field, existing); /** * The default value must be set on every alter or it will be dropped * This is due to column.alter() not being incremental per https://knexjs.org/guide/schema-builder.html#alter */ const defaultValue = field.schema?.default_value !== undefined ? field.schema?.default_value : existing?.default_value; if (defaultValue !== undefined) { const newDefaultValueIsString = typeof defaultValue === 'string'; const newDefaultIsNowFunction = newDefaultValueIsString && defaultValue.toLowerCase() === 'now()'; const newDefaultIsCurrentTimestamp = newDefaultValueIsString && defaultValue === 'CURRENT_TIMESTAMP'; const newDefaultIsSetToCurrentTime = newDefaultIsNowFunction || newDefaultIsCurrentTimestamp; const newDefaultIsAFunction = newDefaultValueIsString && ALLOWED_DB_DEFAULT_FUNCTIONS.includes(defaultValue); const newDefaultIsTimestampWithPrecision = newDefaultValueIsString && defaultValue.includes('CURRENT_TIMESTAMP(') && defaultValue.includes(')'); if (newDefaultIsSetToCurrentTime) { column.defaultTo(this.knex.fn.now()); } else if (newDefaultIsTimestampWithPrecision) { const precision = defaultValue.match(REGEX_BETWEEN_PARENS)[1]; column.defaultTo(this.knex.fn.now(Number(precision))); } else if (newDefaultIsAFunction) { column.defaultTo(this.knex.raw(defaultValue)); } else { column.defaultTo(defaultValue); } } if (field.schema?.is_primary_key) { column.primary().notNullable(); } else if (!existing?.is_primary_key) { // primary key will already have unique/index constraints if (field.schema?.is_unique === true) { if ((!existing || existing.is_unique === false) && !options?.attemptConcurrentIndex) { column.unique({ indexName: this.helpers.schema.generateIndexName('unique', collection, field.field) }); } } else if (field.schema?.is_unique === false && existing?.is_unique === true) { table.dropUnique([field.field], this.helpers.schema.generateIndexName('unique', collection, field.field)); } if (field.schema?.is_indexed === true) { if ((!existing || existing.is_indexed === false) && !options?.attemptConcurrentIndex) { column.index(this.helpers.schema.generateIndexName('index', collection, field.field)); } } else if (field.schema?.is_indexed === false && existing?.is_indexed === true) { table.dropIndex([field.field], this.helpers.schema.generateIndexName('index', collection, field.field)); } } if (existing) { column.alter(); } } async addColumnIndex(collection, field, options) { const attemptConcurrentIndex = Boolean(options?.attemptConcurrentIndex); const knex = options?.knex ?? this.knex; const existing = options?.existing ?? null; // Don't attempt to index a DB column for alias / corrupt fields if (field.type === 'alias' || field.type === 'unknown') return; // primary key will already have unique/index constraints if (field.schema?.is_primary_key || existing?.is_primary_key) return; const helpers = getHelpers(knex); if (field.schema?.is_unique === true && (!existing || existing.is_unique == false)) { await helpers.schema.createIndex(collection, field.field, { unique: true, attemptConcurrentIndex, }); } if (field.schema?.is_indexed === true && (!existing || existing.is_indexed === false)) { await helpers.schema.createIndex(collection, field.field, { unique: false, attemptConcurrentIndex, }); } } }