@dalgoridim/headless-cms 0.1.0 → 0.2.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/ARCHITECTURE.md +126 -0
- package/README.md +238 -48
- package/REDESIGN.md +250 -0
- package/dist/adapters/firestore/index.cjs +24 -3
- package/dist/adapters/firestore/index.cjs.map +1 -1
- package/dist/adapters/firestore/index.js +24 -3
- package/dist/adapters/firestore/index.js.map +1 -1
- package/dist/adapters/postgres/index.cjs +37 -11
- package/dist/adapters/postgres/index.cjs.map +1 -1
- package/dist/adapters/postgres/index.js +37 -11
- package/dist/adapters/postgres/index.js.map +1 -1
- package/dist/client/index.cjs +94 -543
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +89 -26
- package/dist/client/index.d.ts +89 -26
- package/dist/client/index.js +96 -547
- package/dist/client/index.js.map +1 -1
- package/dist/index.cjs +16 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -16
- package/dist/index.d.ts +81 -16
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/server/index.cjs +63 -9
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +35 -5
- package/dist/server/index.d.ts +35 -5
- package/dist/server/index.js +61 -8
- package/dist/server/index.js.map +1 -1
- package/package.json +5 -10
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Architecture & design notes
|
|
2
|
+
|
|
3
|
+
Core/internal documentation for `@dalgoridim/headless-cms`. For consumer usage,
|
|
4
|
+
see the [README](./README.md). For the history of the headless redesign, see
|
|
5
|
+
[REDESIGN.md](./REDESIGN.md).
|
|
6
|
+
|
|
7
|
+
## Philosophy: harness, not framework
|
|
8
|
+
|
|
9
|
+
The package wires **behavior** and imposes nothing else. Concretely:
|
|
10
|
+
|
|
11
|
+
- **Zero runtime dependencies.** No CSS framework, icon set, toast library, or
|
|
12
|
+
markdown renderer is bundled. Anything visual or opinionated is the consumer's.
|
|
13
|
+
- **The UI primitives are headless.** They manage `contentEditable`, uploads,
|
|
14
|
+
saves, and edit state, and expose that state (via `data-*` attributes,
|
|
15
|
+
render-props, and hooks) so the consumer owns 100% of the markup.
|
|
16
|
+
- **The store is schemaless.** The engine only ever adds an `id` and `collection`
|
|
17
|
+
for addressing; your content type `T` is never forced to declare them
|
|
18
|
+
(`Editable<T> = T & { id; collection }`).
|
|
19
|
+
|
|
20
|
+
This is a deliberate reaction to the original extraction, where the components
|
|
21
|
+
shipped a finished, portfolio-specific look (Tailwind + a dark palette + a
|
|
22
|
+
`primary` token + a bespoke markup syntax). Adopting the package then meant
|
|
23
|
+
inheriting that design. The redesign split **behavior (package)** from
|
|
24
|
+
**appearance (consumer)**.
|
|
25
|
+
|
|
26
|
+
## Two layers
|
|
27
|
+
|
|
28
|
+
| Layer | Responsibility | Freedom |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| **Data / server** | CRUD, the neutral `Query`, auth gating, storage signing | Schemaless; adapters are generic over `T` |
|
|
31
|
+
| **UI / client** | inline-edit behavior, save buffering, upload flow | Headless; consumer supplies all styling/markup |
|
|
32
|
+
|
|
33
|
+
## Subpath exports & the client/server bundle boundary
|
|
34
|
+
|
|
35
|
+
Server-only SDKs must never reach the client bundle. Two mechanisms enforce this:
|
|
36
|
+
|
|
37
|
+
1. **Split entries per concern.** Each storage adapter is two modules:
|
|
38
|
+
- `storage/<x>` — the **client** half (`upload`), pure `fetch`, safe in client
|
|
39
|
+
components.
|
|
40
|
+
- `storage/<x>/server` — the **server** half (`sign`), pulls in the SDK.
|
|
41
|
+
|
|
42
|
+
This split exists because a single module that *also* contained a lazy
|
|
43
|
+
`import("cloudinary")` still got traced into the client bundle by
|
|
44
|
+
Next/Turbopack → `Can't resolve 'fs'`. Splitting the entry removes the trace
|
|
45
|
+
entirely.
|
|
46
|
+
|
|
47
|
+
2. **Auth/data adapters live behind `/server`, `/adapters/*`, `/auth/*`** so a
|
|
48
|
+
client import can never transitively pull a Node-only SDK.
|
|
49
|
+
|
|
50
|
+
## One React context across entries
|
|
51
|
+
|
|
52
|
+
The built-in auth providers (e.g. `auth/firebase/client`) import
|
|
53
|
+
`CmsAuthProvider` through the package's **public** `@dalgoridim/headless-cms/client`
|
|
54
|
+
specifier — not a relative path — so the provider and the consumer's primitives
|
|
55
|
+
share the **same** context instance at runtime. This is enforced with a tsup
|
|
56
|
+
`external` regex (`/^@dalgoridim\/headless-cms(\/.*)?$/`) plus `tsconfig` paths.
|
|
57
|
+
Using a relative import here would create a second context instance and silently
|
|
58
|
+
break `useCmsAuth`.
|
|
59
|
+
|
|
60
|
+
## The neutral Query & backend parity
|
|
61
|
+
|
|
62
|
+
`Query` is intentionally backend-agnostic so no native query type (Firestore's
|
|
63
|
+
`Query`, raw SQL) leaks through the engine. Not every backend can honor every
|
|
64
|
+
operator; rather than return wrong results, an adapter **throws** on what it
|
|
65
|
+
cannot do.
|
|
66
|
+
|
|
67
|
+
| Capability | Postgres | Firestore |
|
|
68
|
+
|---|:--:|:--:|
|
|
69
|
+
| `eq` `ne` `lt` `lte` `gt` `gte` | ✅ | ✅ |
|
|
70
|
+
| `in` / `nin` | ✅ | ✅ |
|
|
71
|
+
| `contains` (case-insensitive substring) | ✅ (`ILIKE`) | ❌ throws (no native substring) |
|
|
72
|
+
| `{ or: [...] }` groups | ✅ | ❌ throws |
|
|
73
|
+
| `offset` pagination | ✅ | ✅ |
|
|
74
|
+
|
|
75
|
+
## Hybrid Postgres adapter
|
|
76
|
+
|
|
77
|
+
Two storage strategies under one adapter:
|
|
78
|
+
|
|
79
|
+
- **Unregistered collections** → a shared JSONB `documents` table, keyed by a
|
|
80
|
+
composite `PRIMARY KEY (collection, id)` (global `id` PKs collided across
|
|
81
|
+
collections — e.g. `project/1`, `skill/1`). Fully schemaless, zero config.
|
|
82
|
+
- **Registered collections** → flat fields mapped onto typed columns, **plus** a
|
|
83
|
+
JSONB `extra` column so unmapped fields are never dropped.
|
|
84
|
+
|
|
85
|
+
Notable details:
|
|
86
|
+
|
|
87
|
+
- `ON CONFLICT DO UPDATE` for typed-table upsert qualifies `extra` as
|
|
88
|
+
`<table>.extra` — a bare `extra` is ambiguous between the target table and the
|
|
89
|
+
`excluded` pseudo-relation.
|
|
90
|
+
- **Caveat:** JSONB fields are extracted as **text** (`data->>'field'`), so
|
|
91
|
+
numeric ordering/comparison in the schemaless table is lexicographic
|
|
92
|
+
(`"10" < "5"`). Register the collection with a typed `int`/`float` column for
|
|
93
|
+
true numeric behavior.
|
|
94
|
+
|
|
95
|
+
## Relations
|
|
96
|
+
|
|
97
|
+
References are plain field values — a self-describing `{ collection, id }`, a bare
|
|
98
|
+
id string, or an array of either. `resolveRelations(adapter, docs, opts)` runs
|
|
99
|
+
**engine-side** (never in an adapter) so it works identically over Postgres,
|
|
100
|
+
Firestore, or any custom `DataAdapter`. Loads are deduplicated (one `fetchById`
|
|
101
|
+
per unique `(collection, id)`) and parallelized; unresolved refs are left
|
|
102
|
+
untouched.
|
|
103
|
+
|
|
104
|
+
## Auth model
|
|
105
|
+
|
|
106
|
+
`AuthIdentity` is minimal and open: only `isAdmin` is meaningful to the default
|
|
107
|
+
gate; carry any other claims (roles, scopes, tenant) as extra keys. The gate
|
|
108
|
+
authorizes via an injectable `AuthorizeFn` (`(identity, req) => boolean`),
|
|
109
|
+
defaulting to `identity.isAdmin === true`. This lets a consumer gate on anything
|
|
110
|
+
without the package prescribing an identity shape.
|
|
111
|
+
|
|
112
|
+
## Headless UI internals
|
|
113
|
+
|
|
114
|
+
- **`ContentEditSpan`** — wires `contentEditable`, persists on blur, and emits
|
|
115
|
+
`data-cms-editable` / `data-cms-editing` / `data-cms-focused`. Rich-text
|
|
116
|
+
rendering is injected via `renderValue(raw) => ReactNode` (default: plain text).
|
|
117
|
+
- **`EditableImage`** — owns the file input, object-URL previews, external-URL
|
|
118
|
+
validation, and the pending-upload queue; hands all state to a render-prop.
|
|
119
|
+
- **`useMarkdownEditor`** — value state + a selection-aware `insert` command +
|
|
120
|
+
save/reset, with no UI. (The reference app skins it with `@uiw/react-md-editor`.)
|
|
121
|
+
|
|
122
|
+
## Build
|
|
123
|
+
|
|
124
|
+
`tsup` produces ESM + CJS + `.d.ts` per subpath entry, with every backend SDK and
|
|
125
|
+
the package's own specifier marked `external`. `files: ["dist"]` (plus the
|
|
126
|
+
markdown docs) controls what ships to npm.
|
package/README.md
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
# @dalgoridim/headless-cms
|
|
2
2
|
|
|
3
|
-
Database-agnostic, inline-edit headless CMS engine for React / Next.js apps.
|
|
3
|
+
Database-agnostic, **inline-edit** headless CMS engine for React / Next.js apps.
|
|
4
4
|
|
|
5
|
-
Content lives in your database and is editable **inline on the live site** by an
|
|
6
|
-
authenticated admin.
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
Content lives in *your* database and is editable **inline on the live site** by an
|
|
6
|
+
authenticated admin. Persistence, auth, and storage are fully pluggable — swap
|
|
7
|
+
Firestore for Postgres, Cloudinary for S3, Firebase auth for NextAuth, without
|
|
8
|
+
touching your UI.
|
|
9
|
+
|
|
10
|
+
**Harness, not framework.** Zero runtime dependencies; the editing components are
|
|
11
|
+
headless (you own all markup and styling); your content shape stays unconstrained.
|
|
12
|
+
You build a thin skin over the primitives (see [Styling](#styling-bring-your-own-look)) —
|
|
13
|
+
the package never pushes a look onto your site. The design rationale and internals
|
|
14
|
+
live in [ARCHITECTURE.md](./ARCHITECTURE.md).
|
|
9
15
|
|
|
10
16
|
## Install
|
|
11
17
|
|
|
12
18
|
```bash
|
|
13
19
|
npm install @dalgoridim/headless-cms
|
|
14
|
-
# plus the adapters you actually use
|
|
20
|
+
# plus only the adapters you actually use:
|
|
15
21
|
npm install firebase firebase-admin cloudinary # Firestore + Cloudinary + Firebase
|
|
16
22
|
npm install pg # Postgres
|
|
17
23
|
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner # S3
|
|
18
24
|
```
|
|
19
25
|
|
|
20
26
|
`react` / `react-dom` are peer deps; every backend SDK is an **optional** peer —
|
|
21
|
-
|
|
27
|
+
install only what your chosen adapters need.
|
|
22
28
|
|
|
23
29
|
## Subpath exports
|
|
24
30
|
|
|
@@ -27,31 +33,32 @@ Server-only code never leaks into the client bundle. Import from the right entry
|
|
|
27
33
|
| Entry | Contents |
|
|
28
34
|
|---|---|
|
|
29
35
|
| `@dalgoridim/headless-cms` | Shared types only (safe anywhere) |
|
|
30
|
-
| `.../client` | `PageProvider`, `usePageContext`, `ContentEditSpan`, `EditableImage`, `
|
|
31
|
-
| `.../server` | `createCmsHandlers`, `createAdminGate` |
|
|
36
|
+
| `.../client` | `PageProvider`, `usePageContext`, `ContentEditSpan`, `EditableImage`, `useMarkdownEditor`, `CmsAuthProvider`, `useCmsAuth` |
|
|
37
|
+
| `.../server` | `createCmsHandlers`, `createAdminGate`, `resolveRelations` |
|
|
32
38
|
| `.../adapters/firestore` | `FirestoreDataAdapter` |
|
|
33
39
|
| `.../adapters/postgres` | `PostgresDataAdapter` (hybrid JSONB + typed tables) |
|
|
34
|
-
| `.../storage/cloudinary
|
|
35
|
-
| `.../storage/cloudinary
|
|
40
|
+
| `.../storage/{cloudinary,s3,local}` | **Client** upload adapters — pure `fetch`, safe in client components |
|
|
41
|
+
| `.../storage/{cloudinary,s3,local}/server` | **Server** signers — pull in SDKs, server-only |
|
|
36
42
|
| `.../auth/firebase` | `firebaseAuth` (server gate) |
|
|
37
43
|
| `.../auth/firebase/client` | `FirebaseAuthProvider`, `useFirebaseAuth` |
|
|
38
44
|
| `.../auth/nextauth` | `nextAuthAuth`, `customAuth` |
|
|
39
45
|
|
|
40
|
-
##
|
|
46
|
+
## Core interfaces
|
|
41
47
|
|
|
42
48
|
```ts
|
|
43
49
|
import type { DataAdapter, AuthAdapter, StorageAdapter } from "@dalgoridim/headless-cms";
|
|
44
50
|
```
|
|
45
51
|
|
|
46
|
-
- **`DataAdapter`** — backend CRUD over a neutral `Query
|
|
47
|
-
- **`AuthAdapter`** — `verifyRequest(req)`
|
|
48
|
-
-
|
|
49
|
-
|
|
52
|
+
- **`DataAdapter`** — backend CRUD over a neutral [`Query`](#the-query-language).
|
|
53
|
+
- **`AuthAdapter`** — `verifyRequest(req)` resolves an identity; the gate decides
|
|
54
|
+
([auth](#auth-the-gate-is-yours)).
|
|
55
|
+
- **Storage** is split in two so server SDKs never reach the client bundle:
|
|
56
|
+
`ClientStorageAdapter` (`upload`, from `.../storage/<x>`) and
|
|
50
57
|
`ServerStorageAdapter` (`sign`, from `.../storage/<x>/server`).
|
|
51
58
|
|
|
52
|
-
##
|
|
59
|
+
## Quick start (Next.js App Router)
|
|
53
60
|
|
|
54
|
-
|
|
61
|
+
### 1. Admin CRUD route
|
|
55
62
|
|
|
56
63
|
```ts
|
|
57
64
|
// app/api/admin/[collection]/[id]/route.ts
|
|
@@ -71,7 +78,7 @@ export const { GET, PATCH, PUT, DELETE } = createCmsHandlers({
|
|
|
71
78
|
});
|
|
72
79
|
```
|
|
73
80
|
|
|
74
|
-
|
|
81
|
+
### 2. Storage sign route (server signer)
|
|
75
82
|
|
|
76
83
|
```ts
|
|
77
84
|
// app/api/admin/sign/route.ts
|
|
@@ -90,16 +97,15 @@ export const POST = createCmsHandlers({
|
|
|
90
97
|
}).sign;
|
|
91
98
|
```
|
|
92
99
|
|
|
93
|
-
|
|
100
|
+
### 3. Providers (client)
|
|
94
101
|
|
|
95
102
|
```tsx
|
|
96
103
|
"use client";
|
|
97
|
-
import { PageProvider
|
|
104
|
+
import { PageProvider } from "@dalgoridim/headless-cms/client";
|
|
98
105
|
import { FirebaseAuthProvider } from "@dalgoridim/headless-cms/auth/firebase/client";
|
|
99
106
|
import { cloudinaryStorage } from "@dalgoridim/headless-cms/storage/cloudinary";
|
|
100
107
|
import { auth, googleProvider } from "@/lib/firebase/config";
|
|
101
108
|
|
|
102
|
-
// Client upload adapter — posts to the sign route, then to Cloudinary.
|
|
103
109
|
const storage = cloudinaryStorage({ folder: "uploads", signEndpoint: "/api/admin/sign" });
|
|
104
110
|
|
|
105
111
|
export function Providers({ initialSections, children }) {
|
|
@@ -111,18 +117,156 @@ export function Providers({ initialSections, children }) {
|
|
|
111
117
|
</FirebaseAuthProvider>
|
|
112
118
|
);
|
|
113
119
|
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`PageProvider` saves `PATCH ${apiBasePath}/{collection}/{id}` (default `apiBasePath`
|
|
123
|
+
= `/api/admin`). Edit mode is driven by `useCmsAuth().isEditing`, which the auth
|
|
124
|
+
providers feed. Pass `notify` to surface your own toasts — the default logs to the
|
|
125
|
+
console so the package carries no toast dependency.
|
|
126
|
+
|
|
127
|
+
## The editing primitives (headless)
|
|
128
|
+
|
|
129
|
+
### `ContentEditSpan` — inline-editable text
|
|
114
130
|
|
|
115
|
-
|
|
116
|
-
|
|
131
|
+
Wires `contentEditable`, reads/writes the field, and exposes edit state via
|
|
132
|
+
`data-*` attributes. It ships **no styling and no markup language** — supply your
|
|
133
|
+
own via `className` and `renderValue`.
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import { ContentEditSpan } from "@dalgoridim/headless-cms/client";
|
|
137
|
+
|
|
138
|
+
<ContentEditSpan
|
|
139
|
+
collection="pages"
|
|
140
|
+
sectionKey="hero"
|
|
141
|
+
fieldKey="title"
|
|
142
|
+
as="h1" // any element/tag; defaults to "span"
|
|
143
|
+
renderValue={(raw) => raw} // turn the stored string into nodes (default: plain text)
|
|
144
|
+
className="my-heading"
|
|
145
|
+
>
|
|
117
146
|
Default title
|
|
118
147
|
</ContentEditSpan>
|
|
119
|
-
<EditableImage collection="portfolio" sectionKey="hero" fieldKey="image"
|
|
120
|
-
docId="hero" src={section.image} />
|
|
121
148
|
```
|
|
122
149
|
|
|
123
|
-
|
|
124
|
-
`
|
|
125
|
-
|
|
150
|
+
State is exposed as attributes so you can style with plain CSS (or Tailwind
|
|
151
|
+
`data-[...]` variants):
|
|
152
|
+
|
|
153
|
+
| Attribute | Present when |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `data-cms-editable` | always (the primitive is mounted) |
|
|
156
|
+
| `data-cms-editing` | the current user is in edit mode |
|
|
157
|
+
| `data-cms-focused` | the field is actively being edited |
|
|
158
|
+
|
|
159
|
+
```css
|
|
160
|
+
[data-cms-focused] { outline: 2px solid var(--accent); border-radius: 4px; }
|
|
161
|
+
[data-cms-editing]:hover { outline: 1px dashed var(--accent); cursor: text; }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Want rich text? Pass a `renderValue` that parses your own syntax (markdown, a
|
|
165
|
+
custom mini-language, etc.) into React nodes — the package no longer dictates one.
|
|
166
|
+
|
|
167
|
+
### `EditableImage` — upload / external-URL via render-prop
|
|
168
|
+
|
|
169
|
+
Manages file picking, object URLs, external-URL validation, and the pending-upload
|
|
170
|
+
queue. You render the chrome:
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
import { EditableImage } from "@dalgoridim/headless-cms/client";
|
|
174
|
+
|
|
175
|
+
<EditableImage collection="pages" sectionKey="hero" fieldKey="image"
|
|
176
|
+
docId="hero" src={section.image}>
|
|
177
|
+
{({ isEditing, saving, hasError, openFilePicker, setExternalUrl, imgProps }) => (
|
|
178
|
+
<div className="relative">
|
|
179
|
+
<img {...imgProps} alt="" />
|
|
180
|
+
{isEditing && (
|
|
181
|
+
<button onClick={openFilePicker} disabled={saving}>Replace</button>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
</EditableImage>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Omit the render-prop child and it falls back to a bare unstyled `<img>`.
|
|
189
|
+
|
|
190
|
+
### `useMarkdownEditor` — headless markdown editing
|
|
191
|
+
|
|
192
|
+
State + a selection-aware `insert` command. Bring your own modal, toolbar, icons,
|
|
193
|
+
and preview (e.g. `react-markdown`):
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
import { useMarkdownEditor } from "@dalgoridim/headless-cms/client";
|
|
197
|
+
|
|
198
|
+
function Editor({ initialValue, onSave }) {
|
|
199
|
+
const md = useMarkdownEditor({ initialValue, onSave });
|
|
200
|
+
return (
|
|
201
|
+
<>
|
|
202
|
+
<button onClick={() => md.insert("**", "**", "bold")}>Bold</button>
|
|
203
|
+
<textarea ref={md.textareaRef} value={md.value}
|
|
204
|
+
onChange={(e) => md.setValue(e.target.value)} />
|
|
205
|
+
<span>{md.charCount} chars</span>
|
|
206
|
+
<button onClick={md.save}>Save</button>
|
|
207
|
+
</>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## The Query language
|
|
213
|
+
|
|
214
|
+
A neutral, backend-agnostic query passed to `DataAdapter.fetchCollection`:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
type Query = {
|
|
218
|
+
filters?: (QueryFilter | { or: QueryFilter[] })[]; // top level AND; groups OR
|
|
219
|
+
orderBy?: { field: string; direction: "asc" | "desc" }[];
|
|
220
|
+
limit?: number;
|
|
221
|
+
offset?: number;
|
|
222
|
+
populate?: string[]; // reference fields to inline-resolve (see Relations)
|
|
223
|
+
};
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Operators** (`QueryFilter = { field, op, value }`):
|
|
227
|
+
|
|
228
|
+
| op | meaning | Postgres | Firestore |
|
|
229
|
+
|---|---|:--:|:--:|
|
|
230
|
+
| `eq` / `ne` | equals / not equals | ✅ | ✅ |
|
|
231
|
+
| `lt` `lte` `gt` `gte` | comparisons | ✅ | ✅ |
|
|
232
|
+
| `in` / `nin` | value in / not in array | ✅ | ✅ |
|
|
233
|
+
| `contains` | case-insensitive substring | ✅ | ❌ throws |
|
|
234
|
+
| `{ or: [...] }` | disjunction | ✅ | ❌ throws |
|
|
235
|
+
|
|
236
|
+
Where a backend can't honor an op, it throws a clear error rather than returning
|
|
237
|
+
wrong results.
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
await data.fetchCollection("posts", {
|
|
241
|
+
filters: [
|
|
242
|
+
{ field: "title", op: "contains", value: "hello" },
|
|
243
|
+
{ or: [{ field: "tag", op: "eq", value: "react" },
|
|
244
|
+
{ field: "tag", op: "eq", value: "sql" }] },
|
|
245
|
+
],
|
|
246
|
+
orderBy: [{ field: "createdAt", direction: "desc" }],
|
|
247
|
+
limit: 20,
|
|
248
|
+
offset: 40,
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Relations
|
|
253
|
+
|
|
254
|
+
References are just fields — a self-describing `{ collection, id }`, a bare id
|
|
255
|
+
string, or an array of either. Resolve them after a fetch with `resolveRelations`
|
|
256
|
+
(engine-side, works over any adapter):
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
import { resolveRelations } from "@dalgoridim/headless-cms/server";
|
|
260
|
+
|
|
261
|
+
const posts = await data.fetchCollection("posts");
|
|
262
|
+
await resolveRelations(data, posts, {
|
|
263
|
+
populate: ["authorId"],
|
|
264
|
+
relations: { authorId: { collection: "authors" } }, // needed for bare-id refs
|
|
265
|
+
});
|
|
266
|
+
// each post.authorId is now the resolved author document
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Loads are deduplicated and parallelized; unresolved refs are left untouched.
|
|
126
270
|
|
|
127
271
|
## Postgres (hybrid)
|
|
128
272
|
|
|
@@ -136,35 +280,81 @@ import { PostgresDataAdapter } from "@dalgoridim/headless-cms/adapters/postgres"
|
|
|
136
280
|
const data = new PostgresDataAdapter({
|
|
137
281
|
connectionString: process.env.DATABASE_URL!,
|
|
138
282
|
collections: {
|
|
139
|
-
projects: { table: "projects", columns: { title: "text", date: "date" } },
|
|
283
|
+
projects: { table: "projects", columns: { title: "text", views: "int", date: "date" } },
|
|
140
284
|
},
|
|
141
285
|
});
|
|
142
286
|
await data.migrate(); // creates documents + registered tables if missing
|
|
143
287
|
```
|
|
144
288
|
|
|
145
|
-
|
|
289
|
+
> **Caveat:** fields in the schemaless JSONB `documents` table are extracted as
|
|
290
|
+
> **text**, so numeric ordering/comparison is lexicographic (`"10" < "5"`).
|
|
291
|
+
> Register the collection with a typed `int`/`float` column for true numeric
|
|
292
|
+
> behavior.
|
|
146
293
|
|
|
147
|
-
|
|
148
|
-
`^^primary color^^`, `__underline__`, `~~br~~`, and `[label](https://url)`.
|
|
149
|
-
`MarkdownEditor` provides full GitHub-flavored markdown editing.
|
|
294
|
+
## Auth: the gate is yours
|
|
150
295
|
|
|
151
|
-
|
|
296
|
+
`AuthIdentity` is intentionally minimal and open — only `isAdmin` is meaningful to
|
|
297
|
+
the default gate; carry any other claims and authorize on them yourself.
|
|
152
298
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
299
|
+
```ts
|
|
300
|
+
interface AuthIdentity {
|
|
301
|
+
isAdmin: boolean;
|
|
302
|
+
userId?: string;
|
|
303
|
+
email?: string;
|
|
304
|
+
[claim: string]: unknown; // roles, scopes, tenant, …
|
|
305
|
+
}
|
|
306
|
+
```
|
|
157
307
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
@import "tailwindcss";
|
|
161
|
-
@source "../node_modules/@dalgoridim/headless-cms/dist";
|
|
308
|
+
Pass an `authorize` predicate to gate on whatever you like (defaults to
|
|
309
|
+
`identity.isAdmin === true`):
|
|
162
310
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
311
|
+
```ts
|
|
312
|
+
createCmsHandlers({
|
|
313
|
+
data,
|
|
314
|
+
auth,
|
|
315
|
+
authorize: (identity) => identity.role === "editor" || identity.isAdmin,
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Your content types stay unconstrained
|
|
320
|
+
|
|
321
|
+
The engine only adds addressing fields; your domain type `T` is never forced to
|
|
322
|
+
declare them:
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
import type { Editable } from "@dalgoridim/headless-cms";
|
|
326
|
+
|
|
327
|
+
interface Post { title: string; body: string } // your shape — clean
|
|
328
|
+
type StoredPost = Editable<Post>; // Post & { id: string; collection: string }
|
|
166
329
|
```
|
|
167
330
|
|
|
331
|
+
## Styling: bring your own look
|
|
332
|
+
|
|
333
|
+
The package ships **no styles**. Skin the primitives in your app — for example, a
|
|
334
|
+
thin wrapper that applies your design system:
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
// components/EditableTitle.tsx
|
|
338
|
+
import { ContentEditSpan } from "@dalgoridim/headless-cms/client";
|
|
339
|
+
|
|
340
|
+
export const EditableTitle = (props) => (
|
|
341
|
+
<ContentEditSpan
|
|
342
|
+
{...props}
|
|
343
|
+
className="text-4xl font-bold data-[cms-focused]:ring-2 data-[cms-focused]:ring-blue-500"
|
|
344
|
+
/>
|
|
345
|
+
);
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
This keeps your design isolated from the engine and lets the package upgrade
|
|
349
|
+
without ever changing how your site looks.
|
|
350
|
+
|
|
351
|
+
## Documentation
|
|
352
|
+
|
|
353
|
+
- **[README](./README.md)** — this guide: install and usage.
|
|
354
|
+
- **[ARCHITECTURE.md](./ARCHITECTURE.md)** — design rationale and internals
|
|
355
|
+
(bundle boundaries, backend parity, the hybrid Postgres model, headless UI).
|
|
356
|
+
- **[REDESIGN.md](./REDESIGN.md)** — the "harness, not framework" redesign spec.
|
|
357
|
+
|
|
168
358
|
## Build
|
|
169
359
|
|
|
170
360
|
```bash
|
|
@@ -174,5 +364,5 @@ npm run typecheck # tsc --noEmit
|
|
|
174
364
|
|
|
175
365
|
## Stability
|
|
176
366
|
|
|
177
|
-
Pre-`1.0`: the API may change between minor versions as the engine evolves.
|
|
178
|
-
|
|
367
|
+
Pre-`1.0`: the API may change between minor versions as the engine evolves. Pin an
|
|
368
|
+
exact version if you depend on it in production.
|