@easy-cms/core 0.28.0 → 0.30.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 +128 -5
- package/dist/index.js +304 -5
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -238,6 +238,8 @@ interface EmailMessage {
|
|
|
238
238
|
* development. Throw to report a failure: the message is retried later.
|
|
239
239
|
*/
|
|
240
240
|
interface EmailAdapter {
|
|
241
|
+
/** What sends the email, e.g. `smtp`; shown to admins on the dashboard (System). */
|
|
242
|
+
readonly name?: string;
|
|
241
243
|
/** The sender when a message has none, e.g. `Easy CMS <no-reply@example.com>`. */
|
|
242
244
|
readonly from?: string | undefined;
|
|
243
245
|
send(message: EmailMessage & {
|
|
@@ -1063,6 +1065,18 @@ declare class EasyCMS<C extends Config = Config> {
|
|
|
1063
1065
|
})[]>;
|
|
1064
1066
|
private pendingJobs;
|
|
1065
1067
|
private removeJob;
|
|
1068
|
+
/**
|
|
1069
|
+
* @internal One attempt now for a saved webhook delivery or email (the admin's Retry): removed
|
|
1070
|
+
* when it is sent, otherwise kept as failed with the new error.
|
|
1071
|
+
*/
|
|
1072
|
+
retryDelivery(kind: 'webhook' | 'email', id: ID): Promise<{
|
|
1073
|
+
ok: true;
|
|
1074
|
+
} | {
|
|
1075
|
+
ok: false;
|
|
1076
|
+
error: string;
|
|
1077
|
+
}>;
|
|
1078
|
+
/** Removes webhook deliveries and emails that failed more than 30 days ago. */
|
|
1079
|
+
private pruneFailedDeliveries;
|
|
1066
1080
|
/** @internal */
|
|
1067
1081
|
collection(slug: string): CollectionConfig;
|
|
1068
1082
|
private global;
|
|
@@ -1726,8 +1740,17 @@ interface Endpoint {
|
|
|
1726
1740
|
*/
|
|
1727
1741
|
readonly handler: (request: EndpointRequest) => MaybePromise<unknown>;
|
|
1728
1742
|
}
|
|
1743
|
+
/** A plugin's package and version, listed for admins on the dashboard (System). */
|
|
1744
|
+
interface PluginInfo {
|
|
1745
|
+
/** Usually the package name, e.g. `@acme/easy-cms-plugin-stats`. */
|
|
1746
|
+
readonly name: string;
|
|
1747
|
+
readonly version?: string;
|
|
1748
|
+
}
|
|
1729
1749
|
/** Receives the config and returns a modified copy. Runs before validation. */
|
|
1730
|
-
type Plugin = (config: Config) => MaybePromise<Config
|
|
1750
|
+
type Plugin = ((config: Config) => MaybePromise<Config>) & {
|
|
1751
|
+
/** Set by `definePlugin(plugin, info)`. */
|
|
1752
|
+
readonly info?: PluginInfo;
|
|
1753
|
+
};
|
|
1731
1754
|
/** Holds a plugin's `PluginTypes` at the type level only: nothing is stored at runtime. */
|
|
1732
1755
|
declare const PLUGIN_TYPES: unique symbol;
|
|
1733
1756
|
/**
|
|
@@ -1749,17 +1772,18 @@ type TypedPlugin<T extends PluginTypes> = Plugin & {
|
|
|
1749
1772
|
readonly [PLUGIN_TYPES]?: T;
|
|
1750
1773
|
};
|
|
1751
1774
|
/**
|
|
1752
|
-
* Returns the plugin
|
|
1753
|
-
*
|
|
1775
|
+
* Returns the plugin with what it adds as types, so `cms.find()` and `CollectionDocument` know
|
|
1776
|
+
* the plugin's fields; `info` (its name and version) is listed for admins on the dashboard:
|
|
1754
1777
|
*
|
|
1755
1778
|
* ```ts
|
|
1756
1779
|
* export const colorPlugin = (options: { collections: readonly string[] }) =>
|
|
1757
1780
|
* definePlugin<{ fields: { posts: readonly [{ readonly name: 'color'; readonly type: 'text' }] } }>(
|
|
1758
1781
|
* (config) => ({ ...config, collections: … }),
|
|
1782
|
+
* { name: '@acme/easy-cms-plugin-color', version: '1.2.0' },
|
|
1759
1783
|
* )
|
|
1760
1784
|
* ```
|
|
1761
1785
|
*/
|
|
1762
|
-
declare function definePlugin<const T extends PluginTypes = Record<never, never>>(plugin: Plugin): TypedPlugin<T>;
|
|
1786
|
+
declare function definePlugin<const T extends PluginTypes = Record<never, never>>(plugin: Plugin, info?: PluginInfo): TypedPlugin<T>;
|
|
1763
1787
|
interface Config {
|
|
1764
1788
|
/** Signs sessions. At least 32 characters; read it from `process.env.EASY_CMS_SECRET`. */
|
|
1765
1789
|
readonly secret: string;
|
|
@@ -1831,6 +1855,8 @@ interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes'
|
|
|
1831
1855
|
readonly endpoints: readonly Endpoint[];
|
|
1832
1856
|
readonly commands: readonly CliCommand[];
|
|
1833
1857
|
readonly fieldTypes: readonly FieldTypeDefinition[];
|
|
1858
|
+
/** The config's plugins in order, with their `info` where they give one. */
|
|
1859
|
+
readonly installedPlugins: readonly Partial<PluginInfo>[];
|
|
1834
1860
|
}
|
|
1835
1861
|
/**
|
|
1836
1862
|
* Declares the Easy CMS config. Returns it unchanged, keeping literal types
|
|
@@ -2013,6 +2039,47 @@ declare function applyPlugins(input: Config): Promise<Config>;
|
|
|
2013
2039
|
*/
|
|
2014
2040
|
declare function resolveConfig(input: Config | ResolvedConfig): Promise<ResolvedConfig>;
|
|
2015
2041
|
|
|
2042
|
+
type DeliveryKind = 'webhook' | 'email';
|
|
2043
|
+
type DeliveryState = 'failed' | 'pending';
|
|
2044
|
+
interface DeliveryBase {
|
|
2045
|
+
id: ID;
|
|
2046
|
+
state: DeliveryState;
|
|
2047
|
+
attempts: number;
|
|
2048
|
+
/** The last attempt's error. */
|
|
2049
|
+
error: string | null;
|
|
2050
|
+
createdAt: string;
|
|
2051
|
+
updatedAt: string;
|
|
2052
|
+
/** When a pending one is tried again. */
|
|
2053
|
+
nextAttemptAt: string;
|
|
2054
|
+
}
|
|
2055
|
+
/** A webhook delivery as the admin shows it (`GET <api>/admin/deliveries?kind=webhook`). */
|
|
2056
|
+
interface AdminWebhookDelivery extends DeliveryBase {
|
|
2057
|
+
kind: 'webhook';
|
|
2058
|
+
url: string;
|
|
2059
|
+
event: string;
|
|
2060
|
+
/** What changed: a document of a collection, or a global. */
|
|
2061
|
+
collection?: string;
|
|
2062
|
+
doc?: ID;
|
|
2063
|
+
global?: string;
|
|
2064
|
+
/** The JSON body as sent. */
|
|
2065
|
+
body: string;
|
|
2066
|
+
}
|
|
2067
|
+
/** An email as the admin shows it: who to and the subject, not the content. */
|
|
2068
|
+
interface AdminEmailDelivery extends DeliveryBase {
|
|
2069
|
+
kind: 'email';
|
|
2070
|
+
to: string[];
|
|
2071
|
+
subject: string;
|
|
2072
|
+
}
|
|
2073
|
+
type AdminDelivery = AdminWebhookDelivery | AdminEmailDelivery;
|
|
2074
|
+
interface AdminDeliveries {
|
|
2075
|
+
docs: AdminDelivery[];
|
|
2076
|
+
totalDocs: number;
|
|
2077
|
+
page: number;
|
|
2078
|
+
totalPages: number;
|
|
2079
|
+
/** How many there are of each state, for the filter. */
|
|
2080
|
+
counts: Record<DeliveryState, number>;
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2016
2083
|
/**
|
|
2017
2084
|
* Finds the file of an admin module (`admin.modules`): a package export or a path relative to
|
|
2018
2085
|
* the project root. Looks from the project root first, then from the server's entry file
|
|
@@ -2156,12 +2223,65 @@ interface AdminSchema {
|
|
|
2156
2223
|
modules: string[];
|
|
2157
2224
|
/** Files can be uploaded from links (`upload.fromURL`). */
|
|
2158
2225
|
uploadFromURL: boolean;
|
|
2226
|
+
/** For admins: which saved deliveries the admin can show (webhooks, emails). */
|
|
2227
|
+
deliveries?: {
|
|
2228
|
+
webhook: boolean;
|
|
2229
|
+
email: boolean;
|
|
2230
|
+
};
|
|
2159
2231
|
/** Pages this user may open (`admin.pages`). */
|
|
2160
2232
|
pages: AdminPageRef[];
|
|
2161
2233
|
/** Dashboard panels this user may see (`admin.dashboard`). */
|
|
2162
2234
|
dashboard: AdminWidgetRef[];
|
|
2163
2235
|
}
|
|
2164
2236
|
|
|
2237
|
+
/** Something an admin should look at, shown on the dashboard with a link to how to fix it. */
|
|
2238
|
+
type AdminAttention =
|
|
2239
|
+
/** Webhook deliveries that failed for good in the last 7 days; `url`: the one failing most. */
|
|
2240
|
+
{
|
|
2241
|
+
id: 'webhooks';
|
|
2242
|
+
count: number;
|
|
2243
|
+
url: string;
|
|
2244
|
+
}
|
|
2245
|
+
/** Emails waiting over an hour, or that failed for good in the last 7 days. */
|
|
2246
|
+
| {
|
|
2247
|
+
id: 'emails';
|
|
2248
|
+
count: number;
|
|
2249
|
+
}
|
|
2250
|
+
/** Scheduled publishing more than 10 minutes late: nothing runs `jobs/run`. */
|
|
2251
|
+
| {
|
|
2252
|
+
id: 'scheduled';
|
|
2253
|
+
count: number;
|
|
2254
|
+
}
|
|
2255
|
+
/** No `email` in the config: no forgotten-password links, invitations or form emails. */
|
|
2256
|
+
| {
|
|
2257
|
+
id: 'no-email';
|
|
2258
|
+
}
|
|
2259
|
+
/** In production without `serverURL`: links in emails can't be made. */
|
|
2260
|
+
| {
|
|
2261
|
+
id: 'no-server-url';
|
|
2262
|
+
};
|
|
2263
|
+
/** What the dashboard shows admins (`GET <api>/admin/status`). */
|
|
2264
|
+
interface AdminStatus {
|
|
2265
|
+
system: {
|
|
2266
|
+
/** `@easy-cms/core`'s version. */
|
|
2267
|
+
version: string;
|
|
2268
|
+
/** The database adapter, e.g. `sqlite` or `postgres`. */
|
|
2269
|
+
database: string;
|
|
2270
|
+
/** Where uploads are stored, e.g. `local` or `s3`. */
|
|
2271
|
+
storage: string;
|
|
2272
|
+
/** The email adapter's name, `custom` when it has none, or `null` without email. */
|
|
2273
|
+
email: string | null;
|
|
2274
|
+
/** The config's plugins, with their name and version when they give them. */
|
|
2275
|
+
plugins: {
|
|
2276
|
+
name?: string;
|
|
2277
|
+
version?: string;
|
|
2278
|
+
}[];
|
|
2279
|
+
/** Field types from packages (`fieldTypes`). */
|
|
2280
|
+
fieldTypes: string[];
|
|
2281
|
+
};
|
|
2282
|
+
attention: AdminAttention[];
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2165
2285
|
declare const SESSION_COOKIE = "ecms-session";
|
|
2166
2286
|
declare const CSRF_COOKIE = "ecms-csrf";
|
|
2167
2287
|
declare const CSRF_HEADER = "x-csrf-token";
|
|
@@ -2197,4 +2317,7 @@ declare const BUILTIN_COLLECTIONS: readonly ["users", "media"];
|
|
|
2197
2317
|
/** Checks a config and returns every problem found. An empty array means it is valid. */
|
|
2198
2318
|
declare function validateConfig(config: Config): ConfigIssue[];
|
|
2199
2319
|
|
|
2200
|
-
|
|
2320
|
+
/** The version of `@easy-cms/core`, set when the package is built. */
|
|
2321
|
+
declare const VERSION: string;
|
|
2322
|
+
|
|
2323
|
+
export { ADMIN_ICONS, API_KEYS, API_KEY_GLOBAL_OPERATIONS, API_KEY_OPERATIONS, type Access, type AccessArgs, type AccessOptions, type AdminAttention, type AdminBrand, type AdminCollection, type AdminComponent, type AdminComponentRef, type AdminConfig, type AdminDeliveries, type AdminDelivery, type AdminEmailDelivery, type AdminField, type AdminGlobal, type AdminIcon, type AdminLocale, type AdminPage, type AdminPageRef, type AdminSchema, type AdminStatus, type AdminViewAccess, type AdminWebhookDelivery, type AdminWidgetRef, type AfterChangeHook, type AfterDeleteHook, type AfterReadHook, type ApiKeyContext, type ApiKeyGlobalOperation, type ApiKeyOperation, type ApiKeyPermissions, type ArrayField, Auth, type AuthConfig, type AuthUser, BUILTIN_COLLECTIONS, type BeforeChangeHook, type BeforeDeleteHook, type BeforeValidateHook, type Block, type BlocksField, type BooleanField, type BuiltinApiKeysCollection, type BuiltinMediaCollection, type BuiltinUsersCollection, type BundleHints, CONFIG_FILE_NAMES, CSRF_COOKIE, CSRF_HEADER, type CliCommand, type CollectionAccess, type CollectionAdmin, type CollectionConfig, type CollectionDocument, type CollectionHooks, type CollectionListAdmin, type CollectionSlug, type Config, ConfigError, type ConfigIssue, type ContainerAdmin, type CopyProgress, type CopyResult, type CreateEasyCMSOptions, type CreateInput, type CreateMigrationResult, type CustomField, type CustomFieldTypes, DEFAULT_ADMIN_PATH, DEFAULT_API_PATH, DEFAULT_DEPTH, DEFAULT_MAX_FILE_SIZE, DEFAULT_MAX_VERSIONS, DEFAULT_PASSWORD_EMAILS, DEFAULT_TOKEN_EXPIRATION, type DashboardWidget, type Database, type DatabaseAdapter, type DatabaseInitArgs, type DatabaseTransfer, type DateField, type DeliveryKind, type DeliveryState, type DepthOptions, type DocumentOf, EXTENSIONS, EasyCMS, EasyCMSError, type EmailAdapter, type EmailField, type EmailMessage, type Endpoint, type EndpointRequest, FIELD_TYPES, FIELD_TYPE_BASES, type Field, type FieldAccess, type FieldAccessFn, type FieldAdmin, type FieldError, type FieldType, type FieldTypeBase, type FieldTypeDefinition, type FieldTypeValidateContext, type FieldValidate, type FieldValidateContext, type FieldValue, type FieldsInput, type FieldsValue, type FilterOptions, type FilterOptionsArgs, type FindArgs, type FindOptions, ForbiddenError, type GlobalAccess, type GlobalConfig, type GlobalDocument, type GlobalHooks, type GlobalInput, type GlobalSlug, type GroupField, type ID, INTERNAL_COLLECTIONS, type ImageSize, type InferCollection, type InferGlobal, type JsonField, type Label, type LivePreview, type LoadConfigOptions, type LocalStorageOptions, type LocalizationConfig, type Logger, type LoginArgs, MAX_DEPTH, MEDIA, MIN_PASSWORD_LENGTH, MIN_SECRET_LENGTH, type MediaDocument, type MediaSize, type MigrationInfo, NotFoundError, type NumberField, type Operation, type PaginatedDocs, type PasswordEmail, type PasswordEmailArgs, type PasswordEmailFn, type PasswordLinkOptions, PayloadTooLargeError, type Plugin, type PluginInfo, type PluginTypes, type PreviewTarget, type PreviewURL, QueryError, type RawDocument, type ReadOptions, type RelationshipField, type ResolvedConfig, type RestHandler, type RestHandlerOptions, type RichTextDocument, type RichTextField, type Role, type RoutesConfig, SESSION_COOKIE, type ScheduledJob, SchemaError, type SchemaMode, type SelectField, type SelectOption, type Session, type SlugField, type SlugOf, type StorageAdapter, type StoredFile, type TextField, type TextareaField, TooManyRequestsError, type TypedPlugin, UnauthorizedError, type UpdateInput, type UpdateOptions, type UploadConfig, type UploadField, type UploadFromURLConfig, VERSION, ValidationError, type Version, type VersionSummary, type VersionsConfig, WEBHOOK_EVENTS, type WebhookConfig, type WebhookEvent, type WebhookPayload, type Where, type WhereOperators, anyone, applyDefaults, applyPlugins, configSignature, consoleEmail, consoleLogger, copyDatabase, createEasyCMS, createRestHandler, createRootEndpointHandler, defineConfig, defineFieldType, definePlugin, fillMissing, findConfigFile, generateSlugs, generateTypes, hasRows, hashPassword, imageDimensions, importConfig, isAdmin, isHasMany, isLoggedIn, keyAllows, loadConfig, localStorage, mergeForUpdate, mimeAllowed, mimeAllowedBy, parseId, populate, readCookie, resolveAdminModule, resolveConfig, rowFields, silentLogger, singularize, slugify, sniffMimeType, validateConfig, validateFields, verifyPassword };
|
package/dist/index.js
CHANGED
|
@@ -1142,7 +1142,8 @@ var ADMIN_ICONS = [
|
|
|
1142
1142
|
"chart-column",
|
|
1143
1143
|
"chart-line"
|
|
1144
1144
|
];
|
|
1145
|
-
function definePlugin(plugin) {
|
|
1145
|
+
function definePlugin(plugin, info) {
|
|
1146
|
+
if (info) Object.defineProperty(plugin, "info", { value: info, configurable: true });
|
|
1146
1147
|
return plugin;
|
|
1147
1148
|
}
|
|
1148
1149
|
function defineConfig(config) {
|
|
@@ -1304,13 +1305,13 @@ function generateSlugs(fields, data, localization = null) {
|
|
|
1304
1305
|
function mergeForUpdate(fields, existing, patch) {
|
|
1305
1306
|
const result = {};
|
|
1306
1307
|
for (const field of fields) {
|
|
1307
|
-
const
|
|
1308
|
+
const has2 = Object.hasOwn(patch, field.name);
|
|
1308
1309
|
const next = patch[field.name];
|
|
1309
1310
|
const prev = existing[field.name];
|
|
1310
|
-
if (field.type === "group" &&
|
|
1311
|
+
if (field.type === "group" && has2 && isPlainObject(next)) {
|
|
1311
1312
|
result[field.name] = mergeForUpdate(field.fields, isPlainObject(prev) ? prev : {}, next);
|
|
1312
1313
|
} else {
|
|
1313
|
-
result[field.name] =
|
|
1314
|
+
result[field.name] = has2 ? next : prev;
|
|
1314
1315
|
}
|
|
1315
1316
|
}
|
|
1316
1317
|
return result;
|
|
@@ -1541,6 +1542,7 @@ function normalizeSelect(field, raw, fail) {
|
|
|
1541
1542
|
function consoleEmail(options = {}) {
|
|
1542
1543
|
const sent = [];
|
|
1543
1544
|
const adapter = {
|
|
1545
|
+
name: "console",
|
|
1544
1546
|
from: options.from ?? "Easy CMS <no-reply@localhost>",
|
|
1545
1547
|
sent,
|
|
1546
1548
|
async send(message) {
|
|
@@ -1634,6 +1636,11 @@ var Mailer = class {
|
|
|
1634
1636
|
}
|
|
1635
1637
|
return { sent, failed };
|
|
1636
1638
|
}
|
|
1639
|
+
/** One attempt now for a saved email (the admin's Retry): `undefined` when sent, else the error. */
|
|
1640
|
+
async sendNow(message) {
|
|
1641
|
+
if (!this.adapter) return "email is not set up (`email` in the config)";
|
|
1642
|
+
return this.attempt(JSON.parse(message));
|
|
1643
|
+
}
|
|
1637
1644
|
track(delivery) {
|
|
1638
1645
|
const tracked = delivery.catch((error) => this.logger.error(`Email queue failed: ${error.message}`)).finally(() => this.pending.delete(tracked));
|
|
1639
1646
|
this.pending.add(tracked);
|
|
@@ -1874,6 +1881,15 @@ var Webhooks = class {
|
|
|
1874
1881
|
}
|
|
1875
1882
|
return { sent, failed };
|
|
1876
1883
|
}
|
|
1884
|
+
/**
|
|
1885
|
+
* One attempt now for a saved delivery (the admin's Retry), with the webhook's current headers
|
|
1886
|
+
* and secret.
|
|
1887
|
+
*/
|
|
1888
|
+
async sendNow(delivery) {
|
|
1889
|
+
const hook = this.hooks.find((h) => h.url === delivery.url);
|
|
1890
|
+
if (!hook) return { ok: false, error: "this webhook is no longer in the config" };
|
|
1891
|
+
return this.attempt(hook, delivery.event, delivery.body, delivery.delivery);
|
|
1892
|
+
}
|
|
1877
1893
|
track(delivery) {
|
|
1878
1894
|
const tracked = delivery.catch((error) => this.logger.error(`Webhook queue failed: ${error.message}`)).finally(() => this.pending.delete(tracked));
|
|
1879
1895
|
this.pending.add(tracked);
|
|
@@ -2840,7 +2856,7 @@ async function resolveConfig(input) {
|
|
|
2840
2856
|
config = applied.config;
|
|
2841
2857
|
const issues = [...applied.issues, ...validateConfig(config)];
|
|
2842
2858
|
if (issues.length > 0) throw new ConfigError(issues);
|
|
2843
|
-
const { plugins
|
|
2859
|
+
const { plugins, ...rest } = config;
|
|
2844
2860
|
const result = {
|
|
2845
2861
|
...rest,
|
|
2846
2862
|
cors: config.cors ?? [],
|
|
@@ -2890,6 +2906,9 @@ async function resolveConfig(input) {
|
|
|
2890
2906
|
globals: config.globals ?? [],
|
|
2891
2907
|
endpoints: config.endpoints ?? [],
|
|
2892
2908
|
commands: config.commands ?? [],
|
|
2909
|
+
installedPlugins: (plugins ?? []).map(
|
|
2910
|
+
(plugin) => typeof plugin === "function" && plugin.info ? { ...plugin.info } : {}
|
|
2911
|
+
),
|
|
2893
2912
|
fieldTypes: config.fieldTypes ?? []
|
|
2894
2913
|
};
|
|
2895
2914
|
resolved.add(result);
|
|
@@ -4420,6 +4439,7 @@ var EasyCMS = class {
|
|
|
4420
4439
|
*/
|
|
4421
4440
|
async runJobs(now = /* @__PURE__ */ new Date()) {
|
|
4422
4441
|
const scheduled = await this.runScheduled(now);
|
|
4442
|
+
await this.pruneFailedDeliveries(now);
|
|
4423
4443
|
return {
|
|
4424
4444
|
...scheduled,
|
|
4425
4445
|
webhooks: await this.retryWebhooks(now),
|
|
@@ -4648,6 +4668,51 @@ var EasyCMS = class {
|
|
|
4648
4668
|
if (!job) throw new NotFoundError("scheduled job", jobId);
|
|
4649
4669
|
await this.db.delete({ collection: SCHEDULED_JOBS, id: job.id });
|
|
4650
4670
|
}
|
|
4671
|
+
/**
|
|
4672
|
+
* @internal One attempt now for a saved webhook delivery or email (the admin's Retry): removed
|
|
4673
|
+
* when it is sent, otherwise kept as failed with the new error.
|
|
4674
|
+
*/
|
|
4675
|
+
async retryDelivery(kind, id) {
|
|
4676
|
+
const collection2 = kind === "webhook" ? WEBHOOK_DELIVERIES : EMAIL_DELIVERIES;
|
|
4677
|
+
const row = await this.db.findById({ collection: collection2, id });
|
|
4678
|
+
if (!row) throw new NotFoundError(collection2, id);
|
|
4679
|
+
const error = kind === "webhook" ? await this.webhooks.sendNow(row).then((result) => result.ok ? void 0 : result.error) : await this.mailer.sendNow(String(row.message));
|
|
4680
|
+
if (error === void 0) {
|
|
4681
|
+
await this.db.delete({ collection: collection2, id });
|
|
4682
|
+
return { ok: true };
|
|
4683
|
+
}
|
|
4684
|
+
const { id: _id, ...data } = row;
|
|
4685
|
+
await this.db.update({
|
|
4686
|
+
collection: collection2,
|
|
4687
|
+
id,
|
|
4688
|
+
data: {
|
|
4689
|
+
...data,
|
|
4690
|
+
attempts: Number(row.attempts ?? 0) + 1,
|
|
4691
|
+
state: "failed",
|
|
4692
|
+
error,
|
|
4693
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4694
|
+
}
|
|
4695
|
+
});
|
|
4696
|
+
return { ok: false, error };
|
|
4697
|
+
}
|
|
4698
|
+
/** Removes webhook deliveries and emails that failed more than 30 days ago. */
|
|
4699
|
+
async pruneFailedDeliveries(now) {
|
|
4700
|
+
const before = new Date(now.getTime() - 30 * 864e5).toISOString();
|
|
4701
|
+
for (const collection2 of [WEBHOOK_DELIVERIES, EMAIL_DELIVERIES]) {
|
|
4702
|
+
if (!this.config.collections.some((c) => c.slug === collection2)) continue;
|
|
4703
|
+
for (; ; ) {
|
|
4704
|
+
const { docs } = await this.db.find({
|
|
4705
|
+
collection: collection2,
|
|
4706
|
+
where: { and: [{ state: { equals: "failed" } }, { updatedAt: { lt: before } }] },
|
|
4707
|
+
sort: ["updatedAt"],
|
|
4708
|
+
limit: 100,
|
|
4709
|
+
page: 1
|
|
4710
|
+
});
|
|
4711
|
+
for (const doc of docs) await this.db.delete({ collection: collection2, id: doc.id });
|
|
4712
|
+
if (docs.length < 100) break;
|
|
4713
|
+
}
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4651
4716
|
// -------------------------------------------------------------------------
|
|
4652
4717
|
/** @internal */
|
|
4653
4718
|
collection(slug) {
|
|
@@ -5199,6 +5264,123 @@ async function readAdminModule(cms, index) {
|
|
|
5199
5264
|
return { body, etag: `"${createHash3("sha1").update(body).digest("base64url")}"` };
|
|
5200
5265
|
}
|
|
5201
5266
|
|
|
5267
|
+
// src/rest/admin-deliveries.ts
|
|
5268
|
+
var PER_PAGE = 50;
|
|
5269
|
+
var collectionOf = (kind) => kind === "webhook" ? WEBHOOK_DELIVERIES : EMAIL_DELIVERIES;
|
|
5270
|
+
var exists = (cms, kind) => cms.config.collections.some((c) => c.slug === collectionOf(kind));
|
|
5271
|
+
var isDeliveryKind = (value) => value === "webhook" || value === "email";
|
|
5272
|
+
function parse(text) {
|
|
5273
|
+
try {
|
|
5274
|
+
const value = JSON.parse(String(text ?? ""));
|
|
5275
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
5276
|
+
} catch {
|
|
5277
|
+
return {};
|
|
5278
|
+
}
|
|
5279
|
+
}
|
|
5280
|
+
function toAdmin(kind, row) {
|
|
5281
|
+
const base = {
|
|
5282
|
+
id: row.id,
|
|
5283
|
+
state: row.state === "failed" ? "failed" : "pending",
|
|
5284
|
+
attempts: Number(row.attempts ?? 0),
|
|
5285
|
+
error: typeof row.error === "string" ? row.error : null,
|
|
5286
|
+
createdAt: String(row.createdAt ?? ""),
|
|
5287
|
+
updatedAt: String(row.updatedAt ?? ""),
|
|
5288
|
+
nextAttemptAt: String(row.nextAttemptAt ?? "")
|
|
5289
|
+
};
|
|
5290
|
+
if (kind === "webhook") {
|
|
5291
|
+
const payload = parse(row.body);
|
|
5292
|
+
return {
|
|
5293
|
+
...base,
|
|
5294
|
+
kind,
|
|
5295
|
+
url: String(row.url ?? ""),
|
|
5296
|
+
event: String(row.event ?? ""),
|
|
5297
|
+
...typeof payload.collection === "string" ? { collection: payload.collection } : {},
|
|
5298
|
+
...typeof payload.id === "string" || typeof payload.id === "number" ? { doc: payload.id } : {},
|
|
5299
|
+
...typeof payload.global === "string" ? { global: payload.global } : {},
|
|
5300
|
+
body: String(row.body ?? "")
|
|
5301
|
+
};
|
|
5302
|
+
}
|
|
5303
|
+
const message = parse(row.message);
|
|
5304
|
+
const to = message.to;
|
|
5305
|
+
return {
|
|
5306
|
+
...base,
|
|
5307
|
+
kind,
|
|
5308
|
+
to: Array.isArray(to) ? to.map(String) : typeof to === "string" ? [to] : [],
|
|
5309
|
+
subject: String(message.subject ?? "")
|
|
5310
|
+
};
|
|
5311
|
+
}
|
|
5312
|
+
async function listDeliveries(cms, kind, state, page) {
|
|
5313
|
+
if (!exists(cms, kind))
|
|
5314
|
+
return { docs: [], totalDocs: 0, page: 1, totalPages: 0, counts: { failed: 0, pending: 0 } };
|
|
5315
|
+
const collection2 = collectionOf(kind);
|
|
5316
|
+
const [found, failed, pending] = await Promise.all([
|
|
5317
|
+
cms.db.find({
|
|
5318
|
+
collection: collection2,
|
|
5319
|
+
where: { state: { equals: state } },
|
|
5320
|
+
sort: ["-updatedAt"],
|
|
5321
|
+
limit: PER_PAGE,
|
|
5322
|
+
page
|
|
5323
|
+
}),
|
|
5324
|
+
cms.db.count({ collection: collection2, where: { state: { equals: "failed" } } }),
|
|
5325
|
+
cms.db.count({ collection: collection2, where: { state: { equals: "pending" } } })
|
|
5326
|
+
]);
|
|
5327
|
+
return {
|
|
5328
|
+
docs: found.docs.map((row) => toAdmin(kind, row)),
|
|
5329
|
+
totalDocs: found.totalDocs,
|
|
5330
|
+
page: found.page,
|
|
5331
|
+
totalPages: found.totalPages,
|
|
5332
|
+
counts: { failed, pending }
|
|
5333
|
+
};
|
|
5334
|
+
}
|
|
5335
|
+
async function retryFailedDeliveries(cms, kind) {
|
|
5336
|
+
if (!exists(cms, kind)) return { sent: 0, failed: 0 };
|
|
5337
|
+
const collection2 = collectionOf(kind);
|
|
5338
|
+
const ids = [];
|
|
5339
|
+
for (let page = 1; ; page++) {
|
|
5340
|
+
const { docs, hasNextPage } = await cms.db.find({
|
|
5341
|
+
collection: collection2,
|
|
5342
|
+
where: { state: { equals: "failed" } },
|
|
5343
|
+
sort: ["updatedAt"],
|
|
5344
|
+
limit: 500,
|
|
5345
|
+
page
|
|
5346
|
+
});
|
|
5347
|
+
ids.push(...docs.map((row) => row.id));
|
|
5348
|
+
if (!hasNextPage) break;
|
|
5349
|
+
}
|
|
5350
|
+
let sent = 0;
|
|
5351
|
+
let failed = 0;
|
|
5352
|
+
for (const id of ids) {
|
|
5353
|
+
const result = await cms.retryDelivery(kind, id);
|
|
5354
|
+
if (result.ok) sent++;
|
|
5355
|
+
else failed++;
|
|
5356
|
+
}
|
|
5357
|
+
return { sent, failed };
|
|
5358
|
+
}
|
|
5359
|
+
async function deleteDelivery(cms, kind, id) {
|
|
5360
|
+
const collection2 = collectionOf(kind);
|
|
5361
|
+
if (!exists(cms, kind) || !await cms.db.findById({ collection: collection2, id }))
|
|
5362
|
+
throw new NotFoundError(collection2, id);
|
|
5363
|
+
await cms.db.delete({ collection: collection2, id });
|
|
5364
|
+
}
|
|
5365
|
+
async function deleteFailedDeliveries(cms, kind) {
|
|
5366
|
+
if (!exists(cms, kind)) return { deleted: 0 };
|
|
5367
|
+
const collection2 = collectionOf(kind);
|
|
5368
|
+
let deleted = 0;
|
|
5369
|
+
for (; ; ) {
|
|
5370
|
+
const { docs } = await cms.db.find({
|
|
5371
|
+
collection: collection2,
|
|
5372
|
+
where: { state: { equals: "failed" } },
|
|
5373
|
+
sort: ["updatedAt"],
|
|
5374
|
+
limit: 100,
|
|
5375
|
+
page: 1
|
|
5376
|
+
});
|
|
5377
|
+
for (const row of docs) await cms.db.delete({ collection: collection2, id: row.id });
|
|
5378
|
+
deleted += docs.length;
|
|
5379
|
+
if (docs.length < 100) break;
|
|
5380
|
+
}
|
|
5381
|
+
return { deleted };
|
|
5382
|
+
}
|
|
5383
|
+
|
|
5202
5384
|
// src/rest/admin-schema.ts
|
|
5203
5385
|
var SETTINGS = /* @__PURE__ */ new Set([USERS, API_KEYS]);
|
|
5204
5386
|
function componentRef(component) {
|
|
@@ -5350,6 +5532,12 @@ async function adminSchema(cms, user, origin) {
|
|
|
5350
5532
|
globals: await Promise.all(cms.config.globals.map((g) => global(g, user, localized))),
|
|
5351
5533
|
modules: adminModuleUrls(cms),
|
|
5352
5534
|
uploadFromURL: cms.config.upload.fromURL !== void 0,
|
|
5535
|
+
...user.role === "admin" && !user.apiKey ? {
|
|
5536
|
+
deliveries: {
|
|
5537
|
+
webhook: cms.config.collections.some((c) => c.slug === WEBHOOK_DELIVERIES),
|
|
5538
|
+
email: cms.config.collections.some((c) => c.slug === EMAIL_DELIVERIES)
|
|
5539
|
+
}
|
|
5540
|
+
} : {},
|
|
5353
5541
|
pages: await pages(cms, user),
|
|
5354
5542
|
dashboard: await widgets(cms, user)
|
|
5355
5543
|
};
|
|
@@ -5385,6 +5573,80 @@ async function widgets(cms, user) {
|
|
|
5385
5573
|
return out;
|
|
5386
5574
|
}
|
|
5387
5575
|
|
|
5576
|
+
// src/version.ts
|
|
5577
|
+
var VERSION = true ? "0.30.0" : "0.0.0-dev";
|
|
5578
|
+
|
|
5579
|
+
// src/rest/admin-status.ts
|
|
5580
|
+
var DAY = 864e5;
|
|
5581
|
+
var has = (cms, slug) => cms.config.collections.some((c) => c.slug === slug);
|
|
5582
|
+
async function adminStatus(cms) {
|
|
5583
|
+
const config = cms.config;
|
|
5584
|
+
const attention = [];
|
|
5585
|
+
const now = Date.now();
|
|
5586
|
+
const weekAgo = new Date(now - 7 * DAY).toISOString();
|
|
5587
|
+
if (has(cms, WEBHOOK_DELIVERIES)) {
|
|
5588
|
+
const failed = await cms.db.find({
|
|
5589
|
+
collection: WEBHOOK_DELIVERIES,
|
|
5590
|
+
where: { and: [{ state: { equals: "failed" } }, { updatedAt: { gte: weekAgo } }] },
|
|
5591
|
+
sort: ["-updatedAt"],
|
|
5592
|
+
limit: 200,
|
|
5593
|
+
page: 1
|
|
5594
|
+
});
|
|
5595
|
+
if (failed.totalDocs > 0) {
|
|
5596
|
+
const byUrl = /* @__PURE__ */ new Map();
|
|
5597
|
+
for (const row of failed.docs) {
|
|
5598
|
+
const url2 = String(row.url ?? "");
|
|
5599
|
+
byUrl.set(url2, (byUrl.get(url2) ?? 0) + 1);
|
|
5600
|
+
}
|
|
5601
|
+
const [url] = [...byUrl.entries()].sort((a, b) => b[1] - a[1])[0] ?? [""];
|
|
5602
|
+
attention.push({ id: "webhooks", count: failed.totalDocs, url });
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5605
|
+
if (has(cms, EMAIL_DELIVERIES)) {
|
|
5606
|
+
const count = await cms.db.count({
|
|
5607
|
+
collection: EMAIL_DELIVERIES,
|
|
5608
|
+
where: {
|
|
5609
|
+
or: [
|
|
5610
|
+
{ and: [{ state: { equals: "failed" } }, { updatedAt: { gte: weekAgo } }] },
|
|
5611
|
+
{
|
|
5612
|
+
and: [
|
|
5613
|
+
{ state: { equals: "pending" } },
|
|
5614
|
+
{ createdAt: { lte: new Date(now - 60 * 6e4).toISOString() } }
|
|
5615
|
+
]
|
|
5616
|
+
}
|
|
5617
|
+
]
|
|
5618
|
+
}
|
|
5619
|
+
});
|
|
5620
|
+
if (count > 0) attention.push({ id: "emails", count });
|
|
5621
|
+
}
|
|
5622
|
+
if (has(cms, SCHEDULED_JOBS)) {
|
|
5623
|
+
const count = await cms.db.count({
|
|
5624
|
+
collection: SCHEDULED_JOBS,
|
|
5625
|
+
where: {
|
|
5626
|
+
and: [
|
|
5627
|
+
{ state: { equals: "pending" } },
|
|
5628
|
+
{ runAt: { lte: new Date(now - 10 * 6e4).toISOString() } }
|
|
5629
|
+
]
|
|
5630
|
+
}
|
|
5631
|
+
});
|
|
5632
|
+
if (count > 0) attention.push({ id: "scheduled", count });
|
|
5633
|
+
}
|
|
5634
|
+
if (!config.email) attention.push({ id: "no-email" });
|
|
5635
|
+
if (!config.serverURL && process.env.NODE_ENV === "production")
|
|
5636
|
+
attention.push({ id: "no-server-url" });
|
|
5637
|
+
return {
|
|
5638
|
+
system: {
|
|
5639
|
+
version: VERSION,
|
|
5640
|
+
database: config.db.name,
|
|
5641
|
+
storage: config.upload.storage?.name ?? "local",
|
|
5642
|
+
email: config.email ? config.email.name ?? "custom" : null,
|
|
5643
|
+
plugins: config.installedPlugins.map((p) => ({ ...p })),
|
|
5644
|
+
fieldTypes: config.fieldTypes.map((t) => t.name)
|
|
5645
|
+
},
|
|
5646
|
+
attention
|
|
5647
|
+
};
|
|
5648
|
+
}
|
|
5649
|
+
|
|
5388
5650
|
// src/rest/filter-options.ts
|
|
5389
5651
|
function fieldAt(fields, path) {
|
|
5390
5652
|
let current = fields;
|
|
@@ -5752,6 +6014,38 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
5752
6014
|
}
|
|
5753
6015
|
return { body: await cms.runJobs() };
|
|
5754
6016
|
}
|
|
6017
|
+
if (first === "admin" && second === "deliveries") {
|
|
6018
|
+
if (!ctx.user) throw new UnauthorizedError();
|
|
6019
|
+
if (ctx.user.role !== "admin" || ctx.user.apiKey) throw new ForbiddenError();
|
|
6020
|
+
const [, , kind, id2, action, extra] = segments;
|
|
6021
|
+
if (kind === void 0) {
|
|
6022
|
+
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
6023
|
+
const k = ctx.url.searchParams.get("kind") ?? "webhook";
|
|
6024
|
+
const state = ctx.url.searchParams.get("state") === "pending" ? "pending" : "failed";
|
|
6025
|
+
if (!isDeliveryKind(k)) throw new HttpError("kind must be webhook or email", 400);
|
|
6026
|
+
const page = Math.max(1, Number.parseInt(ctx.url.searchParams.get("page") ?? "1", 10) || 1);
|
|
6027
|
+
return { body: await listDeliveries(cms, k, state, page) };
|
|
6028
|
+
}
|
|
6029
|
+
if (!isDeliveryKind(kind) || extra !== void 0) throw new HttpError("Not found", 404);
|
|
6030
|
+
if (id2 === "retry" && action === void 0) {
|
|
6031
|
+
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
6032
|
+
return { body: await retryFailedDeliveries(cms, kind) };
|
|
6033
|
+
}
|
|
6034
|
+
if (id2 === void 0) {
|
|
6035
|
+
if (method !== "DELETE") throw methodNotAllowed(ctx, "DELETE");
|
|
6036
|
+
return { body: await deleteFailedDeliveries(cms, kind) };
|
|
6037
|
+
}
|
|
6038
|
+
const parsed = parseId(id2);
|
|
6039
|
+
if (parsed === void 0) throw new HttpError("Not found", 404);
|
|
6040
|
+
if (action === "retry") {
|
|
6041
|
+
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
6042
|
+
return { body: await cms.retryDelivery(kind, parsed) };
|
|
6043
|
+
}
|
|
6044
|
+
if (action !== void 0) throw new HttpError("Not found", 404);
|
|
6045
|
+
if (method !== "DELETE") throw methodNotAllowed(ctx, "DELETE");
|
|
6046
|
+
await deleteDelivery(cms, kind, parsed);
|
|
6047
|
+
return { body: { deleted: 1 } };
|
|
6048
|
+
}
|
|
5755
6049
|
if (first === "admin") {
|
|
5756
6050
|
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
5757
6051
|
if (!ctx.user) throw new UnauthorizedError();
|
|
@@ -5771,6 +6065,10 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
5771
6065
|
return { body: new Response(null, { status: 304, headers }) };
|
|
5772
6066
|
return { body: new Response(file.body, { headers }) };
|
|
5773
6067
|
}
|
|
6068
|
+
if (second === "status" && third === void 0) {
|
|
6069
|
+
if (ctx.user.role !== "admin" || ctx.user.apiKey) throw new ForbiddenError();
|
|
6070
|
+
return { body: await adminStatus(cms) };
|
|
6071
|
+
}
|
|
5774
6072
|
if (second === "scheduled" && third === void 0)
|
|
5775
6073
|
return { body: await cms.upcomingJobs({ user: ctx.user, overrideAccess: false }) };
|
|
5776
6074
|
const [, , collection3, id2, extra] = segments;
|
|
@@ -6382,6 +6680,7 @@ export {
|
|
|
6382
6680
|
SchemaError,
|
|
6383
6681
|
TooManyRequestsError,
|
|
6384
6682
|
UnauthorizedError,
|
|
6683
|
+
VERSION,
|
|
6385
6684
|
ValidationError,
|
|
6386
6685
|
WEBHOOK_EVENTS,
|
|
6387
6686
|
anyone,
|