@easy-cms/core 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -254,6 +254,58 @@ declare function consoleEmail(options?: {
254
254
  })[];
255
255
  };
256
256
 
257
+ interface ConfigIssue {
258
+ /** Where the problem is, e.g. `collections.posts.fields.slug.from`. */
259
+ readonly path: string;
260
+ readonly message: string;
261
+ /** How to fix it. */
262
+ readonly hint?: string;
263
+ }
264
+ declare class ConfigError extends Error {
265
+ readonly issues: readonly ConfigIssue[];
266
+ constructor(issues: readonly ConfigIssue[]);
267
+ static format(issues: readonly ConfigIssue[]): string;
268
+ }
269
+ /** Base class for errors that map to an HTTP status in the REST API. */
270
+ declare class EasyCMSError extends Error {
271
+ readonly status: number;
272
+ constructor(message: string, status: number);
273
+ }
274
+ interface FieldError {
275
+ /** Dotted path, e.g. `links.0.url`. */
276
+ readonly field: string;
277
+ readonly message: string;
278
+ }
279
+ declare class ValidationError extends EasyCMSError {
280
+ readonly errors: readonly FieldError[];
281
+ constructor(collection: string, errors: readonly FieldError[]);
282
+ }
283
+ declare class NotFoundError extends EasyCMSError {
284
+ constructor(collection: string, id: unknown);
285
+ }
286
+ /** A bad `where`, `sort` or unknown collection/global. */
287
+ declare class QueryError extends EasyCMSError {
288
+ constructor(message: string);
289
+ }
290
+ /** The database schema does not match the config (pending or missing migrations). */
291
+ declare class SchemaError extends EasyCMSError {
292
+ constructor(message: string);
293
+ }
294
+ /** Not logged in, or the session is invalid. */
295
+ declare class UnauthorizedError extends EasyCMSError {
296
+ constructor(message?: string);
297
+ }
298
+ /** Logged in, but not allowed to do this. */
299
+ declare class ForbiddenError extends EasyCMSError {
300
+ constructor(message?: string);
301
+ }
302
+ declare class TooManyRequestsError extends EasyCMSError {
303
+ constructor(message: string);
304
+ }
305
+ declare class PayloadTooLargeError extends EasyCMSError {
306
+ constructor(message: string);
307
+ }
308
+
257
309
  /** What a preview token opens: one document of a collection, or one global. */
258
310
  type PreviewTarget = {
259
311
  readonly collection: string;
@@ -533,7 +585,13 @@ type FieldValue<F extends Field, C extends Config = Config> = F extends {
533
585
  } ? FieldsValue<Sub, C> : F extends {
534
586
  readonly type: 'blocks';
535
587
  readonly blocks: infer B extends readonly Block[];
536
- } ? BlockValue<B[number], C>[] : never;
588
+ } ? BlockValue<B[number], C>[] : CustomValue<F>;
589
+ /** The value of a field of an added type (`CustomFieldTypes`). */
590
+ type CustomValue<F> = F extends {
591
+ readonly type: infer T extends keyof CustomFieldTypes;
592
+ } ? CustomFieldTypes[T] extends {
593
+ value: infer V;
594
+ } ? V : unknown : never;
537
595
  /** One row of a `blocks` field: the block's fields plus `id` and `blockType` (distributes over kinds). */
