@easy-cms/core 0.20.1 → 0.22.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 +197 -133
- package/dist/index.js +150 -18
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -230,137 +230,6 @@ declare function consoleEmail(options?: {
|
|
|
230
230
|
})[];
|
|
231
231
|
};
|
|
232
232
|
|
|
233
|
-
/** A label shown in the admin UI. Either one string or one string per admin locale. */
|
|
234
|
-
type Label = string | {
|
|
235
|
-
readonly [locale: string]: string;
|
|
236
|
-
};
|
|
237
|
-
interface FieldValidateContext {
|
|
238
|
-
/** The whole document being validated, including sibling fields. */
|
|
239
|
-
readonly data: Readonly<Record<string, unknown>>;
|
|
240
|
-
readonly operation: 'create' | 'update';
|
|
241
|
-
}
|
|
242
|
-
/** Return `true` when valid, or an error message to show on the field. */
|
|
243
|
-
type FieldValidate<TValue> = (value: TValue | null | undefined, ctx: FieldValidateContext) => true | string | Promise<true | string>;
|
|
244
|
-
/**
|
|
245
|
-
* A Web Component from an admin module (`admin.modules`), by its tag name, which must start
|
|
246
|
-
* with `ecms-`. `props` (plain JSON) reach the element as its `options` property.
|
|
247
|
-
*/
|
|
248
|
-
type AdminComponent = string | {
|
|
249
|
-
readonly tag: string;
|
|
250
|
-
readonly props?: Readonly<Record<string, unknown>>;
|
|
251
|
-
};
|
|
252
|
-
/** How the admin shows a field, with components from admin modules. */
|
|
253
|
-
interface FieldAdmin {
|
|
254
|
-
/** Shown instead of the field's input; the admin keeps the label and error messages. */
|
|
255
|
-
readonly component?: AdminComponent;
|
|
256
|
-
/** Shown below the field, e.g. a length meter or a preview. */
|
|
257
|
-
readonly after?: readonly AdminComponent[];
|
|
258
|
-
}
|
|
259
|
-
interface BaseField<TType extends string, TValue> {
|
|
260
|
-
readonly type: TType;
|
|
261
|
-
readonly name: string;
|
|
262
|
-
readonly label?: Label;
|
|
263
|
-
readonly required?: boolean;
|
|
264
|
-
readonly unique?: boolean;
|
|
265
|
-
readonly index?: boolean;
|
|
266
|
-
readonly defaultValue?: TValue;
|
|
267
|
-
readonly validate?: FieldValidate<TValue>;
|
|
268
|
-
readonly access?: FieldAccess;
|
|
269
|
-
/** Stored but never returned by the API nor accepted as input (e.g. a password hash). */
|
|
270
|
-
readonly hidden?: boolean;
|
|
271
|
-
/**
|
|
272
|
-
* One value per locale (needs `localization` in the config). Not for groups, arrays or
|
|
273
|
-
* hasMany fields: localize the fields inside a group or array instead.
|
|
274
|
-
*/
|
|
275
|
-
readonly localized?: boolean;
|
|
276
|
-
/** `sidebar`: shown in the edit page's side panel instead of the main form (top-level fields). */
|
|
277
|
-
readonly position?: 'sidebar';
|
|
278
|
-
/** Custom admin components for this field. */
|
|
279
|
-
readonly admin?: FieldAdmin;
|
|
280
|
-
}
|
|
281
|
-
interface TextField extends BaseField<'text', string> {
|
|
282
|
-
readonly minLength?: number;
|
|
283
|
-
readonly maxLength?: number;
|
|
284
|
-
}
|
|
285
|
-
interface TextareaField extends BaseField<'textarea', string> {
|
|
286
|
-
readonly minLength?: number;
|
|
287
|
-
readonly maxLength?: number;
|
|
288
|
-
}
|
|
289
|
-
interface NumberField extends BaseField<'number', number> {
|
|
290
|
-
readonly min?: number;
|
|
291
|
-
readonly max?: number;
|
|
292
|
-
}
|
|
293
|
-
interface BooleanField extends BaseField<'boolean', boolean> {
|
|
294
|
-
}
|
|
295
|
-
/** Stored and returned as an ISO 8601 string. */
|
|
296
|
-
interface DateField extends BaseField<'date', string> {
|
|
297
|
-
}
|
|
298
|
-
interface EmailField extends BaseField<'email', string> {
|
|
299
|
-
}
|
|
300
|
-
interface JsonField extends BaseField<'json', unknown> {
|
|
301
|
-
}
|
|
302
|
-
type SelectOption = string | {
|
|
303
|
-
readonly label: Label;
|
|
304
|
-
readonly value: string;
|
|
305
|
-
};
|
|
306
|
-
interface SelectField extends BaseField<'select', string | readonly string[]> {
|
|
307
|
-
readonly options: readonly SelectOption[];
|
|
308
|
-
readonly hasMany?: boolean;
|
|
309
|
-
}
|
|
310
|
-
interface SlugField extends BaseField<'slug', string> {
|
|
311
|
-
/** Name of a sibling `text` field to generate the slug from. */
|
|
312
|
-
readonly from?: string;
|
|
313
|
-
}
|
|
314
|
-
/** Tiptap / ProseMirror JSON document. */
|
|
315
|
-
interface RichTextDocument {
|
|
316
|
-
readonly type: 'doc';
|
|
317
|
-
readonly content?: readonly unknown[];
|
|
318
|
-
}
|
|
319
|
-
interface RichTextField extends BaseField<'richText', RichTextDocument> {
|
|
320
|
-
}
|
|
321
|
-
/** References a document in the built-in `media` collection. */
|
|
322
|
-
interface UploadField extends BaseField<'upload', never> {
|
|
323
|
-
}
|
|
324
|
-
interface RelationshipField extends BaseField<'relationship', never> {
|
|
325
|
-
/** Slug of the target collection. */
|
|
326
|
-
readonly to: string;
|
|
327
|
-
readonly hasMany?: boolean;
|
|
328
|
-
}
|
|
329
|
-
interface ArrayField extends BaseField<'array', never> {
|
|
330
|
-
readonly fields: readonly Field[];
|
|
331
|
-
readonly minRows?: number;
|
|
332
|
-
readonly maxRows?: number;
|
|
333
|
-
}
|
|
334
|
-
interface GroupField extends BaseField<'group', never> {
|
|
335
|
-
readonly fields: readonly Field[];
|
|
336
|
-
}
|
|
337
|
-
/** One kind of block in a `blocks` field. */
|
|
338
|
-
interface Block {
|
|
339
|
-
/** Stored in each row as `blockType`. */
|
|
340
|
-
readonly slug: string;
|
|
341
|
-
readonly labels?: {
|
|
342
|
-
readonly singular?: Label;
|
|
343
|
-
readonly plural?: Label;
|
|
344
|
-
};
|
|
345
|
-
readonly fields: readonly Field[];
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* A list of rows, each one of several block kinds (e.g. hero, text, gallery), for pages that
|
|
349
|
-
* editors lay out themselves. Rows are `{ id, blockType, ...fields }`, stored as JSON.
|
|
350
|
-
*/
|
|
351
|
-
interface BlocksField extends BaseField<'blocks', never> {
|
|
352
|
-
readonly blocks: readonly Block[];
|
|
353
|
-
readonly minRows?: number;
|
|
354
|
-
readonly maxRows?: number;
|
|
355
|
-
}
|
|
356
|
-
/** Fields whose value is a list of rows: `array` and `blocks`. */
|
|
357
|
-
declare const hasRows: (field: Field) => field is ArrayField | BlocksField;
|
|
358
|
-
/** The fields of one row of an `array` or `blocks` field (`undefined` for an unknown block). */
|
|
359
|
-
declare function rowFields(field: ArrayField | BlocksField, row: unknown): readonly Field[] | undefined;
|
|
360
|
-
type Field = TextField | TextareaField | NumberField | BooleanField | DateField | EmailField | JsonField | SelectField | SlugField | RichTextField | UploadField | RelationshipField | ArrayField | GroupField | BlocksField;
|
|
361
|
-
type FieldType = Field['type'];
|
|
362
|
-
declare const FIELD_TYPES: readonly ["text", "textarea", "number", "boolean", "date", "email", "json", "select", "slug", "richText", "upload", "relationship", "array", "group", "blocks"];
|
|
363
|
-
|
|
364
233
|
/** What a preview token opens: one document of a collection, or one global. */
|
|
365
234
|
type PreviewTarget = {
|
|
366
235
|
readonly collection: string;
|
|
@@ -757,6 +626,14 @@ interface DepthOptions extends AccessOptions {
|
|
|
757
626
|
*/
|
|
758
627
|
readonly locale?: string;
|
|
759
628
|
}
|
|
629
|
+
interface UpdateOptions extends DepthOptions {
|
|
630
|
+
/**
|
|
631
|
+
* Upkeep of the live document, e.g. values a plugin keeps up to date (a page's path): with
|
|
632
|
+
* drafts and versions a pending draft stays as it is (edits go to it by default), the status
|
|
633
|
+
* stays, and no version is added to the history.
|
|
634
|
+
*/
|
|
635
|
+
readonly live?: boolean;
|
|
636
|
+
}
|
|
760
637
|
interface ReadOptions extends DepthOptions {
|
|
761
638
|
/**
|
|
762
639
|
* Include drafts. By default only published documents of collections with
|
|
@@ -846,7 +723,7 @@ declare class EasyCMS<C extends Config = Config> {
|
|
|
846
723
|
}>;
|
|
847
724
|
/** Public URL of a stored file. */
|
|
848
725
|
mediaURL(key: string): string;
|
|
849
|
-
update<S extends Slug<C>>(collection: S, id: ID, data: Update<C, S>, options?:
|
|
726
|
+
update<S extends Slug<C>>(collection: S, id: ID, data: Update<C, S>, options?: UpdateOptions): Promise<Doc<C, S>>;
|
|
850
727
|
/**
|
|
851
728
|
* Takes a document off the site: its status becomes `draft`. With versions and drafts, a plain
|
|
852
729
|
* `update` with `status: 'draft'` only saves a draft and leaves the published document live;
|
|
@@ -1056,6 +933,8 @@ declare class EasyCMS<C extends Config = Config> {
|
|
|
1056
933
|
private makeSlugsUnique;
|
|
1057
934
|
private checkUnique;
|
|
1058
935
|
private isTaken;
|
|
936
|
+
/** Each reference is one its relationship's `filterOptions` allow. */
|
|
937
|
+
private checkFilterOptions;
|
|
1059
938
|
private checkReferences;
|
|
1060
939
|
}
|
|
1061
940
|
/** A document as live preview shows it, and the page to show it on. */
|
|
@@ -1065,6 +944,154 @@ interface LivePreview<T> {
|
|
|
1065
944
|
readonly url: string | null;
|
|
1066
945
|
}
|
|
1067
946
|
|
|
947
|
+
/** A label shown in the admin UI. Either one string or one string per admin locale. */
|
|
948
|
+
type Label = string | {
|
|
949
|
+
readonly [locale: string]: string;
|
|
950
|
+
};
|
|
951
|
+
interface FieldValidateContext {
|
|
952
|
+
/** The whole document being validated, including sibling fields. */
|
|
953
|
+
readonly data: Readonly<Record<string, unknown>>;
|
|
954
|
+
readonly operation: 'create' | 'update';
|
|
955
|
+
}
|
|
956
|
+
/** Return `true` when valid, or an error message to show on the field. */
|
|
957
|
+
type FieldValidate<TValue> = (value: TValue | null | undefined, ctx: FieldValidateContext) => true | string | Promise<true | string>;
|
|
958
|
+
/**
|
|
959
|
+
* A Web Component from an admin module (`admin.modules`), by its tag name, which must start
|
|
960
|
+
* with `ecms-`. `props` (plain JSON) reach the element as its `options` property.
|
|
961
|
+
*/
|
|
962
|
+
type AdminComponent = string | {
|
|
963
|
+
readonly tag: string;
|
|
964
|
+
readonly props?: Readonly<Record<string, unknown>>;
|
|
965
|
+
};
|
|
966
|
+
/** How the admin shows a field, with components from admin modules. */
|
|
967
|
+
interface FieldAdmin {
|
|
968
|
+
/** Shown instead of the field's input; the admin keeps the label and error messages. */
|
|
969
|
+
readonly component?: AdminComponent;
|
|
970
|
+
/** Shown below the field, e.g. a length meter or a preview. */
|
|
971
|
+
readonly after?: readonly AdminComponent[];
|
|
972
|
+
}
|
|
973
|
+
interface BaseField<TType extends string, TValue> {
|
|
974
|
+
readonly type: TType;
|
|
975
|
+
readonly name: string;
|
|
976
|
+
readonly label?: Label;
|
|
977
|
+
readonly required?: boolean;
|
|
978
|
+
readonly unique?: boolean;
|
|
979
|
+
readonly index?: boolean;
|
|
980
|
+
readonly defaultValue?: TValue;
|
|
981
|
+
readonly validate?: FieldValidate<TValue>;
|
|
982
|
+
readonly access?: FieldAccess;
|
|
983
|
+
/** Stored but never returned by the API nor accepted as input (e.g. a password hash). */
|
|
984
|
+
readonly hidden?: boolean;
|
|
985
|
+
/**
|
|
986
|
+
* One value per locale (needs `localization` in the config). Not for groups, arrays or
|
|
987
|
+
* hasMany fields: localize the fields inside a group or array instead.
|
|
988
|
+
*/
|
|
989
|
+
readonly localized?: boolean;
|
|
990
|
+
/** `sidebar`: shown in the edit page's side panel instead of the main form (top-level fields). */
|
|
991
|
+
readonly position?: 'sidebar';
|
|
992
|
+
/** Custom admin components for this field. */
|
|
993
|
+
readonly admin?: FieldAdmin;
|
|
994
|
+
}
|
|
995
|
+
interface TextField extends BaseField<'text', string> {
|
|
996
|
+
readonly minLength?: number;
|
|
997
|
+
readonly maxLength?: number;
|
|
998
|
+
}
|
|
999
|
+
interface TextareaField extends BaseField<'textarea', string> {
|
|
1000
|
+
readonly minLength?: number;
|
|
1001
|
+
readonly maxLength?: number;
|
|
1002
|
+
}
|
|
1003
|
+
interface NumberField extends BaseField<'number', number> {
|
|
1004
|
+
readonly min?: number;
|
|
1005
|
+
readonly max?: number;
|
|
1006
|
+
}
|
|
1007
|
+
interface BooleanField extends BaseField<'boolean', boolean> {
|
|
1008
|
+
}
|
|
1009
|
+
/** Stored and returned as an ISO 8601 string. */
|
|
1010
|
+
interface DateField extends BaseField<'date', string> {
|
|
1011
|
+
}
|
|
1012
|
+
interface EmailField extends BaseField<'email', string> {
|
|
1013
|
+
}
|
|
1014
|
+
interface JsonField extends BaseField<'json', unknown> {
|
|
1015
|
+
}
|
|
1016
|
+
type SelectOption = string | {
|
|
1017
|
+
readonly label: Label;
|
|
1018
|
+
readonly value: string;
|
|
1019
|
+
};
|
|
1020
|
+
interface SelectField extends BaseField<'select', string | readonly string[]> {
|
|
1021
|
+
readonly options: readonly SelectOption[];
|
|
1022
|
+
readonly hasMany?: boolean;
|
|
1023
|
+
}
|
|
1024
|
+
interface SlugField extends BaseField<'slug', string> {
|
|
1025
|
+
/** Name of a sibling `text` field to generate the slug from. */
|
|
1026
|
+
readonly from?: string;
|
|
1027
|
+
/**
|
|
1028
|
+
* Name of a sibling field (e.g. `parent`): slugs then only need to differ among documents
|
|
1029
|
+
* with the same value there, so `/about/team` and `/careers/team` can both be `team`.
|
|
1030
|
+
*/
|
|
1031
|
+
readonly uniqueWithin?: string;
|
|
1032
|
+
}
|
|
1033
|
+
/** Tiptap / ProseMirror JSON document. */
|
|
1034
|
+
interface RichTextDocument {
|
|
1035
|
+
readonly type: 'doc';
|
|
1036
|
+
readonly content?: readonly unknown[];
|
|
1037
|
+
}
|
|
1038
|
+
interface RichTextField extends BaseField<'richText', RichTextDocument> {
|
|
1039
|
+
}
|
|
1040
|
+
/** References a document in the built-in `media` collection. */
|
|
1041
|
+
interface UploadField extends BaseField<'upload', never> {
|
|
1042
|
+
}
|
|
1043
|
+
interface FilterOptionsArgs {
|
|
1044
|
+
/** The document being edited; `undefined` while it is being created. */
|
|
1045
|
+
readonly id: ID | undefined;
|
|
1046
|
+
readonly user: AuthUser | null;
|
|
1047
|
+
readonly cms: EasyCMS;
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Which documents a relationship may point to: a `where` on the target collection, or `true`
|
|
1051
|
+
* for any. Runs on the server: the admin's picker offers only these, and saving checks them.
|
|
1052
|
+
*/
|
|
1053
|
+
type FilterOptions = (args: FilterOptionsArgs) => Where | true | Promise<Where | true>;
|
|
1054
|
+
interface RelationshipField extends BaseField<'relationship', never> {
|
|
1055
|
+
/** Slug of the target collection. */
|
|
1056
|
+
readonly to: string;
|
|
1057
|
+
readonly hasMany?: boolean;
|
|
1058
|
+
readonly filterOptions?: FilterOptions;
|
|
1059
|
+
}
|
|
1060
|
+
interface ArrayField extends BaseField<'array', never> {
|
|
1061
|
+
readonly fields: readonly Field[];
|
|
1062
|
+
readonly minRows?: number;
|
|
1063
|
+
readonly maxRows?: number;
|
|
1064
|
+
}
|
|
1065
|
+
interface GroupField extends BaseField<'group', never> {
|
|
1066
|
+
readonly fields: readonly Field[];
|
|
1067
|
+
}
|
|
1068
|
+
/** One kind of block in a `blocks` field. */
|
|
1069
|
+
interface Block {
|
|
1070
|
+
/** Stored in each row as `blockType`. */
|
|
1071
|
+
readonly slug: string;
|
|
1072
|
+
readonly labels?: {
|
|
1073
|
+
readonly singular?: Label;
|
|
1074
|
+
readonly plural?: Label;
|
|
1075
|
+
};
|
|
1076
|
+
readonly fields: readonly Field[];
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* A list of rows, each one of several block kinds (e.g. hero, text, gallery), for pages that
|
|
1080
|
+
* editors lay out themselves. Rows are `{ id, blockType, ...fields }`, stored as JSON.
|
|
1081
|
+
*/
|
|
1082
|
+
interface BlocksField extends BaseField<'blocks', never> {
|
|
1083
|
+
readonly blocks: readonly Block[];
|
|
1084
|
+
readonly minRows?: number;
|
|
1085
|
+
readonly maxRows?: number;
|
|
1086
|
+
}
|
|
1087
|
+
/** Fields whose value is a list of rows: `array` and `blocks`. */
|
|
1088
|
+
declare const hasRows: (field: Field) => field is ArrayField | BlocksField;
|
|
1089
|
+
/** The fields of one row of an `array` or `blocks` field (`undefined` for an unknown block). */
|
|
1090
|
+
declare function rowFields(field: ArrayField | BlocksField, row: unknown): readonly Field[] | undefined;
|
|
1091
|
+
type Field = TextField | TextareaField | NumberField | BooleanField | DateField | EmailField | JsonField | SelectField | SlugField | RichTextField | UploadField | RelationshipField | ArrayField | GroupField | BlocksField;
|
|
1092
|
+
type FieldType = Field['type'];
|
|
1093
|
+
declare const FIELD_TYPES: readonly ["text", "textarea", "number", "boolean", "date", "email", "json", "select", "slug", "richText", "upload", "relationship", "array", "group", "blocks"];
|
|
1094
|
+
|
|
1068
1095
|
declare const WEBHOOK_EVENTS: readonly ["create", "update", "delete", "publish", "unpublish", "draft"];
|
|
1069
1096
|
/**
|
|
1070
1097
|
* `create`, `update`, `delete`: the stored document changed. `publish` / `unpublish`: its status
|
|
@@ -1179,6 +1206,16 @@ interface CollectionAdmin extends ContainerAdmin {
|
|
|
1179
1206
|
* instead of with the content, e.g. for redirects or forms settings.
|
|
1180
1207
|
*/
|
|
1181
1208
|
readonly group?: 'settings';
|
|
1209
|
+
readonly list?: CollectionListAdmin;
|
|
1210
|
+
}
|
|
1211
|
+
interface CollectionListAdmin {
|
|
1212
|
+
/**
|
|
1213
|
+
* Show the list as a tree: the name of a relationship field to this same collection (e.g.
|
|
1214
|
+
* `parent`). Top-level documents come first; their children open below them.
|
|
1215
|
+
*/
|
|
1216
|
+
readonly tree?: string;
|
|
1217
|
+
/** The list's default order, e.g. `title` or `-updatedAt` (the default). */
|
|
1218
|
+
readonly sort?: string;
|
|
1182
1219
|
}
|
|
1183
1220
|
interface CollectionConfig {
|
|
1184
1221
|
/** URL and table name. Lowercase letters, digits, `-` and `_`. */
|
|
@@ -1330,6 +1367,22 @@ interface EndpointRequest {
|
|
|
1330
1367
|
* A custom REST endpoint, served under `routes.api`. Writes from the browser pass the same
|
|
1331
1368
|
* CSRF check as the built-in endpoints.
|
|
1332
1369
|
*/
|
|
1370
|
+
/** An `easy-cms <name> [args…]` command. Built-in commands keep their names. */
|
|
1371
|
+
interface CliCommand {
|
|
1372
|
+
/** Lowercase, e.g. `nested:rebuild`. */
|
|
1373
|
+
readonly name: string;
|
|
1374
|
+
/** One line for `easy-cms --help`. */
|
|
1375
|
+
readonly description: string;
|
|
1376
|
+
/** Shown by `easy-cms <name> --help`. */
|
|
1377
|
+
readonly help?: string;
|
|
1378
|
+
/** Runs with the CMS open (no schema push); return a non-zero exit code on failure. */
|
|
1379
|
+
readonly run: (args: {
|
|
1380
|
+
readonly cms: EasyCMS;
|
|
1381
|
+
/** Positional arguments after the command name. */
|
|
1382
|
+
readonly args: readonly string[];
|
|
1383
|
+
readonly log: (line: string) => void;
|
|
1384
|
+
}) => MaybePromise<number | undefined>;
|
|
1385
|
+
}
|
|
1333
1386
|
interface Endpoint {
|
|
1334
1387
|
/**
|
|
1335
1388
|
* Path under the API, e.g. `/seo/generate` or `/stats/:collection`. The first segment must
|
|
@@ -1389,6 +1442,8 @@ interface Config {
|
|
|
1389
1442
|
readonly globals?: readonly GlobalConfig[];
|
|
1390
1443
|
/** Custom REST endpoints, e.g. from plugins. */
|
|
1391
1444
|
readonly endpoints?: readonly Endpoint[];
|
|
1445
|
+
/** Extra `easy-cms <name>` CLI commands, e.g. from plugins. */
|
|
1446
|
+
readonly commands?: readonly CliCommand[];
|
|
1392
1447
|
/**
|
|
1393
1448
|
* API keys for scripts and other apps (`Authorization: Bearer ecms_…`), managed in the admin
|
|
1394
1449
|
* under Settings. A key acts as its owner, limited to the collections and operations it lists.
|
|
@@ -1414,6 +1469,7 @@ interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes'
|
|
|
1414
1469
|
readonly collections: readonly CollectionConfig[];
|
|
1415
1470
|
readonly globals: readonly GlobalConfig[];
|
|
1416
1471
|
readonly endpoints: readonly Endpoint[];
|
|
1472
|
+
readonly commands: readonly CliCommand[];
|
|
1417
1473
|
}
|
|
1418
1474
|
/**
|
|
1419
1475
|
* Declares the Easy CMS config. Returns it unchanged, keeping literal types
|
|
@@ -1565,6 +1621,8 @@ interface Reference {
|
|
|
1565
1621
|
readonly field: string;
|
|
1566
1622
|
readonly collection: string;
|
|
1567
1623
|
readonly id: ID;
|
|
1624
|
+
/** The relationship's `filterOptions`, checked when saving. */
|
|
1625
|
+
readonly filterOptions?: FilterOptions;
|
|
1568
1626
|
}
|
|
1569
1627
|
interface ValidateOptions {
|
|
1570
1628
|
readonly operation: 'create' | 'update';
|
|
@@ -1674,6 +1732,8 @@ interface AdminField {
|
|
|
1674
1732
|
}[];
|
|
1675
1733
|
hasMany?: boolean;
|
|
1676
1734
|
to?: string;
|
|
1735
|
+
/** A relationship with `filterOptions`: the picker asks the server which documents fit. */
|
|
1736
|
+
filtered?: boolean;
|
|
1677
1737
|
from?: string;
|
|
1678
1738
|
minRows?: number;
|
|
1679
1739
|
maxRows?: number;
|
|
@@ -1720,6 +1780,10 @@ interface AdminCollection {
|
|
|
1720
1780
|
sidebar?: AdminComponentRef[];
|
|
1721
1781
|
/** Listed under Settings in the menu (users, API keys, `admin.group: 'settings'`). */
|
|
1722
1782
|
group?: 'settings';
|
|
1783
|
+
/** The list is a tree along this relationship field (`admin.list.tree`). */
|
|
1784
|
+
tree?: string;
|
|
1785
|
+
/** The list's default order (`admin.list.sort`). */
|
|
1786
|
+
defaultSort?: string;
|
|
1723
1787
|
permissions: {
|
|
1724
1788
|
read: boolean;
|
|
1725
1789
|
create: boolean;
|
|
@@ -1795,4 +1859,4 @@ declare const BUILTIN_COLLECTIONS: readonly ["users", "media"];
|
|
|
1795
1859
|
/** Checks a config and returns every problem found. An empty array means it is valid. */
|
|
1796
1860
|
declare function validateConfig(config: Config): ConfigIssue[];
|
|
1797
1861
|
|
|
1798
|
-
export { ADMIN_ICONS, API_KEYS, API_KEY_GLOBAL_OPERATIONS, API_KEY_OPERATIONS, type Access, type AccessArgs, type AccessOptions, type AdminBrand, type AdminCollection, type AdminComponent, type AdminComponentRef, type AdminConfig, type AdminField, type AdminGlobal, type AdminIcon, type AdminLocale, type AdminSchema, 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 CollectionAccess, type CollectionAdmin, type CollectionConfig, type CollectionDocument, type CollectionHooks, type CollectionSlug, type Config, ConfigError, type ConfigIssue, type ContainerAdmin, type CopyProgress, type CopyResult, 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 DatabaseTransfer, type DateField, type DepthOptions, EXTENSIONS, EasyCMS, EasyCMSError, type EmailAdapter, type EmailField, type EmailMessage, 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, configSignature, consoleEmail, consoleLogger, copyDatabase, createEasyCMS, createRestHandler, createRootEndpointHandler, defineConfig, fillMissing, findConfigFile, generateSlugs, generateTypes, hasRows, hashPassword, imageDimensions, importConfig, isAdmin, isLoggedIn, keyAllows, loadConfig, localStorage, mergeForUpdate, mimeAllowed, parseId, populate, readCookie, resolveAdminModule, resolveConfig, rowFields, silentLogger, singularize, slugify, sniffMimeType, validateConfig, validateFields, verifyPassword };
|
|
1862
|
+
export { ADMIN_ICONS, API_KEYS, API_KEY_GLOBAL_OPERATIONS, API_KEY_OPERATIONS, type Access, type AccessArgs, type AccessOptions, type AdminBrand, type AdminCollection, type AdminComponent, type AdminComponentRef, type AdminConfig, type AdminField, type AdminGlobal, type AdminIcon, type AdminLocale, type AdminSchema, 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, 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 DatabaseTransfer, type DateField, type DepthOptions, EXTENSIONS, EasyCMS, EasyCMSError, type EmailAdapter, type EmailField, type EmailMessage, 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 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, 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 UpdateOptions, 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, configSignature, consoleEmail, consoleLogger, copyDatabase, createEasyCMS, createRestHandler, createRootEndpointHandler, defineConfig, fillMissing, findConfigFile, generateSlugs, generateTypes, hasRows, hashPassword, imageDimensions, importConfig, isAdmin, isLoggedIn, keyAllows, loadConfig, localStorage, mergeForUpdate, mimeAllowed, parseId, populate, readCookie, resolveAdminModule, resolveConfig, rowFields, silentLogger, singularize, slugify, sniffMimeType, validateConfig, validateFields, verifyPassword };
|
package/dist/index.js
CHANGED
|
@@ -1240,7 +1240,12 @@ async function normalizeValue(field, raw, path, options, fail, errors, reference
|
|
|
1240
1240
|
if (id === void 0)
|
|
1241
1241
|
return fail(`must be ${many ? "ids" : "an id"} of "${target}" documents`);
|
|
1242
1242
|
ids.push(id);
|
|
1243
|
-
references.push({
|
|
1243
|
+
references.push({
|
|
1244
|
+
field: path,
|
|
1245
|
+
collection: target,
|
|
1246
|
+
id,
|
|
1247
|
+
...field.type === "relationship" && field.filterOptions ? { filterOptions: field.filterOptions } : {}
|
|
1248
|
+
});
|
|
1244
1249
|
}
|
|
1245
1250
|
return many ? ids : ids[0];
|
|
1246
1251
|
}
|
|
@@ -1673,6 +1678,7 @@ function validateConfig(config) {
|
|
|
1673
1678
|
checkSlug(global2, `globals[${i}]`, seenGlobals, add);
|
|
1674
1679
|
});
|
|
1675
1680
|
validateEndpoints(config, collectionSlugs, add);
|
|
1681
|
+
validateCommands(config, add);
|
|
1676
1682
|
const apiKeys = config.apiKeys;
|
|
1677
1683
|
if (apiKeys !== void 0 && typeof apiKeys !== "boolean") add("apiKeys", "must be true or false");
|
|
1678
1684
|
collections.forEach((collection2, i) => {
|
|
@@ -1997,6 +2003,8 @@ function validateContainer(container, path, collectionSlugs, add) {
|
|
|
1997
2003
|
const group = admin.group;
|
|
1998
2004
|
if (group !== void 0 && group !== "settings")
|
|
1999
2005
|
add(`${path}.admin.group`, `must be 'settings' (got ${JSON.stringify(group)})`);
|
|
2006
|
+
const list = admin.list;
|
|
2007
|
+
if (list !== void 0) validateList(list, container, `${path}.admin.list`, add);
|
|
2000
2008
|
}
|
|
2001
2009
|
}
|
|
2002
2010
|
const reserved = new Set(SYSTEM_FIELD_NAMES);
|
|
@@ -2034,6 +2042,24 @@ function validateFields2(fields, path, reserved, collectionSlugs, add) {
|
|
|
2034
2042
|
validateField(field, fieldPath, byName, collectionSlugs, add);
|
|
2035
2043
|
}
|
|
2036
2044
|
}
|
|
2045
|
+
function validateList(list, container, path, add) {
|
|
2046
|
+
if (typeof list !== "object" || list === null) {
|
|
2047
|
+
add(path, "must be an object", "e.g. list: { tree: 'parent' }");
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
const { tree, sort } = list;
|
|
2051
|
+
if (sort !== void 0 && (typeof sort !== "string" || !sort.replace(/^-/, "")))
|
|
2052
|
+
add(`${path}.sort`, "must be a field name, with - for descending", "e.g. sort: 'title'");
|
|
2053
|
+
if (tree === void 0) return;
|
|
2054
|
+
const field = container.fields.find((f) => f.name === tree);
|
|
2055
|
+
if (field?.type !== "relationship" || field.to !== container.slug || field.hasMany || field.localized) {
|
|
2056
|
+
add(
|
|
2057
|
+
`${path}.tree`,
|
|
2058
|
+
`must name a relationship field to "${container.slug}" (not hasMany or localized)`,
|
|
2059
|
+
`e.g. { name: 'parent', type: 'relationship', to: '${container.slug}' }`
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2037
2063
|
function validateField(field, path, siblings, collectionSlugs, add) {
|
|
2038
2064
|
const position = field.position;
|
|
2039
2065
|
if (position !== void 0 && position !== "sidebar") {
|
|
@@ -2069,6 +2095,17 @@ function validateField(field, path, siblings, collectionSlugs, add) {
|
|
|
2069
2095
|
add(`${path}.from`, `"${field.from}" must be a text field (got ${source.type})`);
|
|
2070
2096
|
}
|
|
2071
2097
|
}
|
|
2098
|
+
if (field.uniqueWithin !== void 0) {
|
|
2099
|
+
const scope = siblings.get(field.uniqueWithin);
|
|
2100
|
+
if (!scope || scope === field) {
|
|
2101
|
+
add(`${path}.uniqueWithin`, `no sibling field named "${field.uniqueWithin}"`);
|
|
2102
|
+
} else if (!["relationship", "select", "text", "number"].includes(scope.type) || "hasMany" in scope && scope.hasMany || scope.localized) {
|
|
2103
|
+
add(
|
|
2104
|
+
`${path}.uniqueWithin`,
|
|
2105
|
+
`"${field.uniqueWithin}" must be a single, unlocalized relationship, select, text or number field`
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2072
2109
|
break;
|
|
2073
2110
|
case "relationship":
|
|
2074
2111
|
if (typeof field.to !== "string" || !collectionSlugs.has(field.to)) {
|
|
@@ -2157,6 +2194,25 @@ function validateComponents(components, path, add) {
|
|
|
2157
2194
|
var ENDPOINT_METHODS = ["get", "post", "put", "patch", "delete"];
|
|
2158
2195
|
var ENDPOINT_SEGMENT = /^(:[A-Za-z_][A-Za-z0-9_]*|[A-Za-z0-9._~-]+)$/;
|
|
2159
2196
|
var RESERVED_ENDPOINT_ROOTS = /* @__PURE__ */ new Set(["users", "globals", "admin", "jobs", "media", "api-keys"]);
|
|
2197
|
+
function validateCommands(config, add) {
|
|
2198
|
+
const commands = config.commands;
|
|
2199
|
+
if (commands === void 0) return;
|
|
2200
|
+
if (!Array.isArray(commands)) {
|
|
2201
|
+
add("commands", "must be an array", "e.g. commands: [{ name: 'my:task', description, run }]");
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2205
|
+
commands.forEach((command, i) => {
|
|
2206
|
+
const path = `commands[${i}]`;
|
|
2207
|
+
const { name, description, run } = command ?? {};
|
|
2208
|
+
if (typeof name !== "string" || !/^[a-z][a-z0-9-]*(:[a-z0-9-]+)*$/.test(name))
|
|
2209
|
+
add(`${path}.name`, "must be lowercase letters, digits, - and :", "e.g. 'nested:rebuild'");
|
|
2210
|
+
else if (seen.has(name)) add(`${path}.name`, `"${name}" is already used by another command`);
|
|
2211
|
+
else seen.add(name);
|
|
2212
|
+
if (typeof description !== "string") add(`${path}.description`, "must be a string");
|
|
2213
|
+
if (typeof run !== "function") add(`${path}.run`, "must be a function");
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2160
2216
|
function validateEndpoints(config, collectionSlugs, add) {
|
|
2161
2217
|
const endpoints = config.endpoints;
|
|
2162
2218
|
if (endpoints === void 0) return;
|
|
@@ -2323,7 +2379,8 @@ async function resolveConfig(input) {
|
|
|
2323
2379
|
...config.email ? [emailDeliveriesCollection] : []
|
|
2324
2380
|
],
|
|
2325
2381
|
globals: config.globals ?? [],
|
|
2326
|
-
endpoints: config.endpoints ?? []
|
|
2382
|
+
endpoints: config.endpoints ?? [],
|
|
2383
|
+
commands: config.commands ?? []
|
|
2327
2384
|
};
|
|
2328
2385
|
resolved.add(result);
|
|
2329
2386
|
return result;
|
|
@@ -2368,7 +2425,7 @@ async function loadConfig(options = {}) {
|
|
|
2368
2425
|
const looked = options.configFile ?? CONFIG_FILE_NAMES.join(", ");
|
|
2369
2426
|
throw new Error(
|
|
2370
2427
|
`Easy CMS config not found in ${cwd} (looked for ${looked}).
|
|
2371
|
-
\u2192
|
|
2428
|
+
\u2192 create one with create-easy-cms (\`npm create easy-cms\`, or pnpm, yarn or bun create easy-cms)`
|
|
2372
2429
|
);
|
|
2373
2430
|
}
|
|
2374
2431
|
const jiti = createJiti(import.meta.url, { moduleCache: false, interopDefault: false });
|
|
@@ -3269,7 +3326,13 @@ var EasyCMS = class {
|
|
|
3269
3326
|
await this.checkDocumentAccess(config, "update", guard, parsed, raw);
|
|
3270
3327
|
if (config.drafts && (mode === "unpublish" || raw.status === "published"))
|
|
3271
3328
|
this.checkKey(guard, { collection: collection2 }, "publish");
|
|
3272
|
-
const [
|
|
3329
|
+
const [drafted] = await this.withDrafts(config, [existing]);
|
|
3330
|
+
const live = options.live === true && mode === "save";
|
|
3331
|
+
const current = live ? existing : drafted;
|
|
3332
|
+
if (live && Object.hasOwn(raw, "status")) {
|
|
3333
|
+
const { status: _status, ...rest } = raw;
|
|
3334
|
+
raw = rest;
|
|
3335
|
+
}
|
|
3273
3336
|
const { input, password } = splitPassword(config, raw);
|
|
3274
3337
|
const filtered = await filterInput(
|
|
3275
3338
|
config.fields,
|
|
@@ -3292,7 +3355,8 @@ var EasyCMS = class {
|
|
|
3292
3355
|
generateSlugs(config.fields, merged, this.config.localization),
|
|
3293
3356
|
"update",
|
|
3294
3357
|
parsed,
|
|
3295
|
-
options
|
|
3358
|
+
options,
|
|
3359
|
+
guard.user
|
|
3296
3360
|
);
|
|
3297
3361
|
prepared = await this.transform(
|
|
3298
3362
|
config.hooks?.beforeChange,
|
|
@@ -3329,7 +3393,7 @@ var EasyCMS = class {
|
|
|
3329
3393
|
id: parsed,
|
|
3330
3394
|
data: { ...prepared, createdAt: existing.createdAt, updatedAt: now }
|
|
3331
3395
|
});
|
|
3332
|
-
await this.saveVersion(config, collectionParent(collection2), parsed, doc, guard);
|
|
3396
|
+
if (!live) await this.saveVersion(config, collectionParent(collection2), parsed, doc, guard);
|
|
3333
3397
|
if (password !== void 0) await this.auth.revokeSessions(parsed);
|
|
3334
3398
|
await this.notify(config.hooks?.afterChange, "afterChange", config.slug, {
|
|
3335
3399
|
...base,
|
|
@@ -3476,7 +3540,7 @@ var EasyCMS = class {
|
|
|
3476
3540
|
);
|
|
3477
3541
|
if (config.drafts) merged.status = input.status ?? current.status ?? "draft";
|
|
3478
3542
|
const base = this.hookArgs(config, guard);
|
|
3479
|
-
let prepared = await this.prepare(config, merged, "update", void 0, options);
|
|
3543
|
+
let prepared = await this.prepare(config, merged, "update", void 0, options, guard.user);
|
|
3480
3544
|
prepared = await this.transform(
|
|
3481
3545
|
config.hooks?.beforeChange,
|
|
3482
3546
|
"data",
|
|
@@ -3969,7 +4033,8 @@ var EasyCMS = class {
|
|
|
3969
4033
|
generateSlugs(config.fields, data, this.config.localization),
|
|
3970
4034
|
"create",
|
|
3971
4035
|
void 0,
|
|
3972
|
-
options
|
|
4036
|
+
options,
|
|
4037
|
+
(hookGuard ?? guard).user
|
|
3973
4038
|
);
|
|
3974
4039
|
prepared = await this.transform(
|
|
3975
4040
|
config.hooks?.beforeChange,
|
|
@@ -4139,7 +4204,7 @@ var EasyCMS = class {
|
|
|
4139
4204
|
return result;
|
|
4140
4205
|
}
|
|
4141
4206
|
/** Validates, checks uniqueness and references. Returns clean data or throws `ValidationError`. */
|
|
4142
|
-
async prepare(config, data, operation, selfId, options = {}) {
|
|
4207
|
+
async prepare(config, data, operation, selfId, options = {}, user = null) {
|
|
4143
4208
|
const isDraft = config.drafts === true && (data.status ?? "draft") === "draft";
|
|
4144
4209
|
const locale = this.localeOf(options);
|
|
4145
4210
|
const result = await validateFields(config.fields, data, {
|
|
@@ -4164,6 +4229,8 @@ var EasyCMS = class {
|
|
|
4164
4229
|
errors.push(...await this.checkUnique(config, clean, selfId));
|
|
4165
4230
|
}
|
|
4166
4231
|
errors.push(...await this.checkReferences(result.references));
|
|
4232
|
+
if (errors.length === 0)
|
|
4233
|
+
errors.push(...await this.checkFilterOptions(result.references, selfId, user));
|
|
4167
4234
|
if (errors.length > 0) throw new ValidationError(config.slug, errors);
|
|
4168
4235
|
return clean;
|
|
4169
4236
|
}
|
|
@@ -4189,15 +4256,16 @@ var EasyCMS = class {
|
|
|
4189
4256
|
}
|
|
4190
4257
|
}
|
|
4191
4258
|
async makeSlugsUnique(config, data, selfId) {
|
|
4192
|
-
const unique = async (path, base) => {
|
|
4193
|
-
let candidate = base;
|
|
4194
|
-
for (let n = 2; await this.isTaken(config.slug, path, candidate, selfId); n++) {
|
|
4195
|
-
candidate = `${base}-${n}`;
|
|
4196
|
-
}
|
|
4197
|
-
return candidate;
|
|
4198
|
-
};
|
|
4199
4259
|
for (const field of config.fields) {
|
|
4200
4260
|
if (field.type !== "slug") continue;
|
|
4261
|
+
const scope = field.uniqueWithin === void 0 ? void 0 : { [field.uniqueWithin]: { equals: data[field.uniqueWithin] ?? null } };
|
|
4262
|
+
const unique = async (path, base) => {
|
|
4263
|
+
let candidate = base;
|
|
4264
|
+
for (let n = 2; await this.isTaken(config.slug, path, candidate, selfId, scope); n++) {
|
|
4265
|
+
candidate = `${base}-${n}`;
|
|
4266
|
+
}
|
|
4267
|
+
return candidate;
|
|
4268
|
+
};
|
|
4201
4269
|
const value = data[field.name];
|
|
4202
4270
|
if (field.localized && value && typeof value === "object") {
|
|
4203
4271
|
const map = { ...value };
|
|
@@ -4230,10 +4298,39 @@ var EasyCMS = class {
|
|
|
4230
4298
|
}
|
|
4231
4299
|
return errors;
|
|
4232
4300
|
}
|
|
4233
|
-
async isTaken(collection2, field, value, selfId) {
|
|
4234
|
-
const where =
|
|
4301
|
+
async isTaken(collection2, field, value, selfId, scope) {
|
|
4302
|
+
const where = {
|
|
4303
|
+
and: [
|
|
4304
|
+
{ [field]: { equals: value } },
|
|
4305
|
+
...selfId === void 0 ? [] : [{ id: { not_equals: selfId } }],
|
|
4306
|
+
...scope ? [scope] : []
|
|
4307
|
+
]
|
|
4308
|
+
};
|
|
4235
4309
|
return await this.db.count({ collection: collection2, where }) > 0;
|
|
4236
4310
|
}
|
|
4311
|
+
/** Each reference is one its relationship's `filterOptions` allow. */
|
|
4312
|
+
async checkFilterOptions(references, selfId, user) {
|
|
4313
|
+
const errors = [];
|
|
4314
|
+
const cms = this;
|
|
4315
|
+
const allowed2 = /* @__PURE__ */ new Map();
|
|
4316
|
+
for (const ref of references) {
|
|
4317
|
+
const filter = ref.filterOptions;
|
|
4318
|
+
if (!filter) continue;
|
|
4319
|
+
if (!allowed2.has(filter)) allowed2.set(filter, await filter({ id: selfId, user, cms }));
|
|
4320
|
+
const where = allowed2.get(filter);
|
|
4321
|
+
if (where === true) continue;
|
|
4322
|
+
const target = this.config.collections.find((c) => c.slug === ref.collection);
|
|
4323
|
+
const localization = this.config.localization;
|
|
4324
|
+
const scoped = target ? this.whereFor(target, where, localization ? { locale: localization.defaultLocale } : {}) : where;
|
|
4325
|
+
const count = await this.db.count({
|
|
4326
|
+
collection: ref.collection,
|
|
4327
|
+
where: { and: [{ id: { equals: ref.id } }, ...scoped ? [scoped] : []] }
|
|
4328
|
+
});
|
|
4329
|
+
if (count === 0)
|
|
4330
|
+
errors.push({ field: ref.field, message: "is not one of the allowed choices" });
|
|
4331
|
+
}
|
|
4332
|
+
return errors;
|
|
4333
|
+
}
|
|
4237
4334
|
async checkReferences(references) {
|
|
4238
4335
|
const byCollection = /* @__PURE__ */ new Map();
|
|
4239
4336
|
for (const ref of references) {
|
|
@@ -4395,6 +4492,7 @@ async function serializeFields(fields, update, localized) {
|
|
|
4395
4492
|
case "relationship":
|
|
4396
4493
|
f.to = field.to;
|
|
4397
4494
|
if (field.hasMany) f.hasMany = true;
|
|
4495
|
+
if (field.filterOptions) f.filtered = true;
|
|
4398
4496
|
break;
|
|
4399
4497
|
case "upload":
|
|
4400
4498
|
f.to = "media";
|
|
@@ -4447,6 +4545,8 @@ async function collection(config, user, localized) {
|
|
|
4447
4545
|
if (config.icon) result.icon = config.icon;
|
|
4448
4546
|
if (config.admin?.sidebar?.length) result.sidebar = config.admin.sidebar.map(componentRef);
|
|
4449
4547
|
if (config.admin?.group === "settings" || SETTINGS.has(config.slug)) result.group = "settings";
|
|
4548
|
+
if (config.admin?.list?.tree) result.tree = config.admin.list.tree;
|
|
4549
|
+
if (config.admin?.list?.sort) result.defaultSort = config.admin.list.sort;
|
|
4450
4550
|
if (config.editIn === "drawer" && !result.drafts && !result.versions && !result.preview)
|
|
4451
4551
|
result.editIn = "drawer";
|
|
4452
4552
|
return result;
|
|
@@ -4487,6 +4587,36 @@ async function adminSchema(cms, user) {
|
|
|
4487
4587
|
};
|
|
4488
4588
|
}
|
|
4489
4589
|
|
|
4590
|
+
// src/rest/filter-options.ts
|
|
4591
|
+
function fieldAt(fields, path) {
|
|
4592
|
+
let current = fields;
|
|
4593
|
+
let found;
|
|
4594
|
+
for (const part of path.split(".")) {
|
|
4595
|
+
if (/^\d+$/.test(part)) continue;
|
|
4596
|
+
found = current?.find((f) => f.name === part);
|
|
4597
|
+
if (!found) return void 0;
|
|
4598
|
+
current = found.type === "array" || found.type === "group" ? found.fields : void 0;
|
|
4599
|
+
}
|
|
4600
|
+
return found;
|
|
4601
|
+
}
|
|
4602
|
+
async function pickerFilter(cms, url, target, user) {
|
|
4603
|
+
const forCollection = url.searchParams.get("filterFor");
|
|
4604
|
+
const forGlobal = url.searchParams.get("filterForGlobal");
|
|
4605
|
+
const spec = forCollection ?? forGlobal;
|
|
4606
|
+
if (spec === null) return void 0;
|
|
4607
|
+
const dot = spec.indexOf(".");
|
|
4608
|
+
const slug = dot > 0 ? spec.slice(0, dot) : "";
|
|
4609
|
+
const owner = forCollection !== null ? cms.config.collections.find((c) => c.slug === slug) : cms.config.globals.find((g) => g.slug === slug);
|
|
4610
|
+
const field = owner ? fieldAt(owner.fields, spec.slice(dot + 1)) : void 0;
|
|
4611
|
+
if (field?.type !== "relationship" || field.to !== target)
|
|
4612
|
+
throw new QueryError(`filterFor: no relationship to "${target}" at "${spec}"`);
|
|
4613
|
+
if (!field.filterOptions) return void 0;
|
|
4614
|
+
const rawId = url.searchParams.get("filterId");
|
|
4615
|
+
const id = rawId === null || rawId === "" ? void 0 : /^\d+$/.test(rawId) ? Number(rawId) : rawId;
|
|
4616
|
+
const where = await field.filterOptions({ id, user, cms });
|
|
4617
|
+
return where === true ? void 0 : where;
|
|
4618
|
+
}
|
|
4619
|
+
|
|
4490
4620
|
// src/rest/query.ts
|
|
4491
4621
|
var MAX_DEPTH2 = 10;
|
|
4492
4622
|
var MAX_INDEX = 100;
|
|
@@ -4879,6 +5009,8 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
4879
5009
|
if (second === void 0) {
|
|
4880
5010
|
if (method === "GET") {
|
|
4881
5011
|
const query = parseListQuery(ctx.url);
|
|
5012
|
+
const filter = await pickerFilter(cms, ctx.url, collection2, ctx.user);
|
|
5013
|
+
if (filter) query.where = query.where ? { and: [query.where, filter] } : filter;
|
|
4882
5014
|
return { body: await cms.find(collection2, { ...access, ...query, draft }) };
|
|
4883
5015
|
}
|
|
4884
5016
|
if (method === "POST") {
|