@easy-cms/core 0.25.0 → 0.27.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
@@ -1404,7 +1526,7 @@ interface GlobalConfig {
1404
1526
  }
1405
1527
  type AdminLocale = 'en' | 'th';
1406
1528
  /** Icons a collection or global can show in the admin menu ([Lucide](https://lucide.dev) names). */
1407
- declare const ADMIN_ICONS: readonly ["file-text", "newspaper", "book-open", "notebook", "folder", "tag", "tags", "image", "images", "video", "music", "file", "users", "user", "building", "store", "shopping-bag", "shopping-cart", "package", "box", "calendar", "calendar-days", "map-pin", "globe", "house", "layout-grid", "layers", "star", "heart", "message-square", "mail", "phone", "briefcase", "graduation-cap", "utensils", "car", "settings", "sliders-horizontal", "palette", "megaphone", "bell", "link", "quote", "circle-help", "award", "ticket", "camera", "key"];
1529
+ declare const ADMIN_ICONS: readonly ["file-text", "newspaper", "book-open", "notebook", "folder", "tag", "tags", "image", "images", "video", "music", "file", "users", "user", "building", "store", "shopping-bag", "shopping-cart", "package", "box", "calendar", "calendar-days", "map-pin", "globe", "house", "layout-grid", "layers", "star", "heart", "message-square", "mail", "phone", "briefcase", "graduation-cap", "utensils", "car", "settings", "sliders-horizontal", "palette", "megaphone", "bell", "link", "quote", "circle-help", "award", "ticket", "camera", "key", "chart-column", "chart-line"];
1408
1530
  type AdminIcon = (typeof ADMIN_ICONS)[number];
1409
1531
  /** Your or your client's brand in the admin UI. */