538
596
  type BlockValue<B, C extends Config> = B extends {
539
597
  readonly slug: infer S;
@@ -1082,7 +1140,31 @@ interface FieldAdmin {
1082
1140
  readonly component?: AdminComponent;
1083
1141
  /** Shown below the field, e.g. a length meter or a preview. */
1084
1142
  readonly after?: readonly AdminComponent[];
1143
+ /** Shows the value in the admin's lists (e.g. a color swatch). Default: the plain value. */
1144
+ readonly cell?: AdminComponent;
1085
1145
  }
1146
+ /**
1147
+ * Field types that packages add (`fieldTypes` in the config), for the types inferred from the
1148
+ * config. A package declares its own by augmenting this interface:
1149
+ *
1150
+ * ```ts
1151
+ * declare module '@easy-cms/core' {
1152
+ * interface CustomFieldTypes {
1153
+ * color: { value: string; options: { readonly presets?: readonly string[] } }
1154
+ * }
1155
+ * }
1156
+ * ```
1157
+ */
1158
+ interface CustomFieldTypes {
1159
+ }
1160
+ /** A field of a type from `CustomFieldTypes`: the common options, plus the type's own. */
1161
+ type CustomField = {
1162
+ [K in keyof CustomFieldTypes]: BaseField<K & string, CustomFieldTypes[K] extends {
1163
+ value: infer V;
1164
+ } ? V : unknown> & (CustomFieldTypes[K] extends {
1165
+ options: infer O;
1166
+ } ? O : unknown);
1167
+ }[keyof CustomFieldTypes];
1086
1168
  interface BaseField<TType extends string, TValue> {
1087
1169
  readonly type: TType;
1088
1170
  readonly name: string;
@@ -1104,6 +1186,8 @@ interface BaseField<TType extends string, TValue> {
1104
1186
  readonly position?: 'sidebar';
1105
1187
  /** Custom admin components for this field. */
1106
1188
  readonly admin?: FieldAdmin;
1189
+ /** Set by Easy CMS on fields of an added type (`fieldTypes`): the type's name, e.g. `color`. */
1190
+ readonly customType?: string;
1107
1191
  }
1108
1192
  interface TextField extends BaseField<'text', string> {
1109
1193
  readonly minLength?: number;
@@ -1220,10 +1304,48 @@ declare function mimeAllowedBy(mimeType: string, patterns: readonly string[]): b
1220
1304
  declare const hasRows: (field: Field) => field is ArrayField | BlocksField;
1221
1305
  /** The fields of one row of an `array` or `blocks` field (`undefined` for an unknown block). */
1222
1306
  declare function rowFields(field: ArrayField | BlocksField, row: unknown): readonly Field[] | undefined;
1223
- type Field = TextField | TextareaField | NumberField | BooleanField | DateField | EmailField | JsonField | SelectField | SlugField | RichTextField | UploadField | RelationshipField | ArrayField | GroupField | BlocksField;
1307
+ type Field = CustomField | TextField | TextareaField | NumberField | BooleanField | DateField | EmailField | JsonField | SelectField | SlugField | RichTextField | UploadField | RelationshipField | ArrayField | GroupField | BlocksField;
1224
1308
  type FieldType = Field['type'];
1225
1309
  declare const FIELD_TYPES: readonly ["text", "textarea", "number", "boolean", "date", "email", "json", "select", "slug", "richText", "upload", "relationship", "array", "group", "blocks"];
1226
1310
 
1311
+ /** Built-in types a field type can be stored as. */
1312
+ declare const FIELD_TYPE_BASES: readonly ["text", "textarea", "email", "number", "boolean", "date", "json"];
1313
+ type FieldTypeBase = (typeof FIELD_TYPE_BASES)[number];
1314
+ /** What a field type's `validate` gets besides the value: the field, with its own options. */
1315
+ interface FieldTypeValidateContext extends FieldValidateContext {
1316
+ readonly field: Readonly<Record<string, unknown>> & {
1317
+ readonly name: string;
1318
+ };
1319
+ }
1320
+ /**
1321
+ * A field type a package adds, e.g. `color`: stored, queried and translated like its `base`
1322
+ * type, with its own checks and admin components. List it in the config's `fieldTypes`.
1323
+ */
1324
+ interface FieldTypeDefinition {
1325
+ /** The `type` fields use, e.g. `color`. Lowercase; not one of the built-in types. */
1326
+ readonly name: string;
1327
+ /** How values are stored and queried: one of the built-in scalar types. */
1328
+ readonly base: FieldTypeBase;
1329
+ /** Checks a value (not empty ones; `required` covers those). Return `true` or a message. */
1330
+ readonly validate?: (value: unknown, ctx: FieldTypeValidateContext) => true | string | Promise<true | string>;
1331
+ /** Checks a field's own options when the config loads; return a message for a mistake. */
1332
+ readonly checkOptions?: (field: Readonly<Record<string, unknown>>) => string | undefined;
1333
+ readonly admin?: {
1334
+ /** The input: a Web Component from `module`. */
1335
+ readonly component?: string;
1336
+ /** Shown in the admin's lists. */
1337
+ readonly cell?: string;
1338
+ /** The admin module (package export or file) that defines the components. */
1339
+ readonly module?: string;
1340
+ /** The field options its components get (as their `options`), e.g. `['presets']`. */
1341
+ readonly props?: readonly string[];
1342
+ };
1343
+ /** The value's TypeScript type for `generate:types`. Default: the base type's. */
1344
+ readonly typescript?: string;
1345
+ }
1346
+ /** Declares a field type; returns it unchanged. */
1347
+ declare function defineFieldType(definition: FieldTypeDefinition): FieldTypeDefinition;
1348
+
1227
1349
  declare const WEBHOOK_EVENTS: readonly ["create", "update", "delete", "publish", "unpublish", "draft"];
1228
1350
  /**
1229
1351
  * `create`, `update`, `delete`: the stored document changed. `publish` / `unpublish`: its status
@@ -1621,6 +1743,11 @@ interface Config {
1621
1743
  readonly endpoints?: readonly Endpoint[];
1622
1744
  /** Extra `easy-cms <name>` CLI commands, e.g. from plugins. */
1623
1745
  readonly commands?: readonly CliCommand[];
1746
+ /**
1747
+ * Field types from packages, e.g. `color` from `@easy-cms/fields`: fields then use
1748
+ * `type: 'color'`. See `defineFieldType`.
1749
+ */
1750
+ readonly fieldTypes?: readonly FieldTypeDefinition[];
1624
1751
  /**
1625
1752
  * API keys for scripts and other apps (`Authorization: Bearer ecms_…`), managed in the admin
1626
1753
  * under Settings. A key acts as its owner, limited to the collections and operations it lists.
@@ -1647,6 +1774,7 @@ interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes'
1647
1774
  readonly globals: readonly GlobalConfig[];
1648
1775
  readonly endpoints: readonly Endpoint[];
1649
1776
  readonly commands: readonly CliCommand[];
1777
+ readonly fieldTypes: readonly FieldTypeDefinition[];
1650
1778
  }
1651
1779
  /**
1652
1780
  * Declares the Easy CMS config. Returns it unchanged, keeping literal types
@@ -1720,58 +1848,6 @@ declare function copyDatabase(source: Database, target: Database, options?: {
1720
1848
  onTable?: (progress: CopyProgress) => void;
1721
1849
  }): Promise<CopyResult>;
1722
1850
 
1723
- interface ConfigIssue {
1724
- /** Where the problem is, e.g. `collections.posts.fields.slug.from`. */
1725
- readonly path: string;
1726
- readonly message: string;
1727
- /** How to fix it. */
1728
- readonly hint?: string;
1729
- }
1730
- declare class ConfigError extends Error {
1731
- readonly issues: readonly ConfigIssue[];
1732
- constructor(issues: readonly ConfigIssue[]);
1733
- static format(issues: readonly ConfigIssue[]): string;
1734
- }
1735
- /** Base class for errors that map to an HTTP status in the REST API. */
1736
- declare class EasyCMSError extends Error {
1737
- readonly status: number;
1738
- constructor(message: string, status: number);
1739
- }
1740
- interface FieldError {
1741
- /** Dotted path, e.g. `links.0.url`. */
1742
- readonly field: string;
1743
- readonly message: string;
1744
- }
1745
- declare class ValidationError extends EasyCMSError {
1746
- readonly errors: readonly FieldError[];
1747
- constructor(collection: string, errors: readonly FieldError[]);
1748
- }
1749
- declare class NotFoundError extends EasyCMSError {
1750
- constructor(collection: string, id: unknown);
1751
- }
1752
- /** A bad `where`, `sort` or unknown collection/global. */
1753
- declare class QueryError extends EasyCMSError {
1754
- constructor(message: string);
1755
- }
1756
- /** The database schema does not match the config (pending or missing migrations). */
1757
- declare class SchemaError extends EasyCMSError {
1758
- constructor(message: string);
1759
- }
1760
- /** Not logged in, or the session is invalid. */
1761
- declare class UnauthorizedError extends EasyCMSError {
1762
- constructor(message?: string);
1763
- }
1764
- /** Logged in, but not allowed to do this. */
1765
- declare class ForbiddenError extends EasyCMSError {
1766
- constructor(message?: string);
1767
- }
1768
- declare class TooManyRequestsError extends EasyCMSError {
1769
- constructor(message: string);
1770
- }
1771
- declare class PayloadTooLargeError extends EasyCMSError {
1772
- constructor(message: string);
1773
- }
1774
-
1775
1851
  type Localization = Required<LocalizationConfig>;
1776
1852
 
1777
1853
  type Data = Record<string, unknown>;
@@ -1934,11 +2010,14 @@ interface AdminField {
1934
2010
  localized?: boolean;
1935
2011
  /** Shown in the edit page's side panel. */
1936
2012
  position?: 'sidebar';
1937
- /** Components from admin modules: instead of the input, and below the field. */
2013
+ /** Components from admin modules: instead of the input, below the field, and in lists. */
1938
2014
  admin?: {
1939
2015
  component?: AdminComponentRef;
1940
2016
  after?: AdminComponentRef[];
2017
+ cell?: AdminComponentRef;
1941
2018
  };
2019
+ /** A field of an added type (`fieldTypes`), e.g. `color`; `type` is the base it's stored as. */
2020
+ customType?: string;
1942
2021
  }
1943
2022
  interface AdminCollection {
1944
2023
  slug: string;
@@ -2042,4 +2121,4 @@ declare const BUILTIN_COLLECTIONS: readonly ["users", "media"];
2042
2121
  /** Checks a config and returns every problem found. An empty array means it is valid. */
2043
2122
  declare function validateConfig(config: Config): ConfigIssue[];
2044
2123
 
2045
- export { ADMIN_ICONS, API_KEYS, API_KEY_GLOBAL_OPERATIONS, API_KEY_OPERATIONS, type Access, type AccessArgs, type AccessOptions, type AdminBrand, type AdminCollection, type AdminComponent, type AdminComponentRef, type AdminConfig, type AdminField, type AdminGlobal, type AdminIcon, type AdminLocale, type AdminSchema, type AfterChangeHook, type AfterDeleteHook, type AfterReadHook, type ApiKeyContext, type ApiKeyGlobalOperation, type ApiKeyOperation, type ApiKeyPermissions, type ArrayField, Auth, type AuthConfig, type AuthUser, BUILTIN_COLLECTIONS, type BeforeChangeHook, type BeforeDeleteHook, type BeforeValidateHook, type Block, type BlocksField, type BooleanField, type BuiltinApiKeysCollection, type BuiltinMediaCollection, type BuiltinUsersCollection, type BundleHints, CONFIG_FILE_NAMES, CSRF_COOKIE, CSRF_HEADER, type CliCommand, type CollectionAccess, type CollectionAdmin, type CollectionConfig, type CollectionDocument, type CollectionHooks, type CollectionListAdmin, type CollectionSlug, type Config, ConfigError, type ConfigIssue, type ContainerAdmin, type CopyProgress, type CopyResult, type CreateEasyCMSOptions, type CreateInput, type CreateMigrationResult, DEFAULT_ADMIN_PATH, DEFAULT_API_PATH, DEFAULT_DEPTH, DEFAULT_MAX_FILE_SIZE, DEFAULT_MAX_VERSIONS, DEFAULT_PASSWORD_EMAILS, DEFAULT_TOKEN_EXPIRATION, type Database, type DatabaseAdapter, type DatabaseInitArgs, type DatabaseTransfer, type DateField, type DepthOptions, type DocumentOf, EXTENSIONS, EasyCMS, EasyCMSError, type EmailAdapter, type EmailField, type EmailMessage, type Endpoint, type EndpointRequest, FIELD_TYPES, type Field, type FieldAccess, type FieldAccessFn, type FieldAdmin, type FieldError, type FieldType, type FieldValidate, type FieldValidateContext, type FieldValue, type FieldsInput, type FieldsValue, type FilterOptions, type FilterOptionsArgs, type FindArgs, type FindOptions, ForbiddenError, type GlobalAccess, type GlobalConfig, type GlobalDocument, type GlobalHooks, type GlobalInput, type GlobalSlug, type GroupField, type ID, INTERNAL_COLLECTIONS, type ImageSize, type InferCollection, type InferGlobal, type JsonField, type Label, type LivePreview, type LoadConfigOptions, type LocalStorageOptions, type LocalizationConfig, type Logger, type LoginArgs, MAX_DEPTH, MEDIA, MIN_PASSWORD_LENGTH, MIN_SECRET_LENGTH, type MediaDocument, type MediaSize, type MigrationInfo, NotFoundError, type NumberField, type Operation, type PaginatedDocs, type PasswordEmail, type PasswordEmailArgs, type PasswordEmailFn, type PasswordLinkOptions, PayloadTooLargeError, type Plugin, type PluginTypes, type PreviewTarget, type PreviewURL, QueryError, type RawDocument, type ReadOptions, type RelationshipField, type ResolvedConfig, type RestHandler, type RestHandlerOptions, type RichTextDocument, type RichTextField, type Role, type RoutesConfig, SESSION_COOKIE, type ScheduledJob, SchemaError, type SchemaMode, type SelectField, type SelectOption, type Session, type SlugField, type SlugOf, type StorageAdapter, type StoredFile, type TextField, type TextareaField, TooManyRequestsError, type TypedPlugin, UnauthorizedError, type UpdateInput, type UpdateOptions, type UploadConfig, type UploadField, ValidationError, type Version, type VersionSummary, type VersionsConfig, WEBHOOK_EVENTS, type WebhookConfig, type WebhookEvent, type WebhookPayload, type Where, type WhereOperators, anyone, applyDefaults, applyPlugins, configSignature, consoleEmail, consoleLogger, copyDatabase, createEasyCMS, createRestHandler, createRootEndpointHandler, defineConfig, definePlugin, fillMissing, findConfigFile, generateSlugs, generateTypes, hasRows, hashPassword, imageDimensions, importConfig, isAdmin, isHasMany, isLoggedIn, keyAllows, loadConfig, localStorage, mergeForUpdate, mimeAllowed, mimeAllowedBy, parseId, populate, readCookie, resolveAdminModule, resolveConfig, rowFields, silentLogger, singularize, slugify, sniffMimeType, validateConfig, validateFields, verifyPassword };
2124
+ export { ADMIN_ICONS, API_KEYS, API_KEY_GLOBAL_OPERATIONS, API_KEY_OPERATIONS, type Access, type AccessArgs, type AccessOptions, type AdminBrand, type AdminCollection, type AdminComponent, type AdminComponentRef, type AdminConfig, type AdminField, type AdminGlobal, type AdminIcon, type AdminLocale, type AdminSchema, type AfterChangeHook, type AfterDeleteHook, type AfterReadHook, type ApiKeyContext, type ApiKeyGlobalOperation, type ApiKeyOperation, type ApiKeyPermissions, type ArrayField, Auth, type AuthConfig, type AuthUser, BUILTIN_COLLECTIONS, type BeforeChangeHook, type BeforeDeleteHook, type BeforeValidateHook, type Block, type BlocksField, type BooleanField, type BuiltinApiKeysCollection, type BuiltinMediaCollection, type BuiltinUsersCollection, type BundleHints, CONFIG_FILE_NAMES, CSRF_COOKIE, CSRF_HEADER, type CliCommand, type CollectionAccess, type CollectionAdmin, type CollectionConfig, type CollectionDocument, type CollectionHooks, type CollectionListAdmin, type CollectionSlug, type Config, ConfigError, type ConfigIssue, type ContainerAdmin, type CopyProgress, type CopyResult, type CreateEasyCMSOptions, type CreateInput, type CreateMigrationResult, type CustomField, type CustomFieldTypes, DEFAULT_ADMIN_PATH, DEFAULT_API_PATH, DEFAULT_DEPTH, DEFAULT_MAX_FILE_SIZE, DEFAULT_MAX_VERSIONS, DEFAULT_PASSWORD_EMAILS, DEFAULT_TOKEN_EXPIRATION, type Database, type DatabaseAdapter, type DatabaseInitArgs, type DatabaseTransfer, type DateField, type DepthOptions, type DocumentOf, EXTENSIONS, EasyCMS, EasyCMSError, type EmailAdapter, type EmailField, type EmailMessage, type Endpoint, type EndpointRequest, FIELD_TYPES, FIELD_TYPE_BASES, type Field, type FieldAccess, type FieldAccessFn, type FieldAdmin, type FieldError, type FieldType, type FieldTypeBase, type FieldTypeDefinition, type FieldTypeValidateContext, type FieldValidate, type FieldValidateContext, type FieldValue, type FieldsInput, type FieldsValue, type FilterOptions, type FilterOptionsArgs, type FindArgs, type FindOptions, ForbiddenError, type GlobalAccess, type GlobalConfig, type GlobalDocument, type GlobalHooks, type GlobalInput, type GlobalSlug, type GroupField, type ID, INTERNAL_COLLECTIONS, type ImageSize, type InferCollection, type InferGlobal, type JsonField, type Label, type LivePreview, type LoadConfigOptions, type LocalStorageOptions, type LocalizationConfig, type Logger, type LoginArgs, MAX_DEPTH, MEDIA, MIN_PASSWORD_LENGTH, MIN_SECRET_LENGTH, type MediaDocument, type MediaSize, type MigrationInfo, NotFoundError, type NumberField, type Operation, type PaginatedDocs, type PasswordEmail, type PasswordEmailArgs, type PasswordEmailFn, type PasswordLinkOptions, PayloadTooLargeError, type Plugin, type PluginTypes, type PreviewTarget, type PreviewURL, QueryError, type RawDocument, type ReadOptions, type RelationshipField, type ResolvedConfig, type RestHandler, type RestHandlerOptions, type RichTextDocument, type RichTextField, type Role, type RoutesConfig, SESSION_COOKIE, type ScheduledJob, SchemaError, type SchemaMode, type SelectField, type SelectOption, type Session, type SlugField, type SlugOf, type StorageAdapter, type StoredFile, type TextField, type TextareaField, TooManyRequestsError, type TypedPlugin, UnauthorizedError, type UpdateInput, type UpdateOptions, type UploadConfig, type UploadField, ValidationError, type Version, type VersionSummary, type VersionsConfig, WEBHOOK_EVENTS, type WebhookConfig, type WebhookEvent, type WebhookPayload, type Where, type WhereOperators, anyone, applyDefaults, applyPlugins, configSignature, consoleEmail, consoleLogger, copyDatabase, createEasyCMS, createRestHandler, createRootEndpointHandler, defineConfig, defineFieldType, definePlugin, fillMissing, findConfigFile, generateSlugs, generateTypes, hasRows, hashPassword, imageDimensions, importConfig, isAdmin, isHasMany, isLoggedIn, keyAllows, loadConfig, localStorage, mergeForUpdate, mimeAllowed, mimeAllowedBy, parseId, populate, readCookie, resolveAdminModule, resolveConfig, rowFields, silentLogger, singularize, slugify, sniffMimeType, validateConfig, validateFields, verifyPassword };
package/dist/index.js CHANGED
@@ -1669,6 +1669,126 @@ var Mailer = class {
1669
1669
  }
1670
1670
  };
1671
1671
 
1672
+ // src/field-types.ts
1673
+ var FIELD_TYPE_BASES = [
1674
+ "text",
1675
+ "textarea",
1676
+ "email",
1677
+ "number",
1678
+ "boolean",
1679
+ "date",
1680
+ "json"
1681
+ ];
1682
+ function defineFieldType(definition) {
1683
+ return definition;
1684
+ }
1685
+ var NAME = /^[a-z][a-zA-Z0-9]*$/;
1686
+ function fieldTypeModules(config) {
1687
+ const list = config.fieldTypes;
1688
+ if (!Array.isArray(list)) return [];
1689
+ return list.map((t) => t?.admin?.module).filter((m) => typeof m === "string");
1690
+ }
1691
+ function applyFieldTypes(config) {
1692
+ const issues = [];
1693
+ const list = config.fieldTypes;
1694
+ if (list === void 0) return { config, issues };
1695
+ if (!Array.isArray(list)) {
1696
+ issues.push({ path: "fieldTypes", message: "must be an array of field types" });
1697
+ return { config, issues };
1698
+ }
1699
+ const types = /* @__PURE__ */ new Map();
1700
+ for (const [i, type] of list.entries()) {
1701
+ const path = `fieldTypes[${i}]`;
1702
+ if (!type || typeof type.name !== "string" || !NAME.test(type.name)) {
1703
+ issues.push({ path: `${path}.name`, message: 'must be a lowercase name like "color"' });
1704
+ } else if (FIELD_TYPES.includes(type.name)) {
1705
+ issues.push({ path: `${path}.name`, message: `"${type.name}" is a built-in field type` });
1706
+ } else if (types.has(type.name)) {
1707
+ issues.push({ path: `${path}.name`, message: `"${type.name}" is listed twice` });
1708
+ } else if (!FIELD_TYPE_BASES.includes(type.base)) {
1709
+ issues.push({
1710
+ path: `${path}.base`,
1711
+ message: `must be one of ${FIELD_TYPE_BASES.join(", ")}`
1712
+ });
1713
+ } else {
1714
+ types.set(type.name, type);
1715
+ }
1716
+ }
1717
+ if (types.size === 0) return { config, issues };
1718
+ const component = (tag, field, props) => {
1719
+ if (!tag) return void 0;
1720
+ const options = Object.fromEntries(
1721
+ (props ?? []).filter((key2) => field[key2] !== void 0).map((key2) => [key2, field[key2]])
1722
+ );
1723
+ return Object.keys(options).length ? { tag, props: options } : tag;
1724
+ };
1725
+ const convert = (fields, path) => fields.map((field, i) => {
1726
+ const here = `${path}[${i}]`;
1727
+ const own = field;
1728
+ const type = types.get(own.type);
1729
+ if (!type) {
1730
+ if ((field.type === "group" || field.type === "array") && Array.isArray(field.fields))
1731
+ return { ...field, fields: convert(field.fields, `${here}.fields`) };
1732
+ if (field.type === "blocks" && Array.isArray(field.blocks))
1733
+ return {
1734
+ ...field,
1735
+ blocks: field.blocks.map((block, b) => ({
1736
+ ...block,
1737
+ fields: convert(block.fields, `${here}.blocks[${b}].fields`)
1738
+ }))
1739
+ };
1740
+ return field;
1741
+ }
1742
+ const problem = type.checkOptions?.(own);
1743
+ if (problem) issues.push({ path: here, message: problem });
1744
+ const ownValidate = own.validate;
1745
+ const check = type.validate;
1746
+ const admin = own.admin ?? {};
1747
+ const input = admin.component ?? component(type.admin?.component, own, type.admin?.props);
1748
+ const cell = admin.cell ?? component(type.admin?.cell, own, type.admin?.props);
1749
+ return {
1750
+ ...own,
1751
+ type: type.base,
1752
+ customType: type.name,
1753
+ ...check || ownValidate ? {
1754
+ validate: async (value, ctx) => {
1755
+ const empty = value === null || value === void 0 || value === "";
1756
+ if (check && !empty) {
1757
+ const result = await check(value, { ...ctx, field: own });
1758
+ if (result !== true) return result;
1759
+ }
1760
+ return ownValidate ? ownValidate(value, ctx) : true;
1761
+ }
1762
+ } : {},
1763
+ ...input || cell ? {
1764
+ admin: {
1765
+ ...admin,
1766
+ ...input ? { component: input } : {},
1767
+ ...cell ? { cell } : {}
1768
+ }
1769
+ } : {}
1770
+ };
1771
+ });
1772
+ return {
1773
+ config: {
1774
+ ...config,
1775
+ ...config.collections ? {
1776
+ collections: config.collections.map((c, i) => ({
1777
+ ...c,
1778
+ fields: convert(c.fields, `collections[${i}].fields`)
1779
+ }))
1780
+ } : {},
1781
+ ...config.globals ? {
1782
+ globals: config.globals.map((g, i) => ({
1783
+ ...g,
1784
+ fields: convert(g.fields, `globals[${i}].fields`)
1785
+ }))
1786
+ } : {}
1787
+ },
1788
+ issues
1789
+ };
1790
+ }
1791
+
1672
1792
  // src/load-config.ts
1673
1793
  import { existsSync } from "fs";
1674
1794
  import { resolve } from "path";
@@ -2326,9 +2446,10 @@ function validateField(field, path, siblings, collectionSlugs, add) {
2326
2446
  if (typeof admin !== "object" || admin === null) {
2327
2447
  add(`${path}.admin`, "must be an object", "e.g. admin: { component: 'ecms-color-picker' }");
2328
2448
  } else {
2329
- const { component, after } = admin;
2449
+ const { component, after, cell } = admin;
2330
2450
  if (component !== void 0) validateComponent(component, `${path}.admin.component`, add);
2331
2451
  if (after !== void 0) validateComponents(after, `${path}.admin.after`, add);
2452
+ if (cell !== void 0) validateComponent(cell, `${path}.admin.cell`, add);
2332
2453
  }
2333
2454
  }
2334
2455
  switch (field.type) {
@@ -2593,6 +2714,15 @@ async function applyPlugins(input) {
2593
2714
  if (typeof plugin !== "function") break;
2594
2715
  config = await plugin(config);
2595
2716
  }
2717
+ const modules = fieldTypeModules(config);
2718
+ if (modules.length > 0)
2719
+ config = {
2720
+ ...config,
2721
+ admin: {
2722
+ ...config.admin,
2723
+ modules: [.../* @__PURE__ */ new Set([...config.admin?.modules ?? [], ...modules])]
2724
+ }
2725
+ };
2596
2726
  return config;
2597
2727
  }
2598
2728
  async function resolveConfig(input) {
@@ -2601,7 +2731,9 @@ async function resolveConfig(input) {
2601
2731
  if (typeof config === "object" && config !== null && Array.isArray(config.collections ?? [])) {
2602
2732
  config = withMedia(withUsers(config));
2603
2733
  }
2604
- const issues = validateConfig(config);
2734
+ const applied = applyFieldTypes(config);
2735
+ config = applied.config;
2736
+ const issues = [...applied.issues, ...validateConfig(config)];
2605
2737
  if (issues.length > 0) throw new ConfigError(issues);
2606
2738
  const { plugins: _plugins, ...rest } = config;
2607
2739
  const result = {
@@ -2649,7 +2781,8 @@ async function resolveConfig(input) {
2649
2781
  ],
2650
2782
  globals: config.globals ?? [],
2651
2783
  endpoints: config.endpoints ?? [],
2652
- commands: config.commands ?? []
2784
+ commands: config.commands ?? [],
2785
+ fieldTypes: config.fieldTypes ?? []
2653
2786
  };
2654
2787
  resolved.add(result);
2655
2788
  return result;
@@ -4740,12 +4873,14 @@ async function serializeFields(fields, update, localized) {
4740
4873
  if (!await update.allows(field)) f.readOnly = true;
4741
4874
  if (field.localized && localized) f.localized = true;
4742
4875
  if (field.position === "sidebar") f.position = "sidebar";
4743
- if (field.admin?.component || field.admin?.after?.length) {
4876
+ if (field.admin?.component || field.admin?.after?.length || field.admin?.cell) {
4744
4877
  f.admin = {
4745
4878
  ...field.admin.component ? { component: componentRef(field.admin.component) } : {},
4746
- ...field.admin.after?.length ? { after: field.admin.after.map(componentRef) } : {}
4879
+ ...field.admin.after?.length ? { after: field.admin.after.map(componentRef) } : {},
4880
+ ...field.admin.cell ? { cell: componentRef(field.admin.cell) } : {}
4747
4881
  };
4748
4882
  }
4883
+ if (field.customType) f.customType = field.customType;
4749
4884
  switch (field.type) {
4750
4885
  case "text":
4751
4886
  case "textarea":
@@ -5680,9 +5815,14 @@ function names(config) {
5680
5815
  }
5681
5816
  const globals = /* @__PURE__ */ new Map();
5682
5817
  for (const g of config.globals) globals.set(g.slug, claim(pascal(g.slug), "Global"));
5683
- return { collections, globals };
5818
+ const custom = new Map(
5819
+ (config.fieldTypes ?? []).filter((t) => t.typescript).map((t) => [t.name, t.typescript])
5820
+ );
5821
+ return { collections, globals, custom };
5684
5822
  }
5685
5823
  function fieldType(field, n, indent) {
5824
+ const custom = field.customType ? n.custom.get(field.customType) : void 0;
5825
+ if (custom) return custom;
5686
5826
  switch (field.type) {
5687
5827
  case "text":
5688
5828
  case "textarea":
@@ -5836,6 +5976,7 @@ export {
5836
5976
  EasyCMS,
5837
5977
  EasyCMSError,
5838
5978
  FIELD_TYPES,
5979
+ FIELD_TYPE_BASES,
5839
5980
  ForbiddenError,
5840
5981
  INTERNAL_COLLECTIONS,
5841
5982
  MAX_DEPTH,
@@ -5862,6 +6003,7 @@ export {
5862
6003
  createRestHandler,
5863
6004
  createRootEndpointHandler,
5864
6005
  defineConfig,
6006
+ defineFieldType,
5865
6007
  definePlugin,
5866
6008
  fillMissing,
5867
6009
  findConfigFile,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/core",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Embedded, code-first headless CMS for Nuxt and Next.js",
5
5
  "license": "MIT",
6
6
  "type": "module",