ronantakizawa/codereview-bench huggingface dataset:
Repository: directus/directus
File: api/src/services/mail/rate-limiter.ts (TypeScript)

Code before:
import { useEnv } from '@directus/env';
import { RateLimiterQueue } from 'rate-limiter-flexible';
import { createRateLimiter } from '../../rate-limiter.js';

let emailQueue: RateLimiterQueue | undefined;

const env = useEnv();

if (Boolean(env['EMAIL_LIMITER_ENABLED']) === true) {
	const rateLimiter = createRateLimiter('EMAIL_LIMITER');

	emailQueue = new RateLimiterQueue(rateLimiter, {
		maxQueueSize: Number(env['EMAIL_LIMITER_QUEUE_SIZE']),
	});
}

export async function emailRateLimiter() {
	if (emailQueue) {
		await emailQueue.removeTokens(1);
	}
}


Review comment (suggestion):
Let's add `queue` to make it clearer?

```suggestion
export async function emailRateLimiterQueue() {
```

Code after:
import { useEnv } from '@directus/env';
import { RateLimiterQueue } from 'rate-limiter-flexible';
import { createRateLimiter } from '../../rate-limiter.js';
import { toBoolean } from '@directus/utils';
import { EmailLimitExceededError } from '@directus/errors';

let emailRateLimiterQueue: RateLimiterQueue | undefined;

const env = useEnv();

if (toBoolean(env['RATE_LIMITER_EMAIL_ENABLED']) === true) {
	emailRateLimiterQueue = new RateLimiterQueue(createRateLimiter('RATE_LIMITER_EMAIL'), {
		maxQueueSize: Number(env['RATE_LIMITER_EMAIL_QUEUE_SIZE']),
	});
}

export async function useEmailRateLimiterQueue() {
	if (!emailRateLimiterQueue) return;

	try {
		await emailRateLimiterQueue.removeTokens(1);
	} catch (err: unknown) {
		if (err instanceof Error) {
			throw new EmailLimitExceededError({
				points: 'RATE_LIMITER_EMAIL_POINTS' in env ? Number(env['RATE_LIMITER_EMAIL_POINTS']) : undefined,
				duration: 'RATE_LIMITER_EMAIL_DURATION' in env ? Number(env['RATE_LIMITER_EMAIL_DURATION']) : undefined,
				message:
					'RATE_LIMITER_EMAIL_ERROR_MESSAGE' in env ? String(env['RATE_LIMITER_EMAIL_ERROR_MESSAGE']) : undefined,
			});
		}

		throw err;
	}
}
Repository: directus/directus
File: packages/env/src/constants/defaults.ts (TypeScript)

Code before:
	QUERY_LIMIT_DEFAULT: 100,
	MAX_BATCH_MUTATION: Infinity,
	ROBOTS_TXT: 'User-agent: *\nDisallow: /',

	TEMP_PATH: './node_modules/.directus',

	DB_EXCLUDE_TABLES: 'spatial_ref_sys,sysdiagrams',

	STORAGE_LOCATIONS: 'local',
	STORAGE_LOCAL_DRIVER: 'local',
	STORAGE_LOCAL_ROOT: './uploads',

	RATE_LIMITER_ENABLED: false,
	RATE_LIMITER_POINTS: 50,
	RATE_LIMITER_DURATION: 1,
	RATE_LIMITER_STORE: 'memory',

	RATE_LIMITER_GLOBAL_ENABLED: false,
	RATE_LIMITER_GLOBAL_POINTS: 1000,
	RATE_LIMITER_GLOBAL_DURATION: 1,

	RATE_LIMITER_REGISTRATION_ENABLED: true,
	RATE_LIMITER_REGISTRATION_POINTS: 5,
	RATE_LIMITER_REGISTRATION_DURATION: 60,

	EMAIL_LIMITER_ENABLED: false,
	EMAIL_LIMITER_POINTS: 60,
	EMAIL_LIMITER_DURATION: 60,
	EMAIL_LIMITER_QUEUE_SIZE: 1_000_000,

	EMAIL_FLOWS_LIMITER_ENABLED: false,
	EMAIL_FLOWS_LIMITER_POINTS: 1,
	EMAIL_FLOWS_LIMITER_DURATION: 60,

	ACCESS_TOKEN_TTL: '15m',
	EMAIL_VERIFICATION_TOKEN_TTL: '7d',

	REFRESH_TOKEN_TTL: '7d',
	REFRESH_TOKEN_COOKIE_NAME: 'directus_refresh_token',
	REFRESH_TOKEN_COOKIE_SECURE: false,
	REFRESH_TOKEN_COOKIE_SAME_SITE: 'lax',

	SESSION_COOKIE_TTL: '1d',
	SESSION_COOKIE_NAME: 'directus_session_token',
	SESSION_COOKIE_SECURE: false,
	SESSION_COOKIE_SAME_SITE: 'lax',
	SESSION_REFRESH_GRACE_PERIOD: '10s',

	USER_INVITE_TOKEN_TTL: '7d',

	LOGIN_STALL_TIME: 500,

