@easy-cms/core 0.24.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;
@@ -439,16 +491,41 @@ interface BuiltinApiKeysCollection {
439
491
  type ApiKeysOf<C extends Config> = C extends {
440
492
  readonly apiKeys: true;
441
493
  } ? BuiltinApiKeysCollection : never;
442
- /** Every collection of a config, including the built-in ones. */
443
- type AllCollections<C extends Config> = NonNullable<C['collections']>[number] | BuiltinUsersCollection | BuiltinMediaCollection | ApiKeysOf<C>;
494
+ /** The `PluginTypes` of each plugin in the config (a union). */
495
+ type PluginTypesOf<C extends Config> = C extends {
496
+ readonly plugins: readonly (infer P)[];
497
+ } ? P extends TypedPlugin<infer T extends PluginTypes> ? T : never : never;
498
+ /** Fields that plugins add to the collection (or global) `S`, as a union. */
499
+ type AddedFields<T, S> = T extends {
500
+ readonly [K in 'fields' | 'globalFields']?: infer M;
501
+ } ? M extends {
502
+ readonly [slug: string]: readonly Field[];
503
+ } ? string extends keyof M ? never : S extends keyof M ? M[S][number] : never : never : never;
504
+ type PluginFields<C extends Config, S, Key extends 'fields' | 'globalFields'> = AddedFields<PluginTypesOf<C> extends infer T ? T extends PluginTypes ? Pick<T, Key & keyof T> : never : never, S>;
505
+ /** Collections that plugins add. */
506
+ type PluginCollections<C extends Config> = PluginTypesOf<C> extends infer T ? T extends {
507
+ readonly collections: readonly (infer K extends CollectionConfig)[];
508
+ } ? K : never : never;
509
+ /** A collection or global with the fields plugins add to it. */
510
+ type WithPluginFields<C extends Config, T extends {
511
+ readonly slug: string;
512
+ readonly fields: readonly Field[];
513
+ }, Key extends 'fields' | 'globalFields'> = [PluginFields<C, T['slug'], Key>] extends [never] ? T : Omit<T, 'fields'> & {
514
+ readonly fields: readonly (T['fields'][number] | PluginFields<C, T['slug'], Key>)[];
515
+ };
516
+ /** Every collection of a config, including the built-in ones and those plugins add. */
517
+ type AllCollections<C extends Config> = NonNullable<C['collections']>[number] | BuiltinUsersCollection | BuiltinMediaCollection | ApiKeysOf<C> | PluginCollections<C>;
444
518
  /** The built-in media collection; its documents are typed as `MediaDocument`. */
445
519
  interface BuiltinMediaCollection {
446
520
  readonly slug: 'media';
447
521
  readonly fields: readonly [];
448
522
  }
449
- type CollectionBySlug<C extends Config, S> = Extract<AllCollections<C>, {
523
+ type CollectionBySlug<C extends Config, S> = WithPluginFields<C, Extract<AllCollections<C>, {
450
524
  readonly slug: S;
451
- }>;
525
+ }>, 'fields'>;
526
+ type GlobalBySlug<C extends Config, S> = WithPluginFields<C, Extract<NonNullable<C['globals']>[number], {
527
+ readonly slug: S;
528
+ }>, 'globalFields'>;
452
529
  /** Relationships are ids at depth 0 and documents when populated. */
453
530
  type RelationValue<C extends Config, S> = [CollectionBySlug<C, S>] extends [never] ? ID | Record<string, unknown> : ID | CollectionDocument<C, S & string>;
454
531
  interface MediaSize {
@@ -508,7 +585,13 @@ type FieldValue<F extends Field, C extends Config = Config> = F extends {
508
585
  } ? FieldsValue<Sub, C> : F extends {
509
586
  readonly type: 'blocks';
510
587
  readonly blocks: infer B extends readonly Block[];
511
- } ? 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;
512
595
  /** One row of a `blocks` field: the block's fields plus `id` and `blockType` (distributes over kinds). */
513
596
  type BlockValue<B, C extends Config> = B extends {
514
597
  readonly slug: infer S;
@@ -544,7 +627,7 @@ type FieldsValue<Fs extends readonly Field[], C extends Config = Config> = Simpl
544
627
  -readonly [F in OptionalFields<Fs> as F['name']]?: FieldValue<F, C> | null;
545
628
  }>;
546
629
  type SystemFields<T extends {
547
- readonly drafts?: boolean;
630
+ readonly drafts?: unknown;
548
631
  }> = {
549
632
  id: ID;
550
633
  createdAt: string;
@@ -554,8 +637,13 @@ type SystemFields<T extends {
554
637
  } ? {
555
638
  status: 'draft' | 'published';
556
639
  } : unknown);
557
- type InferCollection<T extends CollectionConfig, C extends Config = Config> = Simplify<SystemFields<T> & FieldsValue<T['fields'], C>>;
558
- type InferGlobal<T extends GlobalConfig, C extends Config = Config> = Simplify<
640
+ /** What document types are inferred from: a collection's or global's fields and drafts. */
641
+ type FieldsOwner = {
642
+ readonly fields: readonly Field[];
643
+ readonly drafts?: unknown;
644
+ };
645
+ type InferCollection<T extends FieldsOwner, C extends Config = Config> = Simplify<SystemFields<T> & FieldsValue<T['fields'], C>>;
646
+ type InferGlobal<T extends FieldsOwner, C extends Config = Config> = Simplify<
559
647
  /** `null` until the global is saved for the first time. */
560
648
  {
561
649
  updatedAt: string | null;
@@ -564,14 +652,12 @@ type InferGlobal<T extends GlobalConfig, C extends Config = Config> = Simplify<
564
652
  } ? {
565
653
  status: 'draft' | 'published';
566
654
  } : unknown) & FieldsValue<T['fields'], C>>;
567
- type CollectionSlug<C extends Config> = NonNullable<C['collections']>[number]['slug'] | 'users' | 'media' | ApiKeysOf<C>['slug'];
655
+ type CollectionSlug<C extends Config> = NonNullable<C['collections']>[number]['slug'] | 'users' | 'media' | ApiKeysOf<C>['slug'] | PluginCollections<C>['slug'];
568
656
  type GlobalSlug<C extends Config> = NonNullable<C['globals']>[number]['slug'];
569
657
  /** Document type of a collection, e.g. `CollectionDocument<typeof config, 'posts'>`. */
570
658
  type CollectionDocument<C extends Config, S extends CollectionSlug<C>> = S extends 'media' ? MediaDocument : InferCollection<CollectionBySlug<C, S>, C>;
571
659
  /** Data type of a global, e.g. `GlobalDocument<typeof config, 'site'>`. */
572
- type GlobalDocument<C extends Config, S extends GlobalSlug<C>> = InferGlobal<Extract<NonNullable<C['globals']>[number], {
573
- readonly slug: S;
574
- }>, C>;
660
+ type GlobalDocument<C extends Config, S extends GlobalSlug<C>> = InferGlobal<GlobalBySlug<C, S>, C>;
575
661
  type InputValue<F extends Field> = F extends {
576
662
  readonly type: 'relationship';
577
663
  } ? F extends {
@@ -630,11 +716,7 @@ type CreateInput<C extends Config, S extends CollectionSlug<C>> = Simplify<Field
630
716
  /** Data accepted by `update`: any subset of `CreateInput`. */
631
717
  type UpdateInput<C extends Config, S extends CollectionSlug<C>> = Partial<CreateInput<C, S>>;
632
718
  /** Data accepted by `updateGlobal`. */
633
- type GlobalInput<C extends Config, S extends GlobalSlug<C>> = Partial<Simplify<FieldsInput<Extract<NonNullable<C['globals']>[number], {
634
- readonly slug: S;
635
- }>['fields']> & StatusInput<Extract<NonNullable<C['globals']>[number], {
636
- readonly slug: S;
637
- }>>>>;
719
+ type GlobalInput<C extends Config, S extends GlobalSlug<C>> = Partial<Simplify<FieldsInput<GlobalBySlug<C, S>['fields']> & StatusInput<GlobalBySlug<C, S>>>>;
638
720
 
639
721
  interface StoredFile {
640
722
  readonly body: Uint8Array;
@@ -690,6 +772,13 @@ type Create<C extends Config, S> = string extends CollectionSlug<C> ? Data$2 : C
690
772
  type Update<C extends Config, S> = string extends CollectionSlug<C> ? Data$2 : UpdateInput<C, S & CollectionSlug<C>>;
691
773
  type GDoc<C extends Config, S> = string extends GlobalSlug<C> ? Data$2 : GlobalDocument<C, S & GlobalSlug<C>>;
692
774
  type GInput<C extends Config, S> = string extends GlobalSlug<C> ? Data$2 : GlobalInput<C, S & GlobalSlug<C>>;
775
+ /** A collection slug of the config `C` (any string when the config isn't a literal). */
776
+ type SlugOf<C extends Config> = Slug<C>;
777
+ /**
778
+ * A document of collection `S` as the Local API returns it, e.g. for helpers that take an
779
+ * `EasyCMS<C>`: inferred from a literal config, loose otherwise.
780
+ */
781
+ type DocumentOf<C extends Config, S> = Doc<C, S>;
693
782
  interface AccessOptions {
694
783
  /**
695
784
  * The Local API trusts its caller and skips access rules by default.
@@ -1051,7 +1140,31 @@ interface FieldAdmin {
1051
1140
  readonly component?: AdminComponent;
1052
1141
  /** Shown below the field, e.g. a length meter or a preview. */
1053
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;
1054
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];
1055
1168
  interface BaseField<TType extends string, TValue> {
1056
1169
  readonly type: TType;
1057
1170
  readonly name: string;
@@ -1073,6 +1186,8 @@ interface BaseField<TType extends string, TValue> {
1073
1186
  readonly position?: 'sidebar';
1074
1187
  /** Custom admin components for this field. */
1075
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;
1076
1191
  }
1077
1192
  interface TextField extends BaseField<'text', string> {
1078
1193
  readonly minLength?: number;
@@ -1189,10 +1304,48 @@ declare function mimeAllowedBy(mimeType: string, patterns: readonly string[]): b
1189
1304
  declare const hasRows: (field: Field) => field is ArrayField | BlocksField;
1190
1305
  /** The fields of one row of an `array` or `blocks` field (`undefined` for an unknown block). */
1191
1306
  declare function rowFields(field: ArrayField | BlocksField, row: unknown): readonly Field[] | undefined;
1192
- 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;
1193
1308
  type FieldType = Field['type'];
1194
1309
  declare const FIELD_TYPES: readonly ["text", "textarea", "number", "boolean", "date", "email", "json", "select", "slug", "richText", "upload", "relationship", "array", "group", "blocks"];
1195
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
+
1196
1349
  declare const WEBHOOK_EVENTS: readonly ["create", "update", "delete", "publish", "unpublish", "draft"];
1197
1350
  /**
1198
1351
  * `create`, `update`, `delete`: the stored document changed. `publish` / `unpublish`: its status
@@ -1519,6 +1672,38 @@ interface Endpoint {
1519
1672
  }
1520
1673
  /** Receives the config and returns a modified copy. Runs before validation. */
1521
1674
  type Plugin = (config: Config) => MaybePromise<Config>;
1675
+ /** Holds a plugin's `PluginTypes` at the type level only: nothing is stored at runtime. */
1676
+ declare const PLUGIN_TYPES: unique symbol;
1677
+ /**
1678
+ * What a plugin adds, for the document types inferred from the config: fields on collections
1679
+ * and globals (by slug), and whole collections. Field types are written like config fields,
1680
+ * e.g. `{ fields: { posts: readonly [{ readonly name: 'meta'; readonly type: 'group'; … }] } }`.
1681
+ */
1682
+ interface PluginTypes {
1683
+ readonly fields?: {
1684
+ readonly [collection: string]: readonly Field[];
1685
+ };
1686
+ readonly globalFields?: {
1687
+ readonly [global: string]: readonly Field[];
1688
+ };
1689
+ readonly collections?: readonly CollectionConfig[];
1690
+ }
1691
+ /** A plugin that tells the type system what it adds (see `definePlugin`). */
1692
+ type TypedPlugin<T extends PluginTypes> = Plugin & {
1693
+ readonly [PLUGIN_TYPES]?: T;
1694
+ };
1695
+ /**
1696
+ * Returns the plugin unchanged, with what it adds as types, so `cms.find()` and
1697
+ * `CollectionDocument` know the plugin's fields:
1698
+ *
1699
+ * ```ts
1700
+ * export const colorPlugin = (options: { collections: readonly string[] }) =>
1701
+ * definePlugin<{ fields: { posts: readonly [{ readonly name: 'color'; readonly type: 'text' }] } }>(
1702
+ * (config) => ({ ...config, collections: … }),
1703
+ * )
1704
+ * ```
1705
+ */
1706
+ declare function definePlugin<const T extends PluginTypes = Record<never, never>>(plugin: Plugin): TypedPlugin<T>;
1522
1707
  interface Config {
1523
1708
  /** Signs sessions. At least 32 characters; read it from `process.env.EASY_CMS_SECRET`. */
1524
1709
  readonly secret: string;
@@ -1558,6 +1743,11 @@ interface Config {
1558
1743
  readonly endpoints?: readonly Endpoint[];
1559
1744
  /** Extra `easy-cms <name>` CLI commands, e.g. from plugins. */
1560
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[];
1561
1751
  /**
1562
1752
  * API keys for scripts and other apps (`Authorization: Bearer ecms_…`), managed in the admin
1563
1753
  * under Settings. A key acts as its owner, limited to the collections and operations it lists.
@@ -1584,6 +1774,7 @@ interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes'
1584
1774
  readonly globals: readonly GlobalConfig[];
1585
1775
  readonly endpoints: readonly Endpoint[];
1586
1776
  readonly commands: readonly CliCommand[];
1777
+ readonly fieldTypes: readonly FieldTypeDefinition[];
1587
1778
  }
1588
1779
  /**
1589
1780
  * Declares the Easy CMS config. Returns it unchanged, keeping literal types
@@ -1657,58 +1848,6 @@ declare function copyDatabase(source: Database, target: Database, options?: {
1657
1848
  onTable?: (progress: CopyProgress) => void;
1658
1849
  }): Promise<CopyResult>;
1659
1850
 
1660
- interface ConfigIssue {
1661
- /** Where the problem is, e.g. `collections.posts.fields.slug.from`. */
1662
- readonly path: string;
1663
- readonly message: string;
1664
- /** How to fix it. */
1665
- readonly hint?: string;
1666
- }
1667
- declare class ConfigError extends Error {
1668
- readonly issues: readonly ConfigIssue[];
1669
- constructor(issues: readonly ConfigIssue[]);
1670
- static format(issues: readonly ConfigIssue[]): string;
1671
- }
1672
- /** Base class for errors that map to an HTTP status in the REST API. */
1673
- declare class EasyCMSError extends Error {
1674
- readonly status: number;
1675
- constructor(message: string, status: number);
1676
- }
1677
- interface FieldError {
1678
- /** Dotted path, e.g. `links.0.url`. */
1679
- readonly field: string;
1680
- readonly message: string;
1681
- }
1682
- declare class ValidationError extends EasyCMSError {
1683
- readonly errors: readonly FieldError[];
1684
- constructor(collection: string, errors: readonly FieldError[]);
1685
- }
1686
- declare class NotFoundError extends EasyCMSError {
1687
- constructor(collection: string, id: unknown);
1688
- }
1689
- /** A bad `where`, `sort` or unknown collection/global. */
1690
- declare class QueryError extends EasyCMSError {
1691
- constructor(message: string);
1692
- }
1693
- /** The database schema does not match the config (pending or missing migrations). */
1694
- declare class SchemaError extends EasyCMSError {
1695
- constructor(message: string);
1696
- }
1697
- /** Not logged in, or the session is invalid. */
1698
- declare class UnauthorizedError extends EasyCMSError {
1699
- constructor(message?: string);
1700
- }
1701
- /** Logged in, but not allowed to do this. */
1702
- declare class ForbiddenError extends EasyCMSError {
1703
- constructor(message?: string);
1704
- }
1705
- declare class TooManyRequestsError extends EasyCMSError {
1706
- constructor(message: string);
1707
- }
1708
- declare class PayloadTooLargeError extends EasyCMSError {
1709
- constructor(message: string);
1710
- }
1711
-
1712
1851
  type Localization = Required<LocalizationConfig>;
1713
1852
 
1714
1853
  type Data = Record<string, unknown>;
@@ -1871,11 +2010,14 @@ interface AdminField {
1871
2010
  localized?: boolean;
1872
2011
  /** Shown in the edit page's side panel. */
1873
2012
  position?: 'sidebar';
1874
- /** 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. */
1875
2014
  admin?: {
1876
2015
  component?: AdminComponentRef;
1877
2016
  after?: AdminComponentRef[];
2017
+ cell?: AdminComponentRef;
1878
2018
  };
2019
+ /** A field of an added type (`fieldTypes`), e.g. `color`; `type` is the base it's stored as. */
2020
+ customType?: string;
1879
2021
  }
1880
2022
  interface AdminCollection {
1881
2023
  slug: string;
@@ -1979,4 +2121,4 @@ declare const BUILTIN_COLLECTIONS: readonly ["users", "media"];
1979
2121
  /** Checks a config and returns every problem found. An empty array means it is valid. */
1980
2122
  declare function validateConfig(config: Config): ConfigIssue[];
1981
2123
 
1982
- 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, 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 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 StorageAdapter, type StoredFile, type TextField, type TextareaField, TooManyRequestsError, 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, 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
@@ -1140,6 +1140,9 @@ var ADMIN_ICONS = [
1140
1140
  "camera",
1141
1141
  "key"
1142
1142
  ];
1143
+ function definePlugin(plugin) {
1144
+ return plugin;
1145
+ }
1143
1146
  function defineConfig(config) {
1144
1147
  return config;
1145
1148
  }
@@ -1666,6 +1669,126 @@ var Mailer = class {
1666
1669
  }
1667
1670
  };
1668
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
+
1669
1792
  // src/load-config.ts
1670
1793
  import { existsSync } from "fs";
1671
1794
  import { resolve } from "path";
@@ -2323,9 +2446,10 @@ function validateField(field, path, siblings, collectionSlugs, add) {
2323
2446
  if (typeof admin !== "object" || admin === null) {
2324
2447
  add(`${path}.admin`, "must be an object", "e.g. admin: { component: 'ecms-color-picker' }");
2325
2448
  } else {
2326
- const { component, after } = admin;
2449
+ const { component, after, cell } = admin;
2327
2450
  if (component !== void 0) validateComponent(component, `${path}.admin.component`, add);
2328
2451
  if (after !== void 0) validateComponents(after, `${path}.admin.after`, add);
2452
+ if (cell !== void 0) validateComponent(cell, `${path}.admin.cell`, add);
2329
2453
  }
2330
2454
  }
2331
2455
  switch (field.type) {
@@ -2590,6 +2714,15 @@ async function applyPlugins(input) {
2590
2714
  if (typeof plugin !== "function") break;
2591
2715
  config = await plugin(config);
2592
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
+ };
2593
2726
  return config;
2594
2727
  }
2595
2728
  async function resolveConfig(input) {
@@ -2598,7 +2731,9 @@ async function resolveConfig(input) {
2598
2731
  if (typeof config === "object" && config !== null && Array.isArray(config.collections ?? [])) {
2599
2732
  config = withMedia(withUsers(config));
2600
2733
  }
2601
- const issues = validateConfig(config);
2734
+ const applied = applyFieldTypes(config);
2735
+ config = applied.config;
2736
+ const issues = [...applied.issues, ...validateConfig(config)];
2602
2737
  if (issues.length > 0) throw new ConfigError(issues);
2603
2738
  const { plugins: _plugins, ...rest } = config;
2604
2739
  const result = {
@@ -2646,7 +2781,8 @@ async function resolveConfig(input) {
2646
2781
  ],
2647
2782
  globals: config.globals ?? [],
2648
2783
  endpoints: config.endpoints ?? [],
2649
- commands: config.commands ?? []
2784
+ commands: config.commands ?? [],
2785
+ fieldTypes: config.fieldTypes ?? []
2650
2786
  };
2651
2787
  resolved.add(result);
2652
2788
  return result;
@@ -4737,12 +4873,14 @@ async function serializeFields(fields, update, localized) {
4737
4873
  if (!await update.allows(field)) f.readOnly = true;
4738
4874
  if (field.localized && localized) f.localized = true;
4739
4875
  if (field.position === "sidebar") f.position = "sidebar";
4740
- if (field.admin?.component || field.admin?.after?.length) {
4876
+ if (field.admin?.component || field.admin?.after?.length || field.admin?.cell) {
4741
4877
  f.admin = {
4742
4878
  ...field.admin.component ? { component: componentRef(field.admin.component) } : {},
4743
- ...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) } : {}
4744
4881
  };
4745
4882
  }
4883
+ if (field.customType) f.customType = field.customType;
4746
4884
  switch (field.type) {
4747
4885
  case "text":
4748
4886
  case "textarea":
@@ -5677,9 +5815,14 @@ function names(config) {
5677
5815
  }
5678
5816
  const globals = /* @__PURE__ */ new Map();
5679
5817
  for (const g of config.globals) globals.set(g.slug, claim(pascal(g.slug), "Global"));
5680
- 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 };
5681
5822
  }
5682
5823
  function fieldType(field, n, indent) {
5824
+ const custom = field.customType ? n.custom.get(field.customType) : void 0;
5825
+ if (custom) return custom;
5683
5826
  switch (field.type) {
5684
5827
  case "text":
5685
5828
  case "textarea":
@@ -5833,6 +5976,7 @@ export {
5833
5976
  EasyCMS,
5834
5977
  EasyCMSError,
5835
5978
  FIELD_TYPES,
5979
+ FIELD_TYPE_BASES,
5836
5980
  ForbiddenError,
5837
5981
  INTERNAL_COLLECTIONS,
5838
5982
  MAX_DEPTH,
@@ -5859,6 +6003,8 @@ export {
5859
6003
  createRestHandler,
5860
6004
  createRootEndpointHandler,
5861
6005
  defineConfig,
6006
+ defineFieldType,
6007
+ definePlugin,
5862
6008
  fillMissing,
5863
6009
  findConfigFile,
5864
6010
  generateSlugs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/core",
3
- "version": "0.24.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",