@easy-cms/core 0.1.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.
@@ -0,0 +1,1049 @@
1
+ /** A document id. SQLite uses integers, Postgres may use either. */
2
+ type ID = string | number;
3
+ type Role = 'admin' | 'editor' | (string & {});
4
+ /** The logged-in admin user, as seen by access functions and hooks. */
5
+ interface AuthUser {
6
+ readonly id: ID;
7
+ readonly email: string;
8
+ readonly role: Role;
9
+ readonly [field: string]: unknown;
10
+ }
11
+ interface WhereOperators {
12
+ readonly equals?: unknown;
13
+ readonly not_equals?: unknown;
14
+ readonly in?: readonly unknown[];
15
+ readonly not_in?: readonly unknown[];
16
+ readonly gt?: number | string;
17
+ readonly gte?: number | string;
18
+ readonly lt?: number | string;
19
+ readonly lte?: number | string;
20
+ readonly like?: string;
21
+ readonly exists?: boolean;
22
+ }
23
+ type Where = {
24
+ readonly and?: readonly Where[];
25
+ readonly or?: readonly Where[];
26
+ } & {
27
+ readonly [field: string]: WhereOperators | readonly Where[] | undefined;
28
+ };
29
+ interface AccessArgs {
30
+ /** `null` when the request is not logged in. */
31
+ readonly user: AuthUser | null;
32
+ readonly id?: ID;
33
+ readonly data?: Readonly<Record<string, unknown>>;
34
+ }
35
+ /**
36
+ * Decides whether an operation is allowed.
37
+ * Return a `Where` to allow it only for the documents that match.
38
+ */
39
+ type Access = (args: AccessArgs) => boolean | Where | Promise<boolean | Where>;
40
+ interface CollectionAccess {
41
+ readonly read?: Access;
42
+ readonly create?: Access;
43
+ readonly update?: Access;
44
+ readonly delete?: Access;
45
+ }
46
+ interface GlobalAccess {
47
+ readonly read?: Access;
48
+ readonly update?: Access;
49
+ }
50
+ type FieldAccessFn = (args: AccessArgs) => boolean | Promise<boolean>;
51
+ interface FieldAccess {
52
+ readonly read?: FieldAccessFn;
53
+ readonly update?: FieldAccessFn;
54
+ }
55
+ declare const anyone: Access;
56
+ declare const isLoggedIn: Access;
57
+ declare const isAdmin: Access;
58
+
59
+ interface Logger {
60
+ info(message: string): void;
61
+ warn(message: string): void;
62
+ error(message: string): void;
63
+ }
64
+ declare const consoleLogger: Logger;
65
+ declare const silentLogger: Logger;
66
+
67
+ /** A document as stored: relationships and uploads are ids, never populated. */
68
+ type RawDocument = Record<string, unknown> & {
69
+ id: ID;
70
+ };
71
+ interface PaginatedDocs<T> {
72
+ docs: T[];
73
+ totalDocs: number;
74
+ limit: number;
75
+ page: number;
76
+ totalPages: number;
77
+ hasNextPage: boolean;
78
+ hasPrevPage: boolean;
79
+ }
80
+ /**
81
+ * How the adapter treats the database schema on startup.
82
+ * - `push`: apply schema changes directly (development).
83
+ * - `verify`: fail if migrations are pending or the config changed without a migration (production).
84
+ * - `skip`: do nothing (used by the migration CLI).
85
+ */
86
+ type SchemaMode = 'push' | 'verify' | 'skip';
87
+ interface DatabaseInitArgs {
88
+ readonly config: ResolvedConfig;
89
+ /** Project root; relative paths (database file, migrations) resolve from here. */
90
+ readonly cwd: string;
91
+ readonly schema: SchemaMode;
92
+ readonly logger: Logger;
93
+ /** Whether the process can prompt the user (the CLI in a terminal). */
94
+ readonly interactive?: boolean;
95
+ }
96
+ interface FindArgs {
97
+ readonly collection: string;
98
+ readonly where?: Where | undefined;
99
+ /** Field paths; prefix with `-` for descending. */
100
+ readonly sort: readonly string[];
101
+ /** `0` returns every matching document. */
102
+ readonly limit: number;
103
+ readonly page: number;
104
+ }
105
+ interface MigrationInfo {
106
+ readonly name: string;
107
+ readonly applied: boolean;
108
+ }
109
+ interface CreateMigrationResult {
110
+ readonly name: string;
111
+ readonly file: string;
112
+ readonly statements: readonly string[];
113
+ }
114
+ /** A connected database bound to one config. Returned by `DatabaseAdapter.init`. */
115
+ interface Database {
116
+ find(args: FindArgs): Promise<PaginatedDocs<RawDocument>>;
117
+ findById(args: {
118
+ collection: string;
119
+ id: ID;
120
+ }): Promise<RawDocument | null>;
121
+ /** Documents of one collection by id, in any order. Used to populate relationships. */
122
+ findByIds(args: {
123
+ collection: string;
124
+ ids: readonly ID[];
125
+ }): Promise<RawDocument[]>;
126
+ count(args: {
127
+ collection: string;
128
+ where?: Where | undefined;
129
+ }): Promise<number>;
130
+ /** `data` holds every field, `createdAt` and `updatedAt`. Runs in one transaction. */
131
+ create(args: {
132
+ collection: string;
133
+ data: Record<string, unknown>;
134
+ }): Promise<RawDocument>;
135
+ /** `data` holds the whole document after merging. Runs in one transaction. */
136
+ update(args: {
137
+ collection: string;
138
+ id: ID;
139
+ data: Record<string, unknown>;
140
+ }): Promise<RawDocument>;
141
+ delete(args: {
142
+ collection: string;
143
+ id: ID;
144
+ }): Promise<void>;
145
+ findGlobal(args: {
146
+ slug: string;
147
+ }): Promise<Record<string, unknown> | null>;
148
+ updateGlobal(args: {
149
+ slug: string;
150
+ data: Record<string, unknown>;
151
+ }): Promise<Record<string, unknown>>;
152
+ /** Writes a migration file for the difference between the config and the latest migration. `null` when nothing changed. */
153
+ createMigration(args: {
154
+ name: string;
155
+ }): Promise<CreateMigrationResult | null>;
156
+ /** Applies pending migrations in order. Returns the names applied. */
157
+ migrate(): Promise<string[]>;
158
+ migrationStatus(): Promise<MigrationInfo[]>;
159
+ destroy(): Promise<void>;
160
+ }
161
+ /** What `sqlite()` / `postgres()` return and what `config.db` holds. */
162
+ interface DatabaseAdapter {
163
+ readonly name: string;
164
+ init(args: DatabaseInitArgs): Promise<Database>;
165
+ /** Hints for bundlers (Nitro, Next.js output tracing) about files static analysis cannot find. */
166
+ readonly bundle?: BundleHints;
167
+ }
168
+ interface BundleHints {
169
+ /** Absolute paths of files to ship with the server build, e.g. native binaries loaded dynamically. */
170
+ readonly traceInclude: readonly string[];
171
+ }
172
+
173
+ /** A label shown in the admin UI. Either one string or one string per admin locale. */
174
+ type Label = string | {
175
+ readonly [locale: string]: string;
176
+ };
177
+ interface FieldValidateContext {
178
+ /** The whole document being validated, including sibling fields. */
179
+ readonly data: Readonly<Record<string, unknown>>;
180
+ readonly operation: 'create' | 'update';
181
+ }
182
+ /** Return `true` when valid, or an error message to show on the field. */
183
+ type FieldValidate<TValue> = (value: TValue | null | undefined, ctx: FieldValidateContext) => true | string | Promise<true | string>;
184
+ interface BaseField<TType extends string, TValue> {
185
+ readonly type: TType;
186
+ readonly name: string;
187
+ readonly label?: Label;
188
+ readonly required?: boolean;
189
+ readonly unique?: boolean;
190
+ readonly index?: boolean;
191
+ readonly defaultValue?: TValue;
192
+ readonly validate?: FieldValidate<TValue>;
193
+ readonly access?: FieldAccess;
194
+ /** Stored but never returned by the API nor accepted as input (e.g. a password hash). */
195
+ readonly hidden?: boolean;
196
+ }
197
+ interface TextField extends BaseField<'text', string> {
198
+ readonly minLength?: number;
199
+ readonly maxLength?: number;
200
+ }
201
+ interface TextareaField extends BaseField<'textarea', string> {
202
+ readonly minLength?: number;
203
+ readonly maxLength?: number;
204
+ }
205
+ interface NumberField extends BaseField<'number', number> {
206
+ readonly min?: number;
207
+ readonly max?: number;
208
+ }
209
+ interface BooleanField extends BaseField<'boolean', boolean> {
210
+ }
211
+ /** Stored and returned as an ISO 8601 string. */
212
+ interface DateField extends BaseField<'date', string> {
213
+ }
214
+ interface EmailField extends BaseField<'email', string> {
215
+ }
216
+ interface JsonField extends BaseField<'json', unknown> {
217
+ }
218
+ type SelectOption = string | {
219
+ readonly label: Label;
220
+ readonly value: string;
221
+ };
222
+ interface SelectField extends BaseField<'select', string | readonly string[]> {
223
+ readonly options: readonly SelectOption[];
224
+ readonly hasMany?: boolean;
225
+ }
226
+ interface SlugField extends BaseField<'slug', string> {
227
+ /** Name of a sibling `text` field to generate the slug from. */
228
+ readonly from?: string;
229
+ }
230
+ /** Tiptap / ProseMirror JSON document. */
231
+ interface RichTextDocument {
232
+ readonly type: 'doc';
233
+ readonly content?: readonly unknown[];
234
+ }
235
+ interface RichTextField extends BaseField<'richText', RichTextDocument> {
236
+ }
237
+ /** References a document in the built-in `media` collection. */
238
+ interface UploadField extends BaseField<'upload', never> {
239
+ }
240
+ interface RelationshipField extends BaseField<'relationship', never> {
241
+ /** Slug of the target collection. */
242
+ readonly to: string;
243
+ readonly hasMany?: boolean;
244
+ }
245
+ interface ArrayField extends BaseField<'array', never> {
246
+ readonly fields: readonly Field[];
247
+ readonly minRows?: number;
248
+ readonly maxRows?: number;
249
+ }
250
+ interface GroupField extends BaseField<'group', never> {
251
+ readonly fields: readonly Field[];
252
+ }
253
+ type Field = TextField | TextareaField | NumberField | BooleanField | DateField | EmailField | JsonField | SelectField | SlugField | RichTextField | UploadField | RelationshipField | ArrayField | GroupField;
254
+ type FieldType = Field['type'];
255
+ declare const FIELD_TYPES: readonly ["text", "textarea", "number", "boolean", "date", "email", "json", "select", "slug", "richText", "upload", "relationship", "array", "group"];
256
+
257
+ interface StoredFile {
258
+ readonly body: Uint8Array;
259
+ readonly size: number;
260
+ }
261
+ /** Where uploaded files live. Keys are generated by Easy CMS: `<name>-<random>.<ext>`. */
262
+ interface StorageAdapter {
263
+ readonly name: string;
264
+ /** Called once with the project root, before any other method. */
265
+ init?(args: {
266
+ cwd: string;
267
+ }): void | Promise<void>;
268
+ put(key: string, data: Uint8Array, options: {
269
+ contentType: string;
270
+ }): Promise<void>;
271
+ get(key: string): Promise<StoredFile | null>;
272
+ delete(key: string): Promise<void>;
273
+ /**
274
+ * Public URL of a file, or `undefined` to serve it through the REST API
275
+ * (`<api>/media/file/<key>`), as the local adapter does.
276
+ */
277
+ url?(key: string): string | undefined;
278
+ }
279
+ interface LocalStorageOptions {
280
+ /** Directory, relative to the project root. Default `uploads`. */
281
+ readonly dir?: string;
282
+ }
283
+ /** Stores files on the local disk. Suitable for servers with a persistent filesystem. */
284
+ declare function localStorage(options?: LocalStorageOptions): StorageAdapter;
285
+
286
+ type Data$2 = Record<string, unknown>;
287
+ type MaybePromise<T> = T | Promise<T>;
288
+ type Operation = 'create' | 'update';
289
+ interface HookBase {
290
+ /** The user the operation runs for; `null` for anonymous or trusted Local API calls. */
291
+ readonly user: AuthUser | null;
292
+ /** The Local API, e.g. to query other collections or trigger a revalidation. */
293
+ readonly cms: EasyCMS;
294
+ /** Slug of the collection or global. */
295
+ readonly slug: string;
296
+ }
297
+ type BeforeValidateHook = (args: HookBase & {
298
+ data: Data$2;
299
+ operation: Operation;
300
+ originalDoc?: Data$2;
301
+ }) => MaybePromise<Data$2 | undefined>;
302
+ type BeforeChangeHook = (args: HookBase & {
303
+ data: Data$2;
304
+ operation: Operation;
305
+ originalDoc?: Data$2;
306
+ }) => MaybePromise<Data$2 | undefined>;
307
+ type AfterChangeHook = (args: HookBase & {
308
+ doc: Data$2;
309
+ operation: Operation;
310
+ previousDoc?: Data$2;
311
+ }) => MaybePromise<void>;
312
+ type BeforeDeleteHook = (args: HookBase & {
313
+ id: ID;
314
+ }) => MaybePromise<void>;
315
+ type AfterDeleteHook = (args: HookBase & {
316
+ id: ID;
317
+ doc: Data$2;
318
+ }) => MaybePromise<void>;
319
+ type AfterReadHook = (args: HookBase & {
320
+ doc: Data$2;
321
+ }) => MaybePromise<Data$2 | undefined>;
322
+ interface CollectionHooks {
323
+ readonly beforeValidate?: readonly BeforeValidateHook[];
324
+ readonly beforeChange?: readonly BeforeChangeHook[];
325
+ readonly afterChange?: readonly AfterChangeHook[];
326
+ readonly beforeDelete?: readonly BeforeDeleteHook[];
327
+ readonly afterDelete?: readonly AfterDeleteHook[];
328
+ readonly afterRead?: readonly AfterReadHook[];
329
+ }
330
+ interface GlobalHooks {
331
+ readonly beforeChange?: readonly BeforeChangeHook[];
332
+ readonly afterChange?: readonly AfterChangeHook[];
333
+ readonly afterRead?: readonly AfterReadHook[];
334
+ }
335
+ interface CollectionConfig {
336
+ /** URL and table name. Lowercase letters, digits, `-` and `_`. */
337
+ readonly slug: string;
338
+ readonly labels?: {
339
+ readonly singular?: Label;
340
+ readonly plural?: Label;
341
+ };
342
+ readonly fields: readonly Field[];
343
+ /** Top-level field shown as the document title in the admin UI. */
344
+ readonly useAsTitle?: string;
345
+ /** Adds a `status` field (`draft` | `published`). */
346
+ readonly drafts?: boolean;
347
+ readonly access?: CollectionAccess;
348
+ readonly hooks?: CollectionHooks;
349
+ }
350
+ interface GlobalConfig {
351
+ readonly slug: string;
352
+ readonly label?: Label;
353
+ readonly fields: readonly Field[];
354
+ readonly drafts?: boolean;
355
+ readonly access?: GlobalAccess;
356
+ readonly hooks?: GlobalHooks;
357
+ }
358
+ type AdminLocale = 'en' | 'th';
359
+ interface AdminConfig {
360
+ /** Where the admin UI is served. Default `/admin`. */
361
+ readonly path?: string;
362
+ /** Default admin UI language. Default `en`. */
363
+ readonly locale?: AdminLocale;
364
+ }
365
+ interface ImageSize {
366
+ /** Key in `media.sizes`, e.g. `thumbnail`. */
367
+ readonly name: string;
368
+ readonly width: number;
369
+ readonly height?: number;
370
+ /** How to fit when both width and height are set. Default `cover`. */
371
+ readonly fit?: 'cover' | 'contain' | 'inside';
372
+ }
373
+ interface UploadConfig {
374
+ /** Directory for uploaded files with the default local storage, relative to the project root. Default `uploads`. */
375
+ readonly dir?: string;
376
+ /** Maximum file size in bytes. Default 10 MB. */
377
+ readonly maxFileSize?: number;
378
+ /** Allowed MIME types, detected from file contents. `image/*` style wildcards are allowed. */
379
+ readonly mimeTypes?: readonly string[];
380
+ /** Where files are stored. Default: local disk in `dir`. */
381
+ readonly storage?: StorageAdapter;
382
+ /** Resized copies generated for images when `sharp` is installed. */
383
+ readonly imageSizes?: readonly ImageSize[];
384
+ }
385
+ interface RoutesConfig {
386
+ /** Where the REST API is served. Default `/api/cms`. */
387
+ readonly api?: string;
388
+ }
389
+ interface AuthConfig {
390
+ /** Roles a user can have. Must include `admin`. Default `['admin', 'editor']`. */
391
+ readonly roles?: readonly string[];
392
+ /** Session lifetime in seconds. Default 7 days. */
393
+ readonly tokenExpiration?: number;
394
+ /** Failed logins allowed per email (and IP) within `lockWindow`. Default 5. */
395
+ readonly maxLoginAttempts?: number;
396
+ /** Window for `maxLoginAttempts`, in seconds. Default 15 minutes. */
397
+ readonly lockWindow?: number;
398
+ /**
399
+ * Origins allowed to send cookie-authenticated requests, besides the API's own origin.
400
+ * Needed when the admin or frontend is served from another origin, or behind a proxy that rewrites the host.
401
+ */
402
+ readonly trustedOrigins?: readonly string[];
403
+ }
404
+ /** Receives the config and returns a modified copy. Runs before validation. */
405
+ type Plugin = (config: Config) => MaybePromise<Config>;
406
+ interface Config {
407
+ /** Signs sessions. At least 32 characters; read it from `process.env.EASY_CMS_SECRET`. */
408
+ readonly secret: string;
409
+ readonly db: DatabaseAdapter;
410
+ /**
411
+ * Public origin of the app, e.g. `https://example.com`. When set, media URLs are absolute
412
+ * so frontends on other origins can use them. Default: relative URLs.
413
+ */
414
+ readonly serverURL?: string;
415
+ readonly routes?: RoutesConfig;
416
+ readonly admin?: AdminConfig;
417
+ readonly upload?: UploadConfig;
418
+ readonly auth?: AuthConfig;
419
+ /**
420
+ * Collections. A collection with slug `users` adds fields, access or hooks
421
+ * to the built-in users collection.
422
+ */
423
+ readonly collections?: readonly CollectionConfig[];
424
+ readonly globals?: readonly GlobalConfig[];
425
+ readonly plugins?: readonly Plugin[];
426
+ }
427
+ /** The config after plugins ran, validation passed and defaults were applied. */
428
+ interface ResolvedConfig extends Omit<Config, 'routes' | 'admin' | 'upload' | 'auth' | 'collections' | 'globals' | 'plugins'> {
429
+ readonly routes: Required<RoutesConfig>;
430
+ readonly admin: Required<AdminConfig>;
431
+ readonly upload: Required<Omit<UploadConfig, 'storage'>> & Pick<UploadConfig, 'storage'>;
432
+ readonly auth: Required<AuthConfig>;
433
+ readonly collections: readonly CollectionConfig[];
434
+ readonly globals: readonly GlobalConfig[];
435
+ }
436
+ /**
437
+ * Declares the Easy CMS config. Returns it unchanged, keeping literal types
438
+ * so document types can be inferred from it.
439
+ */
440
+ declare function defineConfig<const TConfig extends Config>(config: TConfig): TConfig;
441
+
442
+ type Simplify<T> = {
443
+ [K in keyof T]: T[K];
444
+ } & {};
445
+ type OptionValue<O> = O extends string ? O : O extends {
446
+ readonly value: infer V;
447
+ } ? V : never;
448
+ type SelectValue<F> = F extends {
449
+ readonly options: readonly (infer O extends SelectOption)[];
450
+ } ? F extends {
451
+ readonly hasMany: true;
452
+ } ? OptionValue<O>[] : OptionValue<O> : never;
453
+ /** The built-in users collection as seen by the type system (hidden fields left out). */
454
+ interface BuiltinUsersCollection {
455
+ readonly slug: 'users';
456
+ readonly fields: readonly [
457
+ {
458
+ readonly name: 'email';
459
+ readonly type: 'email';
460
+ readonly required: true;
461
+ },
462
+ {
463
+ readonly name: 'name';
464
+ readonly type: 'text';
465
+ },
466
+ {
467
+ readonly name: 'role';
468
+ readonly type: 'select';
469
+ readonly options: readonly string[];
470
+ readonly required: true;
471
+ },
472
+ {
473
+ readonly name: 'active';
474
+ readonly type: 'boolean';
475
+ }
476
+ ];
477
+ }
478
+ /** Every collection of a config, including the built-in users collection. */
479
+ type AllCollections<C extends Config> = NonNullable<C['collections']>[number] | BuiltinUsersCollection | BuiltinMediaCollection;
480
+ /** The built-in media collection; its documents are typed as `MediaDocument`. */
481
+ interface BuiltinMediaCollection {
482
+ readonly slug: 'media';
483
+ readonly fields: readonly [];
484
+ }
485
+ type CollectionBySlug<C extends Config, S> = Extract<AllCollections<C>, {
486
+ readonly slug: S;
487
+ }>;
488
+ /** Relationships are ids at depth 0 and documents when populated. */
489
+ type RelationValue<C extends Config, S> = [CollectionBySlug<C, S>] extends [never] ? ID | Record<string, unknown> : ID | CollectionDocument<C, S & string>;
490
+ interface MediaSize {
491
+ filename: string;
492
+ width: number;
493
+ height: number;
494
+ filesize: number;
495
+ url: string;
496
+ }
497
+ /** A document of the built-in `media` collection, as returned by the API. */
498
+ interface MediaDocument {
499
+ id: ID;
500
+ filename: string;
501
+ originalName?: string | null;
502
+ mimeType: string;
503
+ filesize: number;
504
+ width?: number | null;
505
+ height?: number | null;
506
+ alt?: string | null;
507
+ /** Public URL of the file. */
508
+ url: string;
509
+ /** Resized copies, when `upload.imageSizes` is set and sharp is installed. */
510
+ sizes: Record<string, MediaSize>;
511
+ createdAt: string;
512
+ updatedAt: string;
513
+ [field: string]: unknown;
514
+ }
515
+ type FieldValue<F extends Field, C extends Config = Config> = F extends {
516
+ readonly type: 'text' | 'textarea' | 'email' | 'slug' | 'date';
517
+ } ? string : F extends {
518
+ readonly type: 'number';
519
+ } ? number : F extends {
520
+ readonly type: 'boolean';
521
+ } ? boolean : F extends {
522
+ readonly type: 'json';
523
+ } ? unknown : F extends {
524
+ readonly type: 'select';
525
+ } ? SelectValue<F> : F extends {
526
+ readonly type: 'richText';
527
+ } ? RichTextDocument : F extends {
528
+ readonly type: 'upload';
529
+ } ? ID | MediaDocument : F extends {
530
+ readonly type: 'relationship';
531
+ readonly to: infer S;
532
+ } ? F extends {
533
+ readonly hasMany: true;
534
+ } ? RelationValue<C, S>[] : RelationValue<C, S> : F extends {
535
+ readonly type: 'array';
536
+ readonly fields: infer Sub extends readonly Field[];
537
+ } ? Simplify<FieldsValue<Sub, C> & {
538
+ id: string;
539
+ }>[] : F extends {
540
+ readonly type: 'group';
541
+ readonly fields: infer Sub extends readonly Field[];
542
+ } ? FieldsValue<Sub, C> : never;
543
+ type Visible<Fs extends readonly Field[]> = Exclude<Fs[number], {
544
+ readonly hidden: true;
545
+ }>;
546
+ type RequiredFields<Fs extends readonly Field[]> = Extract<Visible<Fs>, {
547
+ readonly required: true;
548
+ }>;
549
+ type OptionalFields<Fs extends readonly Field[]> = Exclude<Visible<Fs>, {
550
+ readonly required: true;
551
+ }>;
552
+ /** The shape of the data described by a list of fields. */
553
+ type FieldsValue<Fs extends readonly Field[], C extends Config = Config> = Simplify<{
554
+ -readonly [F in RequiredFields<Fs> as F['name']]: FieldValue<F, C>;
555
+ } & {
556
+ -readonly [F in OptionalFields<Fs> as F['name']]?: FieldValue<F, C> | null;
557
+ }>;
558
+ type SystemFields<T extends {
559
+ readonly drafts?: boolean;
560
+ }> = {
561
+ id: ID;
562
+ createdAt: string;
563
+ updatedAt: string;
564
+ } & (T extends {
565
+ readonly drafts: true;
566
+ } ? {
567
+ status: 'draft' | 'published';
568
+ } : unknown);
569
+ type InferCollection<T extends CollectionConfig, C extends Config = Config> = Simplify<SystemFields<T> & FieldsValue<T['fields'], C>>;
570
+ type InferGlobal<T extends GlobalConfig, C extends Config = Config> = Simplify<
571
+ /** `null` until the global is saved for the first time. */
572
+ {
573
+ updatedAt: string | null;
574
+ } & (T extends {
575
+ readonly drafts: true;
576
+ } ? {
577
+ status: 'draft' | 'published';
578
+ } : unknown) & FieldsValue<T['fields'], C>>;
579
+ type CollectionSlug<C extends Config> = NonNullable<C['collections']>[number]['slug'] | 'users' | 'media';
580
+ type GlobalSlug<C extends Config> = NonNullable<C['globals']>[number]['slug'];
581
+ /** Document type of a collection, e.g. `CollectionDocument<typeof config, 'posts'>`. */
582
+ type CollectionDocument<C extends Config, S extends CollectionSlug<C>> = S extends 'media' ? MediaDocument : InferCollection<CollectionBySlug<C, S>, C>;
583
+ /** Data type of a global, e.g. `GlobalDocument<typeof config, 'site'>`. */
584
+ type GlobalDocument<C extends Config, S extends GlobalSlug<C>> = InferGlobal<Extract<NonNullable<C['globals']>[number], {
585
+ readonly slug: S;
586
+ }>, C>;
587
+ type InputValue<F extends Field> = F extends {
588
+ readonly type: 'relationship';
589
+ } ? F extends {
590
+ readonly hasMany: true;
591
+ } ? readonly ID[] : ID : F extends {
592
+ readonly type: 'select';
593
+ readonly hasMany: true;
594
+ } ? Readonly<SelectValue<F>> : F extends {
595
+ readonly type: 'upload';
596
+ } ? ID : F extends {
597
+ readonly type: 'date';
598
+ } ? string | Date : F extends {
599
+ readonly type: 'array';
600
+ readonly fields: infer Sub extends readonly Field[];
601
+ } ? readonly Simplify<FieldsInput<Sub> & {
602
+ id?: string;
603
+ }>[] : F extends {
604
+ readonly type: 'group';
605
+ readonly fields: infer Sub extends readonly Field[];
606
+ } ? FieldsInput<Sub> : FieldValue<F>;
607
+ /** Required fields must be given, unless Easy CMS can fill them (default value, slug from another field). */
608
+ type NeedsInput<F extends Field> = F extends {
609
+ readonly required: true;
610
+ } ? F extends {
611
+ readonly defaultValue: unknown;
612
+ } ? never : F extends {
613
+ readonly type: 'slug';
614
+ readonly from: string;
615
+ } ? never : F : never;
616
+ type FieldsInput<Fs extends readonly Field[]> = Simplify<{
617
+ -readonly [F in NeedsInput<Visible<Fs>> as F['name']]: InputValue<F>;
618
+ } & {
619
+ -readonly [F in Exclude<Visible<Fs>, NeedsInput<Visible<Fs>>> as F['name']]?: InputValue<F> | null;
620
+ }>;
621
+ type StatusInput<T> = T extends {
622
+ readonly drafts: true;
623
+ } ? {
624
+ status?: 'draft' | 'published';
625
+ } : unknown;
626
+ /** Users are created with a password, which is hashed and never returned. */
627
+ type PasswordInput<S, Required extends boolean> = S extends 'users' ? Required extends true ? {
628
+ password: string;
629
+ } : {
630
+ password?: string;
631
+ } : unknown;
632
+ /** Data accepted by `create`, e.g. `CreateInput<typeof config, 'posts'>`. */
633
+ type CreateInput<C extends Config, S extends CollectionSlug<C>> = Simplify<FieldsInput<CollectionBySlug<C, S>['fields']> & StatusInput<CollectionBySlug<C, S>> & PasswordInput<S, true>>;
634
+ /** Data accepted by `update`: any subset of `CreateInput`. */
635
+ type UpdateInput<C extends Config, S extends CollectionSlug<C>> = Partial<CreateInput<C, S>>;
636
+ /** Data accepted by `updateGlobal`. */
637
+ type GlobalInput<C extends Config, S extends GlobalSlug<C>> = Partial<Simplify<FieldsInput<Extract<NonNullable<C['globals']>[number], {
638
+ readonly slug: S;
639
+ }>['fields']> & StatusInput<Extract<NonNullable<C['globals']>[number], {
640
+ readonly slug: S;
641
+ }>>>>;
642
+
643
+ type Data$1 = Record<string, unknown>;
644
+ type Slug<C extends Config> = string extends CollectionSlug<C> ? string : CollectionSlug<C>;
645
+ type GSlug<C extends Config> = string extends GlobalSlug<C> ? string : GlobalSlug<C>;
646
+ type Doc<C extends Config, S> = string extends CollectionSlug<C> ? RawDocument : CollectionDocument<C, S & CollectionSlug<C>>;
647
+ type Create<C extends Config, S> = string extends CollectionSlug<C> ? Data$1 : CreateInput<C, S & CollectionSlug<C>>;
648
+ type Update<C extends Config, S> = string extends CollectionSlug<C> ? Data$1 : UpdateInput<C, S & CollectionSlug<C>>;
649
+ type GDoc<C extends Config, S> = string extends GlobalSlug<C> ? Data$1 : GlobalDocument<C, S & GlobalSlug<C>>;
650
+ type GInput<C extends Config, S> = string extends GlobalSlug<C> ? Data$1 : GlobalInput<C, S & GlobalSlug<C>>;
651
+ interface AccessOptions {
652
+ /**
653
+ * The Local API trusts its caller and skips access rules by default.
654
+ * Pass `false` (with `user`) to apply them, as the REST API does.
655
+ */
656
+ readonly overrideAccess?: boolean;
657
+ /** The user to check access for. `null` = not logged in. */
658
+ readonly user?: AuthUser | null;
659
+ }
660
+ interface DepthOptions extends AccessOptions {
661
+ /** How many levels of relationships to populate. Default 1, maximum 3. */
662
+ readonly depth?: number;
663
+ }
664
+ interface ReadOptions extends DepthOptions {
665
+ /**
666
+ * Include drafts. By default only published documents of collections with
667
+ * `drafts: true` are returned, including when populating relationships (FR-DRF-04).
668
+ */
669
+ readonly draft?: boolean;
670
+ }
671
+ interface FindOptions extends ReadOptions {
672
+ readonly where?: Where;
673
+ /** Field path, `-` prefix for descending. Default `-createdAt`. */
674
+ readonly sort?: string | readonly string[];
675
+ /** Default 10. `0` returns every document. */
676
+ readonly limit?: number;
677
+ /** 1-based. Default 1. */
678
+ readonly page?: number;
679
+ }
680
+ interface CreateEasyCMSOptions {
681
+ /** Project root. Default `process.cwd()`. */
682
+ readonly cwd?: string;
683
+ /** Default `verify` when `NODE_ENV=production`, otherwise `push`. */
684
+ readonly schema?: SchemaMode;
685
+ readonly logger?: Logger;
686
+ readonly interactive?: boolean;
687
+ }
688
+ /** Connects to the database and returns the Local API. */
689
+ declare function createEasyCMS<const C extends Config>(config: C, options?: CreateEasyCMSOptions): Promise<EasyCMS<C>>;
690
+ declare class EasyCMS<C extends Config = Config> {
691
+ readonly config: ResolvedConfig;
692
+ readonly db: Database;
693
+ readonly logger: Logger;
694
+ /** Login, logout and session checks. */
695
+ readonly auth: Auth;
696
+ /** Where uploaded files are stored. */
697
+ readonly storage: StorageAdapter;
698
+ constructor(config: ResolvedConfig, db: Database, logger?: Logger, storage?: StorageAdapter);
699
+ find<S extends Slug<C>>(collection: S, options?: FindOptions): Promise<PaginatedDocs<Doc<C, S>>>;
700
+ findById<S extends Slug<C>>(collection: S, id: ID, options?: ReadOptions): Promise<Doc<C, S> | null>;
701
+ count<S extends Slug<C>>(collection: S, options?: {
702
+ where?: Where;
703
+ draft?: boolean;
704
+ } & AccessOptions): Promise<number>;
705
+ create<S extends Slug<C>>(collection: S, data: Create<C, S>, options?: DepthOptions): Promise<Doc<C, S>>;
706
+ /**
707
+ * Stores a file and creates its `media` document (FR-UPL). The type is detected from
708
+ * the contents; images get width/height, and resized copies when `sharp` is installed.
709
+ */
710
+ upload(file: {
711
+ data: Uint8Array;
712
+ name: string;
713
+ }, data?: Record<string, unknown>, options?: DepthOptions): Promise<MediaDocument>;
714
+ /** Public URL of a stored file. */
715
+ mediaURL(key: string): string;
716
+ update<S extends Slug<C>>(collection: S, id: ID, data: Update<C, S>, options?: DepthOptions): Promise<Doc<C, S>>;
717
+ delete<S extends Slug<C>>(collection: S, id: ID, options?: AccessOptions): Promise<Doc<C, S>>;
718
+ findGlobal<S extends GSlug<C>>(slug: S, options?: ReadOptions): Promise<GDoc<C, S>>;
719
+ updateGlobal<S extends GSlug<C>>(slug: S, data: GInput<C, S>, options?: DepthOptions): Promise<GDoc<C, S>>;
720
+ /**
721
+ * What `user` may do with one document. Resolves `where`-style access
722
+ * against the document, so the admin UI can hide actions precisely.
723
+ */
724
+ documentPermissions(collection: Slug<C>, id: ID, user: AuthUser | null): Promise<{
725
+ update: boolean;
726
+ delete: boolean;
727
+ }>;
728
+ /** Closes the database connection. */
729
+ destroy(): Promise<void>;
730
+ /** @internal */
731
+ collection(slug: string): CollectionConfig;
732
+ private global;
733
+ /** The query constraint read access adds, or `undefined` when there is none. Throws when denied. */
734
+ private readWhere;
735
+ private checkDocumentAccess;
736
+ private checkGlobalAccess;
737
+ private fieldChecker;
738
+ /** Runs afterRead hooks, populates relationships and removes what the caller may not see. */
739
+ private output;
740
+ /** Creates a document: access, input filtering, hooks, validation, then afterChange. */
741
+ private createDocument;
742
+ private hookArgs;
743
+ /**
744
+ * Runs hooks that may replace a value, passed as `data` (before hooks) or `doc` (afterRead).
745
+ * Returning `undefined` keeps the value; a throw cancels the operation.
746
+ */
747
+ private transform;
748
+ /** Runs hooks after the change is saved: failures are logged, never undo the save (NFR-REL-03). */
749
+ private notify;
750
+ /** Writes resized copies with sharp, when installed and configured. */
751
+ private resizeImage;
752
+ /** Validates, checks uniqueness and references. Returns clean data or throws `ValidationError`. */
753
+ private prepare;
754
+ /** Refuses changes that would leave no active admin (and lock everyone out). */
755
+ private guardLastAdmin;
756
+ private makeSlugsUnique;
757
+ private checkUnique;
758
+ private isTaken;
759
+ private checkReferences;
760
+ }
761
+
762
+ interface LoginArgs {
763
+ readonly email: string;
764
+ readonly password: string;
765
+ /** Client IP, used together with the email for rate limiting. */
766
+ readonly ip?: string | undefined;
767
+ }
768
+ interface Session {
769
+ readonly user: AuthUser;
770
+ /** Signed session token, the value of the session cookie. */
771
+ readonly token: string;
772
+ /** Send back in the `X-CSRF-Token` header on cookie-authenticated writes. */
773
+ readonly csrfToken: string;
774
+ readonly expiresAt: string;
775
+ }
776
+ /** Login, logout and session verification. Available as `cms.auth`. */
777
+ declare class Auth {
778
+ private readonly cms;
779
+ constructor(cms: EasyCMS);
780
+ private get db();
781
+ private get config();
782
+ /** Checks credentials and starts a session. Throws `UnauthorizedError` or `TooManyRequestsError`. */
783
+ login(args: LoginArgs): Promise<Session>;
784
+ /** Creates the first admin. Only works while there are no users. */
785
+ registerFirstUser(args: {
786
+ email: string;
787
+ password: string;
788
+ name?: string;
789
+ }): Promise<Session>;
790
+ hasUsers(): Promise<boolean>;
791
+ /** Ends the session for a signed token. Unknown tokens are ignored. */
792
+ logout(signedToken: string): Promise<void>;
793
+ /** Returns the user for a signed session token, or `null` if it is invalid or expired. */
794
+ verify(signedToken: string | undefined | null): Promise<AuthUser | null>;
795
+ /** The CSRF token clients must echo for a signed session token. */
796
+ csrfFor(signedToken: string): string | undefined;
797
+ /** Starts a new session for a user, e.g. after they changed their own password. */
798
+ createSession(userId: ID): Promise<Session>;
799
+ /** Signs a user out everywhere. */
800
+ revokeSessions(userId: ID): Promise<void>;
801
+ private startSession;
802
+ private findSession;
803
+ private toAuthUser;
804
+ private deleteExpiredSessions;
805
+ private windowStart;
806
+ private checkRateLimit;
807
+ private recordFailure;
808
+ private clearFailures;
809
+ }
810
+
811
+ declare const MIN_PASSWORD_LENGTH = 8;
812
+ /** Hashes a password as `scrypt$N$r$p$salt$hash` (base64url). */
813
+ declare function hashPassword(password: string): Promise<string>;
814
+ /** Checks a password against a hash in constant time. Malformed hashes never match. */
815
+ declare function verifyPassword(password: string, stored: string): Promise<boolean>;
816
+
817
+ declare const MEDIA = "media";
818
+ /** Collections Easy CMS uses internally. Not exposed over REST or in the admin UI. */
819
+ declare const INTERNAL_COLLECTIONS: ReadonlySet<string>;
820
+
821
+ interface ConfigIssue {
822
+ /** Where the problem is, e.g. `collections.posts.fields.slug.from`. */
823
+ readonly path: string;
824
+ readonly message: string;
825
+ /** How to fix it. */
826
+ readonly hint?: string;
827
+ }
828
+ declare class ConfigError extends Error {
829
+ readonly issues: readonly ConfigIssue[];
830
+ constructor(issues: readonly ConfigIssue[]);
831
+ static format(issues: readonly ConfigIssue[]): string;
832
+ }
833
+ /** Base class for errors that map to an HTTP status in the REST API. */
834
+ declare class EasyCMSError extends Error {
835
+ readonly status: number;
836
+ constructor(message: string, status: number);
837
+ }
838
+ interface FieldError {
839
+ /** Dotted path, e.g. `links.0.url`. */
840
+ readonly field: string;
841
+ readonly message: string;
842
+ }
843
+ declare class ValidationError extends EasyCMSError {
844
+ readonly errors: readonly FieldError[];
845
+ constructor(collection: string, errors: readonly FieldError[]);
846
+ }
847
+ declare class NotFoundError extends EasyCMSError {
848
+ constructor(collection: string, id: unknown);
849
+ }
850
+ /** A bad `where`, `sort` or unknown collection/global. */
851
+ declare class QueryError extends EasyCMSError {
852
+ constructor(message: string);
853
+ }
854
+ /** The database schema does not match the config (pending or missing migrations). */
855
+ declare class SchemaError extends EasyCMSError {
856
+ constructor(message: string);
857
+ }
858
+ /** Not logged in, or the session is invalid. */
859
+ declare class UnauthorizedError extends EasyCMSError {
860
+ constructor(message?: string);
861
+ }
862
+ /** Logged in, but not allowed to do this. */
863
+ declare class ForbiddenError extends EasyCMSError {
864
+ constructor(message?: string);
865
+ }
866
+ declare class TooManyRequestsError extends EasyCMSError {
867
+ constructor(message: string);
868
+ }
869
+ declare class PayloadTooLargeError extends EasyCMSError {
870
+ constructor(message: string);
871
+ }
872
+
873
+ type Data = Record<string, unknown>;
874
+ /**
875
+ * Turns text into a URL slug. Keeps letters and digits of any script,
876
+ * so Thai titles stay readable: "สวัสดี World!" → "สวัสดี-world".
877
+ */
878
+ declare function slugify(text: string): string;
879
+ /** Fills in `defaultValue` for fields that are missing, including inside groups and array rows. */
880
+ declare function applyDefaults(fields: readonly Field[], data: Data): Data;
881
+ /** Gives every field a value (`null`, `[]` or a nested object) so documents always have the same shape. */
882
+ declare function fillMissing(fields: readonly Field[], data: Data): Data;
883
+ /** Fills empty `slug` fields from their `from` field. Does not check uniqueness. */
884
+ declare function generateSlugs(fields: readonly Field[], data: Data): Data;
885
+ /**
886
+ * Merges an update into the stored document. Top-level fields and group
887
+ * fields merge key by key; arrays and hasMany values are replaced.
888
+ */
889
+ declare function mergeForUpdate(fields: readonly Field[], existing: Data, patch: Data): Data;
890
+ /** Parses an id from user input. Integer ids may arrive as strings from URLs. */
891
+ declare function parseId(value: unknown): ID | undefined;
892
+ /** A reference to another document found while validating, checked against the database afterwards. */
893
+ interface Reference {
894
+ readonly field: string;
895
+ readonly collection: string;
896
+ readonly id: ID;
897
+ }
898
+ interface ValidateOptions {
899
+ readonly operation: 'create' | 'update';
900
+ /** Drafts may be incomplete: skip `required`, but still check types (FR-DRF-02). */
901
+ readonly skipRequired?: boolean;
902
+ /** The whole top-level document, passed to custom `validate` functions. */
903
+ readonly root: Data;
904
+ }
905
+ interface ValidateResult {
906
+ /** Only known fields, with values normalized (ids parsed, dates as ISO strings, row ids filled). */
907
+ readonly data: Data;
908
+ readonly errors: FieldError[];
909
+ readonly references: Reference[];
910
+ }
911
+ /** Checks and normalizes data against fields. Unknown keys are dropped. */
912
+ declare function validateFields(fields: readonly Field[], input: Data, options: ValidateOptions, prefix?: string): Promise<ValidateResult>;
913
+
914
+ declare const CONFIG_FILE_NAMES: readonly ["easy-cms.config.ts", "easy-cms.config.mts", "easy-cms.config.js", "easy-cms.config.mjs"];
915
+ interface LoadConfigOptions {
916
+ /** Directory to search. Default `process.cwd()`. */
917
+ readonly cwd?: string;
918
+ /** Explicit config file path, relative to `cwd`. */
919
+ readonly configFile?: string;
920
+ }
921
+ /** Finds the config file in `cwd`, returning its absolute path or `undefined`. */
922
+ declare function findConfigFile(cwd?: string): string | undefined;
923
+ /**
924
+ * Imports a config file without resolving or validating it.
925
+ * Build tools use this to read hints (e.g. `db.bundle`) when env vars may be missing.
926
+ */
927
+ declare function importConfig(file: string): Promise<Config | undefined>;
928
+ /** Loads `easy-cms.config.ts` (or `.mts`/`.js`/`.mjs`) and resolves it. */
929
+ declare function loadConfig(options?: LoadConfigOptions): Promise<ResolvedConfig>;
930
+
931
+ /** File types Easy CMS recognizes from their contents (FR-UPL-03). */
932
+ declare const EXTENSIONS: Readonly<Record<string, string>>;
933
+ /** Detects the MIME type from file contents, ignoring the name the client sent. */
934
+ declare function sniffMimeType(data: Uint8Array): string | undefined;
935
+ /** `image/*` style matching. */
936
+ declare function mimeAllowed(type: string, allowed: readonly string[]): boolean;
937
+ /** Width and height of PNG, GIF, JPEG and WebP images without decoding them. */
938
+ declare function imageDimensions(data: Uint8Array, type: string): {
939
+ width: number;
940
+ height: number;
941
+ } | undefined;
942
+
943
+ declare const DEFAULT_DEPTH = 1;
944
+ declare const MAX_DEPTH = 3;
945
+ /** Loads the documents of one collection that the caller may see. */
946
+ type Loader = (collection: CollectionConfig, ids: readonly ID[]) => Promise<RawDocument[]>;
947
+ /**
948
+ * Replaces relationship and upload ids with documents, `depth` levels deep.
949
+ * Missing (or unreadable) documents become `null` (single) or are dropped (hasMany).
950
+ * Returns new objects; the input is not modified.
951
+ */
952
+ declare function populate(load: Loader, collections: readonly CollectionConfig[], fields: readonly Field[], docs: RawDocument[], depth: number): Promise<RawDocument[]>;
953
+
954
+ declare const DEFAULT_ADMIN_PATH = "/admin";
955
+ declare const DEFAULT_API_PATH = "/api/cms";
956
+ declare const DEFAULT_MAX_FILE_SIZE: number;
957
+ declare const DEFAULT_TOKEN_EXPIRATION: number;
958
+ /**
959
+ * Runs plugins in order, adds the built-in collections, validates the result
960
+ * and fills in defaults. Throws `ConfigError`. Resolving twice is a no-op.
961
+ */
962
+ declare function resolveConfig(input: Config | ResolvedConfig): Promise<ResolvedConfig>;
963
+
964
+ /** A field as the admin UI sees it: plain JSON, no functions. */
965
+ interface AdminField {
966
+ name: string;
967
+ type: Field['type'];
968
+ label?: Label;
969
+ required?: boolean;
970
+ unique?: boolean;
971
+ defaultValue?: unknown;
972
+ minLength?: number;
973
+ maxLength?: number;
974
+ min?: number;
975
+ max?: number;
976
+ options?: {
977
+ label: Label;
978
+ value: string;
979
+ }[];
980
+ hasMany?: boolean;
981
+ to?: string;
982
+ from?: string;
983
+ minRows?: number;
984
+ maxRows?: number;
985
+ fields?: AdminField[];
986
+ /** The current user may not change this field. */
987
+ readOnly?: boolean;
988
+ }
989
+ interface AdminCollection {
990
+ slug: string;
991
+ labels?: {
992
+ singular?: Label;
993
+ plural?: Label;
994
+ };
995
+ useAsTitle?: string;
996
+ drafts: boolean;
997
+ fields: AdminField[];
998
+ permissions: {
999
+ read: boolean;
1000
+ create: boolean;
1001
+ update: boolean;
1002
+ delete: boolean;
1003
+ };
1004
+ }
1005
+ interface AdminGlobal {
1006
+ slug: string;
1007
+ label?: Label;
1008
+ drafts: boolean;
1009
+ fields: AdminField[];
1010
+ permissions: {
1011
+ read: boolean;
1012
+ update: boolean;
1013
+ };
1014
+ }
1015
+ interface AdminSchema {
1016
+ locale: AdminLocale;
1017
+ collections: AdminCollection[];
1018
+ globals: AdminGlobal[];
1019
+ }
1020
+
1021
+ declare const SESSION_COOKIE = "ecms-session";
1022
+ declare const CSRF_COOKIE = "ecms-csrf";
1023
+ declare const CSRF_HEADER = "x-csrf-token";
1024
+ interface RestHandlerOptions {
1025
+ /** Path the handler is mounted at. Default: `routes.api` from the config (`/api/cms`). */
1026
+ readonly basePath?: string;
1027
+ /** Client IP, used with the email to rate-limit logins. Adapters provide it. */
1028
+ readonly getClientIp?: (request: Request) => string | undefined;
1029
+ }
1030
+ type RestHandler = (request: Request) => Promise<Response>;
1031
+ /** Creates the REST API as a Web-standard `(Request) => Response` handler. */
1032
+ declare function createRestHandler<C extends Config>(instance: EasyCMS<C>, options?: RestHandlerOptions): RestHandler;
1033
+ declare function readCookie(request: Request, name: string): string | undefined;
1034
+
1035
+ /** Best-effort English singular: "categories" → "category". */
1036
+ declare function singularize(word: string): string;
1037
+ /**
1038
+ * TypeScript declarations for every collection and global, as the REST API returns them.
1039
+ * The output has no imports, so a frontend in another repository can use it as is.
1040
+ */
1041
+ declare function generateTypes(config: ResolvedConfig): string;
1042
+
1043
+ declare const MIN_SECRET_LENGTH = 32;
1044
+ /** Collections that Easy CMS provides. They can be relationship targets. */
1045
+ declare const BUILTIN_COLLECTIONS: readonly ["users", "media"];
1046
+ /** Checks a config and returns every problem found. An empty array means it is valid. */
1047
+ declare function validateConfig(config: Config): ConfigIssue[];
1048
+
1049
+ export { type Access, type AccessArgs, type AccessOptions, type AdminCollection, type AdminConfig, type AdminField, type AdminGlobal, type AdminLocale, type AdminSchema, type AfterChangeHook, type AfterDeleteHook, type AfterReadHook, type ArrayField, Auth, type AuthConfig, type AuthUser, BUILTIN_COLLECTIONS, type BeforeChangeHook, type BeforeDeleteHook, type BeforeValidateHook, type 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_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 LoadConfigOptions, type LocalStorageOptions, 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, QueryError, type RawDocument, type ReadOptions, type RelationshipField, type ResolvedConfig, type RestHandler, type RestHandlerOptions, type RichTextDocument, type RichTextField, type Role, type RoutesConfig, SESSION_COOKIE, 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 Where, type WhereOperators, anyone, applyDefaults, consoleLogger, createEasyCMS, createRestHandler, defineConfig, fillMissing, findConfigFile, generateSlugs, generateTypes, hashPassword, imageDimensions, importConfig, isAdmin, isLoggedIn, loadConfig, localStorage, mergeForUpdate, mimeAllowed, parseId, populate, readCookie, resolveConfig, silentLogger, singularize, slugify, sniffMimeType, validateConfig, validateFields, verifyPassword };