Review comment (suggestion):
Should these start with `RATE_LIMITER_*` to be consistent with the other rate limiter configuration envs (e.g. global, registration etc)

Code after:
	QUERY_LIMIT_DEFAULT: 100,
	MAX_BATCH_MUTATION: Infinity,
	ROBOTS_TXT: 'User-agent: *\nDisallow: /',

	TEMP_PATH: './node_modules/.directus',

	DB_EXCLUDE_TABLES: 'spatial_ref_sys,sysdiagrams',

	STORAGE_LOCATIONS: 'local',
	STORAGE_LOCAL_DRIVER: 'local',
	STORAGE_LOCAL_ROOT: './uploads',

	RATE_LIMITER_ENABLED: false,
	RATE_LIMITER_POINTS: 50,
	RATE_LIMITER_DURATION: 1,
	RATE_LIMITER_STORE: 'memory',

	RATE_LIMITER_GLOBAL_ENABLED: false,
	RATE_LIMITER_GLOBAL_POINTS: 1000,
	RATE_LIMITER_GLOBAL_DURATION: 1,

	RATE_LIMITER_REGISTRATION_ENABLED: true,
	RATE_LIMITER_REGISTRATION_POINTS: 5,
	RATE_LIMITER_REGISTRATION_DURATION: 60,

	RATE_LIMITER_EMAIL_ENABLED: false,
	RATE_LIMITER_EMAIL_POINTS: 60,
	RATE_LIMITER_EMAIL_DURATION: 60,
	RATE_LIMITER_EMAIL_QUEUE_SIZE: 1_000_000,

	RATE_LIMITER_EMAIL_FLOWS_ENABLED: false,
	RATE_LIMITER_EMAIL_FLOWS_POINTS: 1,
	RATE_LIMITER_EMAIL_FLOWS_DURATION: 60,

	ACCESS_TOKEN_TTL: '15m',
	EMAIL_VERIFICATION_TOKEN_TTL: '7d',

	REFRESH_TOKEN_TTL: '7d',
	REFRESH_TOKEN_COOKIE_NAME: 'directus_refresh_token',
	REFRESH_TOKEN_COOKIE_SECURE: false,
	REFRESH_TOKEN_COOKIE_SAME_SITE: 'lax',

	SESSION_COOKIE_TTL: '1d',
	SESSION_COOKIE_NAME: 'directus_session_token',
	SESSION_COOKIE_SECURE: false,
	SESSION_COOKIE_SAME_SITE: 'lax',
	SESSION_REFRESH_GRACE_PERIOD: '10s',

	USER_INVITE_TOKEN_TTL: '7d',

	LOGIN_STALL_TIME: 500,
Repository: directus/directus
File: packages/env/src/constants/directus-variables.ts (TypeScript)

