@nby.ai/ucm 1.0.1

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/dist/react.js ADDED
@@ -0,0 +1,313 @@
1
+ // src/react.tsx
2
+ import { createContext, useContext, useMemo } from "react";
3
+
4
+ // src/core.ts
5
+ import { createClient } from "@supabase/supabase-js";
6
+
7
+ // src/types.ts
8
+ function buildCategoryTree(categories) {
9
+ const map = /* @__PURE__ */ new Map();
10
+ const roots = [];
11
+ categories.forEach((cat) => {
12
+ map.set(cat.id, { ...cat, children: [] });
13
+ });
14
+ categories.forEach((cat) => {
15
+ const node = map.get(cat.id);
16
+ if (cat.parent_id && map.has(cat.parent_id)) {
17
+ map.get(cat.parent_id).children.push(node);
18
+ } else {
19
+ roots.push(node);
20
+ }
21
+ });
22
+ const sortByOrder = (a, b) => a.sort_order - b.sort_order;
23
+ roots.sort(sortByOrder);
24
+ roots.forEach((root) => root.children?.sort(sortByOrder));
25
+ return roots;
26
+ }
27
+
28
+ // src/core.ts
29
+ function createUCMClient(config) {
30
+ const { url, anonKey, options = {} } = config;
31
+ if (!url || !anonKey) {
32
+ throw new Error("UCM: url and anonKey are required");
33
+ }
34
+ const supabase = createClient(url, anonKey, {
35
+ auth: {
36
+ persistSession: options.persistSession ?? false,
37
+ autoRefreshToken: options.autoRefreshToken ?? false
38
+ }
39
+ });
40
+ const contents = {
41
+ async list(params = {}) {
42
+ const {
43
+ type,
44
+ category_id,
45
+ tags,
46
+ featured,
47
+ visibility,
48
+ // v2.0: 可见性筛选
49
+ limit = 20,
50
+ offset = 0,
51
+ orderBy = "published_at",
52
+ orderDirection = "desc"
53
+ } = params;
54
+ let query = supabase.from("contents").select("*").order("pinned", { ascending: false }).order(orderBy, { ascending: orderDirection === "asc", nullsFirst: false }).range(offset, offset + limit - 1);
55
+ if (type) query = query.eq("type", type);
56
+ if (category_id) query = query.eq("category_id", category_id);
57
+ if (tags && tags.length > 0) query = query.overlaps("tags", tags);
58
+ if (featured !== void 0) query = query.eq("featured", featured);
59
+ if (visibility) query = query.eq("visibility", visibility);
60
+ const { data, error } = await query;
61
+ if (error) {
62
+ throw new Error(`UCM: Failed to fetch contents: ${error.message}`);
63
+ }
64
+ return data || [];
65
+ },
66
+ async getBySlug(slug) {
67
+ const { data, error } = await supabase.from("contents").select("*").eq("slug", slug).single();
68
+ if (error) {
69
+ if (error.code === "PGRST116") return null;
70
+ throw new Error(`UCM: Failed to fetch content: ${error.message}`);
71
+ }
72
+ return data;
73
+ },
74
+ async getById(id) {
75
+ const { data, error } = await supabase.from("contents").select("*").eq("id", id).single();
76
+ if (error) {
77
+ if (error.code === "PGRST116") return null;
78
+ throw new Error(`UCM: Failed to fetch content: ${error.message}`);
79
+ }
80
+ return data;
81
+ },
82
+ async count(params = {}) {
83
+ const { type, category_id, tags, featured, visibility } = params;
84
+ let query = supabase.from("contents").select("*", { count: "exact", head: true });
85
+ if (type) query = query.eq("type", type);
86
+ if (category_id) query = query.eq("category_id", category_id);
87
+ if (tags && tags.length > 0) query = query.overlaps("tags", tags);
88
+ if (featured !== void 0) query = query.eq("featured", featured);
89
+ if (visibility) query = query.eq("visibility", visibility);
90
+ const { count, error } = await query;
91
+ if (error) {
92
+ console.error("UCM: Failed to get count:", error);
93
+ return 0;
94
+ }
95
+ return count || 0;
96
+ },
97
+ async create(input) {
98
+ const { data, error } = await supabase.from("contents").insert(input).select().single();
99
+ if (error) {
100
+ throw new Error(`UCM: Failed to create content: ${error.message}`);
101
+ }
102
+ return data;
103
+ },
104
+ async update(id, input) {
105
+ const { data, error } = await supabase.from("contents").update(input).eq("id", id).select().single();
106
+ if (error) {
107
+ throw new Error(`UCM: Failed to update content: ${error.message}`);
108
+ }
109
+ return data;
110
+ },
111
+ async delete(id) {
112
+ const { error } = await supabase.from("contents").delete().eq("id", id);
113
+ if (error) {
114
+ throw new Error(`UCM: Failed to delete content: ${error.message}`);
115
+ }
116
+ },
117
+ async incrementView(id) {
118
+ try {
119
+ await supabase.rpc("increment_view_count", { content_id: id });
120
+ } catch {
121
+ }
122
+ },
123
+ async search(query, type, visibility = "public") {
124
+ if (!query || query.trim().length === 0) {
125
+ return [];
126
+ }
127
+ const { data, error } = await supabase.rpc("search_contents", {
128
+ search_query: query.trim(),
129
+ content_type: type || null,
130
+ content_visibility: visibility
131
+ });
132
+ if (error) {
133
+ throw new Error(`UCM: Failed to search contents: ${error.message}`);
134
+ }
135
+ return data || [];
136
+ },
137
+ async getTags(type) {
138
+ let query = supabase.from("contents").select("tags");
139
+ if (type) {
140
+ query = query.eq("type", type);
141
+ }
142
+ const { data, error } = await query;
143
+ if (error) {
144
+ console.error("UCM: Failed to fetch tags:", error);
145
+ return [];
146
+ }
147
+ const allTags = /* @__PURE__ */ new Set();
148
+ data?.forEach((item) => {
149
+ if (item.tags && Array.isArray(item.tags)) {
150
+ item.tags.forEach((tag) => allTags.add(tag));
151
+ }
152
+ });
153
+ return Array.from(allTags).sort((a, b) => a.localeCompare(b));
154
+ }
155
+ };
156
+ const categories = {
157
+ async list(params = {}) {
158
+ const { parent_id } = params;
159
+ let query = supabase.from("categories").select("*").order("sort_order");
160
+ if (parent_id !== void 0) {
161
+ if (parent_id === null) {
162
+ query = query.is("parent_id", null);
163
+ } else {
164
+ query = query.eq("parent_id", parent_id);
165
+ }
166
+ }
167
+ const { data, error } = await query;
168
+ if (error) {
169
+ throw new Error(`UCM: Failed to fetch categories: ${error.message}`);
170
+ }
171
+ return data || [];
172
+ },
173
+ async getTree() {
174
+ const allCategories = await this.list();
175
+ return buildCategoryTree(allCategories);
176
+ },
177
+ async getBySlug(slug) {
178
+ const { data, error } = await supabase.from("categories").select("*").eq("slug", slug).single();
179
+ if (error) {
180
+ if (error.code === "PGRST116") return null;
181
+ throw new Error(`UCM: Failed to fetch category: ${error.message}`);
182
+ }
183
+ return data;
184
+ },
185
+ async getById(id) {
186
+ const { data, error } = await supabase.from("categories").select("*").eq("id", id).single();
187
+ if (error) {
188
+ if (error.code === "PGRST116") return null;
189
+ throw new Error(`UCM: Failed to fetch category: ${error.message}`);
190
+ }
191
+ return data;
192
+ },
193
+ async getChildren(parentId) {
194
+ return this.list({ parent_id: parentId });
195
+ },
196
+ async getRoots() {
197
+ return this.list({ parent_id: null });
198
+ },
199
+ async getBreadcrumb(categoryId) {
200
+ const breadcrumb = [];
201
+ let currentId = categoryId;
202
+ while (currentId) {
203
+ const category = await this.getById(currentId);
204
+ if (!category) break;
205
+ breadcrumb.unshift(category);
206
+ currentId = category.parent_id;
207
+ }
208
+ return breadcrumb;
209
+ }
210
+ };
211
+ const storage = {
212
+ getUrl(path, bucket = "content-images") {
213
+ if (!path) return "";
214
+ if (path.startsWith("http")) return path;
215
+ return `${url}/storage/v1/object/public/${bucket}/${path}`;
216
+ },
217
+ getImageUrl(path, options2) {
218
+ const baseUrl = this.getUrl(path);
219
+ if (!baseUrl || !options2) return baseUrl;
220
+ const params = new URLSearchParams();
221
+ if (options2.width) params.set("width", options2.width.toString());
222
+ if (options2.height) params.set("height", options2.height.toString());
223
+ if (options2.quality) params.set("quality", options2.quality.toString());
224
+ if (options2.format) params.set("format", options2.format);
225
+ const queryString = params.toString();
226
+ return queryString ? `${baseUrl}?${queryString}` : baseUrl;
227
+ }
228
+ };
229
+ return {
230
+ supabase,
231
+ contents,
232
+ categories,
233
+ storage,
234
+ isConfigured: () => true
235
+ };
236
+ }
237
+ function createNullClient() {
238
+ const nullSupabase = null;
239
+ return {
240
+ supabase: nullSupabase,
241
+ contents: {
242
+ list: async () => [],
243
+ getBySlug: async () => null,
244
+ getById: async () => null,
245
+ count: async () => 0,
246
+ search: async () => [],
247
+ getTags: async () => [],
248
+ create: async () => {
249
+ throw new Error("UCM: Client not configured");
250
+ },
251
+ update: async () => {
252
+ throw new Error("UCM: Client not configured");
253
+ },
254
+ delete: async () => {
255
+ throw new Error("UCM: Client not configured");
256
+ },
257
+ incrementView: async () => {
258
+ }
259
+ },
260
+ categories: {
261
+ list: async () => [],
262
+ getTree: async () => [],
263
+ getBySlug: async () => null,
264
+ getById: async () => null,
265
+ getChildren: async () => [],
266
+ getRoots: async () => [],
267
+ getBreadcrumb: async () => []
268
+ },
269
+ storage: {
270
+ getUrl: () => "",
271
+ getImageUrl: () => ""
272
+ },
273
+ isConfigured: () => false
274
+ };
275
+ }
276
+
277
+ // src/react.tsx
278
+ import { jsx } from "react/jsx-runtime";
279
+ var UCMContext = createContext(null);
280
+ function UCMProvider({ config, client, children }) {
281
+ const ucmClient = useMemo(() => {
282
+ if (client) {
283
+ return client;
284
+ }
285
+ if (config?.url && config?.anonKey) {
286
+ return createUCMClient(config);
287
+ }
288
+ console.warn("UCM: No config provided, using null client");
289
+ return createNullClient();
290
+ }, [config?.url, config?.anonKey, client]);
291
+ return /* @__PURE__ */ jsx(UCMContext.Provider, { value: ucmClient, children });
292
+ }
293
+ function useUCM() {
294
+ const context = useContext(UCMContext);
295
+ if (!context) {
296
+ throw new Error("useUCM must be used within a UCMProvider");
297
+ }
298
+ return context;
299
+ }
300
+ function useUCMOptional() {
301
+ const context = useContext(UCMContext);
302
+ return context || createNullClient();
303
+ }
304
+ function useUCMConfigured() {
305
+ const ucm = useUCMOptional();
306
+ return ucm.isConfigured();
307
+ }
308
+ export {
309
+ UCMProvider,
310
+ useUCM,
311
+ useUCMConfigured,
312
+ useUCMOptional
313
+ };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@nby.ai/ucm",
3
+ "version": "1.0.1",
4
+ "description": "Universal Content Module - Supabase-based multilingual CMS",
5
+ "author": "NBY Team",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "supabase",
9
+ "cms",
10
+ "i18n",
11
+ "content",
12
+ "react",
13
+ "typescript"
14
+ ],
15
+ "type": "module",
16
+ "main": "./dist/index.cjs",
17
+ "module": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js",
23
+ "require": "./dist/index.cjs"
24
+ },
25
+ "./react": {
26
+ "types": "./dist/react.d.ts",
27
+ "import": "./dist/react.js",
28
+ "require": "./dist/react.cjs"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsup",
37
+ "dev": "tsup --watch",
38
+ "test": "vitest run",
39
+ "test:watch": "vitest",
40
+ "lint": "eslint src/",
41
+ "prepublishOnly": "pnpm build"
42
+ },
43
+ "peerDependencies": {
44
+ "@supabase/supabase-js": "^2.0.0",
45
+ "@tanstack/react-query": "^5.0.0",
46
+ "react": "^18.0.0 || ^19.0.0"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "react": {
50
+ "optional": true
51
+ },
52
+ "@tanstack/react-query": {
53
+ "optional": true
54
+ }
55
+ },
56
+ "devDependencies": {
57
+ "@supabase/supabase-js": "^2.95.0",
58
+ "@tanstack/react-query": "^5.90.0",
59
+ "@types/react": "^19.0.0",
60
+ "react": "^19.0.0",
61
+ "tsup": "^8.0.0",
62
+ "typescript": "^5.7.0",
63
+ "vitest": "^4.0.0"
64
+ }
65
+ }