@easy-cms/core 0.12.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 +100 -3
- package/dist/index.js +249 -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;
|
|
@@ -213,6 +228,8 @@ interface BaseField<TType extends string, TValue> {
|
|
|
213
228
|
readonly localized?: boolean;
|
|
214
229
|
/** `sidebar`: shown in the edit page's side panel instead of the main form (top-level fields). */
|
|
215
230
|
readonly position?: 'sidebar';
|
|
231
|
+
/** Custom admin components for this field. */
|
|
232
|
+
readonly admin?: FieldAdmin;
|
|
216
233
|
}
|
|
217
234
|
interface TextField extends BaseField<'text', string> {
|
|
218
235
|
readonly minLength?: number;
|
|
@@ -429,6 +446,11 @@ interface VersionsConfig {
|
|
|
429
446
|
/** Versions kept per document; older ones are deleted. Default 50. */
|
|
430
447
|
readonly max?: number;
|
|
431
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
|
+
}
|
|
432
454
|
interface CollectionConfig {
|
|
433
455
|
/** URL and table name. Lowercase letters, digits, `-` and `_`. */
|
|
434
456
|
readonly slug: string;
|
|
@@ -461,6 +483,8 @@ interface CollectionConfig {
|
|
|
461
483
|
readonly preview?: PreviewURL;
|
|
462
484
|
readonly access?: CollectionAccess;
|
|
463
485
|
readonly hooks?: CollectionHooks;
|
|
486
|
+
/** Custom admin components. */
|
|
487
|
+
readonly admin?: ContainerAdmin;
|
|
464
488
|
}
|
|
465
489
|
interface GlobalConfig {
|
|
466
490
|
readonly slug: string;
|
|
@@ -477,6 +501,8 @@ interface GlobalConfig {
|
|
|
477
501
|
readonly preview?: PreviewURL;
|
|
478
502
|
readonly access?: GlobalAccess;
|
|
479
503
|
readonly hooks?: GlobalHooks;
|
|
504
|
+
/** Custom admin components. */
|
|
505
|
+
readonly admin?: ContainerAdmin;
|
|
480
506
|
}
|
|
481
507
|
type AdminLocale = 'en' | 'th';
|
|
482
508
|
/** Icons a collection or global can show in the admin menu ([Lucide](https://lucide.dev) names). */
|
|
@@ -508,6 +534,14 @@ interface AdminConfig {
|
|
|
508
534
|
* Collections not listed follow in config order, with the media library last.
|
|
509
535
|
*/
|
|
510
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[];
|
|
511
545
|
}
|
|
512
546
|
interface ImageSize {
|
|
513
547
|
/** Key in `media.sizes`, e.g. `thumbnail`. */
|
|
@@ -548,6 +582,36 @@ interface AuthConfig {
|
|
|
548
582
|
*/
|
|
549
583
|
readonly trustedOrigins?: readonly string[];
|
|
550
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
|
+
}
|
|
551
615
|
/** Receives the config and returns a modified copy. Runs before validation. */
|
|
552
616
|
type Plugin = (config: Config) => MaybePromise<Config>;
|
|
553
617
|
interface Config {
|
|
@@ -585,10 +649,12 @@ interface Config {
|
|
|
585
649
|
*/
|
|
586
650
|
readonly collections?: readonly CollectionConfig[];
|
|
587
651
|
readonly globals?: readonly GlobalConfig[];
|
|
652
|
+
/** Custom REST endpoints, e.g. from plugins. */
|
|
653
|
+
readonly endpoints?: readonly Endpoint[];
|
|
588
654
|
readonly plugins?: readonly Plugin[];
|
|
589
655
|
}
|
|
590
656
|
/** The config after plugins ran, validation passed and defaults were applied. */
|
|
591
|
-
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'> {
|
|
592
658
|
readonly cors: readonly string[] | '*';
|
|
593
659
|
/** `null` without localization. */
|
|
594
660
|
readonly localization: Required<LocalizationConfig> | null;
|
|
@@ -598,6 +664,7 @@ interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes'
|
|
|
598
664
|
readonly auth: Required<AuthConfig>;
|
|
599
665
|
readonly collections: readonly CollectionConfig[];
|
|
600
666
|
readonly globals: readonly GlobalConfig[];
|
|
667
|
+
readonly endpoints: readonly Endpoint[];
|
|
601
668
|
}
|
|
602
669
|
/**
|
|
603
670
|
* Declares the Easy CMS config. Returns it unchanged, keeping literal types
|
|
@@ -926,9 +993,11 @@ declare class EasyCMS<C extends Config = Config> {
|
|
|
926
993
|
readonly auth: Auth;
|
|
927
994
|
/** Where uploaded files are stored. */
|
|
928
995
|
readonly storage: StorageAdapter;
|
|
996
|
+
/** Project root: relative paths in the config (e.g. `admin.modules`) start here. */
|
|
997
|
+
readonly cwd: string;
|
|
929
998
|
private readonly versions;
|
|
930
999
|
private readonly webhooks;
|
|
931
|
-
constructor(config: ResolvedConfig, db: Database, logger?: Logger, storage?: StorageAdapter);
|
|
1000
|
+
constructor(config: ResolvedConfig, db: Database, logger?: Logger, storage?: StorageAdapter, cwd?: string);
|
|
932
1001
|
find<S extends Slug<C>>(collection: S, options?: FindOptions): Promise<PaginatedDocs<Doc<C, S>>>;
|
|
933
1002
|
findById<S extends Slug<C>>(collection: S, id: ID, options?: ReadOptions): Promise<Doc<C, S> | null>;
|
|
934
1003
|
count<S extends Slug<C>>(collection: S, options?: {
|
|
@@ -1355,12 +1424,29 @@ declare const DEFAULT_ADMIN_PATH = "/admin";
|
|
|
1355
1424
|
declare const DEFAULT_API_PATH = "/api/cms";
|
|
1356
1425
|
declare const DEFAULT_MAX_FILE_SIZE: number;
|
|
1357
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>;
|
|
1358
1432
|
/**
|
|
1359
1433
|
* Runs plugins in order, adds the built-in collections, validates the result
|
|
1360
1434
|
* and fills in defaults. Throws `ConfigError`. Resolving twice is a no-op.
|
|
1361
1435
|
*/
|
|
1362
1436
|
declare function resolveConfig(input: Config | ResolvedConfig): Promise<ResolvedConfig>;
|
|
1363
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
|
+
}
|
|
1364
1450
|
/** A field as the admin UI sees it: plain JSON, no functions. */
|
|
1365
1451
|
interface AdminField {
|
|
1366
1452
|
name: string;
|
|
@@ -1398,6 +1484,11 @@ interface AdminField {
|
|
|
1398
1484
|
localized?: boolean;
|
|
1399
1485
|
/** Shown in the edit page's side panel. */
|
|
1400
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
|
+
};
|
|
1401
1492
|
}
|
|
1402
1493
|
interface AdminCollection {
|
|
1403
1494
|
slug: string;
|
|
@@ -1416,6 +1507,8 @@ interface AdminCollection {
|
|
|
1416
1507
|
/** Publishing can be scheduled. */
|
|
1417
1508
|
schedule: boolean;
|
|
1418
1509
|
fields: AdminField[];
|
|
1510
|
+
/** Panels from admin modules in the edit page's side column. */
|
|
1511
|
+
sidebar?: AdminComponentRef[];
|
|
1419
1512
|
permissions: {
|
|
1420
1513
|
read: boolean;
|
|
1421
1514
|
create: boolean;
|
|
@@ -1434,6 +1527,8 @@ interface AdminGlobal {
|
|
|
1434
1527
|
/** Publishing can be scheduled. */
|
|
1435
1528
|
schedule: boolean;
|
|
1436
1529
|
fields: AdminField[];
|
|
1530
|
+
/** Panels from admin modules in the edit page's side column. */
|
|
1531
|
+
sidebar?: AdminComponentRef[];
|
|
1437
1532
|
permissions: {
|
|
1438
1533
|
read: boolean;
|
|
1439
1534
|
update: boolean;
|
|
@@ -1450,6 +1545,8 @@ interface AdminSchema {
|
|
|
1450
1545
|
} | null;
|
|
1451
1546
|
collections: AdminCollection[];
|
|
1452
1547
|
globals: AdminGlobal[];
|
|
1548
|
+
/** URLs under the API of the admin modules to load (`admin.modules`). */
|
|
1549
|
+
modules: string[];
|
|
1453
1550
|
}
|
|
1454
1551
|
|
|
1455
1552
|
declare const SESSION_COOKIE = "ecms-session";
|
|
@@ -1480,4 +1577,4 @@ declare const BUILTIN_COLLECTIONS: readonly ["users", "media"];
|
|
|
1480
1577
|
/** Checks a config and returns every problem found. An empty array means it is valid. */
|
|
1481
1578
|
declare function validateConfig(config: Config): ConfigIssue[];
|
|
1482
1579
|
|
|
1483
|
-
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
|
@@ -1294,6 +1294,7 @@ function validateConfig(config) {
|
|
|
1294
1294
|
globals.forEach((global2, i) => {
|
|
1295
1295
|
checkSlug(global2, `globals[${i}]`, seenGlobals, add);
|
|
1296
1296
|
});
|
|
1297
|
+
validateEndpoints(config, collectionSlugs, add);
|
|
1297
1298
|
collections.forEach((collection2, i) => {
|
|
1298
1299
|
const path = `collections.${collection2.slug ?? `[${i}]`}`;
|
|
1299
1300
|
validateContainer(collection2, path, collectionSlugs, add);
|
|
@@ -1343,6 +1344,24 @@ function validateAdmin(config, add) {
|
|
|
1343
1344
|
"e.g. siteUrl: 'https://example.com'"
|
|
1344
1345
|
);
|
|
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
|
+
}
|
|
1346
1365
|
const brand = admin.brand;
|
|
1347
1366
|
if (brand === void 0) return;
|
|
1348
1367
|
if (typeof brand !== "object" || brand === null) {
|
|
@@ -1581,6 +1600,15 @@ function validateContainer(container, path, collectionSlugs, add) {
|
|
|
1581
1600
|
"preview: ({ doc }) => `/posts/${doc.slug}`"
|
|
1582
1601
|
);
|
|
1583
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
|
+
}
|
|
1584
1612
|
const reserved = new Set(SYSTEM_FIELD_NAMES);
|
|
1585
1613
|
if (container.drafts) reserved.add("status");
|
|
1586
1614
|
validateFields2(container.fields, `${path}.fields`, reserved, collectionSlugs, add);
|
|
@@ -1621,6 +1649,16 @@ function validateField(field, path, siblings, collectionSlugs, add) {
|
|
|
1621
1649
|
if (position !== void 0 && position !== "sidebar") {
|
|
1622
1650
|
add(`${path}.position`, `must be "sidebar" (got ${JSON.stringify(position)})`);
|
|
1623
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
|
+
}
|
|
1624
1662
|
switch (field.type) {
|
|
1625
1663
|
case "text":
|
|
1626
1664
|
case "textarea":
|
|
@@ -1700,6 +1738,84 @@ function validateField(field, path, siblings, collectionSlugs, add) {
|
|
|
1700
1738
|
}
|
|
1701
1739
|
}
|
|
1702
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
|
+
}
|
|
1703
1819
|
function checkRange(min, max, minName, maxName, path, add) {
|
|
1704
1820
|
if (min !== void 0 && max !== void 0 && min > max) {
|
|
1705
1821
|
add(path, `${minName} (${min}) is greater than ${maxName} (${max})`);
|
|
@@ -1745,14 +1861,18 @@ var DEFAULT_API_PATH = "/api/cms";
|
|
|
1745
1861
|
var DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
1746
1862
|
var DEFAULT_TOKEN_EXPIRATION = 7 * 24 * 60 * 60;
|
|
1747
1863
|
var resolved = /* @__PURE__ */ new WeakSet();
|
|
1748
|
-
async function
|
|
1749
|
-
if (resolved.has(input)) return input;
|
|
1864
|
+
async function applyPlugins(input) {
|
|
1750
1865
|
let config = input;
|
|
1751
1866
|
const plugins = Array.isArray(config?.plugins) ? config.plugins : [];
|
|
1752
1867
|
for (const plugin of plugins) {
|
|
1753
1868
|
if (typeof plugin !== "function") break;
|
|
1754
1869
|
config = await plugin(config);
|
|
1755
1870
|
}
|
|
1871
|
+
return config;
|
|
1872
|
+
}
|
|
1873
|
+
async function resolveConfig(input) {
|
|
1874
|
+
if (resolved.has(input)) return input;
|
|
1875
|
+
let config = await applyPlugins(input);
|
|
1756
1876
|
if (typeof config === "object" && config !== null && Array.isArray(config.collections ?? [])) {
|
|
1757
1877
|
config = withMedia(withUsers(config));
|
|
1758
1878
|
}
|
|
@@ -1773,7 +1893,8 @@ async function resolveConfig(input) {
|
|
|
1773
1893
|
locale: config.admin?.locale ?? "en",
|
|
1774
1894
|
brand: config.admin?.brand ?? {},
|
|
1775
1895
|
siteUrl: config.admin?.siteUrl ?? "",
|
|
1776
|
-
menu: config.admin?.menu ?? []
|
|
1896
|
+
menu: config.admin?.menu ?? [],
|
|
1897
|
+
modules: config.admin?.modules ?? []
|
|
1777
1898
|
},
|
|
1778
1899
|
upload: {
|
|
1779
1900
|
dir: config.upload?.dir ?? "uploads",
|
|
@@ -1796,7 +1917,8 @@ async function resolveConfig(input) {
|
|
|
1796
1917
|
...usesSchedule(config) ? [scheduledJobsCollection] : [],
|
|
1797
1918
|
...(config.webhooks?.length ?? 0) > 0 ? [webhookDeliveriesCollection] : []
|
|
1798
1919
|
],
|
|
1799
|
-
globals: config.globals ?? []
|
|
1920
|
+
globals: config.globals ?? [],
|
|
1921
|
+
endpoints: config.endpoints ?? []
|
|
1800
1922
|
};
|
|
1801
1923
|
resolved.add(result);
|
|
1802
1924
|
return result;
|
|
@@ -2304,20 +2426,20 @@ var VersionStore = class {
|
|
|
2304
2426
|
|
|
2305
2427
|
// src/local-api.ts
|
|
2306
2428
|
async function createEasyCMS(config, options = {}) {
|
|
2307
|
-
const
|
|
2429
|
+
const resolved3 = await resolveConfig(config);
|
|
2308
2430
|
const logger = options.logger ?? consoleLogger;
|
|
2309
2431
|
const cwd = options.cwd ?? process.cwd();
|
|
2310
|
-
const storage =
|
|
2432
|
+
const storage = resolved3.upload.storage ?? localStorage({ dir: resolved3.upload.dir });
|
|
2311
2433
|
await storage.init?.({ cwd });
|
|
2312
|
-
const db = await
|
|
2313
|
-
config:
|
|
2434
|
+
const db = await resolved3.db.init({
|
|
2435
|
+
config: resolved3,
|
|
2314
2436
|
cwd,
|
|
2315
2437
|
schema: options.schema ?? (process.env.NODE_ENV === "production" ? "verify" : "push"),
|
|
2316
2438
|
logger,
|
|
2317
2439
|
interactive: options.interactive ?? false
|
|
2318
2440
|
});
|
|
2319
|
-
const cms = new EasyCMS(
|
|
2320
|
-
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;
|
|
2321
2443
|
if (scheduling && options.scheduler !== false) cms.startScheduler();
|
|
2322
2444
|
return cms;
|
|
2323
2445
|
}
|
|
@@ -2337,13 +2459,16 @@ var EasyCMS = class {
|
|
|
2337
2459
|
auth;
|
|
2338
2460
|
/** Where uploaded files are stored. */
|
|
2339
2461
|
storage;
|
|
2462
|
+
/** Project root: relative paths in the config (e.g. `admin.modules`) start here. */
|
|
2463
|
+
cwd;
|
|
2340
2464
|
versions;
|
|
2341
2465
|
webhooks;
|
|
2342
|
-
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()) {
|
|
2343
2467
|
this.config = config;
|
|
2344
2468
|
this.db = db;
|
|
2345
2469
|
this.logger = logger;
|
|
2346
2470
|
this.storage = storage;
|
|
2471
|
+
this.cwd = cwd;
|
|
2347
2472
|
this.versions = new VersionStore(db);
|
|
2348
2473
|
this.webhooks = new Webhooks(
|
|
2349
2474
|
config.webhooks ?? [],
|
|
@@ -3659,7 +3784,56 @@ function asObject(data, name) {
|
|
|
3659
3784
|
return data;
|
|
3660
3785
|
}
|
|
3661
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
|
+
|
|
3662
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
|
+
}
|
|
3663
3837
|
async function allowed(access, user) {
|
|
3664
3838
|
return await evaluateAccess(access, { user }) !== false;
|
|
3665
3839
|
}
|
|
@@ -3675,6 +3849,12 @@ async function serializeFields(fields, update, localized) {
|
|
|
3675
3849
|
if (!await update.allows(field)) f.readOnly = true;
|
|
3676
3850
|
if (field.localized && localized) f.localized = true;
|
|
3677
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
|
+
}
|
|
3678
3858
|
switch (field.type) {
|
|
3679
3859
|
case "text":
|
|
3680
3860
|
case "textarea":
|
|
@@ -3747,6 +3927,7 @@ async function collection(config, user, localized) {
|
|
|
3747
3927
|
if (config.labels) result.labels = config.labels;
|
|
3748
3928
|
if (config.useAsTitle) result.useAsTitle = config.useAsTitle;
|
|
3749
3929
|
if (config.icon) result.icon = config.icon;
|
|
3930
|
+
if (config.admin?.sidebar?.length) result.sidebar = config.admin.sidebar.map(componentRef);
|
|
3750
3931
|
if (config.editIn === "drawer" && !result.drafts && !result.versions && !result.preview)
|
|
3751
3932
|
result.editIn = "drawer";
|
|
3752
3933
|
return result;
|
|
@@ -3770,6 +3951,7 @@ async function global(config, user, localized) {
|
|
|
3770
3951
|
};
|
|
3771
3952
|
if (config.label !== void 0) result.label = config.label;
|
|
3772
3953
|
if (config.icon) result.icon = config.icon;
|
|
3954
|
+
if (config.admin?.sidebar?.length) result.sidebar = config.admin.sidebar.map(componentRef);
|
|
3773
3955
|
return result;
|
|
3774
3956
|
}
|
|
3775
3957
|
async function adminSchema(cms, user) {
|
|
@@ -3781,7 +3963,8 @@ async function adminSchema(cms, user) {
|
|
|
3781
3963
|
menu: [...cms.config.admin.menu],
|
|
3782
3964
|
localization: localization ? { locales: [...localization.locales], defaultLocale: localization.defaultLocale } : null,
|
|
3783
3965
|
collections: await Promise.all(collections.map((c) => collection(c, user, localized))),
|
|
3784
|
-
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)
|
|
3785
3968
|
};
|
|
3786
3969
|
}
|
|
3787
3970
|
|
|
@@ -3912,7 +4095,7 @@ function createRestHandler(instance, options = {}) {
|
|
|
3912
4095
|
const ctx = { request, url, user, via: user ? via : null, token, headers };
|
|
3913
4096
|
const method = request.method.toUpperCase();
|
|
3914
4097
|
if (method !== "GET" && method !== "HEAD") checkCsrf(cms, ctx);
|
|
3915
|
-
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);
|
|
3916
4099
|
if (result.body instanceof Response) return result.body;
|
|
3917
4100
|
return new Response(JSON.stringify(result.body), { status: result.status ?? 200, headers });
|
|
3918
4101
|
} catch (error) {
|
|
@@ -3950,6 +4133,43 @@ function withHeaders(response, extra) {
|
|
|
3950
4133
|
headers
|
|
3951
4134
|
});
|
|
3952
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
|
+
}
|
|
3953
4173
|
async function route(cms, ctx, method, segments, options) {
|
|
3954
4174
|
const [first, second, third] = segments;
|
|
3955
4175
|
const access = { overrideAccess: false, user: ctx.user, ...parseLocale(ctx.url) };
|
|
@@ -4013,6 +4233,20 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
4013
4233
|
if (!ctx.user) throw new UnauthorizedError();
|
|
4014
4234
|
if (second === "schema" && third === void 0)
|
|
4015
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
|
+
}
|
|
4016
4250
|
if (second === "scheduled" && third === void 0)
|
|
4017
4251
|
return { body: await cms.upcomingJobs({ user: ctx.user, overrideAccess: false }) };
|
|
4018
4252
|
const [, , collection3, id2, extra] = segments;
|
|
@@ -4578,6 +4812,7 @@ export {
|
|
|
4578
4812
|
WEBHOOK_EVENTS,
|
|
4579
4813
|
anyone,
|
|
4580
4814
|
applyDefaults,
|
|
4815
|
+
applyPlugins,
|
|
4581
4816
|
consoleLogger,
|
|
4582
4817
|
createEasyCMS,
|
|
4583
4818
|
createRestHandler,
|
|
@@ -4599,6 +4834,7 @@ export {
|
|
|
4599
4834
|
parseId,
|
|
4600
4835
|
populate,
|
|
4601
4836
|
readCookie,
|
|
4837
|
+
resolveAdminModule,
|
|
4602
4838
|
resolveConfig,
|
|
4603
4839
|
rowFields,
|
|
4604
4840
|
silentLogger,
|