1410
1532
  interface AdminBrand {
@@ -1415,6 +1537,34 @@ interface AdminBrand {
1415
1537
  /** Main color as `#rrggbb`; lighter and darker shades are derived from it. */
1416
1538
  readonly color?: string;
1417
1539
  }
1540
+ /** Who may see an admin page or dashboard widget. Default: every logged-in user. */
1541
+ type AdminViewAccess = (args: {
1542
+ readonly user: AuthUser;
1543
+ }) => boolean | Promise<boolean>;
1544
+ /** A page of its own in the admin, e.g. a plugin's report, at `<admin>/p/<path>`. */
1545
+ interface AdminPage {
1546
+ /** Its address under `<admin>/p/`: lowercase letters, digits and `-`, e.g. `forms-overview`. */
1547
+ readonly path: string;
1548
+ /** The page's body: a Web Component from an admin module. The admin draws the header. */
1549
+ readonly component: AdminComponent;
1550
+ /** Its title in the header and the menu. */
1551
+ readonly label: Label;
1552
+ /** Icon in the admin menu. Default `file-text`. */
1553
+ readonly icon?: AdminIcon;
1554
+ /** Listed under Content (default) or Settings in the menu, or not listed (`false`): reached by links. */
1555
+ readonly group?: 'content' | 'settings' | false;
1556
+ /** Who may open it. Pages a user may not open are left out of their admin. */
1557
+ readonly access?: AdminViewAccess;
1558
+ }
1559
+ /** A panel on the admin's dashboard, after the built-in ones. */
1560
+ interface DashboardWidget {
1561
+ /** A Web Component from an admin module. */
1562
+ readonly component: AdminComponent;
1563
+ /** Half the dashboard's width (default) or all of it. Full width on phones. */
1564
+ readonly width?: 'half' | 'full';
1565
+ /** Who sees it. */
1566
+ readonly access?: AdminViewAccess;
1567
+ }
1418
1568
  interface AdminConfig {
1419
1569
  /** Where the admin UI is served. Default `/admin`. */
1420
1570
  readonly path?: string;
@@ -1440,6 +1590,10 @@ interface AdminConfig {
1440
1590
  * module file. Remote URLs are not allowed.
1441
1591
  */
1442
1592
  readonly modules?: readonly string[];
1593
+ /** Pages of their own, e.g. from plugins, at `<admin>/p/<path>`. */
1594
+ readonly pages?: readonly AdminPage[];
1595
+ /** Panels on the dashboard, after the built-in ones. */
1596
+ readonly dashboard?: readonly DashboardWidget[];
1443
1597
  }
1444
1598
  interface ImageSize {
1445
1599
  /** Key in `media.sizes`, e.g. `thumbnail`. */
@@ -1621,6 +1775,11 @@ interface Config {
1621
1775
  readonly endpoints?: readonly Endpoint[];
1622
1776
  /** Extra `easy-cms <name>` CLI commands, e.g. from plugins. */
1623
1777
  readonly commands?: readonly CliCommand[];
1778
+ /**
1779
+ * Field types from packages, e.g. `color` from `@easy-cms/fields`: fields then use
1780
+ * `type: 'color'`. See `defineFieldType`.
1781
+ */
1782
+ readonly fieldTypes?: readonly FieldTypeDefinition[];
1624
1783
  /**
1625
1784
  * API keys for scripts and other apps (`Authorization: Bearer ecms_…`), managed in the admin
1626
1785
  * under Settings. A key acts as its owner, limited to the collections and operations it lists.
@@ -1647,6 +1806,7 @@ interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes'
1647
1806
  readonly globals: readonly GlobalConfig[];
1648
1807
  readonly endpoints: readonly Endpoint[];
1649
1808
  readonly commands: readonly CliCommand[];
1809
+ readonly fieldTypes: readonly FieldTypeDefinition[];
1650
1810
  }
1651
1811
  /**
1652
1812
  * Declares the Easy CMS config. Returns it unchanged, keeping literal types
@@ -1720,58 +1880,6 @@ declare function copyDatabase(source: Database, target: Database, options?: {
1720
1880
  onTable?: (progress: CopyProgress) => void;
1721
1881
  }): Promise<CopyResult>;
1722
1882
 
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
1883
  type Localization = Required<LocalizationConfig>;
1776
1884
 
1777
1885
  type Data = Record<string, unknown>;
@@ -1934,11 +2042,14 @@ interface AdminField {
1934
2042
  localized?: boolean;
1935
2043
  /** Shown in the edit page's side panel. */
1936
2044
  position?: 'sidebar';
1937
- /** Components from admin modules: instead of the input, and below the field. */
2045
+ /** Components from admin modules: instead of the input, below the field, and in lists. */
1938
2046
  admin?: {
1939
2047
  component?: AdminComponentRef;
1940
2048
  after?: AdminComponentRef[];
2049
+ cell?: AdminComponentRef;
1941
2050
  };
2051
+ /** A field of an added type (`fieldTypes`), e.g. `color`; `type` is the base it's stored as. */
2052
+ customType?: string;
1942
2053
  }
1943
2054
  interface AdminCollection {
1944
2055
  slug: string;
@@ -1990,6 +2101,20 @@ interface AdminGlobal {
1990
2101
  update: boolean;
1991
2102
  };
1992
2103
  }
2104
+ /** A page of its own in the admin (`admin.pages`) that this user may open. */
2105
+ interface AdminPageRef {
2106
+ path: string;
2107
+ label: Label;
2108
+ icon?: string;
2109
+ /** Where it is listed in the menu; `false`: not listed. */
2110
+ group: 'content' | 'settings' | false;
2111
+ component: AdminComponentRef;
2112
+ }
2113
+ /** A dashboard panel (`admin.dashboard`) this user may see. */
2114
+ interface AdminWidgetRef {
2115
+ component: AdminComponentRef;
2116
+ width: 'half' | 'full';
2117
+ }
1993
2118
  interface AdminSchema {
1994
2119
  locale: AdminLocale;
1995
2120
  /** Admins can email users links to set their password (needs `email` and `serverURL`). */
@@ -2005,6 +2130,10 @@ interface AdminSchema {
2005
2130
  globals: AdminGlobal[];
2006
2131
  /** URLs under the API of the admin modules to load (`admin.modules`). */
2007
2132
  modules: string[];
2133
+ /** Pages this user may open (`admin.pages`). */
2134
+ pages: AdminPageRef[];
2135
+ /** Dashboard panels this user may see (`admin.dashboard`). */
2136
+ dashboard: AdminWidgetRef[];
2008
2137
  }
2009
2138
 
2010
2139
  declare const SESSION_COOKIE = "ecms-session";
@@ -2042,4 +2171,4 @@ declare const BUILTIN_COLLECTIONS: readonly ["users", "media"];
2042
2171
  /** Checks a config and returns every problem found. An empty array means it is valid. */
2043
2172
  declare function validateConfig(config: Config): ConfigIssue[];
2044
2173
 
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 };
2174
+ 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 AdminPage, type AdminPageRef, type AdminSchema, type AdminViewAccess, type AdminWidgetRef, 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 DashboardWidget, 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
@@ -1138,7 +1138,9 @@ var ADMIN_ICONS = [
1138
1138
  "award",
1139
1139
  "ticket",
1140
1140
  "camera",
1141
- "key"
1141
+ "key",
1142
+ "chart-column",
1143
+ "chart-line"
1142
1144
  ];
1143
1145
  function definePlugin(plugin) {
1144
1146
  return plugin;
@@ -1669,6 +1671,126 @@ var Mailer = class {
1669
1671
  }
1670
1672
  };
1671
1673
 
1674
+ // src/field-types.ts
1675
+ var FIELD_TYPE_BASES = [
1676
+ "text",
1677
+ "textarea",
1678
+ "email",
1679
+ "number",
1680
+ "boolean",
1681
+ "date",
1682
+ "json"
1683
+ ];
1684
+ function defineFieldType(definition) {
1685
+ return definition;
1686
+ }
1687
+ var NAME = /^[a-z][a-zA-Z0-9]*$/;
1688
+ function fieldTypeModules(config) {
1689
+ const list = config.fieldTypes;
1690
+ if (!Array.isArray(list)) return [];
1691
+ return list.map((t) => t?.admin?.module).filter((m) => typeof m === "string");
1692
+ }
1693
+ function applyFieldTypes(config) {
1694
+ const issues = [];
1695
+ const list = config.fieldTypes;
1696
+ if (list === void 0) return { config, issues };
1697
+ if (!Array.isArray(list)) {
1698
+ issues.push({ path: "fieldTypes", message: "must be an array of field types" });
1699
+ return { config, issues };
1700
+ }
1701
+ const types = /* @__PURE__ */ new Map();
1702
+ for (const [i, type] of list.entries()) {
1703
+ const path = `fieldTypes[${i}]`;
1704
+ if (!type || typeof type.name !== "string" || !NAME.test(type.name)) {
1705
+ issues.push({ path: `${path}.name`, message: 'must be a lowercase name like "color"' });
1706
+ } else if (FIELD_TYPES.includes(type.name)) {
1707
+ issues.push({ path: `${path}.name`, message: `"${type.name}" is a built-in field type` });
1708
+ } else if (types.has(type.name)) {
1709
+ issues.push({ path: `${path}.name`, message: `"${type.name}" is listed twice` });
1710
+ } else if (!FIELD_TYPE_BASES.includes(type.base)) {
1711
+ issues.push({
1712
+ path: `${path}.base`,
1713
+ message: `must be one of ${FIELD_TYPE_BASES.join(", ")}`
1714
+ });
1715
+ } else {
1716
+ types.set(type.name, type);
1717
+ }
1718
+ }
1719
+ if (types.size === 0) return { config, issues };
1720
+ const component = (tag, field, props) => {
1721
+ if (!tag) return void 0;
1722
+ const options = Object.fromEntries(
1723
+ (props ?? []).filter((key2) => field[key2] !== void 0).map((key2) => [key2, field[key2]])
1724
+ );
1725
+ return Object.keys(options).length ? { tag, props: options } : tag;
1726
+ };
1727
+ const convert = (fields, path) => fields.map((field, i) => {
1728
+ const here = `${path}[${i}]`;
1729
+ const own = field;
1730
+ const type = types.get(own.type);
1731
+ if (!type) {
1732
+ if ((field.type === "group" || field.type === "array") && Array.isArray(field.fields))
1733
+ return { ...field, fields: convert(field.fields, `${here}.fields`) };
1734
+ if (field.type === "blocks" && Array.isArray(field.blocks))
1735
+ return {
1736
+ ...field,
1737
+ blocks: field.blocks.map((block, b) => ({
1738
+ ...block,
1739
+ fields: convert(block.fields, `${here}.blocks[${b}].fields`)
1740
+ }))
1741
+ };
1742
+ return field;
1743
+ }
1744
+ const problem = type.checkOptions?.(own);
1745
+ if (problem) issues.push({ path: here, message: problem });
1746
+ const ownValidate = own.validate;
1747
+ const check = type.validate;
1748
+ const admin = own.admin ?? {};
1749
+ const input = admin.component ?? component(type.admin?.component, own, type.admin?.props);
1750
+ const cell = admin.cell ?? component(type.admin?.cell, own, type.admin?.props);
1751
+ return {
1752
+ ...own,
1753
+ type: type.base,
1754
+ customType: type.name,
1755
+ ...check || ownValidate ? {
1756
+ validate: async (value, ctx) => {
1757
+ const empty = value === null || value === void 0 || value === "";
1758
+ if (check && !empty) {
1759
+ const result = await check(value, { ...ctx, field: own });
1760
+ if (result !== true) return result;
1761
+ }
1762
+ return ownValidate ? ownValidate(value, ctx) : true;
1763
+ }
1764
+ } : {},
1765
+ ...input || cell ? {
1766
+ admin: {
1767
+ ...admin,
1768
+ ...input ? { component: input } : {},
1769
+ ...cell ? { cell } : {}
1770
+ }
1771
+ } : {}
1772
+ };
1773
+ });
1774
+ return {
1775
+ config: {
1776
+ ...config,
1777
+ ...config.collections ? {
1778
+ collections: config.collections.map((c, i) => ({
1779
+ ...c,
1780
+ fields: convert(c.fields, `collections[${i}].fields`)
1781
+ }))
1782
+ } : {},
1783
+ ...config.globals ? {
1784
+ globals: config.globals.map((g, i) => ({
1785
+ ...g,
1786
+ fields: convert(g.fields, `globals[${i}].fields`)
1787
+ }))
1788
+ } : {}
1789
+ },
1790
+ issues
1791
+ };
1792
+ }
1793
+
1672
1794
  // src/load-config.ts
1673
1795
  import { existsSync } from "fs";
1674
1796
  import { resolve } from "path";
@@ -1874,6 +1996,7 @@ function validateConfig(config) {
1874
1996
  add("db", "is required", "pass a database adapter, e.g. db: sqlite({ url: 'file:./cms.db' })");
1875
1997
  }
1876
1998
  validateAdmin(config, add);
1999
+ validateAdminViews(config, add);
1877
2000
  validateUpload(config, add);
1878
2001
  validateAuth(config, add);
1879
2002
  validateCors(config.cors, add);
@@ -1994,6 +2117,83 @@ function validateAdmin(config, add) {
1994
2117
  add("admin.brand.color", `must be a hex color like "#0f766e" (got ${JSON.stringify(color)})`);
1995
2118
  }
1996
2119
  }
2120
+ var PAGE_PATH = /^[a-z0-9]+(-[a-z0-9]+)*$/;
2121
+ function validateAdminViews(config, add) {
2122
+ const access = (value, path) => {
2123
+ if (value !== void 0 && typeof value !== "function")
2124
+ add(
2125
+ `${path}.access`,
2126
+ "must be a function",
2127
+ 'e.g. access: ({ user }) => user.role === "admin"'
2128
+ );
2129
+ };
2130
+ const pages2 = config.admin?.pages;
2131
+ if (pages2 !== void 0) {
2132
+ if (!Array.isArray(pages2)) {
2133
+ add(
2134
+ "admin.pages",
2135
+ "must be an array",
2136
+ "e.g. pages: [{ path: 'stats', label: 'Stats', component: 'ecms-stats' }]"
2137
+ );
2138
+ } else {
2139
+ const seen = /* @__PURE__ */ new Set();
2140
+ pages2.forEach((page, i) => {
2141
+ const path = `admin.pages[${i}]`;
2142
+ if (typeof page !== "object" || page === null) {
2143
+ add(path, "must be an object");
2144
+ return;
2145
+ }
2146
+ const p = page;
2147
+ if (typeof p.path !== "string" || !PAGE_PATH.test(p.path)) {
2148
+ add(
2149
+ `${path}.path`,
2150
+ `must be lowercase letters, digits and "-" (got ${JSON.stringify(p.path)})`,
2151
+ "e.g. path: 'forms-overview'"
2152
+ );
2153
+ } else if (seen.has(p.path)) {
2154
+ add(`${path}.path`, `"${p.path}" is used by another page`);
2155
+ } else {
2156
+ seen.add(p.path);
2157
+ }
2158
+ validateComponent(p.component, `${path}.component`, add);
2159
+ const label = p.label;
2160
+ if (!(typeof label === "string" && label.trim() !== "") && !(typeof label === "object" && label !== null && !Array.isArray(label)))
2161
+ add(
2162
+ `${path}.label`,
2163
+ "must be a string or { en, th }",
2164
+ "e.g. label: { en: 'Stats', th: '\u0E2A\u0E16\u0E34\u0E15\u0E34' }"
2165
+ );
2166
+ validateIcon(p, path, add);
2167
+ if (p.group !== void 0 && p.group !== "content" && p.group !== "settings" && p.group !== false)
2168
+ add(`${path}.group`, 'must be "content", "settings" or false');
2169
+ access(p.access, path);
2170
+ });
2171
+ }
2172
+ }
2173
+ const widgets2 = config.admin?.dashboard;
2174
+ if (widgets2 !== void 0) {
2175
+ if (!Array.isArray(widgets2)) {
2176
+ add(
2177
+ "admin.dashboard",
2178
+ "must be an array",
2179
+ "e.g. dashboard: [{ component: 'ecms-stats-widget' }]"
2180
+ );
2181
+ } else {
2182
+ widgets2.forEach((widget, i) => {
2183
+ const path = `admin.dashboard[${i}]`;
2184
+ if (typeof widget !== "object" || widget === null) {
2185
+ add(path, "must be an object");
2186
+ return;
2187
+ }
2188
+ const w = widget;
2189
+ validateComponent(w.component, `${path}.component`, add);
2190
+ if (w.width !== void 0 && w.width !== "half" && w.width !== "full")
2191
+ add(`${path}.width`, 'must be "half" or "full"');
2192
+ access(w.access, path);
2193
+ });
2194
+ }
2195
+ }
2196
+ }
1997
2197
  function validateIcon(container, path, add) {
1998
2198
  const icon = container.icon;
1999
2199
  if (icon === void 0 || ADMIN_ICONS.includes(icon)) return;
@@ -2326,9 +2526,10 @@ function validateField(field, path, siblings, collectionSlugs, add) {
2326
2526
  if (typeof admin !== "object" || admin === null) {
2327
2527
  add(`${path}.admin`, "must be an object", "e.g. admin: { component: 'ecms-color-picker' }");
2328
2528
  } else {
2329
- const { component, after } = admin;
2529
+ const { component, after, cell } = admin;
2330
2530
  if (component !== void 0) validateComponent(component, `${path}.admin.component`, add);
2331
2531
  if (after !== void 0) validateComponents(after, `${path}.admin.after`, add);
2532
+ if (cell !== void 0) validateComponent(cell, `${path}.admin.cell`, add);
2332
2533
  }
2333
2534
  }
2334
2535
  switch (field.type) {
@@ -2593,6 +2794,15 @@ async function applyPlugins(input) {
2593
2794
  if (typeof plugin !== "function") break;
2594
2795
  config = await plugin(config);
2595
2796
  }
2797
+ const modules = fieldTypeModules(config);
2798
+ if (modules.length > 0)
2799
+ config = {
2800
+ ...config,
2801
+ admin: {
2802
+ ...config.admin,
2803
+ modules: [.../* @__PURE__ */ new Set([...config.admin?.modules ?? [], ...modules])]
2804
+ }
2805
+ };
2596
2806
  return config;
2597
2807
  }
2598
2808
  async function resolveConfig(input) {
@@ -2601,7 +2811,9 @@ async function resolveConfig(input) {
2601
2811
  if (typeof config === "object" && config !== null && Array.isArray(config.collections ?? [])) {
2602
2812
  config = withMedia(withUsers(config));
2603
2813
  }
2604
- const issues = validateConfig(config);
2814
+ const applied = applyFieldTypes(config);
2815
+ config = applied.config;
2816
+ const issues = [...applied.issues, ...validateConfig(config)];
2605
2817
  if (issues.length > 0) throw new ConfigError(issues);
2606
2818
  const { plugins: _plugins, ...rest } = config;
2607
2819
  const result = {
@@ -2619,7 +2831,9 @@ async function resolveConfig(input) {
2619
2831
  brand: config.admin?.brand ?? {},
2620
2832
  siteUrl: config.admin?.siteUrl ?? "",
2621
2833
  menu: config.admin?.menu ?? [],
2622
- modules: config.admin?.modules ?? []
2834
+ modules: config.admin?.modules ?? [],
2835
+ pages: config.admin?.pages ?? [],
2836
+ dashboard: config.admin?.dashboard ?? []
2623
2837
  },
2624
2838
  upload: {
2625
2839
  dir: config.upload?.dir ?? "uploads",
@@ -2649,7 +2863,8 @@ async function resolveConfig(input) {
2649
2863
  ],
2650
2864
  globals: config.globals ?? [],
2651
2865
  endpoints: config.endpoints ?? [],
2652
- commands: config.commands ?? []
2866
+ commands: config.commands ?? [],
2867
+ fieldTypes: config.fieldTypes ?? []
2653
2868
  };
2654
2869
  resolved.add(result);
2655
2870
  return result;
@@ -4740,12 +4955,14 @@ async function serializeFields(fields, update, localized) {
4740
4955
  if (!await update.allows(field)) f.readOnly = true;
4741
4956
  if (field.localized && localized) f.localized = true;
4742
4957
  if (field.position === "sidebar") f.position = "sidebar";
4743
- if (field.admin?.component || field.admin?.after?.length) {
4958
+ if (field.admin?.component || field.admin?.after?.length || field.admin?.cell) {
4744
4959
  f.admin = {
4745
4960
  ...field.admin.component ? { component: componentRef(field.admin.component) } : {},
4746
- ...field.admin.after?.length ? { after: field.admin.after.map(componentRef) } : {}
4961
+ ...field.admin.after?.length ? { after: field.admin.after.map(componentRef) } : {},
4962
+ ...field.admin.cell ? { cell: componentRef(field.admin.cell) } : {}
4747
4963
  };
4748
4964
  }
4965
+ if (field.customType) f.customType = field.customType;
4749
4966
  switch (field.type) {
4750
4967
  case "text":
4751
4968
  case "textarea":
@@ -4866,9 +5083,41 @@ async function adminSchema(cms, user, origin) {
4866
5083
  localization: localization ? { locales: [...localization.locales], defaultLocale: localization.defaultLocale } : null,
4867
5084
  collections: await Promise.all(collections.map((c) => collection(c, user, localized))),
4868
5085
  globals: await Promise.all(cms.config.globals.map((g) => global(g, user, localized))),
4869
- modules: adminModuleUrls(cms)
5086
+ modules: adminModuleUrls(cms),
5087
+ pages: await pages(cms, user),
5088
+ dashboard: await widgets(cms, user)
4870
5089
  };
4871
5090
  }
5091
+ async function shown(access, user) {
5092
+ if (!access) return true;
5093
+ try {
5094
+ return await access({ user }) === true;
5095
+ } catch {
5096
+ return false;
5097
+ }
5098
+ }
5099
+ async function pages(cms, user) {
5100
+ const out = [];
5101
+ for (const page of cms.config.admin.pages) {
5102
+ if (!await shown(page.access, user)) continue;
5103
+ out.push({
5104
+ path: page.path,
5105
+ label: page.label,
5106
+ ...page.icon ? { icon: page.icon } : {},
5107
+ group: page.group ?? "content",
5108
+ component: componentRef(page.component)
5109
+ });
5110
+ }
5111
+ return out;
5112
+ }
5113
+ async function widgets(cms, user) {
5114
+ const out = [];
5115
+ for (const widget of cms.config.admin.dashboard) {
5116
+ if (!await shown(widget.access, user)) continue;
5117
+ out.push({ component: componentRef(widget.component), width: widget.width ?? "half" });
5118
+ }
5119
+ return out;
5120
+ }
4872
5121
 
4873
5122
  // src/rest/filter-options.ts
4874
5123
  function fieldAt(fields, path) {
@@ -5680,9 +5929,14 @@ function names(config) {
5680
5929
  }
5681
5930
  const globals = /* @__PURE__ */ new Map();
5682
5931
  for (const g of config.globals) globals.set(g.slug, claim(pascal(g.slug), "Global"));
5683
- return { collections, globals };
5932
+ const custom = new Map(
5933
+ (config.fieldTypes ?? []).filter((t) => t.typescript).map((t) => [t.name, t.typescript])
5934
+ );
5935
+ return { collections, globals, custom };
5684
5936
  }
5685
5937
  function fieldType(field, n, indent) {
5938
+ const custom = field.customType ? n.custom.get(field.customType) : void 0;
5939
+ if (custom) return custom;
5686
5940
  switch (field.type) {
5687
5941
  case "text":
5688
5942
  case "textarea":
@@ -5836,6 +6090,7 @@ export {
5836
6090
  EasyCMS,
5837
6091
  EasyCMSError,
5838
6092
  FIELD_TYPES,
6093
+ FIELD_TYPE_BASES,
5839
6094
  ForbiddenError,
5840
6095
  INTERNAL_COLLECTIONS,
5841
6096
  MAX_DEPTH,
@@ -5862,6 +6117,7 @@ export {
5862
6117
  createRestHandler,
5863
6118
  createRootEndpointHandler,
5864
6119
  defineConfig,
6120
+ defineFieldType,
5865
6121
  definePlugin,
5866
6122
  fillMissing,
5867
6123
  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.27.0",
4
4
  "description": "Embedded, code-first headless CMS for Nuxt and Next.js",
5
5
  "license": "MIT",
6
6
  "type": "module",