@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 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 };