Code before:
	'USER_REGISTER_URL_ALLOW_LIST',
	'IP_TRUST_PROXY',
	'IP_CUSTOM_HEADER',
	'ASSETS_CONTENT_SECURITY_POLICY',
	'IMPORT_IP_DENY_LIST',
	'CONTENT_SECURITY_POLICY_.+',
	'HSTS_.+',

	// hashing
	'HASH_.+',

	// cors
	'CORS_ENABLED',
	'CORS_ORIGIN',
	'CORS_METHODS',
	'CORS_ALLOWED_HEADERS',
	'CORS_EXPOSED_HEADERS',
	'CORS_CREDENTIALS',
	'CORS_MAX_AGE',

	// rate limiting
	'RATE_LIMITER_GLOBAL_.+',
	'RATE_LIMITER_REGISTRATION_.+',
	'RATE_LIMITER_.+',
	'EMAIL_LIMITER_.+',
	'EMAIL_FLOWS_LIMITER_.+',

	// cache
	'CACHE_ENABLED',
	'CACHE_TTL',
	'CACHE_CONTROL_S_MAXAGE',
	'CACHE_AUTO_PURGE',
	'CACHE_AUTO_PURGE_IGNORE_LIST',
	'CACHE_SYSTEM_TTL',
	'CACHE_SCHEMA',
	'CACHE_SCHEMA_MAX_ITERATIONS',
	'CACHE_SCHEMA_SYNC_TIMEOUT',
	'CACHE_SCHEMA_FREEZE_ENABLED',
	'CACHE_NAMESPACE',
	'CACHE_STORE',
	'CACHE_STATUS_HEADER',
	'CACHE_VALUE_MAX_SIZE',
	'CACHE_SKIP_ALLOWED',
	'CACHE_HEALTHCHECK_THRESHOLD',

	// storage
	'STORAGE_LOCATIONS',
	'STORAGE_.+_DRIVER',
	'STORAGE_.+_ROOT',
	'STORAGE_.+_KEY',
	'STORAGE_.+_SECRET',

Review comment (suggestion):
Assuming we rename the bottom 2,  should we need all these? `RATE_LIMITER_.+` would catch all these
```suggestion
  'RATE_LIMITER_.+',
```

Code after:
	'USER_REGISTER_URL_ALLOW_LIST',
	'IP_TRUST_PROXY',
	'IP_CUSTOM_HEADER',
	'ASSETS_CONTENT_SECURITY_POLICY',
	'IMPORT_IP_DENY_LIST',
	'CONTENT_SECURITY_POLICY_.+',
	'HSTS_.+',

	// hashing
	'HASH_.+',

	// cors
	'CORS_ENABLED',
	'CORS_ORIGIN',
	'CORS_METHODS',
	'CORS_ALLOWED_HEADERS',
	'CORS_EXPOSED_HEADERS',
	'CORS_CREDENTIALS',
	'CORS_MAX_AGE',

	// rate limiting
	'RATE_LIMITER_.+',

	// cache
	'CACHE_ENABLED',
	'CACHE_TTL',
	'CACHE_CONTROL_S_MAXAGE',
	'CACHE_AUTO_PURGE',
	'CACHE_AUTO_PURGE_IGNORE_LIST',
	'CACHE_SYSTEM_TTL',
	'CACHE_SCHEMA',
	'CACHE_SCHEMA_MAX_ITERATIONS',
	'CACHE_SCHEMA_SYNC_TIMEOUT',
	'CACHE_SCHEMA_FREEZE_ENABLED',
	'CACHE_NAMESPACE',
	'CACHE_STORE',
	'CACHE_STATUS_HEADER',
	'CACHE_VALUE_MAX_SIZE',
	'CACHE_SKIP_ALLOWED',
	'CACHE_HEALTHCHECK_THRESHOLD',

	// storage
	'STORAGE_LOCATIONS',
	'STORAGE_.+_DRIVER',
	'STORAGE_.+_ROOT',
	'STORAGE_.+_KEY',
	'STORAGE_.+_SECRET',
	'STORAGE_.+_BUCKET',
	'STORAGE_.+_REGION',
	'STORAGE_.+_ENDPOINT',
	'STORAGE_.+_ACL',
Repository: directus/directus
File: api/src/services/mail/index.ts (TypeScript)

