@flightdev/cms 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/LICENSE +21 -0
- package/README.md +188 -0
- package/dist/adapters/contentful.d.ts +51 -0
- package/dist/adapters/contentful.js +182 -0
- package/dist/adapters/contentful.js.map +1 -0
- package/dist/adapters/sanity.d.ts +73 -0
- package/dist/adapters/sanity.js +206 -0
- package/dist/adapters/sanity.js.map +1 -0
- package/dist/adapters/strapi.d.ts +47 -0
- package/dist/adapters/strapi.js +178 -0
- package/dist/adapters/strapi.js.map +1 -0
- package/dist/index.d.ts +221 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.d.ts +165 -0
- package/dist/react/index.js +175 -0
- package/dist/react/index.js.map +1 -0
- package/dist/vue/index.d.ts +123 -0
- package/dist/vue/index.js +201 -0
- package/dist/vue/index.js.map +1 -0
- package/package.json +78 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Flight Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# @flight-framework/cms
|
|
2
|
+
|
|
3
|
+
Unified CMS adapters for Flight Framework. One API for Strapi, Contentful, Sanity, and more.
|
|
4
|
+
|
|
5
|
+
## Philosophy
|
|
6
|
+
|
|
7
|
+
**Flight doesn't impose** - you choose your CMS. All adapters are optional, swap providers without changing your code.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Adapter pattern** - Same API for any CMS
|
|
12
|
+
- **Zero lock-in** - Switch CMS providers without code changes
|
|
13
|
+
- **React hooks** - useCMSQuery, useCMSOne, mutations
|
|
14
|
+
- **Vue composables** - Reactive queries with auto-refetch
|
|
15
|
+
- **Full CRUD** - Read, create, update, delete
|
|
16
|
+
- **i18n support** - Locale-aware queries
|
|
17
|
+
- **Preview mode** - Draft content for editors
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @flight-framework/cms
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { createCMS } from '@flight-framework/cms';
|
|
29
|
+
import { strapi } from '@flight-framework/cms/strapi';
|
|
30
|
+
|
|
31
|
+
const cms = createCMS(strapi({
|
|
32
|
+
url: process.env.STRAPI_URL,
|
|
33
|
+
token: process.env.STRAPI_TOKEN,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
// Query posts
|
|
37
|
+
const { data: posts, meta } = await cms.findMany('posts', {
|
|
38
|
+
limit: 10,
|
|
39
|
+
sort: { publishedAt: 'desc' },
|
|
40
|
+
populate: ['author', 'cover'],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Get single post
|
|
44
|
+
const post = await cms.findOne('posts', {
|
|
45
|
+
where: { slug: 'hello-world' },
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Adapters
|
|
50
|
+
|
|
51
|
+
### Strapi
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { strapi } from '@flight-framework/cms/strapi';
|
|
55
|
+
|
|
56
|
+
const adapter = strapi({
|
|
57
|
+
url: 'http://localhost:1337',
|
|
58
|
+
token: 'your-api-token',
|
|
59
|
+
preview: true, // Enable draft mode
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Contentful
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { contentful } from '@flight-framework/cms/contentful';
|
|
67
|
+
|
|
68
|
+
const adapter = contentful({
|
|
69
|
+
spaceId: 'your-space-id',
|
|
70
|
+
accessToken: 'your-access-token',
|
|
71
|
+
environment: 'master',
|
|
72
|
+
preview: true,
|
|
73
|
+
previewToken: 'preview-token',
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Sanity
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { sanity } from '@flight-framework/cms/sanity';
|
|
81
|
+
|
|
82
|
+
const adapter = sanity({
|
|
83
|
+
projectId: 'your-project-id',
|
|
84
|
+
dataset: 'production',
|
|
85
|
+
token: 'your-token', // Optional for public datasets
|
|
86
|
+
useCdn: true,
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## React Integration
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { CMSProvider, useCMSQuery, useCMSOne } from '@flight-framework/cms/react';
|
|
94
|
+
|
|
95
|
+
// App
|
|
96
|
+
function App() {
|
|
97
|
+
return (
|
|
98
|
+
<CMSProvider cms={cms}>
|
|
99
|
+
<PostList />
|
|
100
|
+
</CMSProvider>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Query many
|
|
105
|
+
function PostList() {
|
|
106
|
+
const { data: posts, loading, meta, refetch } = useCMSQuery('posts', {
|
|
107
|
+
limit: 10,
|
|
108
|
+
populate: ['author'],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (loading) return <Skeleton />;
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<>
|
|
115
|
+
{posts.map(post => <PostCard key={post.id} post={post} />)}
|
|
116
|
+
<p>Total: {meta?.total}</p>
|
|
117
|
+
</>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Query one
|
|
122
|
+
function PostPage({ slug }) {
|
|
123
|
+
const { data: post, loading, error } = useCMSOne('posts', {
|
|
124
|
+
where: { slug },
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (loading) return <Skeleton />;
|
|
128
|
+
if (!post) return <NotFound />;
|
|
129
|
+
|
|
130
|
+
return <Post post={post} />;
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Vue Integration
|
|
135
|
+
|
|
136
|
+
```vue
|
|
137
|
+
<script setup>
|
|
138
|
+
import { provideCMS, useCMSQuery } from '@flight-framework/cms/vue';
|
|
139
|
+
|
|
140
|
+
// Provide CMS in root component
|
|
141
|
+
provideCMS(cms);
|
|
142
|
+
|
|
143
|
+
// Query posts
|
|
144
|
+
const { data: posts, loading, meta } = useCMSQuery('posts', {
|
|
145
|
+
limit: 10,
|
|
146
|
+
sort: { publishedAt: 'desc' },
|
|
147
|
+
});
|
|
148
|
+
</script>
|
|
149
|
+
|
|
150
|
+
<template>
|
|
151
|
+
<div v-if="loading">Loading...</div>
|
|
152
|
+
<PostGrid v-else :posts="posts" />
|
|
153
|
+
</template>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## API Reference
|
|
157
|
+
|
|
158
|
+
### Query Options
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
interface FindManyOptions {
|
|
162
|
+
where?: Record<string, unknown>; // Filter conditions
|
|
163
|
+
populate?: string[]; // Relations to include
|
|
164
|
+
limit?: number; // Max results
|
|
165
|
+
offset?: number; // Skip results
|
|
166
|
+
page?: number; // Page number
|
|
167
|
+
pageSize?: number; // Items per page
|
|
168
|
+
sort?: Record<string, 'asc' | 'desc'>; // Sort order
|
|
169
|
+
fields?: string[]; // Select fields
|
|
170
|
+
locale?: string; // Content locale
|
|
171
|
+
preview?: boolean; // Draft mode
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### CMS Methods
|
|
176
|
+
|
|
177
|
+
| Method | Description |
|
|
178
|
+
|--------|-------------|
|
|
179
|
+
| `findOne(collection, options)` | Get single entity |
|
|
180
|
+
| `findMany(collection, options)` | Get multiple with pagination |
|
|
181
|
+
| `findById(collection, id, options)` | Get by ID |
|
|
182
|
+
| `create(collection, data)` | Create entity |
|
|
183
|
+
| `update(collection, id, data)` | Update entity |
|
|
184
|
+
| `delete(collection, id)` | Delete entity |
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Adapter as CMSAdapter } from '../index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Contentful CMS Adapter
|
|
5
|
+
*
|
|
6
|
+
* Integration with Contentful Content Delivery API.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { createCMS } from '@flightdev/cms';
|
|
11
|
+
* import { contentful } from '@flightdev/cms/contentful';
|
|
12
|
+
*
|
|
13
|
+
* const cms = createCMS(contentful({
|
|
14
|
+
* spaceId: process.env.CONTENTFUL_SPACE_ID,
|
|
15
|
+
* accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
|
|
16
|
+
* }));
|
|
17
|
+
*
|
|
18
|
+
* const posts = await cms.findMany('blogPost', {
|
|
19
|
+
* limit: 10,
|
|
20
|
+
* sort: { 'fields.publishDate': 'desc' },
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
interface ContentfulConfig {
|
|
26
|
+
/** Contentful Space ID */
|
|
27
|
+
spaceId: string;
|
|
28
|
+
/** Content Delivery API access token */
|
|
29
|
+
accessToken: string;
|
|
30
|
+
/** Environment (default: master) */
|
|
31
|
+
environment?: string;
|
|
32
|
+
/** Use Preview API for draft content */
|
|
33
|
+
preview?: boolean;
|
|
34
|
+
/** Preview API access token (required if preview is true) */
|
|
35
|
+
previewToken?: string;
|
|
36
|
+
/** API host (default: cdn.contentful.com) */
|
|
37
|
+
host?: string;
|
|
38
|
+
/** Request timeout in ms */
|
|
39
|
+
timeout?: number;
|
|
40
|
+
/** Custom fetch function */
|
|
41
|
+
fetch?: typeof fetch;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Create a Contentful CMS adapter.
|
|
45
|
+
*
|
|
46
|
+
* @param config - Contentful configuration
|
|
47
|
+
* @returns CMS adapter instance
|
|
48
|
+
*/
|
|
49
|
+
declare function contentful(config: ContentfulConfig): CMSAdapter;
|
|
50
|
+
|
|
51
|
+
export { type ContentfulConfig, contentful };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// src/adapters/contentful.ts
|
|
2
|
+
function contentful(config) {
|
|
3
|
+
const {
|
|
4
|
+
spaceId,
|
|
5
|
+
accessToken,
|
|
6
|
+
environment = "master",
|
|
7
|
+
preview = false,
|
|
8
|
+
previewToken,
|
|
9
|
+
host = preview ? "preview.contentful.com" : "cdn.contentful.com",
|
|
10
|
+
timeout = 3e4,
|
|
11
|
+
fetch: customFetch = globalThis.fetch
|
|
12
|
+
} = config;
|
|
13
|
+
const token = preview && previewToken ? previewToken : accessToken;
|
|
14
|
+
const baseUrl = `https://${host}/spaces/${spaceId}/environments/${environment}`;
|
|
15
|
+
async function request(path) {
|
|
16
|
+
const controller = new AbortController();
|
|
17
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
18
|
+
try {
|
|
19
|
+
const response = await customFetch(`${baseUrl}${path}`, {
|
|
20
|
+
headers: {
|
|
21
|
+
"Authorization": `Bearer ${token}`,
|
|
22
|
+
"Content-Type": "application/json"
|
|
23
|
+
},
|
|
24
|
+
signal: controller.signal
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
const error = await response.json().catch(() => ({}));
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Contentful error: ${response.status} - ${error.message || response.statusText}`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return response.json();
|
|
33
|
+
} finally {
|
|
34
|
+
clearTimeout(timeoutId);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function buildQuery(contentType, options) {
|
|
38
|
+
const params = new URLSearchParams();
|
|
39
|
+
params.append("content_type", contentType);
|
|
40
|
+
if (options) {
|
|
41
|
+
if (options.where) {
|
|
42
|
+
for (const [key, value] of Object.entries(options.where)) {
|
|
43
|
+
if (value !== void 0 && value !== null) {
|
|
44
|
+
const fieldKey = key.startsWith("fields.") ? key : `fields.${key}`;
|
|
45
|
+
params.append(fieldKey, String(value));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (options.limit !== void 0) {
|
|
50
|
+
params.append("limit", String(options.limit));
|
|
51
|
+
}
|
|
52
|
+
if (options.offset !== void 0) {
|
|
53
|
+
params.append("skip", String(options.offset));
|
|
54
|
+
}
|
|
55
|
+
if (options.page !== void 0 && options.pageSize !== void 0) {
|
|
56
|
+
params.append("skip", String((options.page - 1) * options.pageSize));
|
|
57
|
+
params.append("limit", String(options.pageSize));
|
|
58
|
+
}
|
|
59
|
+
if (options.sort) {
|
|
60
|
+
if (Array.isArray(options.sort)) {
|
|
61
|
+
params.append("order", options.sort.join(","));
|
|
62
|
+
} else {
|
|
63
|
+
const sortFields = Object.entries(options.sort).map(([field, order]) => `${order === "desc" ? "-" : ""}${field}`).join(",");
|
|
64
|
+
params.append("order", sortFields);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (options.fields) {
|
|
68
|
+
params.append("select", options.fields.map((f) => `fields.${f}`).join(","));
|
|
69
|
+
}
|
|
70
|
+
if (options.locale) {
|
|
71
|
+
params.append("locale", options.locale);
|
|
72
|
+
}
|
|
73
|
+
if (options.populate) {
|
|
74
|
+
params.append("include", "2");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return `?${params.toString()}`;
|
|
78
|
+
}
|
|
79
|
+
function transformEntry(entry, includes) {
|
|
80
|
+
const result = {
|
|
81
|
+
id: entry.sys.id,
|
|
82
|
+
contentType: entry.sys.contentType?.sys.id,
|
|
83
|
+
createdAt: entry.sys.createdAt,
|
|
84
|
+
updatedAt: entry.sys.updatedAt
|
|
85
|
+
};
|
|
86
|
+
for (const [key, value] of Object.entries(entry.fields)) {
|
|
87
|
+
result[key] = resolveLinks(value, includes);
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
function resolveLinks(value, includes) {
|
|
92
|
+
if (!value || typeof value !== "object") {
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
if ("sys" in value && value.sys.type === "Link") {
|
|
96
|
+
const link = value;
|
|
97
|
+
if (link.sys.linkType === "Entry" && includes?.Entry) {
|
|
98
|
+
const entry = includes.Entry.find((e) => e.sys.id === link.sys.id);
|
|
99
|
+
if (entry) {
|
|
100
|
+
return transformEntry(entry, includes);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (link.sys.linkType === "Asset" && includes?.Asset) {
|
|
104
|
+
const asset = includes.Asset.find((a) => a.sys.id === link.sys.id);
|
|
105
|
+
if (asset) {
|
|
106
|
+
return transformAsset(asset);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
if (Array.isArray(value)) {
|
|
112
|
+
return value.map((item) => resolveLinks(item, includes));
|
|
113
|
+
}
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
function transformAsset(asset) {
|
|
117
|
+
const file = asset.fields.file;
|
|
118
|
+
return {
|
|
119
|
+
id: asset.sys.id,
|
|
120
|
+
title: asset.fields.title,
|
|
121
|
+
description: asset.fields.description,
|
|
122
|
+
url: file?.url ? `https:${file.url}` : void 0,
|
|
123
|
+
width: file?.details?.image?.width,
|
|
124
|
+
height: file?.details?.image?.height,
|
|
125
|
+
size: file?.details?.size,
|
|
126
|
+
mime: file?.contentType
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
name: "contentful",
|
|
131
|
+
async findOne(collection, options) {
|
|
132
|
+
const query = buildQuery(collection, { ...options, limit: 1 });
|
|
133
|
+
const response = await request(`/entries${query}`);
|
|
134
|
+
if (!response.items || response.items.length === 0) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
return transformEntry(response.items[0], response.includes);
|
|
138
|
+
},
|
|
139
|
+
async findMany(collection, options) {
|
|
140
|
+
const query = buildQuery(collection, options);
|
|
141
|
+
const response = await request(`/entries${query}`);
|
|
142
|
+
return {
|
|
143
|
+
data: response.items.map((entry) => transformEntry(entry, response.includes)),
|
|
144
|
+
meta: {
|
|
145
|
+
total: response.total,
|
|
146
|
+
page: Math.floor(response.skip / response.limit) + 1,
|
|
147
|
+
pageSize: response.limit,
|
|
148
|
+
pageCount: Math.ceil(response.total / response.limit)
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
async findById(collection, id, options) {
|
|
153
|
+
const params = new URLSearchParams();
|
|
154
|
+
if (options?.locale) {
|
|
155
|
+
params.append("locale", options.locale);
|
|
156
|
+
}
|
|
157
|
+
if (options?.populate) {
|
|
158
|
+
params.append("include", "2");
|
|
159
|
+
}
|
|
160
|
+
const queryString = params.toString();
|
|
161
|
+
const query = queryString ? `?${queryString}` : "";
|
|
162
|
+
try {
|
|
163
|
+
const response = await request(`/entries/${id}${query}`);
|
|
164
|
+
return transformEntry(response);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (error.message.includes("404")) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
// Contentful CDA is read-only
|
|
173
|
+
// Use Management API for write operations
|
|
174
|
+
getClient() {
|
|
175
|
+
return { request, baseUrl };
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export { contentful };
|
|
181
|
+
//# sourceMappingURL=contentful.js.map
|
|
182
|
+
//# sourceMappingURL=contentful.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/contentful.ts"],"names":[],"mappings":"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","file":"contentful.js","sourcesContent":["/**\r\n * Contentful CMS Adapter\r\n * \r\n * Integration with Contentful Content Delivery API.\r\n * \r\n * @example\r\n * ```typescript\r\n * import { createCMS } from '@flightdev/cms';\r\n * import { contentful } from '@flightdev/cms/contentful';\r\n * \r\n * const cms = createCMS(contentful({\r\n * spaceId: process.env.CONTENTFUL_SPACE_ID,\r\n * accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,\r\n * }));\r\n * \r\n * const posts = await cms.findMany('blogPost', {\r\n * limit: 10,\r\n * sort: { 'fields.publishDate': 'desc' },\r\n * });\r\n * ```\r\n */\r\n\r\nimport type { CMSAdapter, CMSResult, FindOneOptions, FindManyOptions } from '../index';\r\n\r\n// =============================================================================\r\n// Types\r\n// =============================================================================\r\n\r\nexport interface ContentfulConfig {\r\n /** Contentful Space ID */\r\n spaceId: string;\r\n /** Content Delivery API access token */\r\n accessToken: string;\r\n /** Environment (default: master) */\r\n environment?: string;\r\n /** Use Preview API for draft content */\r\n preview?: boolean;\r\n /** Preview API access token (required if preview is true) */\r\n previewToken?: string;\r\n /** API host (default: cdn.contentful.com) */\r\n host?: string;\r\n /** Request timeout in ms */\r\n timeout?: number;\r\n /** Custom fetch function */\r\n fetch?: typeof fetch;\r\n}\r\n\r\ninterface ContentfulEntry {\r\n sys: {\r\n id: string;\r\n type: string;\r\n contentType?: {\r\n sys: { id: string };\r\n };\r\n createdAt: string;\r\n updatedAt: string;\r\n };\r\n fields: Record<string, unknown>;\r\n}\r\n\r\ninterface ContentfulResponse {\r\n sys: { type: string };\r\n total: number;\r\n skip: number;\r\n limit: number;\r\n items: ContentfulEntry[];\r\n includes?: {\r\n Entry?: ContentfulEntry[];\r\n Asset?: ContentfulAsset[];\r\n };\r\n}\r\n\r\ninterface ContentfulAsset {\r\n sys: { id: string };\r\n fields: {\r\n title?: string;\r\n description?: string;\r\n file?: {\r\n url: string;\r\n details?: {\r\n size: number;\r\n image?: { width: number; height: number };\r\n };\r\n contentType?: string;\r\n };\r\n };\r\n}\r\n\r\n// =============================================================================\r\n// Adapter Implementation\r\n// =============================================================================\r\n\r\n/**\r\n * Create a Contentful CMS adapter.\r\n * \r\n * @param config - Contentful configuration\r\n * @returns CMS adapter instance\r\n */\r\nexport function contentful(config: ContentfulConfig): CMSAdapter {\r\n const {\r\n spaceId,\r\n accessToken,\r\n environment = 'master',\r\n preview = false,\r\n previewToken,\r\n host = preview ? 'preview.contentful.com' : 'cdn.contentful.com',\r\n timeout = 30000,\r\n fetch: customFetch = globalThis.fetch,\r\n } = config;\r\n\r\n const token = preview && previewToken ? previewToken : accessToken;\r\n const baseUrl = `https://${host}/spaces/${spaceId}/environments/${environment}`;\r\n\r\n /**\r\n * Make a request to Contentful API.\r\n */\r\n async function request<T>(path: string): Promise<T> {\r\n const controller = new AbortController();\r\n const timeoutId = setTimeout(() => controller.abort(), timeout);\r\n\r\n try {\r\n const response = await customFetch(`${baseUrl}${path}`, {\r\n headers: {\r\n 'Authorization': `Bearer ${token}`,\r\n 'Content-Type': 'application/json',\r\n },\r\n signal: controller.signal,\r\n });\r\n\r\n if (!response.ok) {\r\n const error = await response.json().catch(() => ({}));\r\n throw new Error(\r\n `Contentful error: ${response.status} - ${error.message || response.statusText}`\r\n );\r\n }\r\n\r\n return response.json();\r\n } finally {\r\n clearTimeout(timeoutId);\r\n }\r\n }\r\n\r\n /**\r\n * Build query string from options.\r\n */\r\n function buildQuery(\r\n contentType: string,\r\n options?: FindOneOptions & FindManyOptions\r\n ): string {\r\n const params = new URLSearchParams();\r\n\r\n params.append('content_type', contentType);\r\n\r\n if (options) {\r\n // Filters (where)\r\n if (options.where) {\r\n for (const [key, value] of Object.entries(options.where)) {\r\n if (value !== undefined && value !== null) {\r\n // Support nested field syntax\r\n const fieldKey = key.startsWith('fields.') ? key : `fields.${key}`;\r\n params.append(fieldKey, String(value));\r\n }\r\n }\r\n }\r\n\r\n // Pagination\r\n if (options.limit !== undefined) {\r\n params.append('limit', String(options.limit));\r\n }\r\n if (options.offset !== undefined) {\r\n params.append('skip', String(options.offset));\r\n }\r\n if (options.page !== undefined && options.pageSize !== undefined) {\r\n params.append('skip', String((options.page - 1) * options.pageSize));\r\n params.append('limit', String(options.pageSize));\r\n }\r\n\r\n // Sort\r\n if (options.sort) {\r\n if (Array.isArray(options.sort)) {\r\n params.append('order', options.sort.join(','));\r\n } else {\r\n const sortFields = Object.entries(options.sort)\r\n .map(([field, order]) => `${order === 'desc' ? '-' : ''}${field}`)\r\n .join(',');\r\n params.append('order', sortFields);\r\n }\r\n }\r\n\r\n // Fields selection\r\n if (options.fields) {\r\n params.append('select', options.fields.map(f => `fields.${f}`).join(','));\r\n }\r\n\r\n // Locale\r\n if (options.locale) {\r\n params.append('locale', options.locale);\r\n }\r\n\r\n // Include linked entries (populate)\r\n if (options.populate) {\r\n params.append('include', '2'); // 2 levels of linked entries\r\n }\r\n }\r\n\r\n return `?${params.toString()}`;\r\n }\r\n\r\n /**\r\n * Transform Contentful entry to normalized format.\r\n */\r\n function transformEntry<T>(\r\n entry: ContentfulEntry,\r\n includes?: ContentfulResponse['includes']\r\n ): T {\r\n const result: Record<string, unknown> = {\r\n id: entry.sys.id,\r\n contentType: entry.sys.contentType?.sys.id,\r\n createdAt: entry.sys.createdAt,\r\n updatedAt: entry.sys.updatedAt,\r\n };\r\n\r\n // Transform fields\r\n for (const [key, value] of Object.entries(entry.fields)) {\r\n result[key] = resolveLinks(value, includes);\r\n }\r\n\r\n return result as T;\r\n }\r\n\r\n /**\r\n * Resolve linked entries and assets.\r\n */\r\n function resolveLinks(\r\n value: unknown,\r\n includes?: ContentfulResponse['includes']\r\n ): unknown {\r\n if (!value || typeof value !== 'object') {\r\n return value;\r\n }\r\n\r\n // Check if it's a link\r\n if ('sys' in value && (value as any).sys.type === 'Link') {\r\n const link = value as { sys: { linkType: string; id: string } };\r\n\r\n if (link.sys.linkType === 'Entry' && includes?.Entry) {\r\n const entry = includes.Entry.find(e => e.sys.id === link.sys.id);\r\n if (entry) {\r\n return transformEntry(entry, includes);\r\n }\r\n }\r\n\r\n if (link.sys.linkType === 'Asset' && includes?.Asset) {\r\n const asset = includes.Asset.find(a => a.sys.id === link.sys.id);\r\n if (asset) {\r\n return transformAsset(asset);\r\n }\r\n }\r\n\r\n return null; // Unresolved link\r\n }\r\n\r\n // Recursively resolve arrays\r\n if (Array.isArray(value)) {\r\n return value.map(item => resolveLinks(item, includes));\r\n }\r\n\r\n return value;\r\n }\r\n\r\n /**\r\n * Transform Contentful asset to normalized format.\r\n */\r\n function transformAsset(asset: ContentfulAsset) {\r\n const file = asset.fields.file;\r\n return {\r\n id: asset.sys.id,\r\n title: asset.fields.title,\r\n description: asset.fields.description,\r\n url: file?.url ? `https:${file.url}` : undefined,\r\n width: file?.details?.image?.width,\r\n height: file?.details?.image?.height,\r\n size: file?.details?.size,\r\n mime: file?.contentType,\r\n };\r\n }\r\n\r\n return {\r\n name: 'contentful',\r\n\r\n async findOne<T>(collection: string, options?: FindOneOptions): Promise<T | null> {\r\n const query = buildQuery(collection, { ...options, limit: 1 });\r\n const response = await request<ContentfulResponse>(`/entries${query}`);\r\n\r\n if (!response.items || response.items.length === 0) {\r\n return null;\r\n }\r\n\r\n return transformEntry<T>(response.items[0], response.includes);\r\n },\r\n\r\n async findMany<T>(collection: string, options?: FindManyOptions): Promise<CMSResult<T>> {\r\n const query = buildQuery(collection, options);\r\n const response = await request<ContentfulResponse>(`/entries${query}`);\r\n\r\n return {\r\n data: response.items.map(entry => transformEntry<T>(entry, response.includes)),\r\n meta: {\r\n total: response.total,\r\n page: Math.floor(response.skip / response.limit) + 1,\r\n pageSize: response.limit,\r\n pageCount: Math.ceil(response.total / response.limit),\r\n },\r\n };\r\n },\r\n\r\n async findById<T>(\r\n collection: string,\r\n id: string | number,\r\n options?: Omit<FindOneOptions, 'where'>\r\n ): Promise<T | null> {\r\n const params = new URLSearchParams();\r\n\r\n if (options?.locale) {\r\n params.append('locale', options.locale);\r\n }\r\n if (options?.populate) {\r\n params.append('include', '2');\r\n }\r\n\r\n const queryString = params.toString();\r\n const query = queryString ? `?${queryString}` : '';\r\n\r\n try {\r\n const response = await request<ContentfulEntry>(`/entries/${id}${query}`);\r\n return transformEntry<T>(response);\r\n } catch (error) {\r\n if ((error as Error).message.includes('404')) {\r\n return null;\r\n }\r\n throw error;\r\n }\r\n },\r\n\r\n // Contentful CDA is read-only\r\n // Use Management API for write operations\r\n\r\n getClient() {\r\n return { request, baseUrl };\r\n },\r\n };\r\n}\r\n"]}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Adapter as CMSAdapter } from '../index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sanity CMS Adapter
|
|
5
|
+
*
|
|
6
|
+
* Integration with Sanity.io using GROQ queries.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { createCMS } from '@flightdev/cms';
|
|
11
|
+
* import { sanity } from '@flightdev/cms/sanity';
|
|
12
|
+
*
|
|
13
|
+
* const cms = createCMS(sanity({
|
|
14
|
+
* projectId: process.env.SANITY_PROJECT_ID,
|
|
15
|
+
* dataset: 'production',
|
|
16
|
+
* token: process.env.SANITY_TOKEN, // Optional for public datasets
|
|
17
|
+
* }));
|
|
18
|
+
*
|
|
19
|
+
* const posts = await cms.findMany('post', {
|
|
20
|
+
* limit: 10,
|
|
21
|
+
* sort: { publishedAt: 'desc' },
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
interface SanityConfig {
|
|
27
|
+
/** Sanity Project ID */
|
|
28
|
+
projectId: string;
|
|
29
|
+
/** Dataset name (default: production) */
|
|
30
|
+
dataset?: string;
|
|
31
|
+
/** API Token (for private datasets or mutations) */
|
|
32
|
+
token?: string;
|
|
33
|
+
/** API version (default: v2024-01-01) */
|
|
34
|
+
apiVersion?: string;
|
|
35
|
+
/** Use CDN for faster reads (default: true) */
|
|
36
|
+
useCdn?: boolean;
|
|
37
|
+
/** Enable draft preview */
|
|
38
|
+
preview?: boolean;
|
|
39
|
+
/** Request timeout in ms */
|
|
40
|
+
timeout?: number;
|
|
41
|
+
/** Custom fetch function */
|
|
42
|
+
fetch?: typeof fetch;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Create a Sanity CMS adapter.
|
|
46
|
+
*
|
|
47
|
+
* @param config - Sanity configuration
|
|
48
|
+
* @returns CMS adapter instance
|
|
49
|
+
*/
|
|
50
|
+
declare function sanity(config: SanityConfig): CMSAdapter;
|
|
51
|
+
/**
|
|
52
|
+
* Helper to build GROQ queries manually.
|
|
53
|
+
* Use when you need more control than the standard adapter methods.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* import { sanity, groq } from '@flightdev/cms/sanity';
|
|
58
|
+
*
|
|
59
|
+
* const cms = createCMS(sanity({ projectId: '...' }));
|
|
60
|
+
* const client = cms.getClient() as { query: Function };
|
|
61
|
+
*
|
|
62
|
+
* const posts = await client.query(groq`
|
|
63
|
+
* *[_type == "post" && publishedAt < now()] | order(publishedAt desc) {
|
|
64
|
+
* title,
|
|
65
|
+
* slug,
|
|
66
|
+
* "author": author->name
|
|
67
|
+
* }
|
|
68
|
+
* `);
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
declare function groq(strings: TemplateStringsArray, ...values: unknown[]): string;
|
|
72
|
+
|
|
73
|
+
export { type SanityConfig, groq, sanity };
|