@easy-cms/core 0.10.0 → 0.11.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
@@ -434,6 +434,8 @@ interface CollectionConfig {
434
434
  readonly singular?: Label;
435
435
  readonly plural?: Label;
436
436
  };
437
+ /** Icon in the admin menu. Default `file-text`. */
438
+ readonly icon?: AdminIcon;
437
439
  readonly fields: readonly Field[];
438
440
  /** Top-level field shown as the document title in the admin UI. */
439
441
  readonly useAsTitle?: string;
@@ -455,6 +457,8 @@ interface CollectionConfig {
455
457
  interface GlobalConfig {
456
458
  readonly slug: string;
457
459
  readonly label?: Label;
460
+ /** Icon in the admin menu. Default `settings`. */
461
+ readonly icon?: AdminIcon;
458
462
  readonly fields: readonly Field[];
459
463
  readonly drafts?: boolean;
460
464
  /** Keep a snapshot of every save. See `CollectionConfig.versions`. */
@@ -467,11 +471,25 @@ interface GlobalConfig {
467
471
  readonly hooks?: GlobalHooks;
468
472
  }
469
473
  type AdminLocale = 'en' | 'th';
474
+ /** Icons a collection or global can show in the admin menu ([Lucide](https://lucide.dev) names). */
475
+ 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"];
476
+ type AdminIcon = (typeof ADMIN_ICONS)[number];
477
+ /** Your or your client's brand in the admin UI. */
478
+ interface AdminBrand {
479
+ /** Shown in the menu, on the login page and in the browser tab. Default "Easy CMS". */
480
+ readonly name?: string;
481
+ /** Logo URL: a path on your site (`/logo.svg`) or an `https://` address. */
482
+ readonly logo?: string;
483
+ /** Main color as `#rrggbb`; lighter and darker shades are derived from it. */
484
+ readonly color?: string;
485
+ }
470
486
  interface AdminConfig {
471
487
  /** Where the admin UI is served. Default `/admin`. */
472
488
  readonly path?: string;
473
489
  /** Default admin UI language. Default `en`. */
474
490
  readonly locale?: AdminLocale;
491
+ /** Name, logo and color of the admin UI. */
492
+ readonly brand?: AdminBrand;
475
493
  }
476
494
  interface ImageSize {
477
495
  /** Key in `media.sizes`, e.g. `thumbnail`. */
@@ -1047,6 +1065,17 @@ declare class EasyCMS<C extends Config = Config> {
1047
1065
  private scheduleTarget;
1048
1066
  private globalScheduleTarget;
1049
1067
  private addJob;
1068
+ /**
1069
+ * The next pending jobs across all collections and globals, soonest first; with access
1070
+ * enforced, only those the user may update.
1071
+ */
1072
+ upcomingJobs(options?: AccessOptions & {
1073
+ limit?: number;
1074
+ }): Promise<(ScheduledJob & {
1075
+ collection?: string;
1076
+ global?: string;
1077
+ doc: ID;
1078
+ })[]>;
1050
1079
  private pendingJobs;
1051
1080
  private removeJob;
1052
1081
  /** @internal */
@@ -1356,6 +1385,7 @@ interface AdminCollection {
1356
1385
  singular?: Label;
1357
1386
  plural?: Label;
1358
1387
  };
1388
+ icon?: string;
1359
1389
  useAsTitle?: string;
1360
1390
  drafts: boolean;
1361
1391
  versions: boolean;
@@ -1374,6 +1404,7 @@ interface AdminCollection {
1374
1404
  interface AdminGlobal {
1375
1405
  slug: string;
1376
1406
  label?: Label;
1407
+ icon?: string;
1377
1408
  drafts: boolean;
1378
1409
  versions: boolean;
1379
1410
  /** Has live preview (`preview` in the config). */
@@ -1425,4 +1456,4 @@ declare const BUILTIN_COLLECTIONS: readonly ["users", "media"];
1425
1456
  /** Checks a config and returns every problem found. An empty array means it is valid. */
1426
1457
  declare function validateConfig(config: Config): ConfigIssue[];
1427
1458
 
1428
- export { type Access, type AccessArgs, type AccessOptions, type AdminCollection, type AdminConfig, type AdminField, type AdminGlobal, type AdminLocale, type AdminSchema, type AfterChangeHook, type AfterDeleteHook, type AfterReadHook, type ArrayField, Auth, type AuthConfig, type AuthUser, BUILTIN_COLLECTIONS, type BeforeChangeHook, type BeforeDeleteHook, type BeforeValidateHook, type Block, type BlocksField, type BooleanField, type BuiltinMediaCollection, type BuiltinUsersCollection, type BundleHints, CONFIG_FILE_NAMES, CSRF_COOKIE, CSRF_HEADER, type CollectionAccess, type CollectionConfig, type CollectionDocument, type CollectionHooks, type CollectionSlug, type Config, ConfigError, type ConfigIssue, type CreateEasyCMSOptions, type CreateInput, type CreateMigrationResult, DEFAULT_ADMIN_PATH, DEFAULT_API_PATH, DEFAULT_DEPTH, DEFAULT_MAX_FILE_SIZE, DEFAULT_MAX_VERSIONS, DEFAULT_TOKEN_EXPIRATION, type Database, type DatabaseAdapter, type DatabaseInitArgs, type DateField, type DepthOptions, EXTENSIONS, EasyCMS, EasyCMSError, type EmailField, FIELD_TYPES, type Field, type FieldAccess, type FieldAccessFn, type FieldError, type FieldType, type FieldValidate, type FieldValidateContext, type FieldValue, type FieldsInput, type FieldsValue, 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, 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 UploadConfig, type UploadField, ValidationError, type Version, type VersionSummary, type VersionsConfig, WEBHOOK_EVENTS, type WebhookConfig, type WebhookEvent, type WebhookPayload, type Where, type WhereOperators, anyone, applyDefaults, consoleLogger, createEasyCMS, createRestHandler, defineConfig, fillMissing, findConfigFile, generateSlugs, generateTypes, hasRows, hashPassword, imageDimensions, importConfig, isAdmin, isLoggedIn, loadConfig, localStorage, mergeForUpdate, mimeAllowed, parseId, populate, readCookie, resolveConfig, rowFields, silentLogger, singularize, slugify, sniffMimeType, validateConfig, validateFields, verifyPassword };
1459
+ export { ADMIN_ICONS, type Access, type AccessArgs, type AccessOptions, type AdminBrand, type AdminCollection, type AdminConfig, type AdminField, type AdminGlobal, type AdminIcon, type AdminLocale, type AdminSchema, type AfterChangeHook, type AfterDeleteHook, type AfterReadHook, type ArrayField, Auth, type AuthConfig, type AuthUser, BUILTIN_COLLECTIONS, type BeforeChangeHook, type BeforeDeleteHook, type BeforeValidateHook, type Block, type BlocksField, type BooleanField, type BuiltinMediaCollection, type BuiltinUsersCollection, type BundleHints, CONFIG_FILE_NAMES, CSRF_COOKIE, CSRF_HEADER, type CollectionAccess, type CollectionConfig, type CollectionDocument, type CollectionHooks, type CollectionSlug, type Config, ConfigError, type ConfigIssue, type CreateEasyCMSOptions, type CreateInput, type CreateMigrationResult, DEFAULT_ADMIN_PATH, DEFAULT_API_PATH, DEFAULT_DEPTH, DEFAULT_MAX_FILE_SIZE, DEFAULT_MAX_VERSIONS, DEFAULT_TOKEN_EXPIRATION, type Database, type DatabaseAdapter, type DatabaseInitArgs, type DateField, type DepthOptions, EXTENSIONS, EasyCMS, EasyCMSError, type EmailField, FIELD_TYPES, type Field, type FieldAccess, type FieldAccessFn, type FieldError, type FieldType, type FieldValidate, type FieldValidateContext, type FieldValue, type FieldsInput, type FieldsValue, 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, 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 UploadConfig, type UploadField, ValidationError, type Version, type VersionSummary, type VersionsConfig, WEBHOOK_EVENTS, type WebhookConfig, type WebhookEvent, type WebhookPayload, type Where, type WhereOperators, anyone, applyDefaults, consoleLogger, createEasyCMS, createRestHandler, defineConfig, fillMissing, findConfigFile, generateSlugs, generateTypes, hasRows, hashPassword, imageDimensions, importConfig, isAdmin, isLoggedIn, loadConfig, localStorage, mergeForUpdate, mimeAllowed, parseId, populate, readCookie, resolveConfig, rowFields, silentLogger, singularize, slugify, sniffMimeType, validateConfig, validateFields, verifyPassword };
package/dist/index.js CHANGED
@@ -242,6 +242,7 @@ function withUsers(config) {
242
242
  slug: USERS,
243
243
  labels: { singular: { en: "User", th: "\u0E1C\u0E39\u0E49\u0E43\u0E0A\u0E49" }, plural: { en: "Users", th: "\u0E1C\u0E39\u0E49\u0E43\u0E0A\u0E49" } },
244
244
  useAsTitle: "email",
245
+ icon: "users",
245
246
  ...custom,
246
247
  fields: [...userFields(roles), ...custom?.fields ?? []],
247
248
  access: {
@@ -302,6 +303,7 @@ function withMedia(config) {
302
303
  const media = {
303
304
  slug: MEDIA,
304
305
  labels: { singular: { en: "Media", th: "\u0E2A\u0E37\u0E48\u0E2D" }, plural: { en: "Media", th: "\u0E04\u0E25\u0E31\u0E07\u0E2A\u0E37\u0E48\u0E2D" } },
306
+ icon: "image",
305
307
  useAsTitle: "filename",
306
308
  ...custom,
307
309
  fields: [...mediaFields(), ...custom?.fields ?? []],
@@ -695,6 +697,55 @@ var Auth = class {
695
697
  };
696
698
 
697
699
  // src/config.ts
700
+ var ADMIN_ICONS = [
701
+ "file-text",
702
+ "newspaper",
703
+ "book-open",
704
+ "notebook",
705
+ "folder",
706
+ "tag",
707
+ "tags",
708
+ "image",
709
+ "images",
710
+ "video",
711
+ "music",
712
+ "file",
713
+ "users",
714
+ "user",
715
+ "building",
716
+ "store",
717
+ "shopping-bag",
718
+ "shopping-cart",
719
+ "package",
720
+ "box",
721
+ "calendar",
722
+ "calendar-days",
723
+ "map-pin",
724
+ "globe",
725
+ "house",
726
+ "layout-grid",
727
+ "layers",
728
+ "star",
729
+ "heart",
730
+ "message-square",
731
+ "mail",
732
+ "phone",
733
+ "briefcase",
734
+ "graduation-cap",
735
+ "utensils",
736
+ "car",
737
+ "settings",
738
+ "sliders-horizontal",
739
+ "palette",
740
+ "megaphone",
741
+ "bell",
742
+ "link",
743
+ "quote",
744
+ "circle-help",
745
+ "award",
746
+ "ticket",
747
+ "camera"
748
+ ];
698
749
  function defineConfig(config) {
699
750
  return config;
700
751
  }
@@ -1236,9 +1287,12 @@ function validateConfig(config) {
1236
1287
  const path = `collections.${collection2.slug ?? `[${i}]`}`;
1237
1288
  validateContainer(collection2, path, collectionSlugs, add);
1238
1289
  validateUseAsTitle(collection2, path, add);
1290
+ validateIcon(collection2, path, add);
1239
1291
  });
1240
1292
  globals.forEach((global2, i) => {
1241
- validateContainer(global2, `globals.${global2.slug ?? `[${i}]`}`, collectionSlugs, add);
1293
+ const path = `globals.${global2.slug ?? `[${i}]`}`;
1294
+ validateContainer(global2, path, collectionSlugs, add);
1295
+ validateIcon(global2, path, add);
1242
1296
  });
1243
1297
  const plugins = config.plugins;
1244
1298
  if (plugins !== void 0 && (!Array.isArray(plugins) || plugins.some((p) => typeof p !== "function"))) {
@@ -1267,6 +1321,31 @@ function validateAdmin(config, add) {
1267
1321
  if (admin.locale !== void 0 && admin.locale !== "en" && admin.locale !== "th") {
1268
1322
  add("admin.locale", `must be "en" or "th" (got ${JSON.stringify(admin.locale)})`);
1269
1323
  }
1324
+ const brand = admin.brand;
1325
+ if (brand === void 0) return;
1326
+ if (typeof brand !== "object" || brand === null) {
1327
+ add("admin.brand", "must be an object", "e.g. brand: { name: 'Acme', color: '#0f766e' }");
1328
+ return;
1329
+ }
1330
+ const { name, logo, color } = brand;
1331
+ if (name !== void 0 && (typeof name !== "string" || name.trim() === "")) {
1332
+ add("admin.brand.name", "must be a non-empty string");
1333
+ }
1334
+ if (logo !== void 0 && (typeof logo !== "string" || !/^(\/|https?:\/\/|data:image\/)/.test(logo))) {
1335
+ add(
1336
+ "admin.brand.logo",
1337
+ 'must be a path starting with "/" or an http(s) URL',
1338
+ "e.g. logo: '/logo.svg'"
1339
+ );
1340
+ }
1341
+ if (color !== void 0 && (typeof color !== "string" || !/^#[0-9a-f]{6}$/i.test(color))) {
1342
+ add("admin.brand.color", `must be a hex color like "#0f766e" (got ${JSON.stringify(color)})`);
1343
+ }
1344
+ }
1345
+ function validateIcon(container, path, add) {
1346
+ const icon = container.icon;
1347
+ if (icon === void 0 || ADMIN_ICONS.includes(icon)) return;
1348
+ add(`${path}.icon`, `unknown icon ${JSON.stringify(icon)}`, `one of: ${ADMIN_ICONS.join(", ")}`);
1270
1349
  }
1271
1350
  function validateAuth(config, add) {
1272
1351
  const auth = config.auth;
@@ -1665,7 +1744,8 @@ async function resolveConfig(input) {
1665
1744
  routes: { api: `/${(config.routes?.api ?? DEFAULT_API_PATH).replace(/^\/+|\/+$/g, "")}` },
1666
1745
  admin: {
1667
1746
  path: config.admin?.path ?? DEFAULT_ADMIN_PATH,
1668
- locale: config.admin?.locale ?? "en"
1747
+ locale: config.admin?.locale ?? "en",
1748
+ brand: config.admin?.brand ?? {}
1669
1749
  },
1670
1750
  upload: {
1671
1751
  dir: config.upload?.dir ?? "uploads",
@@ -3040,6 +3120,42 @@ var EasyCMS = class {
3040
3120
  });
3041
3121
  return toJob(row);
3042
3122
  }
3123
+ /**
3124
+ * The next pending jobs across all collections and globals, soonest first; with access
3125
+ * enforced, only those the user may update.
3126
+ */
3127
+ async upcomingJobs(options = {}) {
3128
+ if (!this.config.collections.some((c) => c.slug === SCHEDULED_JOBS)) return [];
3129
+ const guard = guardOf(options);
3130
+ const limit = options.limit ?? 10;
3131
+ const rows = await this.db.find({
3132
+ collection: SCHEDULED_JOBS,
3133
+ where: { state: { equals: "pending" } },
3134
+ sort: ["runAt"],
3135
+ limit: limit * 5,
3136
+ page: 1
3137
+ });
3138
+ const allowed2 = /* @__PURE__ */ new Map();
3139
+ const may = async (parent) => {
3140
+ if (!guard.enforce) return true;
3141
+ const cached = allowed2.get(parent);
3142
+ if (cached !== void 0) return cached;
3143
+ const config = parent.startsWith("global:") ? this.config.globals.find((g) => g.slug === parent.slice("global:".length)) : this.config.collections.find((c) => c.slug === parent);
3144
+ const access = config ? await evaluateAccess(config.access?.update, { user: guard.user }) : false;
3145
+ const result = access !== false;
3146
+ allowed2.set(parent, result);
3147
+ return result;
3148
+ };
3149
+ const jobs = [];
3150
+ for (const row of rows.docs) {
3151
+ const parent = String(row.parent);
3152
+ if (!await may(parent)) continue;
3153
+ const target = parent.startsWith("global:") ? { global: parent.slice("global:".length) } : { collection: parent };
3154
+ jobs.push({ ...toJob(row), ...target, doc: row.doc });
3155
+ if (jobs.length === limit) break;
3156
+ }
3157
+ return jobs;
3158
+ }
3043
3159
  async pendingJobs(parent, doc) {
3044
3160
  const rows = await this.db.find({
3045
3161
  collection: SCHEDULED_JOBS,
@@ -3601,6 +3717,7 @@ async function collection(config, user, localized) {
3601
3717
  };
3602
3718
  if (config.labels) result.labels = config.labels;
3603
3719
  if (config.useAsTitle) result.useAsTitle = config.useAsTitle;
3720
+ if (config.icon) result.icon = config.icon;
3604
3721
  return result;
3605
3722
  }
3606
3723
  async function global(config, user, localized) {
@@ -3621,6 +3738,7 @@ async function global(config, user, localized) {
3621
3738
  }
3622
3739
  };
3623
3740
  if (config.label !== void 0) result.label = config.label;
3741
+ if (config.icon) result.icon = config.icon;
3624
3742
  return result;
3625
3743
  }
3626
3744
  async function adminSchema(cms, user) {
@@ -3863,6 +3981,8 @@ async function route(cms, ctx, method, segments, options) {
3863
3981
  if (!ctx.user) throw new UnauthorizedError();
3864
3982
  if (second === "schema" && third === void 0)
3865
3983
  return { body: await adminSchema(cms, ctx.user) };
3984
+ if (second === "scheduled" && third === void 0)
3985
+ return { body: await cms.upcomingJobs({ user: ctx.user, overrideAccess: false }) };
3866
3986
  const [, , collection3, id2, extra] = segments;
3867
3987
  if (second === "access" && collection3 && id2 && extra === void 0) {
3868
3988
  if (INTERNAL_COLLECTIONS.has(collection3) || !cms.config.collections.some((c) => c.slug === collection3)) {
@@ -4392,6 +4512,7 @@ ${map(n.globals)}
4392
4512
  return parts.join("\n");
4393
4513
  }
4394
4514
  export {
4515
+ ADMIN_ICONS,
4395
4516
  Auth,
4396
4517
  BUILTIN_COLLECTIONS,
4397
4518
  CONFIG_FILE_NAMES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/core",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Embedded, code-first headless CMS for Nuxt and Next.js",
5
5
  "license": "MIT",
6
6
  "type": "module",