Code before:
	schema: SchemaOverview;
	accountability: Accountability | null;
	knex: Knex;
	mailer: Transporter;

	constructor(opts: AbstractServiceOptions) {
		this.schema = opts.schema;
		this.accountability = opts.accountability || null;
		this.knex = opts?.knex || getDatabase();
		this.mailer = getMailer();

		if (env['EMAIL_VERIFY_SETUP']) {
			this.mailer.verify((error) => {
				if (error) {
					logger.warn(`Email connection failed:`);
					logger.warn(error);
				}
			});
		}
	}

	async send<T>(options: EmailOptions): Promise<T | null> {
		try {
			await getEmailRateLimiterQueue()?.removeTokens(1);
		} catch (err: unknown) {
			// they didnt export the RateLimiterQueueError for more precise error handling
			if (err instanceof Error) {
				throw new HitRateLimitError(
					{
						limit: Number(env['EMAIL_LIMITER_POINTS'] as string),
						reset: new Date(Date.now() + Number(env['EMAIL_LIMITER_DURATION']) * 1000),
					},
					{
						cause: err.message,
					},
				);
			}

			throw err;
		}

		const payload = await emitter.emitFilter(`email.send`, options, {});

		if (!payload) return null;

		const { template, ...emailOptions } = payload;

		let { html } = options;

		const defaultTemplateData = await this.getDefaultTemplateData();


Review comment (suggestion):
```suggestion
			// RateLimiterQueueError is not exported for more precise error handling
```

Code after:
	schema: SchemaOverview;
	accountability: Accountability | null;
	knex: Knex;
	mailer: Transporter;

	constructor(opts: AbstractServiceOptions) {
		this.schema = opts.schema;
		this.accountability = opts.accountability || null;
		this.knex = opts?.knex || getDatabase();
		this.mailer = getMailer();

		if (env['EMAIL_VERIFY_SETUP']) {
			this.mailer.verify((error) => {
				if (error) {
					logger.warn(`Email connection failed:`);
					logger.warn(error);
				}
			});
		}
	}

	async send<T>(options: EmailOptions): Promise<T | null> {
		await useEmailRateLimiterQueue();

		const payload = await emitter.emitFilter(`email.send`, options, {});

		if (!payload) return null;

		const { template, ...emailOptions } = payload;

		let { html } = options;

		const defaultTemplateData = await this.getDefaultTemplateData();

		if (isObject(emailOptions.from) && (!emailOptions.from.name || !emailOptions.from.address)) {
			throw new InvalidPayloadError({ reason: 'A name and address property are required in the "from" object' });
		}

		const from: SendMailOptions['from'] = isObject(emailOptions.from)
			? emailOptions.from
			: {
					name: defaultTemplateData.projectName,
					address: (emailOptions.from as string) || (env['EMAIL_FROM'] as string),
				};

		if (template) {
			let templateData = template.data;

			templateData = {
				...defaultTemplateData,
				...templateData,
Repository: directus/directus
File: api/src/services/mail/rate-limiter.ts (TypeScript)

Code before:
import { useEnv } from '@directus/env';
import { RateLimiterQueue } from 'rate-limiter-flexible';
import { createRateLimiter } from '../../rate-limiter.js';
import { toBoolean } from '@directus/utils';

let emailRateLimiterQueue: RateLimiterQueue | undefined;

const env = useEnv();

export function getEmailRateLimiterQueue() {
    if (!emailRateLimiterQueue && toBoolean(env['EMAIL_LIMITER_ENABLED']) === true) {
	    const rateLimiter = createRateLimiter('EMAIL_LIMITER');

        emailRateLimiterQueue = new RateLimiterQueue(rateLimiter, {
            maxQueueSize: Number(env['EMAIL_LIMITER_QUEUE_SIZE']),
        });
    }

    return emailRateLimiterQueue;
}


Review comment (suggestion):
```suggestion
      emailRateLimiterQueue = new RateLimiterQueue(createRateLimiter('EMAIL_LIMITER'), {
            maxQueueSize: Number(env['EMAIL_LIMITER_QUEUE_SIZE']),
        });
```

Code after:
import { useEnv } from '@directus/env';
import { RateLimiterQueue } from 'rate-limiter-flexible';
import { createRateLimiter } from '../../rate-limiter.js';
import { toBoolean } from '@directus/utils';
import { EmailLimitExceededError } from '@directus/errors';

let emailRateLimiterQueue: RateLimiterQueue | undefined;

const env = useEnv();

if (toBoolean(env['RATE_LIMITER_EMAIL_ENABLED']) === true) {
	emailRateLimiterQueue = new RateLimiterQueue(createRateLimiter('RATE_LIMITER_EMAIL'), {
		maxQueueSize: Number(env['RATE_LIMITER_EMAIL_QUEUE_SIZE']),
	});
}

export async function useEmailRateLimiterQueue() {
	if (!emailRateLimiterQueue) return;

	try {
		await emailRateLimiterQueue.removeTokens(1);
	} catch (err: unknown) {
		if (err instanceof Error) {
			throw new EmailLimitExceededError({
				points: 'RATE_LIMITER_EMAIL_POINTS' in env ? Number(env['RATE_LIMITER_EMAIL_POINTS']) : undefined,
				duration: 'RATE_LIMITER_EMAIL_DURATION' in env ? Number(env['RATE_LIMITER_EMAIL_DURATION']) : undefined,
				message:
					'RATE_LIMITER_EMAIL_ERROR_MESSAGE' in env ? String(env['RATE_LIMITER_EMAIL_ERROR_MESSAGE']) : undefined,
			});
		}

		throw err;
	}
}
Repository: directus/directus
File: api/src/operations/mail/rate-limiter.ts (TypeScript)

Code before:
import { useEnv } from '@directus/env';
import { RateLimiterMemory, RateLimiterRedis } from 'rate-limiter-flexible';
import { createRateLimiter } from '../../rate-limiter.js';
import { toBoolean } from '@directus/utils';
import { HitRateLimitError } from '@directus/errors';

let emailRateLimiter: RateLimiterRedis | RateLimiterMemory | undefined;

const env = useEnv();

if (toBoolean(env['EMAIL_FLOWS_LIMITER_ENABLED']) === true) {
	emailRateLimiter = createRateLimiter('EMAIL_FLOWS_LIMITER');
}

export async function useFlowsEmailRateLimiter(flow_id: string) {
	if (!emailRateLimiter) return;

	try {
		await emailRateLimiter.consume(flow_id, 1);
	} catch (err: unknown) {
		if (err instanceof Error) {
			const resetIn = (err as unknown as any)?.msBeforeNext ?? Number(env['EMAIL_FLOWS_LIMITER_DURATION']) * 1000;
			// perhaps we should use an error that does not require `limit` and `reset`
			throw new HitRateLimitError(
				{
					limit: Number(env['EMAIL_FLOWS_LIMITER_POINTS'] as string),
					reset: new Date(Date.now() + resetIn),
				},
				{
					cause: err.message,
				},
			);
		}

		throw err;
	}
}


Review comment (suggestion):
Per testing, the err is not an instance of `Error` and does not enter this block. It results in the following being displayed on limit hit:

<img width="863" height="827" alt="Image" src="https://github.com/user-attachments/assets/89fbf6e6-b483-4478-80f1-619175a435c2" />

Code after:
import { useEnv } from '@directus/env';
import { RateLimiterMemory, RateLimiterRedis, RateLimiterRes } from 'rate-limiter-flexible';
import { createRateLimiter } from '../../rate-limiter.js';
import { toBoolean } from '@directus/utils';
import { EmailLimitExceededError } from '@directus/errors';

let emailRateLimiter: RateLimiterRedis | RateLimiterMemory | undefined;

const env = useEnv();

if (toBoolean(env['RATE_LIMITER_EMAIL_FLOWS_ENABLED']) === true) {
	emailRateLimiter = createRateLimiter('RATE_LIMITER_EMAIL_FLOWS');
}

export async function useFlowsEmailRateLimiter(flow_id: string) {
	if (!emailRateLimiter) return;

	try {
		await emailRateLimiter.consume(flow_id, 1);
	} catch (err: unknown) {
		if (err instanceof RateLimiterRes) {
			throw new EmailLimitExceededError({
				points: 'RATE_LIMITER_EMAIL_FLOWS_POINTS' in env ? Number(env['RATE_LIMITER_EMAIL_FLOWS_POINTS']) : undefined,
				duration:
					'RATE_LIMITER_EMAIL_FLOWS_DURATION' in env ? Number(env['RATE_LIMITER_EMAIL_FLOWS_DURATION']) : undefined,
				message:
					'RATE_LIMITER_EMAIL_FLOWS_ERROR_MESSAGE' in env
						? String(env['RATE_LIMITER_EMAIL_FLOWS_ERROR_MESSAGE'])
						: undefined,
			});
		}

		throw err;
	}
}
Repository: directus/directus
File: api/src/services/mail/rate-limiter.ts (TypeScript)

Code before:
import { useEnv } from '@directus/env';
import { RateLimiterQueue } from 'rate-limiter-flexible';
import { createRateLimiter } from '../../rate-limiter.js';
import { toBoolean } from '@directus/utils';
import { EmailLimitExceededError } from '@directus/errors';

let emailRateLimiterQueue: RateLimiterQueue | undefined;

const env = useEnv();

if (toBoolean(env['EMAIL_LIMITER_ENABLED']) === true) {
	emailRateLimiterQueue = new RateLimiterQueue(createRateLimiter('EMAIL_LIMITER'), {
		maxQueueSize: Number(env['EMAIL_LIMITER_QUEUE_SIZE']),
	});
}

export async function useEmailRateLimiterQueue() {
	if (!emailRateLimiterQueue) return;

	try {
		await emailRateLimiterQueue.removeTokens(1);
	} catch (err: unknown) {
		if (err instanceof Error) {
			throw new EmailLimitExceededError({
				points: 'EMAIL_LIMITER_POINTS' in env ? Number(env['EMAIL_LIMITER_POINTS']) : undefined,
				duration: 'EMAIL_LIMITER_DURATION' in env ? Number(env['EMAIL_LIMITER_DURATION']) : undefined,
				message: 'EMAIL_LIMITER_ERROR_MESSAGE' in env ? String(env['EMAIL_LIMITER_ERROR_MESSAGE']) : undefined,
			});
		}

		throw err;
	}
}


Review comment (suggestion):
Based on testing, this is triggered whenever the points are exhausted, rather than only when the queue is full. While this is expected behavior of `removeTokens`[^1] we should only be throwing our error when the queue is full.

ENV 
```
EMAIL_LIMITER_ENABLED=true
EMAIL_LIMITER_POINTS=2
EMAIL_LIMITER_DURATION=60
EMAIL_LIMITER_QUEUE_SIZE=50
```

[^1]:https://github.com/animir/node-rate-limiter-flexible/wiki/RateLimiterQueue#when-is-removetokens-method-rejected

Code after:
import { useEnv } from '@directus/env';
import { RateLimiterQueue } from 'rate-limiter-flexible';
import { createRateLimiter } from '../../rate-limiter.js';
import { toBoolean } from '@directus/utils';
import { EmailLimitExceededError } from '@directus/errors';

let emailRateLimiterQueue: RateLimiterQueue | undefined;

const env = useEnv();

if (toBoolean(env['RATE_LIMITER_EMAIL_ENABLED']) === true) {
	emailRateLimiterQueue = new RateLimiterQueue(createRateLimiter('RATE_LIMITER_EMAIL'), {
		maxQueueSize: Number(env['RATE_LIMITER_EMAIL_QUEUE_SIZE']),
	});
}

export async function useEmailRateLimiterQueue() {
	if (!emailRateLimiterQueue) return;

	try {
		await emailRateLimiterQueue.removeTokens(1);
	} catch (err: unknown) {
		if (err instanceof Error) {
			throw new EmailLimitExceededError({
				points: 'RATE_LIMITER_EMAIL_POINTS' in env ? Number(env['RATE_LIMITER_EMAIL_POINTS']) : undefined,
				duration: 'RATE_LIMITER_EMAIL_DURATION' in env ? Number(env['RATE_LIMITER_EMAIL_DURATION']) : undefined,
				message:
					'RATE_LIMITER_EMAIL_ERROR_MESSAGE' in env ? String(env['RATE_LIMITER_EMAIL_ERROR_MESSAGE']) : undefined,
			});
		}

		throw err;
	}
}
Repository: directus/directus
File: api/src/utils/get-schema.ts (TypeScript)

Code before:

		const existing = result.collections[field.collection]?.fields[field.field];
		const column = schemaOverview[field.collection]?.columns[field.field];
		const special = field.special ? toArray(field.special) : [];

		if (ALIAS_TYPES.some((type) => special.includes(type)) === false && !existing) continue;

		const type = (existing && getLocalType(column, { special })) || 'alias';
		let validation = field.validation ?? null;

		if (validation && typeof validation === 'string') validation = parseJSON(validation);

		result.collections[field.collection]!.fields[field.field] = {
			field: field.field,
			defaultValue: existing?.defaultValue ?? null,
			nullable: existing?.nullable ?? true,
			generated: existing?.generated ?? false,
			type: type,
			dbType: existing?.dbType || null,
			precision: existing?.precision || null,
			scale: existing?.scale || null,
			special: special,
			note: field.note,
			alias: existing?.alias ?? true,
			validation: (validation as Filter) ?? null,
			searchable: field.searchable ?? true,
		};
	}

	const relationsService = new RelationsService({ knex: database, schema: result });
	result.relations = await relationsService.readAll(undefined, undefined, true);

	return result;
}


Review comment (refactor):
We should cast `field.searchable` to boolean here. While PostgreSQL returns native boolean values, other databases like SQLite return integers (0/1) for boolean columns. This ensures consistent boolean behavior across all supported databases.

```suggestion
			searchable: toBoolean(field.searchable) ?? true,
```

Note: There's already a `toBoolean` utility in `@directus/utils` that handles this conversion consistently (checking for `true`, `1`, `'1'`, etc.).

Code after:
		const existing = result.collections[field.collection]?.fields[field.field];
		const column = schemaOverview[field.collection]?.columns[field.field];
		const special = field.special ? toArray(field.special) : [];

		if (ALIAS_TYPES.some((type) => special.includes(type)) === false && !existing) continue;

		const type = (existing && getLocalType(column, { special })) || 'alias';
		let validation = field.validation ?? null;

		if (validation && typeof validation === 'string') validation = parseJSON(validation);

		result.collections[field.collection]!.fields[field.field] = {
			field: field.field,
			defaultValue: existing?.defaultValue ?? null,
			nullable: existing?.nullable ?? true,
			generated: existing?.generated ?? false,
			type: type,
			dbType: existing?.dbType || null,
			precision: existing?.precision || null,
			scale: existing?.scale || null,
			special: special,
			note: field.note,
			alias: existing?.alias ?? true,
			validation: (validation as Filter) ?? null,
			searchable: toBoolean(field.searchable) ?? true,
		};
	}

	const relationsService = new RelationsService({ knex: database, schema: result });
	result.relations = await relationsService.readAll(undefined, undefined, true);

	return result;
}
Repository: directus/directus
File: app/src/modules/settings/routes/data-model/field-detail/field-detail-advanced/field-detail-advanced-schema.vue (Vue)

