@sleekcms/client 0.1.2 → 0.1.5
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/index.d.cts +60 -0
- package/index.d.ts +60 -0
- package/package.json +18 -10
- package/__tests__/index.test.ts +0 -355
- package/src/index.ts +0 -260
- package/src/types.ts +0 -20
- package/vitest.config.ts +0 -8
- /package/{dist/index.cjs → index.cjs} +0 -0
package/index.d.cts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
type Entry = Record<string, unknown>;
|
|
2
|
+
type Page = {
|
|
3
|
+
_path: string;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
};
|
|
6
|
+
type Image = {
|
|
7
|
+
url: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
};
|
|
10
|
+
type List = Array<{
|
|
11
|
+
label: string;
|
|
12
|
+
value: string;
|
|
13
|
+
}>;
|
|
14
|
+
interface SleekSiteContent {
|
|
15
|
+
entries?: Record<string, Entry> | Record<string, Entry[]>;
|
|
16
|
+
pages?: Array<Page>;
|
|
17
|
+
images?: Record<string, Image>;
|
|
18
|
+
lists?: Record<string, List>;
|
|
19
|
+
config?: {
|
|
20
|
+
title?: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
interface ClientOptions {
|
|
24
|
+
siteToken: string;
|
|
25
|
+
env?: string;
|
|
26
|
+
cache?: boolean;
|
|
27
|
+
mock?: boolean;
|
|
28
|
+
resolveEnv?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Async SleekCMS client: methods return Promises.
|
|
33
|
+
*/
|
|
34
|
+
interface SleekClient {
|
|
35
|
+
getContent<T = SleekSiteContent>(query?: string): Promise<T>;
|
|
36
|
+
findPages<T = unknown>(path: string, query?: string): Promise<T>;
|
|
37
|
+
getImages(): Promise<SleekSiteContent["images"]>;
|
|
38
|
+
getImage(name: string): Promise<unknown | undefined>;
|
|
39
|
+
getList<T = unknown>(name: string): Promise<T[] | undefined>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Sync client: prefetches full content once; subsequent calls are in-memory only.
|
|
43
|
+
*/
|
|
44
|
+
interface SleekSyncClient {
|
|
45
|
+
getContent<T = SleekSiteContent>(query?: string): T;
|
|
46
|
+
findPages<T = unknown>(path: string, query?: string): T;
|
|
47
|
+
getImages(): SleekSiteContent["images"];
|
|
48
|
+
getImage(name: string): unknown | undefined;
|
|
49
|
+
getList<T = unknown>(name: string): T[] | undefined;
|
|
50
|
+
}
|
|
51
|
+
declare function createClient(options: ClientOptions): SleekClient;
|
|
52
|
+
/**
|
|
53
|
+
* Create a sync SleekCMS client.
|
|
54
|
+
*
|
|
55
|
+
* - Prefetches full content once (no search=).
|
|
56
|
+
* - All operations (including JMESPath) are local and synchronous.
|
|
57
|
+
*/
|
|
58
|
+
declare function createSyncClient(options: ClientOptions): Promise<SleekSyncClient>;
|
|
59
|
+
|
|
60
|
+
export { type ClientOptions, type SleekClient, type SleekSiteContent, type SleekSyncClient, createClient, createSyncClient };
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
type Entry = Record<string, unknown>;
|
|
2
|
+
type Page = {
|
|
3
|
+
_path: string;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
};
|
|
6
|
+
type Image = {
|
|
7
|
+
url: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
};
|
|
10
|
+
type List = Array<{
|
|
11
|
+
label: string;
|
|
12
|
+
value: string;
|
|
13
|
+
}>;
|
|
14
|
+
interface SleekSiteContent {
|
|
15
|
+
entries?: Record<string, Entry> | Record<string, Entry[]>;
|
|
16
|
+
pages?: Array<Page>;
|
|
17
|
+
images?: Record<string, Image>;
|
|
18
|
+
lists?: Record<string, List>;
|
|
19
|
+
config?: {
|
|
20
|
+
title?: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
interface ClientOptions {
|
|
24
|
+
siteToken: string;
|
|
25
|
+
env?: string;
|
|
26
|
+
cache?: boolean;
|
|
27
|
+
mock?: boolean;
|
|
28
|
+
resolveEnv?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Async SleekCMS client: methods return Promises.
|
|
33
|
+
*/
|
|
34
|
+
interface SleekClient {
|
|
35
|
+
getContent<T = SleekSiteContent>(query?: string): Promise<T>;
|
|
36
|
+
findPages<T = unknown>(path: string, query?: string): Promise<T>;
|
|
37
|
+
getImages(): Promise<SleekSiteContent["images"]>;
|
|
38
|
+
getImage(name: string): Promise<unknown | undefined>;
|
|
39
|
+
getList<T = unknown>(name: string): Promise<T[] | undefined>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Sync client: prefetches full content once; subsequent calls are in-memory only.
|
|
43
|
+
*/
|
|
44
|
+
interface SleekSyncClient {
|
|
45
|
+
getContent<T = SleekSiteContent>(query?: string): T;
|
|
46
|
+
findPages<T = unknown>(path: string, query?: string): T;
|
|
47
|
+
getImages(): SleekSiteContent["images"];
|
|
48
|
+
getImage(name: string): unknown | undefined;
|
|
49
|
+
getList<T = unknown>(name: string): T[] | undefined;
|
|
50
|
+
}
|
|
51
|
+
declare function createClient(options: ClientOptions): SleekClient;
|
|
52
|
+
/**
|
|
53
|
+
* Create a sync SleekCMS client.
|
|
54
|
+
*
|
|
55
|
+
* - Prefetches full content once (no search=).
|
|
56
|
+
* - All operations (including JMESPath) are local and synchronous.
|
|
57
|
+
*/
|
|
58
|
+
declare function createSyncClient(options: ClientOptions): Promise<SleekSyncClient>;
|
|
59
|
+
|
|
60
|
+
export { type ClientOptions, type SleekClient, type SleekSiteContent, type SleekSyncClient, createClient, createSyncClient };
|
package/package.json
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sleekcms/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Official SleekCMS content client for Node 18+ and browser",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "
|
|
7
|
-
"module": "
|
|
8
|
-
"types": "
|
|
6
|
+
"main": "index.cjs",
|
|
7
|
+
"module": "index.mjs",
|
|
8
|
+
"types": "index.d.ts",
|
|
9
|
+
"private": false,
|
|
9
10
|
"exports": {
|
|
10
11
|
".": {
|
|
11
|
-
"types": "./
|
|
12
|
-
"import": "./
|
|
13
|
-
"require": "./
|
|
12
|
+
"types": "./index.d.ts",
|
|
13
|
+
"import": "./index.mjs",
|
|
14
|
+
"require": "./index.cjs"
|
|
14
15
|
}
|
|
15
16
|
},
|
|
16
17
|
"scripts": {
|
|
17
|
-
"build": "tsup src/index.ts --dts --format esm,cjs",
|
|
18
|
+
"build": "npm run clean && tsup src/index.ts --dts --format esm,cjs && cp README.md dist/ && jq '.private = false | .files = [\"index.cjs\", \"index.mjs\", \"index.d.ts\", \"index.d.cts\", \"README.md\"]' package.json > dist/package.json",
|
|
18
19
|
"clean": "rimraf dist || true",
|
|
19
20
|
"test": "vitest run",
|
|
20
21
|
"test:watch": "vitest",
|
|
21
|
-
"
|
|
22
|
+
"publish:dist": "cd dist && npm publish --access public"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
25
|
"jmespath": "^0.16.0"
|
|
@@ -34,5 +35,12 @@
|
|
|
34
35
|
"tsup": "^8.5.1",
|
|
35
36
|
"typescript": "^5.4.0",
|
|
36
37
|
"vitest": "^4.0.15"
|
|
37
|
-
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"index.cjs",
|
|
41
|
+
"index.mjs",
|
|
42
|
+
"index.d.ts",
|
|
43
|
+
"index.d.cts",
|
|
44
|
+
"README.md"
|
|
45
|
+
]
|
|
38
46
|
}
|
package/__tests__/index.test.ts
DELETED
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { createClient, createSyncClient } from "../src/index";
|
|
3
|
-
import type { SleekSiteContent } from "../src/types";
|
|
4
|
-
|
|
5
|
-
// Mock data
|
|
6
|
-
const mockSiteContent: SleekSiteContent = {
|
|
7
|
-
pages: [
|
|
8
|
-
{ _path: "/", title: "Home", published: true },
|
|
9
|
-
{ _path: "/blog/post-1", title: "Post 1", published: true, category: "tech" },
|
|
10
|
-
{ _path: "/blog/post-2", title: "Post 2", published: false, category: "tech" },
|
|
11
|
-
{ _path: "/about", title: "About", published: true }
|
|
12
|
-
],
|
|
13
|
-
images: {
|
|
14
|
-
logo: { url: "https://example.com/logo.png", width: 200, height: 100 },
|
|
15
|
-
hero: { url: "https://example.com/hero.jpg", width: 1200, height: 600 }
|
|
16
|
-
},
|
|
17
|
-
lists: {
|
|
18
|
-
categories: [
|
|
19
|
-
{ label: "Technology", value: "tech" },
|
|
20
|
-
{ label: "Business", value: "business" }
|
|
21
|
-
]
|
|
22
|
-
},
|
|
23
|
-
config: {
|
|
24
|
-
title: "Test Site"
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
describe("SleekCMS Client", () => {
|
|
29
|
-
let fetchSpy: any;
|
|
30
|
-
|
|
31
|
-
beforeEach(() => {
|
|
32
|
-
fetchSpy = vi.spyOn(globalThis, "fetch");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
afterEach(() => {
|
|
36
|
-
vi.restoreAllMocks();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe("Search Query Functionality", () => {
|
|
40
|
-
it("should send search query as URL parameter when query is provided", async () => {
|
|
41
|
-
fetchSpy.mockResolvedValueOnce({
|
|
42
|
-
ok: true,
|
|
43
|
-
json: async () => ["Post 1", "Post 2"]
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const client = createClient({
|
|
47
|
-
siteToken: "prod-site123"
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const result = await client.getContent('pages[?published == `true`]');
|
|
51
|
-
|
|
52
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
53
|
-
const calledUrl = fetchSpy.mock.calls[0][0];
|
|
54
|
-
expect(calledUrl).toContain("search=");
|
|
55
|
-
expect(calledUrl).toContain('pages');
|
|
56
|
-
expect(calledUrl).toContain('published');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("should apply JMESPath query on server-side with search parameter", async () => {
|
|
60
|
-
const filteredPages = [
|
|
61
|
-
{ _path: "/", title: "Home", published: true },
|
|
62
|
-
{ _path: "/about", title: "About", published: true }
|
|
63
|
-
];
|
|
64
|
-
|
|
65
|
-
fetchSpy.mockResolvedValueOnce({
|
|
66
|
-
ok: true,
|
|
67
|
-
json: async () => filteredPages
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const client = createClient({
|
|
71
|
-
siteToken: "prod-site123"
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const result = await client.getContent('pages[?published == `true`]');
|
|
75
|
-
|
|
76
|
-
expect(result).toEqual(filteredPages);
|
|
77
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("should apply local JMESPath query in cache mode", async () => {
|
|
81
|
-
fetchSpy.mockResolvedValueOnce({
|
|
82
|
-
ok: true,
|
|
83
|
-
json: async () => mockSiteContent
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const client = createClient({
|
|
87
|
-
siteToken: "prod-site123",
|
|
88
|
-
cache: true
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
// First call to load cache
|
|
92
|
-
const allContent = await client.getContent();
|
|
93
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
94
|
-
|
|
95
|
-
// Second call with query should use local cache
|
|
96
|
-
const pages = await client.getContent<any[]>('pages');
|
|
97
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1); // No additional fetch
|
|
98
|
-
expect(pages).toHaveLength(4);
|
|
99
|
-
expect(pages).toEqual(mockSiteContent.pages);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("should handle complex JMESPath queries with findPages", async () => {
|
|
103
|
-
fetchSpy.mockResolvedValueOnce({
|
|
104
|
-
ok: true,
|
|
105
|
-
json: async () => mockSiteContent
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
const client = createClient({
|
|
109
|
-
siteToken: "prod-site123",
|
|
110
|
-
cache: true
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
await client.getContent(); // Load cache
|
|
114
|
-
|
|
115
|
-
const result = await client.findPages(
|
|
116
|
-
"/blog",
|
|
117
|
-
"[?published == `true`].title"
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
expect(result).toEqual(["Post 1"]);
|
|
121
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
describe("Cache Behavior - Async Client", () => {
|
|
126
|
-
it("should fetch full content once when cache is true", async () => {
|
|
127
|
-
fetchSpy.mockResolvedValueOnce({
|
|
128
|
-
ok: true,
|
|
129
|
-
json: async () => mockSiteContent
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
const client = createClient({
|
|
133
|
-
siteToken: "prod-site123",
|
|
134
|
-
cache: true
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// First call
|
|
138
|
-
const content1 = await client.getContent();
|
|
139
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
140
|
-
expect(content1).toEqual(mockSiteContent);
|
|
141
|
-
|
|
142
|
-
// Second call should use cache
|
|
143
|
-
const content2 = await client.getContent();
|
|
144
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1); // No additional fetch
|
|
145
|
-
expect(content2).toEqual(mockSiteContent);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("should use cached data for all methods after initial fetch", async () => {
|
|
149
|
-
fetchSpy.mockResolvedValueOnce({
|
|
150
|
-
ok: true,
|
|
151
|
-
json: async () => mockSiteContent
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
const client = createClient({
|
|
155
|
-
siteToken: "prod-site123",
|
|
156
|
-
cache: true
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// Initial fetch
|
|
160
|
-
await client.getContent();
|
|
161
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
162
|
-
|
|
163
|
-
// All subsequent calls should use cache
|
|
164
|
-
const images = await client.getImages();
|
|
165
|
-
expect(images).toEqual(mockSiteContent.images);
|
|
166
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
167
|
-
|
|
168
|
-
const logo = await client.getImage("logo");
|
|
169
|
-
expect(logo).toEqual(mockSiteContent.images!.logo);
|
|
170
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
171
|
-
|
|
172
|
-
const list = await client.getList("categories");
|
|
173
|
-
expect(list).toEqual(mockSiteContent.lists!.categories);
|
|
174
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
175
|
-
|
|
176
|
-
const pages = await client.findPages("/blog");
|
|
177
|
-
expect(pages).toHaveLength(2);
|
|
178
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("should enable cache automatically with mock=true and dev token", async () => {
|
|
182
|
-
fetchSpy.mockResolvedValueOnce({
|
|
183
|
-
ok: true,
|
|
184
|
-
json: async () => mockSiteContent
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
const client = createClient({
|
|
188
|
-
siteToken: "dev-site123",
|
|
189
|
-
mock: true
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
// First call
|
|
193
|
-
await client.getContent();
|
|
194
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
195
|
-
|
|
196
|
-
// Second call should use cache (auto-enabled due to mock + dev)
|
|
197
|
-
await client.getContent();
|
|
198
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("should fetch on each call when cache is false and query is provided", async () => {
|
|
202
|
-
fetchSpy
|
|
203
|
-
.mockResolvedValueOnce({
|
|
204
|
-
ok: true,
|
|
205
|
-
json: async () => mockSiteContent.pages
|
|
206
|
-
})
|
|
207
|
-
.mockResolvedValueOnce({
|
|
208
|
-
ok: true,
|
|
209
|
-
json: async () => [{ _path: "/new", title: "New Page" }]
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
const client = createClient({
|
|
213
|
-
siteToken: "prod-site123",
|
|
214
|
-
cache: false
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
const pages1 = await client.getContent('pages');
|
|
218
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
219
|
-
expect(Array.isArray(pages1)).toBe(true);
|
|
220
|
-
|
|
221
|
-
const pages2 = await client.getContent('pages');
|
|
222
|
-
expect(fetchSpy).toHaveBeenCalledTimes(2); // New fetch
|
|
223
|
-
expect(pages2).toEqual([{ _path: "/new", title: "New Page" }]);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("should enable caching after first getContent() call without query", async () => {
|
|
227
|
-
fetchSpy.mockResolvedValueOnce({
|
|
228
|
-
ok: true,
|
|
229
|
-
json: async () => mockSiteContent
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
const client = createClient({
|
|
233
|
-
siteToken: "prod-site123",
|
|
234
|
-
cache: false
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
// First call without query enables cache
|
|
238
|
-
await client.getContent();
|
|
239
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
240
|
-
|
|
241
|
-
// Subsequent calls use cache
|
|
242
|
-
await client.getImages();
|
|
243
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
244
|
-
|
|
245
|
-
await client.getList("categories");
|
|
246
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it("should apply JMESPath queries locally when cache is enabled", async () => {
|
|
250
|
-
fetchSpy.mockResolvedValueOnce({
|
|
251
|
-
ok: true,
|
|
252
|
-
json: async () => mockSiteContent
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
const client = createClient({
|
|
256
|
-
siteToken: "prod-site123",
|
|
257
|
-
cache: true
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// Load cache
|
|
261
|
-
await client.getContent();
|
|
262
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
263
|
-
|
|
264
|
-
// Query should be applied locally
|
|
265
|
-
const publishedPages = await client.getContent<any[]>(
|
|
266
|
-
'pages[?published == `true`]'
|
|
267
|
-
);
|
|
268
|
-
expect(publishedPages).toHaveLength(3);
|
|
269
|
-
expect(publishedPages.every((p: any) => p.published === true)).toBe(true);
|
|
270
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1); // No additional fetch
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it("should handle concurrent requests with cache enabled", async () => {
|
|
274
|
-
fetchSpy.mockResolvedValue({
|
|
275
|
-
ok: true,
|
|
276
|
-
json: async () => mockSiteContent
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
const client = createClient({
|
|
280
|
-
siteToken: "prod-site123",
|
|
281
|
-
cache: true
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
// Multiple concurrent requests - may fetch multiple times initially
|
|
285
|
-
// but should eventually use cache
|
|
286
|
-
const [content1, content2, content3] = await Promise.all([
|
|
287
|
-
client.getContent(),
|
|
288
|
-
client.getContent(),
|
|
289
|
-
client.getContent()
|
|
290
|
-
]);
|
|
291
|
-
|
|
292
|
-
expect(content1).toEqual(mockSiteContent);
|
|
293
|
-
expect(content2).toEqual(mockSiteContent);
|
|
294
|
-
expect(content3).toEqual(mockSiteContent);
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
describe("Sync Client", () => {
|
|
299
|
-
it("should prefetch all content and work synchronously", async () => {
|
|
300
|
-
fetchSpy.mockResolvedValueOnce({
|
|
301
|
-
ok: true,
|
|
302
|
-
json: async () => mockSiteContent
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
const client = await createSyncClient({
|
|
306
|
-
siteToken: "prod-site123"
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
// Initial fetch during creation
|
|
310
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
311
|
-
|
|
312
|
-
// All methods are synchronous and use cached data
|
|
313
|
-
const content = client.getContent();
|
|
314
|
-
expect(content).toEqual(mockSiteContent);
|
|
315
|
-
|
|
316
|
-
const images = client.getImages();
|
|
317
|
-
expect(images).toEqual(mockSiteContent.images);
|
|
318
|
-
|
|
319
|
-
const pages = client.findPages("/blog");
|
|
320
|
-
expect(pages).toHaveLength(2);
|
|
321
|
-
|
|
322
|
-
// Still only one fetch
|
|
323
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
324
|
-
});
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
describe("Error Handling", () => {
|
|
328
|
-
it("should throw error when siteToken is missing", async () => {
|
|
329
|
-
const client = createClient({
|
|
330
|
-
siteToken: ""
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
await expect(client.getContent()).rejects.toThrow(
|
|
334
|
-
"[SleekCMS] siteToken is required"
|
|
335
|
-
);
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
it("should handle HTTP errors gracefully", async () => {
|
|
339
|
-
fetchSpy.mockResolvedValueOnce({
|
|
340
|
-
ok: false,
|
|
341
|
-
status: 404,
|
|
342
|
-
statusText: "Not Found",
|
|
343
|
-
json: async () => ({ message: "Site not found" })
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
const client = createClient({
|
|
347
|
-
siteToken: "prod-invalid"
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
await expect(client.getContent()).rejects.toThrow(
|
|
351
|
-
"[SleekCMS] Request failed (404): Site not found"
|
|
352
|
-
);
|
|
353
|
-
});
|
|
354
|
-
});
|
|
355
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
import * as jmespath from "jmespath";
|
|
2
|
-
import type { SleekSiteContent, ClientOptions } from "./types";
|
|
3
|
-
|
|
4
|
-
function isDevToken(token: string): boolean {
|
|
5
|
-
return token.startsWith("dev-");
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function getBaseUrl(token: string): string {
|
|
9
|
-
let [env, siteId, ...rest] = token.split("-");
|
|
10
|
-
return `https://${env}.sleekcms.com/${siteId}`;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Low-level fetch helper.
|
|
15
|
-
* - If `searchQuery` is provided, it's sent as `?search=<JMESPath>`.
|
|
16
|
-
* - If options.mock === true and token is dev-..., `mock=true` is added.
|
|
17
|
-
*/
|
|
18
|
-
async function fetchSiteContent(
|
|
19
|
-
options: ClientOptions,
|
|
20
|
-
searchQuery?: string
|
|
21
|
-
): Promise<any> {
|
|
22
|
-
const { siteToken, env = "latest", mock } = options;
|
|
23
|
-
|
|
24
|
-
if (!siteToken) {
|
|
25
|
-
throw new Error("[SleekCMS] siteToken is required");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const baseUrl = getBaseUrl(siteToken).replace(/\/$/, "");
|
|
29
|
-
const url = new URL(`${baseUrl}/${env}`);
|
|
30
|
-
|
|
31
|
-
if (searchQuery) {
|
|
32
|
-
url.searchParams.set("search", searchQuery);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (mock && isDevToken(siteToken)) {
|
|
36
|
-
url.searchParams.set("mock", "true");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const res = await fetch(url.toString(), {
|
|
40
|
-
method: "GET",
|
|
41
|
-
headers: {
|
|
42
|
-
"Content-Type": "application/json",
|
|
43
|
-
Authorization: siteToken
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
if (!res.ok) {
|
|
48
|
-
let message = res.statusText;
|
|
49
|
-
try {
|
|
50
|
-
const data = (await res.json()) as { message?: string };
|
|
51
|
-
if (data && data.message) message = data.message;
|
|
52
|
-
} catch {
|
|
53
|
-
// ignore
|
|
54
|
-
}
|
|
55
|
-
throw new Error(`[SleekCMS] Request failed (${res.status}): ${message}`);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return res.json();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function applyJmes<T = unknown>(data: unknown, query?: string): T {
|
|
62
|
-
if (!query) return data as T;
|
|
63
|
-
return jmespath.search(data, query) as T;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export type { SleekSiteContent, ClientOptions };
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Async SleekCMS client: methods return Promises.
|
|
70
|
-
*/
|
|
71
|
-
export interface SleekClient {
|
|
72
|
-
getContent<T = SleekSiteContent>(query?: string): Promise<T>;
|
|
73
|
-
findPages<T = unknown>(path: string, query?: string): Promise<T>;
|
|
74
|
-
getImages(): Promise<SleekSiteContent["images"]>;
|
|
75
|
-
getImage(name: string): Promise<unknown | undefined>;
|
|
76
|
-
getList<T = unknown>(name: string): Promise<T[] | undefined>;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Sync client: prefetches full content once; subsequent calls are in-memory only.
|
|
81
|
-
*/
|
|
82
|
-
export interface SleekSyncClient {
|
|
83
|
-
getContent<T = SleekSiteContent>(query?: string): T;
|
|
84
|
-
findPages<T = unknown>(path: string, query?: string): T;
|
|
85
|
-
getImages(): SleekSiteContent["images"];
|
|
86
|
-
getImage(name: string): unknown | undefined;
|
|
87
|
-
getList<T = unknown>(name: string): T[] | undefined;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function createClient(options: ClientOptions): SleekClient {
|
|
91
|
-
const dev = isDevToken(options.siteToken);
|
|
92
|
-
|
|
93
|
-
let cacheMode = !!options.cache || (!!options.mock && dev);
|
|
94
|
-
let cachedContent: SleekSiteContent | null = null;
|
|
95
|
-
|
|
96
|
-
async function ensureCacheLoaded(): Promise<SleekSiteContent> {
|
|
97
|
-
if (cachedContent) return cachedContent;
|
|
98
|
-
const data = (await fetchSiteContent(options)) as SleekSiteContent;
|
|
99
|
-
cachedContent = data;
|
|
100
|
-
return data;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function getContent<T = SleekSiteContent>(query?: string): Promise<T> {
|
|
104
|
-
if (cacheMode) {
|
|
105
|
-
const data = await ensureCacheLoaded();
|
|
106
|
-
return applyJmes<T>(data, query);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (!query) {
|
|
110
|
-
const data = (await fetchSiteContent(options)) as SleekSiteContent;
|
|
111
|
-
cachedContent = data;
|
|
112
|
-
cacheMode = true;
|
|
113
|
-
return data as T;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const data = await fetchSiteContent(options, query);
|
|
117
|
-
return data as T;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async function findPages<T = unknown>(
|
|
121
|
-
path: string,
|
|
122
|
-
query?: string
|
|
123
|
-
): Promise<T> {
|
|
124
|
-
if (!path) {
|
|
125
|
-
throw new Error("[SleekCMS] path is required for findPages");
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (cacheMode) {
|
|
129
|
-
const data = await ensureCacheLoaded();
|
|
130
|
-
const pages = data.pages ?? [];
|
|
131
|
-
const filtered = pages.filter((p) => {
|
|
132
|
-
const pth = typeof p._path === "string" ? p._path : "";
|
|
133
|
-
return pth.startsWith(path);
|
|
134
|
-
});
|
|
135
|
-
return applyJmes<T>(filtered, query);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const pages = (await fetchSiteContent(
|
|
139
|
-
options,
|
|
140
|
-
"pages"
|
|
141
|
-
)) as SleekSiteContent["pages"];
|
|
142
|
-
|
|
143
|
-
const filtered = (pages ?? []).filter((p) => {
|
|
144
|
-
const pth = typeof p._path === "string" ? p._path : "";
|
|
145
|
-
return pth.startsWith(path);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
return applyJmes<T>(filtered, query);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
async function getImages(): Promise<SleekSiteContent["images"]> {
|
|
152
|
-
if (cacheMode) {
|
|
153
|
-
const data = await ensureCacheLoaded();
|
|
154
|
-
return data.images ?? {};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const images = (await fetchSiteContent(
|
|
158
|
-
options,
|
|
159
|
-
"images"
|
|
160
|
-
)) as SleekSiteContent["images"];
|
|
161
|
-
return images ?? {};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async function getImage(name: string): Promise<unknown | undefined> {
|
|
165
|
-
if (!name) return undefined;
|
|
166
|
-
|
|
167
|
-
if (cacheMode) {
|
|
168
|
-
const data = await ensureCacheLoaded();
|
|
169
|
-
return data.images ? data.images[name] : undefined;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const images = (await fetchSiteContent(
|
|
173
|
-
options,
|
|
174
|
-
"images"
|
|
175
|
-
)) as SleekSiteContent["images"];
|
|
176
|
-
return images ? images[name] : undefined;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async function getList<T = unknown>(
|
|
180
|
-
name: string
|
|
181
|
-
): Promise<T[] | undefined> {
|
|
182
|
-
if (!name) return undefined;
|
|
183
|
-
|
|
184
|
-
if (cacheMode) {
|
|
185
|
-
const data = await ensureCacheLoaded();
|
|
186
|
-
const lists = data.lists ?? {};
|
|
187
|
-
const list = lists[name];
|
|
188
|
-
return Array.isArray(list) ? (list as T[]) : undefined;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const lists = (await fetchSiteContent(
|
|
192
|
-
options,
|
|
193
|
-
"lists"
|
|
194
|
-
)) as SleekSiteContent["lists"];
|
|
195
|
-
const list = lists ? lists[name] : undefined;
|
|
196
|
-
return Array.isArray(list) ? (list as T[]) : undefined;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return {
|
|
200
|
-
getContent,
|
|
201
|
-
findPages,
|
|
202
|
-
getImages,
|
|
203
|
-
getImage,
|
|
204
|
-
getList
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Create a sync SleekCMS client.
|
|
210
|
-
*
|
|
211
|
-
* - Prefetches full content once (no search=).
|
|
212
|
-
* - All operations (including JMESPath) are local and synchronous.
|
|
213
|
-
*/
|
|
214
|
-
export async function createSyncClient(
|
|
215
|
-
options: ClientOptions
|
|
216
|
-
): Promise<SleekSyncClient> {
|
|
217
|
-
const data = (await fetchSiteContent(options)) as SleekSiteContent;
|
|
218
|
-
|
|
219
|
-
function getContent<T = SleekSiteContent>(query?: string): T {
|
|
220
|
-
return applyJmes<T>(data, query);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function findPages<T = unknown>(path: string, query?: string): T {
|
|
224
|
-
if (!path) {
|
|
225
|
-
throw new Error("[SleekCMS] path is required for findPages");
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const pages = data.pages ?? [];
|
|
229
|
-
const filtered = pages.filter((p) => {
|
|
230
|
-
const pth = typeof p._path === "string" ? p._path : "";
|
|
231
|
-
return pth.startsWith(path);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
return applyJmes<T>(filtered, query);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function getImages(): SleekSiteContent["images"] {
|
|
238
|
-
return data.images ?? {};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function getImage(name: string): unknown | undefined {
|
|
242
|
-
if (!name) return undefined;
|
|
243
|
-
return data.images ? data.images[name] : undefined;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function getList<T = unknown>(name: string): T[] | undefined {
|
|
247
|
-
if (!name) return undefined;
|
|
248
|
-
const lists = data.lists ?? {};
|
|
249
|
-
const list = lists[name];
|
|
250
|
-
return Array.isArray(list) ? (list as T[]) : undefined;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
getContent,
|
|
255
|
-
findPages,
|
|
256
|
-
getImages,
|
|
257
|
-
getImage,
|
|
258
|
-
getList
|
|
259
|
-
};
|
|
260
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
type Entry = Record<string, unknown>;
|
|
3
|
-
type Page = { _path: string; [key: string]: unknown };
|
|
4
|
-
type Image = { url: string; [key: string]: unknown };
|
|
5
|
-
type List = Array<{ label: string; value: string }>;
|
|
6
|
-
|
|
7
|
-
export interface SleekSiteContent {
|
|
8
|
-
entries?: Record<string, Entry> | Record<string, Entry[]>;
|
|
9
|
-
pages?: Array<Page>;
|
|
10
|
-
images?: Record<string, Image>;
|
|
11
|
-
lists?: Record<string, List>;
|
|
12
|
-
config?: { title?: string; };
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface ClientOptions {
|
|
16
|
-
siteToken: string;
|
|
17
|
-
env?: string;
|
|
18
|
-
cache?: boolean;
|
|
19
|
-
mock?: boolean;
|
|
20
|
-
}
|
package/vitest.config.ts
DELETED
|
File without changes
|