@easy-cms/core 0.28.0 → 0.29.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
@@ -238,6 +238,8 @@ interface EmailMessage {
238
238
  * development. Throw to report a failure: the message is retried later.
239
239
  */
240
240
  interface EmailAdapter {
241
+ /** What sends the email, e.g. `smtp`; shown to admins on the dashboard (System). */
242
+ readonly name?: string;
241
243
  /** The sender when a message has none, e.g. `Easy CMS <no-reply@example.com>`. */
242
244
  readonly from?: string | undefined;
243
245
  send(message: EmailMessage & {
@@ -1726,8 +1728,17 @@ interface Endpoint {
1726
1728
  */
1727
1729
  readonly handler: (request: EndpointRequest) => MaybePromise<unknown>;
1728
1730
  }
1731
+ /** A plugin's package and version, listed for admins on the dashboard (System). */
1732
+ interface PluginInfo {
1733
+ /** Usually the package name, e.g. `@acme/easy-cms-plugin-stats`. */
1734
+ readonly name: string;
1735
+ readonly version?: string;
1736
+ }
1729
1737
  /** Receives the config and returns a modified copy. Runs before validation. */
1730
- type Plugin = (config: Config) => MaybePromise<Config>;
1738
+ type Plugin = ((config: Config) => MaybePromise<Config>) & {
1739
+ /** Set by `definePlugin(plugin, info)`. */
1740
+ readonly info?: PluginInfo;
1741
+ };
1731
1742
  /** Holds a plugin's `PluginTypes` at the type level only: nothing is stored at runtime. */
1732
1743
  declare const PLUGIN_TYPES: unique symbol;
1733
1744
  /**
@@ -1749,17 +1760,18 @@ type TypedPlugin<T extends PluginTypes> = Plugin & {
1749
1760
  readonly [PLUGIN_TYPES]?: T;
1750
1761
  };
1751
1762
  /**
1752
- * Returns the plugin unchanged, with what it adds as types, so `cms.find()` and
1753
- * `CollectionDocument` know the plugin's fields:
1763
+ * Returns the plugin with what it adds as types, so `cms.find()` and `CollectionDocument` know
1764
+ * the plugin's fields; `info` (its name and version) is listed for admins on the dashboard:
1754
1765
  *
1755
1766
  * ```ts
1756
1767
  * export const colorPlugin = (options: { collections: readonly string[] }) =>
1757
1768
  * definePlugin<{ fields: { posts: readonly [{ readonly name: 'color'; readonly type: 'text' }] } }>(
1758
1769
  * (config) => ({ ...config, collections: … }),
1770
+ * { name: '@acme/easy-cms-plugin-color', version: '1.2.0' },
1759
1771
  * )
1760
1772
  * ```
1761
1773
  */
1762
- declare function definePlugin<const T extends PluginTypes = Record<never, never>>(plugin: Plugin): TypedPlugin<T>;
1774
+ declare function definePlugin<const T extends PluginTypes = Record<never, never>>(plugin: Plugin, info?: PluginInfo): TypedPlugin<T>;
1763
1775
  interface Config {
1764
1776
  /** Signs sessions. At least 32 characters; read it from `process.env.EASY_CMS_SECRET`. */
1765
1777
  readonly secret: string;
@@ -1831,6 +1843,8 @@ interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes'
1831
1843
  readonly endpoints: readonly Endpoint[];
1832
1844
  readonly commands: readonly CliCommand[];
1833
1845
  readonly fieldTypes: readonly FieldTypeDefinition[];
1846
+ /** The config's plugins in order, with their `info` where they give one. */
1847
+ readonly installedPlugins: readonly Partial<PluginInfo>[];
1834
1848
  }
1835
1849
  /**
1836
1850
  * Declares the Easy CMS config. Returns it unchanged, keeping literal types
@@ -2162,6 +2176,54 @@ interface AdminSchema {
2162
2176
  dashboard: AdminWidgetRef[];
2163
2177
  }
2164
2178
 
2179
+ /** Something an admin should look at, shown on the dashboard with a link to how to fix it. */
2180
+ type AdminAttention =
2181
+ /** Webhook deliveries that failed for good in the last 7 days; `url`: the one failing most. */
2182
+ {
2183
+ id: 'webhooks';
2184
+ count: number;
2185
+ url: string;
2186
+ }
2187
+ /** Emails waiting over an hour, or that failed for good in the last 7 days. */
2188
+ | {
2189
+ id: 'emails';
2190
+ count: number;
2191
+ }
2192
+ /** Scheduled publishing more than 10 minutes late: nothing runs `jobs/run`. */
2193
+ | {
2194
+ id: 'scheduled';
2195
+ count: number;
2196
+ }
2197
+ /** No `email` in the config: no forgotten-password links, invitations or form emails. */
2198
+ | {
2199
+ id: 'no-email';
2200
+ }
2201
+ /** In production without `serverURL`: links in emails can't be made. */
2202
+ | {
2203
+ id: 'no-server-url';
2204
+ };
2205
+ /** What the dashboard shows admins (`GET <api>/admin/status`). */
2206
+ interface AdminStatus {
2207
+ system: {
2208
+ /** `@easy-cms/core`'s version. */
2209
+ version: string;
2210
+ /** The database adapter, e.g. `sqlite` or `postgres`. */
2211
+ database: string;
2212
+ /** Where uploads are stored, e.g. `local` or `s3`. */
2213
+ storage: string;
2214
+ /** The email adapter's name, `custom` when it has none, or `null` without email. */
2215
+ email: string | null;
2216
+ /** The config's plugins, with their name and version when they give them. */
2217
+ plugins: {
2218
+ name?: string;
2219
+ version?: string;
2220
+ }[];
2221
+ /** Field types from packages (`fieldTypes`). */
2222
+ fieldTypes: string[];
2223
+ };
2224
+ attention: AdminAttention[];
2225
+ }
2226
+
2165
2227
  declare const SESSION_COOKIE = "ecms-session";
2166
2228
  declare const CSRF_COOKIE = "ecms-csrf";
2167
2229
  declare const CSRF_HEADER = "x-csrf-token";
@@ -2197,4 +2259,7 @@ declare const BUILTIN_COLLECTIONS: readonly ["users", "media"];
2197
2259
  /** Checks a config and returns every problem found. An empty array means it is valid. */
2198
2260
  declare function validateConfig(config: Config): ConfigIssue[];
2199
2261
 
2200
- 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, type UploadFromURLConfig, 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 };
2262
+ /** The version of `@easy-cms/core`, set when the package is built. */
2263
+ declare const VERSION: string;
2264
+
2265
+ export { ADMIN_ICONS, API_KEYS, API_KEY_GLOBAL_OPERATIONS, API_KEY_OPERATIONS, type Access, type AccessArgs, type AccessOptions, type AdminAttention, 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 AdminStatus, 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 PluginInfo, 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, type UploadFromURLConfig, VERSION, 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
@@ -1142,7 +1142,8 @@ var ADMIN_ICONS = [
1142
1142
  "chart-column",
1143
1143
  "chart-line"
1144
1144
  ];
1145
- function definePlugin(plugin) {
1145
+ function definePlugin(plugin, info) {
1146
+ if (info) Object.defineProperty(plugin, "info", { value: info, configurable: true });
1146
1147
  return plugin;
1147
1148
  }
1148
1149
  function defineConfig(config) {
@@ -1304,13 +1305,13 @@ function generateSlugs(fields, data, localization = null) {
1304
1305
  function mergeForUpdate(fields, existing, patch) {
1305
1306
  const result = {};
1306
1307
  for (const field of fields) {
1307
- const has = Object.hasOwn(patch, field.name);
1308
+ const has2 = Object.hasOwn(patch, field.name);
1308
1309
  const next = patch[field.name];
1309
1310
  const prev = existing[field.name];
1310
- if (field.type === "group" && has && isPlainObject(next)) {
1311
+ if (field.type === "group" && has2 && isPlainObject(next)) {
1311
1312
  result[field.name] = mergeForUpdate(field.fields, isPlainObject(prev) ? prev : {}, next);
1312
1313
  } else {
1313
- result[field.name] = has ? next : prev;
1314
+ result[field.name] = has2 ? next : prev;
1314
1315
  }
1315
1316
  }
1316
1317
  return result;
@@ -1541,6 +1542,7 @@ function normalizeSelect(field, raw, fail) {
1541
1542
  function consoleEmail(options = {}) {
1542
1543
  const sent = [];
1543
1544
  const adapter = {
1545
+ name: "console",
1544
1546
  from: options.from ?? "Easy CMS <no-reply@localhost>",
1545
1547
  sent,
1546
1548
  async send(message) {
@@ -2840,7 +2842,7 @@ async function resolveConfig(input) {
2840
2842
  config = applied.config;
2841
2843
  const issues = [...applied.issues, ...validateConfig(config)];
2842
2844
  if (issues.length > 0) throw new ConfigError(issues);
2843
- const { plugins: _plugins, ...rest } = config;
2845
+ const { plugins, ...rest } = config;
2844
2846
  const result = {
2845
2847
  ...rest,
2846
2848
  cors: config.cors ?? [],
@@ -2890,6 +2892,9 @@ async function resolveConfig(input) {
2890
2892
  globals: config.globals ?? [],
2891
2893
  endpoints: config.endpoints ?? [],
2892
2894
  commands: config.commands ?? [],
2895
+ installedPlugins: (plugins ?? []).map(
2896
+ (plugin) => typeof plugin === "function" && plugin.info ? { ...plugin.info } : {}
2897
+ ),
2893
2898
  fieldTypes: config.fieldTypes ?? []
2894
2899
  };
2895
2900
  resolved.add(result);
@@ -5385,6 +5390,80 @@ async function widgets(cms, user) {
5385
5390
  return out;
5386
5391
  }
5387
5392
 
5393
+ // src/version.ts
5394
+ var VERSION = true ? "0.29.0" : "0.0.0-dev";
5395
+
5396
+ // src/rest/admin-status.ts
5397
+ var DAY = 864e5;
5398
+ var has = (cms, slug) => cms.config.collections.some((c) => c.slug === slug);
5399
+ async function adminStatus(cms) {
5400
+ const config = cms.config;
5401
+ const attention = [];
5402
+ const now = Date.now();
5403
+ const weekAgo = new Date(now - 7 * DAY).toISOString();
5404
+ if (has(cms, WEBHOOK_DELIVERIES)) {
5405
+ const failed = await cms.db.find({
5406
+ collection: WEBHOOK_DELIVERIES,
5407
+ where: { and: [{ state: { equals: "failed" } }, { updatedAt: { gte: weekAgo } }] },
5408
+ sort: ["-updatedAt"],
5409
+ limit: 200,
5410
+ page: 1
5411
+ });
5412
+ if (failed.totalDocs > 0) {
5413
+ const byUrl = /* @__PURE__ */ new Map();
5414
+ for (const row of failed.docs) {
5415
+ const url2 = String(row.url ?? "");
5416
+ byUrl.set(url2, (byUrl.get(url2) ?? 0) + 1);
5417
+ }
5418
+ const [url] = [...byUrl.entries()].sort((a, b) => b[1] - a[1])[0] ?? [""];
5419
+ attention.push({ id: "webhooks", count: failed.totalDocs, url });
5420
+ }
5421
+ }
5422
+ if (has(cms, EMAIL_DELIVERIES)) {
5423
+ const count = await cms.db.count({
5424
+ collection: EMAIL_DELIVERIES,
5425
+ where: {
5426
+ or: [
5427
+ { and: [{ state: { equals: "failed" } }, { updatedAt: { gte: weekAgo } }] },
5428
+ {
5429
+ and: [
5430
+ { state: { equals: "pending" } },
5431
+ { createdAt: { lte: new Date(now - 60 * 6e4).toISOString() } }
5432
+ ]
5433
+ }
5434
+ ]
5435
+ }
5436
+ });
5437
+ if (count > 0) attention.push({ id: "emails", count });
5438
+ }
5439
+ if (has(cms, SCHEDULED_JOBS)) {
5440
+ const count = await cms.db.count({
5441
+ collection: SCHEDULED_JOBS,
5442
+ where: {
5443
+ and: [
5444
+ { state: { equals: "pending" } },
5445
+ { runAt: { lte: new Date(now - 10 * 6e4).toISOString() } }
5446
+ ]
5447
+ }
5448
+ });
5449
+ if (count > 0) attention.push({ id: "scheduled", count });
5450
+ }
5451
+ if (!config.email) attention.push({ id: "no-email" });
5452
+ if (!config.serverURL && process.env.NODE_ENV === "production")
5453
+ attention.push({ id: "no-server-url" });
5454
+ return {
5455
+ system: {
5456
+ version: VERSION,
5457
+ database: config.db.name,
5458
+ storage: config.upload.storage?.name ?? "local",
5459
+ email: config.email ? config.email.name ?? "custom" : null,
5460
+ plugins: config.installedPlugins.map((p) => ({ ...p })),
5461
+ fieldTypes: config.fieldTypes.map((t) => t.name)
5462
+ },
5463
+ attention
5464
+ };
5465
+ }
5466
+
5388
5467
  // src/rest/filter-options.ts
5389
5468
  function fieldAt(fields, path) {
5390
5469
  let current = fields;
@@ -5771,6 +5850,10 @@ async function route(cms, ctx, method, segments, options) {
5771
5850
  return { body: new Response(null, { status: 304, headers }) };
5772
5851
  return { body: new Response(file.body, { headers }) };
5773
5852
  }
5853
+ if (second === "status" && third === void 0) {
5854
+ if (ctx.user.role !== "admin" || ctx.user.apiKey) throw new ForbiddenError();
5855
+ return { body: await adminStatus(cms) };
5856
+ }
5774
5857
  if (second === "scheduled" && third === void 0)
5775
5858
  return { body: await cms.upcomingJobs({ user: ctx.user, overrideAccess: false }) };
5776
5859
  const [, , collection3, id2, extra] = segments;
@@ -6382,6 +6465,7 @@ export {
6382
6465
  SchemaError,
6383
6466
  TooManyRequestsError,
6384
6467
  UnauthorizedError,
6468
+ VERSION,
6385
6469
  ValidationError,
6386
6470
  WEBHOOK_EVENTS,
6387
6471
  anyone,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/core",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "Embedded, code-first headless CMS for Nuxt and Next.js",
5
5
  "license": "MIT",
6
6
  "type": "module",