Code before:

const hasCreateUpdateTriggers = computed(() => {
	return ['uuid', 'date', 'time', 'dateTime', 'timestamp'].includes(type.value) && localType.value !== 'file';
});

const isAlias = computed(() => {
	return !fieldDetailStore.field.schema;
});

const isPrimaryKey = computed(() => {
	return fieldDetailStore.field.schema?.is_primary_key === true;
});

const isGenerated = computed(() => {
	return fieldDetailStore.field.schema?.is_generated;
});

const isSearchableType = computed(() => {
	// exclude alias fields (o2m, m2m, m2a) as they don't store searchable data
	if (type.value === 'alias') return false;

	// exclude relational fields except m2o, which stores foreign keys that are typically not useful for search
	if (localType.value && localType.value !== 'standard') return false;

	const searchableTypes = ['text', 'string', 'integer', 'bigInteger', 'float', 'decimal', 'uuid'];
	return searchableTypes.includes(type.value);
});

function useOnCreate() {
	const onCreateSpecials = ['uuid', 'user-created', 'role-created', 'date-created'];

	const onCreateOptions = computed(() => {
		if (type.value === 'uuid') {
			const options = [
				{
					text: t('do_nothing'),
					value: null,
				},
				{
					text: t('generate_and_save_uuid'),
					value: 'uuid',
				},
				{
					text: t('save_current_user_id'),
					value: 'user-created',
				},
				{
					text: t('save_current_user_role'),
					value: 'role-created',
				},
			];

Review comment (suggestion):
```suggestion
	return SEARCHABLE_TYPES.includes(type.value);
```

Code after:
const { onUpdateOptions, onUpdateValue } = useOnUpdate();

const hasCreateUpdateTriggers = computed(() => {
	return ['uuid', 'date', 'time', 'dateTime', 'timestamp'].includes(type.value) && localType.value !== 'file';
});

const isAlias = computed(() => {
	return !fieldDetailStore.field.schema;
});

const isPrimaryKey = computed(() => {
	return fieldDetailStore.field.schema?.is_primary_key === true;
});

const isGenerated = computed(() => {
	return fieldDetailStore.field.schema?.is_generated;
});

const isSearchableType = computed(() => {
	// exclude alias fields (o2m, m2m, m2a) as they don't store searchable data
	if (type.value === 'alias') return false;

	// exclude relational fields except m2o, which stores foreign keys that are typically not useful for search
	if (localType.value && localType.value !== 'standard') return false;

	return SEARCHABLE_TYPES.includes(type.value);
});

function useOnCreate() {
	const onCreateSpecials = ['uuid', 'user-created', 'role-created', 'date-created'];

	const onCreateOptions = computed(() => {
		if (type.value === 'uuid') {
			const options = [
				{
					text: t('do_nothing'),
					value: null,
				},
				{
					text: t('generate_and_save_uuid'),
					value: 'uuid',
				},
				{
					text: t('save_current_user_id'),
					value: 'user-created',
				},
				{
					text: t('save_current_user_role'),
					value: 'role-created',
				},
			];