@easy-cms/core 0.11.0 → 0.13.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 +124 -3
- package/dist/index.js +281 -13
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -194,6 +194,21 @@ interface FieldValidateContext {
|
|
|
194
194
|
}
|
|
195
195
|
/** Return `true` when valid, or an error message to show on the field. */
|
|
196
196
|
type FieldValidate<TValue> = (value: TValue | null | undefined, ctx: FieldValidateContext) => true | string | Promise<true | string>;
|
|
197
|
+
/**
|
|
198
|
+
* A Web Component from an admin module (`admin.modules`), by its tag name, which must start
|
|
199
|
+
* with `ecms-`. `props` (plain JSON) reach the element as its `options` property.
|
|
200
|
+
*/
|
|
201
|
+
type AdminComponent = string | {
|
|
202
|
+
readonly tag: string;
|
|
203
|
+
readonly props?: Readonly<Record<string, unknown>>;
|
|
204
|
+
};
|
|
205
|
+
/** How the admin shows a field, with components from admin modules. */
|
|
206
|
+
interface FieldAdmin {
|
|
207
|
+
/** Shown instead of the field's input; the admin keeps the label and error messages. */
|
|
208
|
+
readonly component?: AdminComponent;
|
|
209
|
+
/** Shown below the field, e.g. a length meter or a preview. */
|
|
210
|
+
readonly after?: readonly AdminComponent[];
|
|
211
|
+
}
|
|
197
212
|
interface BaseField<TType extends string, TValue> {
|
|
198
213
|
readonly type: TType;
|
|
199
214
|
readonly name: string;
|
|
@@ -211,6 +226,10 @@ interface BaseField<TType extends string, TValue> {
|
|
|
211
226
|
* hasMany fields: localize the fields inside a group or array instead.
|
|
212
227
|
*/
|
|
213
228
|
readonly localized?: boolean;
|
|
229
|
+
/** `sidebar`: shown in the edit page's side panel instead of the main form (top-level fields). */
|
|
230
|
+
readonly position?: 'sidebar';
|
|
231
|
+
/** Custom admin components for this field. */
|
|
232
|
+
readonly admin?: FieldAdmin;
|
|
214
233
|
}
|
|
215
234
|
interface TextField extends BaseField<'text', string> {
|
|
216
235
|
readonly minLength?: number;
|
|
@@ -427,6 +446,11 @@ interface VersionsConfig {
|
|
|
427
446
|
/** Versions kept per document; older ones are deleted. Default 50. */
|
|
428
447
|
readonly max?: number;
|
|
429
448
|
}
|
|
449
|
+
/** Custom admin components for a collection's or global's edit page. */
|
|
450
|
+
interface ContainerAdmin {
|
|
451
|
+
/** Panels in the edit page's side column, below publishing (components from admin modules). */
|
|
452
|
+
readonly sidebar?: readonly AdminComponent[];
|
|
453
|
+
}
|
|
430
454
|
interface CollectionConfig {
|
|
431
455
|
/** URL and table name. Lowercase letters, digits, `-` and `_`. */
|
|
432
456
|
readonly slug: string;
|
|
@@ -436,6 +460,12 @@ interface CollectionConfig {
|
|
|
436
460
|
};
|
|
437
461
|
/** Icon in the admin menu. Default `file-text`. */
|
|
438
462
|
readonly icon?: AdminIcon;
|
|
463
|
+
/**
|
|
464
|
+
* How the admin opens documents from the list: `drawer` slides a panel over the list, handy
|
|
465
|
+
* for small collections (categories, tags). Default `page`. Collections with drafts, versions
|
|
466
|
+
* or live preview always use the page.
|
|
467
|
+
*/
|
|
468
|
+
readonly editIn?: 'page' | 'drawer';
|
|
439
469
|
readonly fields: readonly Field[];
|
|
440
470
|
/** Top-level field shown as the document title in the admin UI. */
|
|
441
471
|
readonly useAsTitle?: string;
|
|
@@ -453,6 +483,8 @@ interface CollectionConfig {
|
|
|
453
483
|
readonly preview?: PreviewURL;
|
|
454
484
|
readonly access?: CollectionAccess;
|
|
455
485
|
readonly hooks?: CollectionHooks;
|
|
486
|
+
/** Custom admin components. */
|
|
487
|
+
readonly admin?: ContainerAdmin;
|
|
456
488
|
}
|
|
457
489
|
interface GlobalConfig {
|
|
458
490
|
readonly slug: string;
|
|
@@ -469,6 +501,8 @@ interface GlobalConfig {
|
|
|
469
501
|
readonly preview?: PreviewURL;
|
|
470
502
|
readonly access?: GlobalAccess;
|
|
471
503
|
readonly hooks?: GlobalHooks;
|
|
504
|
+
/** Custom admin components. */
|
|
505
|
+
readonly admin?: ContainerAdmin;
|
|
472
506
|
}
|
|
473
507
|
type AdminLocale = 'en' | 'th';
|
|
474
508
|
/** Icons a collection or global can show in the admin menu ([Lucide](https://lucide.dev) names). */
|
|
@@ -490,6 +524,24 @@ interface AdminConfig {
|
|
|
490
524
|
readonly locale?: AdminLocale;
|
|
491
525
|
/** Name, logo and color of the admin UI. */
|
|
492
526
|
readonly brand?: AdminBrand;
|
|
527
|
+
/**
|
|
528
|
+
* The public site, for the admin's "View site" link: a path (`/`) or an `https://` URL.
|
|
529
|
+
* Default `/` with Nuxt and Next.js; none with the standalone server.
|
|
530
|
+
*/
|
|
531
|
+
readonly siteUrl?: string;
|
|
532
|
+
/**
|
|
533
|
+
* Order of collections in the admin menu, by slug, e.g. `['posts', 'categories', 'media']`.
|
|
534
|
+
* Collections not listed follow in config order, with the media library last.
|
|
535
|
+
*/
|
|
536
|
+
readonly menu?: readonly string[];
|
|
537
|
+
/**
|
|
538
|
+
* JavaScript modules the admin loads after login, which define Web Components used by
|
|
539
|
+
* fields (`admin.component`, `admin.after`) and edit pages (`admin.sidebar`). Each entry is a
|
|
540
|
+
* package export (`'@easy-cms/plugin-seo/admin'`) or a file path relative to the project root
|
|
541
|
+
* (`'./admin/color-picker.js'`), resolved on the server. A module must be one self-contained ES
|
|
542
|
+
* module file. Remote URLs are not allowed.
|
|
543
|
+
*/
|
|
544
|
+
readonly modules?: readonly string[];
|
|
493
545
|
}
|
|
494
546
|
interface ImageSize {
|
|
495
547
|
/** Key in `media.sizes`, e.g. `thumbnail`. */
|
|
@@ -530,6 +582,36 @@ interface AuthConfig {
|
|
|
530
582
|
*/
|
|
531
583
|
readonly trustedOrigins?: readonly string[];
|
|
532
584
|
}
|
|
585
|
+
/** What an endpoint's handler receives. */
|
|
586
|
+
interface EndpointRequest {
|
|
587
|
+
readonly request: Request;
|
|
588
|
+
readonly url: URL;
|
|
589
|
+
/** Values of `:name` segments in the endpoint's path. */
|
|
590
|
+
readonly params: Readonly<Record<string, string>>;
|
|
591
|
+
/** The logged-in user (session cookie or Bearer token), or `null`. */
|
|
592
|
+
readonly user: AuthUser | null;
|
|
593
|
+
/** The Local API. Pass `{ user, overrideAccess: false }` to apply the user's access rules. */
|
|
594
|
+
readonly cms: EasyCMS;
|
|
595
|
+
/** The request's JSON body, which must be an object (at most 1 MB). */
|
|
596
|
+
json(): Promise<Record<string, unknown>>;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* A custom REST endpoint, served under `routes.api`. Writes from the browser pass the same
|
|
600
|
+
* CSRF check as the built-in endpoints.
|
|
601
|
+
*/
|
|
602
|
+
interface Endpoint {
|
|
603
|
+
/**
|
|
604
|
+
* Path under the API, e.g. `/seo/generate` or `/stats/:collection`. The first segment must
|
|
605
|
+
* not be a collection slug or one of `users`, `globals`, `admin`, `jobs`.
|
|
606
|
+
*/
|
|
607
|
+
readonly path: string;
|
|
608
|
+
readonly method: 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
609
|
+
/**
|
|
610
|
+
* Returns a `Response`, or a value that is sent as JSON. Throw an Easy CMS error
|
|
611
|
+
* (`UnauthorizedError`, `ForbiddenError`, `ValidationError`, `NotFoundError`) for error responses.
|
|
612
|
+
*/
|
|
613
|
+
readonly handler: (request: EndpointRequest) => MaybePromise<unknown>;
|
|
614
|
+
}
|
|
533
615
|
/** Receives the config and returns a modified copy. Runs before validation. */
|
|
534
616
|
type Plugin = (config: Config) => MaybePromise<Config>;
|
|
535
617
|
interface Config {
|
|
@@ -567,10 +649,12 @@ interface Config {
|
|
|
567
649
|
*/
|
|
568
650
|
readonly collections?: readonly CollectionConfig[];
|
|
569
651
|
readonly globals?: readonly GlobalConfig[];
|
|
652
|
+
/** Custom REST endpoints, e.g. from plugins. */
|
|
653
|
+
readonly endpoints?: readonly Endpoint[];
|
|
570
654
|
readonly plugins?: readonly Plugin[];
|
|
571
655
|
}
|
|
572
656
|
/** The config after plugins ran, validation passed and defaults were applied. */
|
|
573
|
-
interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes' | 'admin' | 'upload' | 'auth' | 'collections' | 'globals' | 'plugins'> {
|
|
657
|
+
interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes' | 'admin' | 'upload' | 'auth' | 'collections' | 'globals' | 'endpoints' | 'plugins'> {
|
|
574
658
|
readonly cors: readonly string[] | '*';
|
|
575
659
|
/** `null` without localization. */
|
|
576
660
|
readonly localization: Required<LocalizationConfig> | null;
|
|
@@ -580,6 +664,7 @@ interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes'
|
|
|
580
664
|
readonly auth: Required<AuthConfig>;
|
|
581
665
|
readonly collections: readonly CollectionConfig[];
|
|
582
666
|
readonly globals: readonly GlobalConfig[];
|
|
667
|
+
readonly endpoints: readonly Endpoint[];
|
|
583
668
|
}
|
|
584
669
|
/**
|
|
585
670
|
* Declares the Easy CMS config. Returns it unchanged, keeping literal types
|
|
@@ -908,9 +993,11 @@ declare class EasyCMS<C extends Config = Config> {
|
|
|
908
993
|
readonly auth: Auth;
|
|
909
994
|
/** Where uploaded files are stored. */
|
|
910
995
|
readonly storage: StorageAdapter;
|
|
996
|
+
/** Project root: relative paths in the config (e.g. `admin.modules`) start here. */
|
|
997
|
+
readonly cwd: string;
|
|
911
998
|
private readonly versions;
|
|
912
999
|
private readonly webhooks;
|
|
913
|
-
constructor(config: ResolvedConfig, db: Database, logger?: Logger, storage?: StorageAdapter);
|
|
1000
|
+
constructor(config: ResolvedConfig, db: Database, logger?: Logger, storage?: StorageAdapter, cwd?: string);
|
|
914
1001
|
find<S extends Slug<C>>(collection: S, options?: FindOptions): Promise<PaginatedDocs<Doc<C, S>>>;
|
|
915
1002
|
findById<S extends Slug<C>>(collection: S, id: ID, options?: ReadOptions): Promise<Doc<C, S> | null>;
|
|
916
1003
|
count<S extends Slug<C>>(collection: S, options?: {
|
|
@@ -1337,12 +1424,29 @@ declare const DEFAULT_ADMIN_PATH = "/admin";
|
|
|
1337
1424
|
declare const DEFAULT_API_PATH = "/api/cms";
|
|
1338
1425
|
declare const DEFAULT_MAX_FILE_SIZE: number;
|
|
1339
1426
|
declare const DEFAULT_TOKEN_EXPIRATION: number;
|
|
1427
|
+
/**
|
|
1428
|
+
* Runs the config's plugins in order, without validating the result. Build tools use it to
|
|
1429
|
+
* read what plugins add (e.g. `admin.modules`) when the config is not complete yet.
|
|
1430
|
+
*/
|
|
1431
|
+
declare function applyPlugins(input: Config): Promise<Config>;
|
|
1340
1432
|
/**
|
|
1341
1433
|
* Runs plugins in order, adds the built-in collections, validates the result
|
|
1342
1434
|
* and fills in defaults. Throws `ConfigError`. Resolving twice is a no-op.
|
|
1343
1435
|
*/
|
|
1344
1436
|
declare function resolveConfig(input: Config | ResolvedConfig): Promise<ResolvedConfig>;
|
|
1345
1437
|
|
|
1438
|
+
/**
|
|
1439
|
+
* Finds the file of an admin module (`admin.modules`): a package export or a path relative to
|
|
1440
|
+
* the project root. Looks from the project root first, then from the server's entry file
|
|
1441
|
+
* (a Nuxt `.output/server` build keeps traced packages next to it). `undefined` when not found.
|
|
1442
|
+
*/
|
|
1443
|
+
declare function resolveAdminModule(specifier: string, cwd?: string): string | undefined;
|
|
1444
|
+
|
|
1445
|
+
/** A Web Component from an admin module, with its `props` (plain JSON). */
|
|
1446
|
+
interface AdminComponentRef {
|
|
1447
|
+
tag: string;
|
|
1448
|
+
props?: Record<string, unknown>;
|
|
1449
|
+
}
|
|
1346
1450
|
/** A field as the admin UI sees it: plain JSON, no functions. */
|
|
1347
1451
|
interface AdminField {
|
|
1348
1452
|
name: string;
|
|
@@ -1378,6 +1482,13 @@ interface AdminField {
|
|
|
1378
1482
|
readOnly?: boolean;
|
|
1379
1483
|
/** One value per content locale. */
|
|
1380
1484
|
localized?: boolean;
|
|
1485
|
+
/** Shown in the edit page's side panel. */
|
|
1486
|
+
position?: 'sidebar';
|
|
1487
|
+
/** Components from admin modules: instead of the input, and below the field. */
|
|
1488
|
+
admin?: {
|
|
1489
|
+
component?: AdminComponentRef;
|
|
1490
|
+
after?: AdminComponentRef[];
|
|
1491
|
+
};
|
|
1381
1492
|
}
|
|
1382
1493
|
interface AdminCollection {
|
|
1383
1494
|
slug: string;
|
|
@@ -1387,6 +1498,8 @@ interface AdminCollection {
|
|
|
1387
1498
|
};
|
|
1388
1499
|
icon?: string;
|
|
1389
1500
|
useAsTitle?: string;
|
|
1501
|
+
/** Documents open in a panel over the list. */
|
|
1502
|
+
editIn?: 'drawer';
|
|
1390
1503
|
drafts: boolean;
|
|
1391
1504
|
versions: boolean;
|
|
1392
1505
|
/** Has live preview (`preview` in the config). */
|
|
@@ -1394,6 +1507,8 @@ interface AdminCollection {
|
|
|
1394
1507
|
/** Publishing can be scheduled. */
|
|
1395
1508
|
schedule: boolean;
|
|
1396
1509
|
fields: AdminField[];
|
|
1510
|
+
/** Panels from admin modules in the edit page's side column. */
|
|
1511
|
+
sidebar?: AdminComponentRef[];
|
|
1397
1512
|
permissions: {
|
|
1398
1513
|
read: boolean;
|
|
1399
1514
|
create: boolean;
|
|
@@ -1412,6 +1527,8 @@ interface AdminGlobal {
|
|
|
1412
1527
|
/** Publishing can be scheduled. */
|
|
1413
1528
|
schedule: boolean;
|
|
1414
1529
|
fields: AdminField[];
|
|
1530
|
+
/** Panels from admin modules in the edit page's side column. */
|
|
1531
|
+
sidebar?: AdminComponentRef[];
|
|
1415
1532
|
permissions: {
|
|
1416
1533
|
read: boolean;
|
|
1417
1534
|
update: boolean;
|
|
@@ -1419,6 +1536,8 @@ interface AdminGlobal {
|
|
|
1419
1536
|
}
|
|
1420
1537
|
interface AdminSchema {
|
|
1421
1538
|
locale: AdminLocale;
|
|
1539
|
+
/** Menu order of collections (`admin.menu`); unlisted ones follow. */
|
|
1540
|
+
menu: string[];
|
|
1422
1541
|
/** Content locales, when the config has `localization`. */
|
|
1423
1542
|
localization: {
|
|
1424
1543
|
locales: string[];
|
|
@@ -1426,6 +1545,8 @@ interface AdminSchema {
|
|
|
1426
1545
|
} | null;
|
|
1427
1546
|
collections: AdminCollection[];
|
|
1428
1547
|
globals: AdminGlobal[];
|
|
1548
|
+
/** URLs under the API of the admin modules to load (`admin.modules`). */
|
|
1549
|
+
modules: string[];
|
|
1429
1550
|
}
|
|
1430
1551
|
|
|
1431
1552
|
declare const SESSION_COOKIE = "ecms-session";
|
|
@@ -1456,4 +1577,4 @@ declare const BUILTIN_COLLECTIONS: readonly ["users", "media"];
|
|
|
1456
1577
|
/** Checks a config and returns every problem found. An empty array means it is valid. */
|
|
1457
1578
|
declare function validateConfig(config: Config): ConfigIssue[];
|
|
1458
1579
|
|
|
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 };
|
|
1580
|
+
export { ADMIN_ICONS, 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 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 ContainerAdmin, 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, 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 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, applyPlugins, consoleLogger, createEasyCMS, createRestHandler, defineConfig, fillMissing, findConfigFile, generateSlugs, generateTypes, hasRows, hashPassword, imageDimensions, importConfig, isAdmin, isLoggedIn, loadConfig, localStorage, mergeForUpdate, mimeAllowed, parseId, populate, readCookie, resolveAdminModule, resolveConfig, rowFields, silentLogger, singularize, slugify, sniffMimeType, validateConfig, validateFields, verifyPassword };
|
package/dist/index.js
CHANGED
|
@@ -1279,15 +1279,30 @@ function validateConfig(config) {
|
|
|
1279
1279
|
const slug = checkSlug(collection2, `collections[${i}]`, seenCollections, add);
|
|
1280
1280
|
if (slug) collectionSlugs.add(slug);
|
|
1281
1281
|
});
|
|
1282
|
+
const menu = config.admin?.menu;
|
|
1283
|
+
if (menu !== void 0) {
|
|
1284
|
+
if (!Array.isArray(menu)) {
|
|
1285
|
+
add("admin.menu", "must be an array of collection slugs", "e.g. menu: ['posts', 'media']");
|
|
1286
|
+
} else {
|
|
1287
|
+
for (const [i, slug] of menu.entries()) {
|
|
1288
|
+
if (typeof slug !== "string" || !collectionSlugs.has(slug))
|
|
1289
|
+
add(`admin.menu[${i}]`, `unknown collection ${JSON.stringify(slug)}`);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1282
1293
|
const seenGlobals = /* @__PURE__ */ new Set();
|
|
1283
1294
|
globals.forEach((global2, i) => {
|
|
1284
1295
|
checkSlug(global2, `globals[${i}]`, seenGlobals, add);
|
|
1285
1296
|
});
|
|
1297
|
+
validateEndpoints(config, collectionSlugs, add);
|
|
1286
1298
|
collections.forEach((collection2, i) => {
|
|
1287
1299
|
const path = `collections.${collection2.slug ?? `[${i}]`}`;
|
|
1288
1300
|
validateContainer(collection2, path, collectionSlugs, add);
|
|
1289
1301
|
validateUseAsTitle(collection2, path, add);
|
|
1290
1302
|
validateIcon(collection2, path, add);
|
|
1303
|
+
const editIn = collection2.editIn;
|
|
1304
|
+
if (editIn !== void 0 && editIn !== "page" && editIn !== "drawer")
|
|
1305
|
+
add(`${path}.editIn`, `must be "page" or "drawer" (got ${JSON.stringify(editIn)})`);
|
|
1291
1306
|
});
|
|
1292
1307
|
globals.forEach((global2, i) => {
|
|
1293
1308
|
const path = `globals.${global2.slug ?? `[${i}]`}`;
|
|
@@ -1321,6 +1336,32 @@ function validateAdmin(config, add) {
|
|
|
1321
1336
|
if (admin.locale !== void 0 && admin.locale !== "en" && admin.locale !== "th") {
|
|
1322
1337
|
add("admin.locale", `must be "en" or "th" (got ${JSON.stringify(admin.locale)})`);
|
|
1323
1338
|
}
|
|
1339
|
+
const siteUrl = admin.siteUrl;
|
|
1340
|
+
if (siteUrl !== void 0 && (typeof siteUrl !== "string" || !/^(\/|https?:\/\/)/.test(siteUrl))) {
|
|
1341
|
+
add(
|
|
1342
|
+
"admin.siteUrl",
|
|
1343
|
+
'must be a path starting with "/" or an http(s) URL',
|
|
1344
|
+
"e.g. siteUrl: 'https://example.com'"
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
const modules = admin.modules;
|
|
1348
|
+
if (modules !== void 0) {
|
|
1349
|
+
if (!Array.isArray(modules)) {
|
|
1350
|
+
add("admin.modules", "must be an array", "e.g. modules: ['@easy-cms/plugin-seo/admin']");
|
|
1351
|
+
} else {
|
|
1352
|
+
for (const [i, module] of modules.entries()) {
|
|
1353
|
+
if (typeof module !== "string" || module.trim() === "") {
|
|
1354
|
+
add(`admin.modules[${i}]`, "must be a package export or a file path");
|
|
1355
|
+
} else if (/^[a-z][a-z0-9+.-]*:/i.test(module) || module.startsWith("//")) {
|
|
1356
|
+
add(
|
|
1357
|
+
`admin.modules[${i}]`,
|
|
1358
|
+
"must be a package export or a file path, not a URL",
|
|
1359
|
+
"admin modules are served from your server, e.g. '@easy-cms/plugin-seo/admin'"
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1324
1365
|
const brand = admin.brand;
|
|
1325
1366
|
if (brand === void 0) return;
|
|
1326
1367
|
if (typeof brand !== "object" || brand === null) {
|
|
@@ -1559,6 +1600,15 @@ function validateContainer(container, path, collectionSlugs, add) {
|
|
|
1559
1600
|
"preview: ({ doc }) => `/posts/${doc.slug}`"
|
|
1560
1601
|
);
|
|
1561
1602
|
}
|
|
1603
|
+
const admin = container.admin;
|
|
1604
|
+
if (admin !== void 0) {
|
|
1605
|
+
if (typeof admin !== "object" || admin === null) {
|
|
1606
|
+
add(`${path}.admin`, "must be an object", "e.g. admin: { sidebar: ['ecms-my-panel'] }");
|
|
1607
|
+
} else {
|
|
1608
|
+
const sidebar = admin.sidebar;
|
|
1609
|
+
if (sidebar !== void 0) validateComponents(sidebar, `${path}.admin.sidebar`, add);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1562
1612
|
const reserved = new Set(SYSTEM_FIELD_NAMES);
|
|
1563
1613
|
if (container.drafts) reserved.add("status");
|
|
1564
1614
|
validateFields2(container.fields, `${path}.fields`, reserved, collectionSlugs, add);
|
|
@@ -1595,6 +1645,20 @@ function validateFields2(fields, path, reserved, collectionSlugs, add) {
|
|
|
1595
1645
|
}
|
|
1596
1646
|
}
|
|
1597
1647
|
function validateField(field, path, siblings, collectionSlugs, add) {
|
|
1648
|
+
const position = field.position;
|
|
1649
|
+
if (position !== void 0 && position !== "sidebar") {
|
|
1650
|
+
add(`${path}.position`, `must be "sidebar" (got ${JSON.stringify(position)})`);
|
|
1651
|
+
}
|
|
1652
|
+
const admin = field.admin;
|
|
1653
|
+
if (admin !== void 0) {
|
|
1654
|
+
if (typeof admin !== "object" || admin === null) {
|
|
1655
|
+
add(`${path}.admin`, "must be an object", "e.g. admin: { component: 'ecms-color-picker' }");
|
|
1656
|
+
} else {
|
|
1657
|
+
const { component, after } = admin;
|
|
1658
|
+
if (component !== void 0) validateComponent(component, `${path}.admin.component`, add);
|
|
1659
|
+
if (after !== void 0) validateComponents(after, `${path}.admin.after`, add);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1598
1662
|
switch (field.type) {
|
|
1599
1663
|
case "text":
|
|
1600
1664
|
case "textarea":
|
|
@@ -1674,6 +1738,84 @@ function validateField(field, path, siblings, collectionSlugs, add) {
|
|
|
1674
1738
|
}
|
|
1675
1739
|
}
|
|
1676
1740
|
}
|
|
1741
|
+
var ADMIN_COMPONENT_TAG = /^ecms-[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
1742
|
+
function validateComponent(component, path, add) {
|
|
1743
|
+
const tag = typeof component === "object" && component !== null ? component.tag : component;
|
|
1744
|
+
if (typeof tag !== "string" || !ADMIN_COMPONENT_TAG.test(tag)) {
|
|
1745
|
+
add(
|
|
1746
|
+
path,
|
|
1747
|
+
`must be a custom element name starting with "ecms-" (got ${JSON.stringify(tag)})`,
|
|
1748
|
+
"e.g. 'ecms-color-picker' or { tag: 'ecms-color-picker', props: { palette: 'brand' } }"
|
|
1749
|
+
);
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
if (typeof component === "object" && component !== null) {
|
|
1753
|
+
const props = component.props;
|
|
1754
|
+
if (props !== void 0 && (typeof props !== "object" || props === null || Array.isArray(props)))
|
|
1755
|
+
add(`${path}.props`, "must be an object of plain JSON values");
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
function validateComponents(components, path, add) {
|
|
1759
|
+
if (!Array.isArray(components)) {
|
|
1760
|
+
add(path, "must be an array of components", "e.g. ['ecms-seo-preview']");
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
for (const [i, component] of components.entries()) {
|
|
1764
|
+
validateComponent(component, `${path}[${i}]`, add);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
var ENDPOINT_METHODS = ["get", "post", "put", "patch", "delete"];
|
|
1768
|
+
var ENDPOINT_SEGMENT = /^(:[A-Za-z_][A-Za-z0-9_]*|[A-Za-z0-9._~-]+)$/;
|
|
1769
|
+
var RESERVED_ENDPOINT_ROOTS = /* @__PURE__ */ new Set(["users", "globals", "admin", "jobs", "media"]);
|
|
1770
|
+
function validateEndpoints(config, collectionSlugs, add) {
|
|
1771
|
+
const endpoints = config.endpoints;
|
|
1772
|
+
if (endpoints === void 0) return;
|
|
1773
|
+
if (!Array.isArray(endpoints)) {
|
|
1774
|
+
add(
|
|
1775
|
+
"endpoints",
|
|
1776
|
+
"must be an array",
|
|
1777
|
+
"e.g. endpoints: [{ path: '/hello', method: 'get', handler }]"
|
|
1778
|
+
);
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1782
|
+
endpoints.forEach((endpoint, i) => {
|
|
1783
|
+
const path = `endpoints[${i}]`;
|
|
1784
|
+
if (typeof endpoint !== "object" || endpoint === null) {
|
|
1785
|
+
add(path, "must be an object { path, method, handler }");
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
const { path: route2, method, handler } = endpoint;
|
|
1789
|
+
const segments = typeof route2 === "string" ? route2.split("/").slice(1) : [];
|
|
1790
|
+
if (typeof route2 !== "string" || !route2.startsWith("/") || segments.length === 0 || !segments.every((s) => ENDPOINT_SEGMENT.test(s))) {
|
|
1791
|
+
add(`${path}.path`, `must be a path like "/seo/generate" (got ${JSON.stringify(route2)})`);
|
|
1792
|
+
} else {
|
|
1793
|
+
const root = segments[0];
|
|
1794
|
+
if (root.startsWith(":")) {
|
|
1795
|
+
add(`${path}.path`, "must start with a fixed segment, not a parameter");
|
|
1796
|
+
} else if (RESERVED_ENDPOINT_ROOTS.has(root) || collectionSlugs.has(root)) {
|
|
1797
|
+
add(
|
|
1798
|
+
`${path}.path`,
|
|
1799
|
+
`"/${root}" is used by the built-in API`,
|
|
1800
|
+
'start the path with your plugin or feature name, e.g. "/seo/generate"'
|
|
1801
|
+
);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
const verb = typeof method === "string" ? method.toLowerCase() : method;
|
|
1805
|
+
if (typeof verb !== "string" || !ENDPOINT_METHODS.includes(verb)) {
|
|
1806
|
+
add(
|
|
1807
|
+
`${path}.method`,
|
|
1808
|
+
`must be one of ${ENDPOINT_METHODS.join(", ")} (got ${JSON.stringify(method)})`
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
if (typeof handler !== "function") add(`${path}.handler`, "must be a function");
|
|
1812
|
+
if (typeof route2 === "string" && typeof verb === "string") {
|
|
1813
|
+
const key2 = `${verb} ${route2.replace(/:[A-Za-z_][A-Za-z0-9_]*/g, ":")}`;
|
|
1814
|
+
if (seen.has(key2)) add(`${path}.path`, `duplicate endpoint ${verb.toUpperCase()} ${route2}`);
|
|
1815
|
+
seen.add(key2);
|
|
1816
|
+
}
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1677
1819
|
function checkRange(min, max, minName, maxName, path, add) {
|
|
1678
1820
|
if (min !== void 0 && max !== void 0 && min > max) {
|
|
1679
1821
|
add(path, `${minName} (${min}) is greater than ${maxName} (${max})`);
|
|
@@ -1719,14 +1861,18 @@ var DEFAULT_API_PATH = "/api/cms";
|
|
|
1719
1861
|
var DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
1720
1862
|
var DEFAULT_TOKEN_EXPIRATION = 7 * 24 * 60 * 60;
|
|
1721
1863
|
var resolved = /* @__PURE__ */ new WeakSet();
|
|
1722
|
-
async function
|
|
1723
|
-
if (resolved.has(input)) return input;
|
|
1864
|
+
async function applyPlugins(input) {
|
|
1724
1865
|
let config = input;
|
|
1725
1866
|
const plugins = Array.isArray(config?.plugins) ? config.plugins : [];
|
|
1726
1867
|
for (const plugin of plugins) {
|
|
1727
1868
|
if (typeof plugin !== "function") break;
|
|
1728
1869
|
config = await plugin(config);
|
|
1729
1870
|
}
|
|
1871
|
+
return config;
|
|
1872
|
+
}
|
|
1873
|
+
async function resolveConfig(input) {
|
|
1874
|
+
if (resolved.has(input)) return input;
|
|
1875
|
+
let config = await applyPlugins(input);
|
|
1730
1876
|
if (typeof config === "object" && config !== null && Array.isArray(config.collections ?? [])) {
|
|
1731
1877
|
config = withMedia(withUsers(config));
|
|
1732
1878
|
}
|
|
@@ -1745,7 +1891,10 @@ async function resolveConfig(input) {
|
|
|
1745
1891
|
admin: {
|
|
1746
1892
|
path: config.admin?.path ?? DEFAULT_ADMIN_PATH,
|
|
1747
1893
|
locale: config.admin?.locale ?? "en",
|
|
1748
|
-
brand: config.admin?.brand ?? {}
|
|
1894
|
+
brand: config.admin?.brand ?? {},
|
|
1895
|
+
siteUrl: config.admin?.siteUrl ?? "",
|
|
1896
|
+
menu: config.admin?.menu ?? [],
|
|
1897
|
+
modules: config.admin?.modules ?? []
|
|
1749
1898
|
},
|
|
1750
1899
|
upload: {
|
|
1751
1900
|
dir: config.upload?.dir ?? "uploads",
|
|
@@ -1768,7 +1917,8 @@ async function resolveConfig(input) {
|
|
|
1768
1917
|
...usesSchedule(config) ? [scheduledJobsCollection] : [],
|
|
1769
1918
|
...(config.webhooks?.length ?? 0) > 0 ? [webhookDeliveriesCollection] : []
|
|
1770
1919
|
],
|
|
1771
|
-
globals: config.globals ?? []
|
|
1920
|
+
globals: config.globals ?? [],
|
|
1921
|
+
endpoints: config.endpoints ?? []
|
|
1772
1922
|
};
|
|
1773
1923
|
resolved.add(result);
|
|
1774
1924
|
return result;
|
|
@@ -2276,20 +2426,20 @@ var VersionStore = class {
|
|
|
2276
2426
|
|
|
2277
2427
|
// src/local-api.ts
|
|
2278
2428
|
async function createEasyCMS(config, options = {}) {
|
|
2279
|
-
const
|
|
2429
|
+
const resolved3 = await resolveConfig(config);
|
|
2280
2430
|
const logger = options.logger ?? consoleLogger;
|
|
2281
2431
|
const cwd = options.cwd ?? process.cwd();
|
|
2282
|
-
const storage =
|
|
2432
|
+
const storage = resolved3.upload.storage ?? localStorage({ dir: resolved3.upload.dir });
|
|
2283
2433
|
await storage.init?.({ cwd });
|
|
2284
|
-
const db = await
|
|
2285
|
-
config:
|
|
2434
|
+
const db = await resolved3.db.init({
|
|
2435
|
+
config: resolved3,
|
|
2286
2436
|
cwd,
|
|
2287
2437
|
schema: options.schema ?? (process.env.NODE_ENV === "production" ? "verify" : "push"),
|
|
2288
2438
|
logger,
|
|
2289
2439
|
interactive: options.interactive ?? false
|
|
2290
2440
|
});
|
|
2291
|
-
const cms = new EasyCMS(
|
|
2292
|
-
const scheduling = [...
|
|
2441
|
+
const cms = new EasyCMS(resolved3, db, logger, storage, cwd);
|
|
2442
|
+
const scheduling = [...resolved3.collections, ...resolved3.globals].some((c) => c.schedule) || (resolved3.webhooks?.length ?? 0) > 0;
|
|
2293
2443
|
if (scheduling && options.scheduler !== false) cms.startScheduler();
|
|
2294
2444
|
return cms;
|
|
2295
2445
|
}
|
|
@@ -2309,13 +2459,16 @@ var EasyCMS = class {
|
|
|
2309
2459
|
auth;
|
|
2310
2460
|
/** Where uploaded files are stored. */
|
|
2311
2461
|
storage;
|
|
2462
|
+
/** Project root: relative paths in the config (e.g. `admin.modules`) start here. */
|
|
2463
|
+
cwd;
|
|
2312
2464
|
versions;
|
|
2313
2465
|
webhooks;
|
|
2314
|
-
constructor(config, db, logger = consoleLogger, storage = localStorage({ dir: config.upload.dir })) {
|
|
2466
|
+
constructor(config, db, logger = consoleLogger, storage = localStorage({ dir: config.upload.dir }), cwd = process.cwd()) {
|
|
2315
2467
|
this.config = config;
|
|
2316
2468
|
this.db = db;
|
|
2317
2469
|
this.logger = logger;
|
|
2318
2470
|
this.storage = storage;
|
|
2471
|
+
this.cwd = cwd;
|
|
2319
2472
|
this.versions = new VersionStore(db);
|
|
2320
2473
|
this.webhooks = new Webhooks(
|
|
2321
2474
|
config.webhooks ?? [],
|
|
@@ -3631,7 +3784,56 @@ function asObject(data, name) {
|
|
|
3631
3784
|
return data;
|
|
3632
3785
|
}
|
|
3633
3786
|
|
|
3787
|
+
// src/rest/admin-modules.ts
|
|
3788
|
+
import { createHash as createHash2 } from "crypto";
|
|
3789
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
3790
|
+
import { createRequire } from "module";
|
|
3791
|
+
import { join as join2 } from "path";
|
|
3792
|
+
function resolveAdminModule(specifier, cwd = process.cwd()) {
|
|
3793
|
+
const bases = [join2(cwd, "package.json")];
|
|
3794
|
+
const entry = process.argv[1];
|
|
3795
|
+
if (entry && !specifier.startsWith(".")) bases.push(entry);
|
|
3796
|
+
for (const base of bases) {
|
|
3797
|
+
try {
|
|
3798
|
+
return createRequire(base).resolve(specifier);
|
|
3799
|
+
} catch {
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
return void 0;
|
|
3803
|
+
}
|
|
3804
|
+
var resolved2 = /* @__PURE__ */ new WeakMap();
|
|
3805
|
+
function files(cms) {
|
|
3806
|
+
let result = resolved2.get(cms);
|
|
3807
|
+
if (!result) {
|
|
3808
|
+
result = cms.config.admin.modules.map((specifier) => {
|
|
3809
|
+
const file = resolveAdminModule(specifier, cms.cwd);
|
|
3810
|
+
if (!file) {
|
|
3811
|
+
cms.logger.error(
|
|
3812
|
+
`Admin module "${specifier}" not found: install the package, or check the path (relative to ${cms.cwd}). The admin loads without it.`
|
|
3813
|
+
);
|
|
3814
|
+
}
|
|
3815
|
+
return file;
|
|
3816
|
+
});
|
|
3817
|
+
resolved2.set(cms, result);
|
|
3818
|
+
}
|
|
3819
|
+
return result;
|
|
3820
|
+
}
|
|
3821
|
+
function adminModuleUrls(cms) {
|
|
3822
|
+
return files(cms).flatMap((file, i) => file ? [`/admin/modules/${i}.js`] : []);
|
|
3823
|
+
}
|
|
3824
|
+
async function readAdminModule(cms, index) {
|
|
3825
|
+
const file = files(cms)[index];
|
|
3826
|
+
if (!file) return void 0;
|
|
3827
|
+
const body = await readFile2(file, "utf8").catch(() => void 0);
|
|
3828
|
+
if (body === void 0) return void 0;
|
|
3829
|
+
return { body, etag: `"${createHash2("sha1").update(body).digest("base64url")}"` };
|
|
3830
|
+
}
|
|
3831
|
+
|
|
3634
3832
|
// src/rest/admin-schema.ts
|
|
3833
|
+
function componentRef(component) {
|
|
3834
|
+
if (typeof component === "string") return { tag: component };
|
|
3835
|
+
return component.props ? { tag: component.tag, props: JSON.parse(JSON.stringify(component.props)) } : { tag: component.tag };
|
|
3836
|
+
}
|
|
3635
3837
|
async function allowed(access, user) {
|
|
3636
3838
|
return await evaluateAccess(access, { user }) !== false;
|
|
3637
3839
|
}
|
|
@@ -3646,6 +3848,13 @@ async function serializeFields(fields, update, localized) {
|
|
|
3646
3848
|
if (field.defaultValue !== void 0) f.defaultValue = field.defaultValue;
|
|
3647
3849
|
if (!await update.allows(field)) f.readOnly = true;
|
|
3648
3850
|
if (field.localized && localized) f.localized = true;
|
|
3851
|
+
if (field.position === "sidebar") f.position = "sidebar";
|
|
3852
|
+
if (field.admin?.component || field.admin?.after?.length) {
|
|
3853
|
+
f.admin = {
|
|
3854
|
+
...field.admin.component ? { component: componentRef(field.admin.component) } : {},
|
|
3855
|
+
...field.admin.after?.length ? { after: field.admin.after.map(componentRef) } : {}
|
|
3856
|
+
};
|
|
3857
|
+
}
|
|
3649
3858
|
switch (field.type) {
|
|
3650
3859
|
case "text":
|
|
3651
3860
|
case "textarea":
|
|
@@ -3718,6 +3927,9 @@ async function collection(config, user, localized) {
|
|
|
3718
3927
|
if (config.labels) result.labels = config.labels;
|
|
3719
3928
|
if (config.useAsTitle) result.useAsTitle = config.useAsTitle;
|
|
3720
3929
|
if (config.icon) result.icon = config.icon;
|
|
3930
|
+
if (config.admin?.sidebar?.length) result.sidebar = config.admin.sidebar.map(componentRef);
|
|
3931
|
+
if (config.editIn === "drawer" && !result.drafts && !result.versions && !result.preview)
|
|
3932
|
+
result.editIn = "drawer";
|
|
3721
3933
|
return result;
|
|
3722
3934
|
}
|
|
3723
3935
|
async function global(config, user, localized) {
|
|
@@ -3739,6 +3951,7 @@ async function global(config, user, localized) {
|
|
|
3739
3951
|
};
|
|
3740
3952
|
if (config.label !== void 0) result.label = config.label;
|
|
3741
3953
|
if (config.icon) result.icon = config.icon;
|
|
3954
|
+
if (config.admin?.sidebar?.length) result.sidebar = config.admin.sidebar.map(componentRef);
|
|
3742
3955
|
return result;
|
|
3743
3956
|
}
|
|
3744
3957
|
async function adminSchema(cms, user) {
|
|
@@ -3747,9 +3960,11 @@ async function adminSchema(cms, user) {
|
|
|
3747
3960
|
const localized = localization !== null;
|
|
3748
3961
|
return {
|
|
3749
3962
|
locale: cms.config.admin.locale,
|
|
3963
|
+
menu: [...cms.config.admin.menu],
|
|
3750
3964
|
localization: localization ? { locales: [...localization.locales], defaultLocale: localization.defaultLocale } : null,
|
|
3751
3965
|
collections: await Promise.all(collections.map((c) => collection(c, user, localized))),
|
|
3752
|
-
globals: await Promise.all(cms.config.globals.map((g) => global(g, user, localized)))
|
|
3966
|
+
globals: await Promise.all(cms.config.globals.map((g) => global(g, user, localized))),
|
|
3967
|
+
modules: adminModuleUrls(cms)
|
|
3753
3968
|
};
|
|
3754
3969
|
}
|
|
3755
3970
|
|
|
@@ -3880,7 +4095,7 @@ function createRestHandler(instance, options = {}) {
|
|
|
3880
4095
|
const ctx = { request, url, user, via: user ? via : null, token, headers };
|
|
3881
4096
|
const method = request.method.toUpperCase();
|
|
3882
4097
|
if (method !== "GET" && method !== "HEAD") checkCsrf(cms, ctx);
|
|
3883
|
-
const result = await route(cms, ctx, method, segments, options);
|
|
4098
|
+
const result = await customEndpoint(cms, ctx, method, segments) ?? await route(cms, ctx, method, segments, options);
|
|
3884
4099
|
if (result.body instanceof Response) return result.body;
|
|
3885
4100
|
return new Response(JSON.stringify(result.body), { status: result.status ?? 200, headers });
|
|
3886
4101
|
} catch (error) {
|
|
@@ -3918,6 +4133,43 @@ function withHeaders(response, extra) {
|
|
|
3918
4133
|
headers
|
|
3919
4134
|
});
|
|
3920
4135
|
}
|
|
4136
|
+
async function customEndpoint(cms, ctx, method, segments) {
|
|
4137
|
+
let pathMatched = false;
|
|
4138
|
+
const endpoints = [...cms.config.endpoints].sort((a, b) => fixedSegments(b) - fixedSegments(a));
|
|
4139
|
+
for (const endpoint of endpoints) {
|
|
4140
|
+
const params = matchPath(endpoint.path, segments);
|
|
4141
|
+
if (!params) continue;
|
|
4142
|
+
pathMatched = true;
|
|
4143
|
+
const verb = endpoint.method.toUpperCase();
|
|
4144
|
+
if (verb !== method && !(verb === "GET" && method === "HEAD")) continue;
|
|
4145
|
+
const body = await endpoint.handler({
|
|
4146
|
+
request: ctx.request,
|
|
4147
|
+
url: ctx.url,
|
|
4148
|
+
params,
|
|
4149
|
+
user: ctx.user,
|
|
4150
|
+
cms,
|
|
4151
|
+
json: () => readJson(ctx.request)
|
|
4152
|
+
});
|
|
4153
|
+
return { body: body === void 0 ? null : body };
|
|
4154
|
+
}
|
|
4155
|
+
if (pathMatched) {
|
|
4156
|
+
const allow = cms.config.endpoints.filter((e) => matchPath(e.path, segments)).map((e) => e.method.toUpperCase());
|
|
4157
|
+
throw methodNotAllowed(ctx, [...new Set(allow)].join(", "));
|
|
4158
|
+
}
|
|
4159
|
+
return void 0;
|
|
4160
|
+
}
|
|
4161
|
+
var fixedSegments = (endpoint) => endpoint.path.split("/").filter((s) => s && !s.startsWith(":")).length;
|
|
4162
|
+
function matchPath(path, segments) {
|
|
4163
|
+
const parts = path.split("/").filter(Boolean);
|
|
4164
|
+
if (parts.length !== segments.length) return void 0;
|
|
4165
|
+
const params = {};
|
|
4166
|
+
for (const [i, part] of parts.entries()) {
|
|
4167
|
+
const segment = segments[i];
|
|
4168
|
+
if (part.startsWith(":")) params[part.slice(1)] = segment;
|
|
4169
|
+
else if (part !== segment) return void 0;
|
|
4170
|
+
}
|
|
4171
|
+
return params;
|
|
4172
|
+
}
|
|
3921
4173
|
async function route(cms, ctx, method, segments, options) {
|
|
3922
4174
|
const [first, second, third] = segments;
|
|
3923
4175
|
const access = { overrideAccess: false, user: ctx.user, ...parseLocale(ctx.url) };
|
|
@@ -3981,6 +4233,20 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
3981
4233
|
if (!ctx.user) throw new UnauthorizedError();
|
|
3982
4234
|
if (second === "schema" && third === void 0)
|
|
3983
4235
|
return { body: await adminSchema(cms, ctx.user) };
|
|
4236
|
+
if (second === "modules" && third !== void 0 && segments.length === 3) {
|
|
4237
|
+
const index = /^(\d+)\.js$/.exec(third)?.[1];
|
|
4238
|
+
const file = index === void 0 ? void 0 : await readAdminModule(cms, Number(index));
|
|
4239
|
+
if (!file) throw new HttpError("Not found", 404);
|
|
4240
|
+
const headers = {
|
|
4241
|
+
"content-type": "text/javascript; charset=utf-8",
|
|
4242
|
+
"cache-control": "private, no-cache",
|
|
4243
|
+
etag: file.etag,
|
|
4244
|
+
"x-content-type-options": "nosniff"
|
|
4245
|
+
};
|
|
4246
|
+
if (ctx.request.headers.get("if-none-match") === file.etag)
|
|
4247
|
+
return { body: new Response(null, { status: 304, headers }) };
|
|
4248
|
+
return { body: new Response(file.body, { headers }) };
|
|
4249
|
+
}
|
|
3984
4250
|
if (second === "scheduled" && third === void 0)
|
|
3985
4251
|
return { body: await cms.upcomingJobs({ user: ctx.user, overrideAccess: false }) };
|
|
3986
4252
|
const [, , collection3, id2, extra] = segments;
|
|
@@ -4546,6 +4812,7 @@ export {
|
|
|
4546
4812
|
WEBHOOK_EVENTS,
|
|
4547
4813
|
anyone,
|
|
4548
4814
|
applyDefaults,
|
|
4815
|
+
applyPlugins,
|
|
4549
4816
|
consoleLogger,
|
|
4550
4817
|
createEasyCMS,
|
|
4551
4818
|
createRestHandler,
|
|
@@ -4567,6 +4834,7 @@ export {
|
|
|
4567
4834
|
parseId,
|
|
4568
4835
|
populate,
|
|
4569
4836
|
readCookie,
|
|
4837
|
+
resolveAdminModule,
|
|
4570
4838
|
resolveConfig,
|
|
4571
4839
|
rowFields,
|
|
4572
4840
|
silentLogger,
|