# USAGE

How to use the Generic CMS Module day-to-day. Operator-focused.

## Adding a content type

A content type = one file in `cms/schemas/`. Drop a JS module that exports a `Schema` instance.

```js
// cms/schemas/book.js
import { Schema } from '../schema.js';

export const bookSchema = new Schema({
  id: 'book',
  label: 'Book',
  storageKey: 'cms:admin:book',
  fields: {
    id: { type: 'string', required: true, pattern: '^[a-z0-9_]+$' },
    title: { type: 'string', required: true, maxLength: 200 },
    published: { type: 'boolean', required: false, default: false },
    tags: { type: 'array', required: false, itemType: 'string' },
  },
  defaults: { published: false, tags: [] },
  identityKey: 'id',
});
```

Then register:

```js
import { registerSchemas } from './cms/index.js';
import { bookSchema } from './cms/schemas/book.js';
registerSchemas({ book: bookSchema });
```

That's it. Editor form, AI prompt, CSV columns, validation — all auto-generated.

### Field types

Closed enum. See `SCHEMAS.md` for the full list.

| Type | Renders as | Notes |
|---|---|---|
| `string` | text input | pattern, maxLength |
| `textarea` | text area | maxLength |
| `markdown` | textarea + preview | sanitized on render |
| `url` | URL input | allowlist enforced |
| `image` | URL + upload | allowlist enforced |
| `integer` | number step=1 | min, max |
| `number` | number | min, max |
| `boolean` | checkbox | default false |
| `array` | dynamic list editor | itemType, maxLength |
| `enum` | select dropdown | enum: [...] |
| `references` | FK dropdown | references: { storageKey } |
| `datetime` | datetime-local | ISO 8601 |
| `color` | color picker | hex |

## Using editors

```js
import { mountListFor, mountEditFor, mountDeleteFor } from './cms/index.js';

const container = document.getElementById('app');
await mountListFor(container, 'book');           // list view
await mountEditFor(container, 'book', 'forest_trees'); // edit
await mountDeleteFor(container, 'book', 'forest_trees'); // cascade dialog
```

`mountDeleteFor` shows a cascade count. Confirm → calls `cmsDelete({ force: true })`.

## Bulk import

Four modes (JSON, JSONL, CSV, AI) all converge on `cmsBulkUpsert` after validation.

```js
import { cmsBulkUpsert } from './cms/index.js';
import { parseCsv } from './cms/upload/csv.js';

const records = parseCsv(csvText);
const r = await cmsBulkUpsert({ schemaId: 'book', records });
console.log(r.imported, r.failed);
```

AI mode: `parseAiResponse` handles JSON / JSONL output. See `upload/_upload-helpers.js`.

## Backup and restore

```js
import { exportBackup, previewImport, applyImport, autoBackup } from './cms/index.js';

const env = await exportBackup({ exportedBy: 'admin' });
const diff = await previewImport(env);
const result = await applyImport(env, { mode: 'merge' });
// mode: 'merge' (default) keeps records not in envelope
//       'replace' wipes the key first
await autoBackup({ exportedBy: 'admin' }); // rotates every 7 days
```

**Auth is never in the envelope.** Always. By design.

## Extending the storage adapter

Implement the `StorageAdapter` interface:

```ts
interface StorageAdapter {
  type: 'local' | 'memory' | 'indexeddb' | 'remote';
  get(key: string): Promise<any>;
  set(key: string, value: any): Promise<void>;
  remove(key: string): Promise<void>;
  keys(): Promise<string[]>;
}
```

Plug it in:

```js
import { setStorageAdapter } from './cms/index.js';
setStorageAdapter(new MyAdapter());
```

Tests use `MemoryStorageAdapter` (in-memory map, zero deps).

## Programmatic CRUD

```js
import { cmsUpsert, cmsGet, cmsList, cmsDelete } from './cms/index.js';

const r = await cmsUpsert({ schemaId: 'book', record: { id: 'b1', title: 'T' } });
if (!r.ok) console.error(r.errors);

const book = await cmsGet('book', 'b1');
const allBooks = await cmsList('book');

const del = await cmsDelete({ schemaId: 'book', id: 'b1' });
if (del.errors?.includes('cascade_confirm_required')) {
  await cmsDelete({ schemaId: 'book', id: 'b1', force: true });
}
```

## Global search

```js
import { globalSearch } from './cms/index.js';
const hits = await globalSearch('forest');
// → [{ schemaId, schemaLabel, record, matches: [...] }, ...]
```

Substring match against every declared field across every registered schema.

## Activity log

```js
import { logActivity, getActivity, getActivitySummary } from './cms/index.js';

await logActivity({ action: 'create', contentType: 'book', recordId: 'b1' });
const log = await getActivity(50);          // newest first
const summary = await getActivitySummary(); // counts per action
```

Every `cmsUpsert` / `cmsDelete` / `login` / `logout` / `setup` / `password_reset` / `import` / `export` logs automatically. You only call `logActivity` directly for custom actions.

## See also

- `SCHEMAS.md` — built-in + example schemas
- `SECURITY.md` — auth threat model
- `MIGRATION.md` — moving from another CMS
