UNPKG

@lucidcms/core

Version:

The core of the Lucid CMS. It's responsible for spinning up the API and serving the CMS.

1 lines 13.1 kB
{"version":3,"file":"custom-field.mjs","names":[],"sources":["../../../../src/libs/collection/custom-fields/custom-field.ts"],"sourcesContent":["import type {\n\tLucidBricksTable,\n\tSelect,\n\tServiceResponse,\n} from \"../../../types.js\";\nimport { copy } from \"../../i18n/index.js\";\nimport buildSchemaIndex from \"../helpers/build-schema-index.js\";\nimport prefixGeneratedColName from \"../helpers/prefix-generated-column-name.js\";\nimport type {\n\tCFConfig,\n\tCFProps,\n\tCFResponse,\n\tCustomFieldAiConfig,\n\tCustomFieldAiFormatResponse,\n\tCustomFieldErrorItem,\n\tCustomFieldGuidanceConfig,\n\tCustomFieldUserAiConfig,\n\tCustomFieldValidateResponse,\n\tFieldRelationRefTarget,\n\tFieldRelationValidationInput,\n\tFieldTypes,\n\tGetIndexDefinitionProps,\n\tGetSchemaDefinitionProps,\n\tIndexDefinition,\n\tSchemaDefinition,\n} from \"./types.js\";\nimport {\n\thasRuntimeConfig,\n\thasValidationConfig,\n\thasZodValidation,\n} from \"./utils/helpers.js\";\nimport { normalizeRelationValues } from \"./utils/normalize-relation-values.js\";\nimport zodSafeParse from \"./utils/zod-safe-parse.js\";\n\nabstract class CustomField<T extends FieldTypes> {\n\t/** Tree-table parent key when this field belongs to a nested tree scope. */\n\ttreeParent: string | null = null;\n\t/** Tab parent key used to control admin field grouping. */\n\ttabParent: string | null = null;\n\t/**\n\t * Structural parent key when this field sits directly inside a\n\t * section/collapsible. Structural parents only affect UI nesting and client\n\t * response shaping - storage is unaffected.\n\t */\n\tstructuralParent: string | null = null;\n\n\tabstract type: T;\n\tabstract key: string;\n\tabstract props?: CFProps<T>;\n\tabstract config: CFConfig<T>;\n\n\t/**\n\t * Field-level switches for shared validation phases.\n\t * Override in subclasses when a field should skip a shared phase.\n\t */\n\tprotected get sharedValidationFlags() {\n\t\treturn {\n\t\t\tskipValidation: false,\n\t\t\tskipRequiredValidation: false,\n\t\t\tskipZodValidation: false,\n\t\t};\n\t}\n\t/** Whether this field type supports Lucid AI features. */\n\tget supportsAi() {\n\t\treturn false;\n\t}\n\t/** Default guidance presets for fields that support Lucid AI features. */\n\tprotected get defaultAiGuidance(): CustomFieldGuidanceConfig[] {\n\t\treturn [];\n\t}\n\t/** Normalized field metadata used by Lucid AI features. */\n\tget aiConfig(): CustomFieldAiConfig {\n\t\tconst aiConfig = (\n\t\t\tthis.config as CFConfig<T> & {\n\t\t\t\tai?: CustomFieldUserAiConfig;\n\t\t\t}\n\t\t).ai;\n\t\tconst enabled =\n\t\t\tthis.supportsAi === true ? (aiConfig?.enabled ?? true) : false;\n\n\t\treturn {\n\t\t\tenabled,\n\t\t\tinstructions: aiConfig?.instructions,\n\t\t\tguidance: enabled ? (aiConfig?.guidance ?? this.defaultAiGuidance) : [],\n\t\t\tcontext: aiConfig?.context,\n\t\t};\n\t}\n\t/** JSON schema for AI generated values, when a field supports generation. */\n\tget jsonSchema(): Record<string, unknown> | null {\n\t\treturn null;\n\t}\n\t/** Public field display metadata. */\n\tget details(): CFConfig<T>[\"details\"] {\n\t\treturn this.config.details;\n\t}\n\t/** Normalizes input values before validation and persistence. */\n\tpublic normalizeInputValue(value: unknown): unknown {\n\t\treturn value;\n\t}\n\t/** Formats a single AI-generated locale value before it is returned. */\n\tpublic formatAiGeneratedValue(value: unknown): CustomFieldAiFormatResponse {\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tvalue,\n\t\t};\n\t}\n\t/** Whether this field should be processed with localization translations. */\n\tget localizedEnabled(): boolean {\n\t\tif (!hasRuntimeConfig(this.config)) return false;\n\t\treturn this.config.localized ?? false;\n\t}\n\t/** Default fallback value used while normalizing missing field input. */\n\tget defaultValue(): unknown {\n\t\tif (!hasRuntimeConfig(this.config)) return null;\n\t\tif (!Object.hasOwn(this.config, \"default\")) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this.config.default;\n\t}\n\t/** Shared error builders used by `validate*` checks. */\n\tget errors(): {\n\t\tfieldType: CustomFieldErrorItem;\n\t\trequired: CustomFieldErrorItem;\n\t\tzod: CustomFieldErrorItem;\n\t} {\n\t\treturn {\n\t\t\tfieldType: {\n\t\t\t\tcondition: (value: unknown) => value !== this.type,\n\t\t\t\tmessage: copy(\"server:core.fields.validation.type.mismatch\", {\n\t\t\t\t\tdata: {\n\t\t\t\t\t\treceived: \"unknown\",\n\t\t\t\t\t\texpected: this.config.type,\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t},\n\t\t\trequired: {\n\t\t\t\tcondition: (value: unknown) =>\n\t\t\t\t\tvalue === undefined || value === null || value === \"\",\n\t\t\t\tmessage: copy(\"server:core.fields.validation.required\"),\n\t\t\t},\n\t\t\tzod: {\n\t\t\t\tmessage: copy(\"server:core.fields.validation.invalid\"),\n\t\t\t},\n\t\t};\n\t}\n\t/**\n\t * Defines DB schema fragments for this field.\n\t *\n\t * If the foreign key references another custom field key, use\n\t * `prefixGeneratedColName(key)` for the referenced column.\n\t */\n\tabstract getSchemaDefinition(\n\t\tprops: GetSchemaDefinitionProps,\n\t): Awaited<ServiceResponse<SchemaDefinition>>;\n\t/** Defines generated indexes for this field's schema fragments. */\n\tpublic getIndexDefinitions(\n\t\tprops: GetIndexDefinitionProps,\n\t): IndexDefinition[] {\n\t\tif (!props.shouldIndex) return [];\n\n\t\tconst columns = props.columns\n\t\t\t.filter((column) => column.source === \"field\")\n\t\t\t.map((column) => column.name);\n\t\tif (columns.length === 0) return [];\n\n\t\tconst hasDocumentIdColumn = props.columns.some(\n\t\t\t(column) => column.name === \"document_id\",\n\t\t);\n\n\t\treturn columns.flatMap((column) => [\n\t\t\tbuildSchemaIndex({\n\t\t\t\tdb: props.db,\n\t\t\t\ttableName: props.table.name,\n\t\t\t\tcolumns: [column],\n\t\t\t\tsource: \"field\",\n\t\t\t}),\n\t\t\t...(hasDocumentIdColumn\n\t\t\t\t? [\n\t\t\t\t\t\tbuildSchemaIndex({\n\t\t\t\t\t\t\tdb: props.db,\n\t\t\t\t\t\t\ttableName: props.table.name,\n\t\t\t\t\t\t\tcolumns: [column, \"document_id\"],\n\t\t\t\t\t\t\tsource: \"field\",\n\t\t\t\t\t\t}),\n\t\t\t\t\t]\n\t\t\t\t: []),\n\t\t]);\n\t}\n\t/** Formats raw DB values into API response values for this field. */\n\tabstract formatResponseValue(value: unknown): CFResponse<T>[\"value\"];\n\t/** Serializes field values into relation-table row payloads when needed. */\n\tpublic serializeRelationFieldValue(\n\t\t_value: unknown,\n\t): Array<Record<string, unknown>> {\n\t\treturn [];\n\t}\n\t/**\n\t * Returns the unprefixed column name that stores this field's relation ID value\n\t * inside a relation-table row.\n\t */\n\tget relationValueColumn(): string | null {\n\t\treturn null;\n\t}\n\t/**\n\t * Extracts a single field value item from a relation-table row.\n\t */\n\tpublic extractRelationFieldValue(\n\t\trow: Select<LucidBricksTable>,\n\t): unknown | null {\n\t\tconst relationValueColumn = this.relationValueColumn;\n\t\tif (!relationValueColumn) return null;\n\n\t\tconst columnName = prefixGeneratedColName(relationValueColumn);\n\t\treturn row[columnName] ?? null;\n\t}\n\t/**\n\t * Returns relation field values grouped for shared validation fetches.\n\t */\n\tpublic getRelationFieldValidationInput(\n\t\tvalue: unknown,\n\t): FieldRelationValidationInput {\n\t\treturn {\n\t\t\tdefault: normalizeRelationValues(this.normalizeInputValue(value)),\n\t\t};\n\t}\n\t/**\n\t * Returns any relation field ref targets that cannot be derived from schema\n\t * foreign keys.\n\t */\n\tpublic getRelationFieldRefTargets(\n\t\t_row: Select<LucidBricksTable>,\n\t): FieldRelationRefTarget[] {\n\t\treturn [];\n\t}\n\t/** Runs field-specific validation once shared checks have passed. */\n\tabstract uniqueValidation(\n\t\tvalue: unknown,\n\t\trefData?: unknown,\n\t): CustomFieldValidateResponse;\n\t/** Runs shared validation and then delegates to `uniqueValidation`. */\n\tpublic validate(props: {\n\t\ttype: FieldTypes;\n\t\tvalue: unknown;\n\t\trefData?: unknown;\n\t}): CustomFieldValidateResponse {\n\t\tif (this.sharedValidationFlags.skipValidation) return { valid: true };\n\n\t\tconst fieldTypeRes = this.validateFieldType(props.type);\n\t\tif (fieldTypeRes.valid === false) return fieldTypeRes;\n\n\t\tconst normalizedValue = this.normalizeInputValue(props.value);\n\n\t\tconst requiredRes = this.validateRequired(normalizedValue);\n\t\tif (!requiredRes.valid) return requiredRes;\n\n\t\tif (\n\t\t\tnormalizedValue === null ||\n\t\t\tnormalizedValue === undefined ||\n\t\t\tnormalizedValue === \"\"\n\t\t) {\n\t\t\treturn { valid: true };\n\t\t}\n\n\t\tconst zodRes = this.validateZodConstraint(normalizedValue);\n\t\tif (!zodRes.valid) return zodRes;\n\n\t\treturn this.uniqueValidation(normalizedValue, props.refData);\n\t}\n\t/** Validates that the submitted field type matches this instance type. */\n\tprivate validateFieldType(type: FieldTypes) {\n\t\tif (this.errors.fieldType.condition?.(type)) {\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\tmessage: copy(\"server:core.fields.validation.type.mismatch\", {\n\t\t\t\t\tdata: {\n\t\t\t\t\t\treceived: type,\n\t\t\t\t\t\texpected: this.config.type,\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t};\n\t\t}\n\t\treturn { valid: true };\n\t}\n\t/** Applies shared required checks where supported. */\n\tprivate validateRequired(value: unknown): CustomFieldValidateResponse {\n\t\tif (this.sharedValidationFlags.skipRequiredValidation) {\n\t\t\treturn { valid: true };\n\t\t}\n\t\tif (!hasValidationConfig(this.config)) {\n\t\t\treturn { valid: true };\n\t\t}\n\n\t\tif (\n\t\t\tthis.config.validation?.required === true &&\n\t\t\tthis.errors.required.condition?.(value)\n\t\t) {\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\tmessage: this.errors.required.message,\n\t\t\t};\n\t\t}\n\t\treturn { valid: true };\n\t}\n\t/** Applies optional zod checks when the field exposes a zod validator. */\n\tprivate validateZodConstraint(value: unknown): CustomFieldValidateResponse {\n\t\tif (this.sharedValidationFlags.skipZodValidation) {\n\t\t\treturn { valid: true };\n\t\t}\n\t\tif (!hasValidationConfig(this.config)) {\n\t\t\treturn { valid: true };\n\t\t}\n\n\t\tif (\n\t\t\t!hasZodValidation(this.config.validation) ||\n\t\t\t!this.config.validation.zod\n\t\t)\n\t\t\treturn { valid: true };\n\n\t\treturn zodSafeParse(value, this.config.validation.zod);\n\t}\n}\n\nexport default CustomField;\n"],"mappings":"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"}