target-supabase-sdk 0.1.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 +70 -0
- package/dist/auth/auth.api.d.ts +28 -0
- package/dist/auth/auth.api.d.ts.map +1 -0
- package/dist/auth/auth.api.js +50 -0
- package/dist/auth/auth.api.js.map +1 -0
- package/dist/core.api.d.ts +68 -0
- package/dist/core.api.d.ts.map +1 -0
- package/dist/core.api.js +226 -0
- package/dist/core.api.js.map +1 -0
- package/dist/core.interface.d.ts +36 -0
- package/dist/core.interface.d.ts.map +1 -0
- package/dist/core.interface.js +46 -0
- package/dist/core.interface.js.map +1 -0
- package/dist/core.utils.d.ts +19 -0
- package/dist/core.utils.d.ts.map +1 -0
- package/dist/core.utils.js +86 -0
- package/dist/core.utils.js.map +1 -0
- package/dist/feed/feed.interface.d.ts +27 -0
- package/dist/feed/feed.interface.d.ts.map +1 -0
- package/dist/feed/feed.interface.js +17 -0
- package/dist/feed/feed.interface.js.map +1 -0
- package/dist/file/file-manager.d.ts +24 -0
- package/dist/file/file-manager.d.ts.map +1 -0
- package/dist/file/file-manager.js +109 -0
- package/dist/file/file-manager.js.map +1 -0
- package/dist/file/file.api.d.ts +15 -0
- package/dist/file/file.api.d.ts.map +1 -0
- package/dist/file/file.api.js +30 -0
- package/dist/file/file.api.js.map +1 -0
- package/dist/file/file.interface.d.ts +25 -0
- package/dist/file/file.interface.d.ts.map +1 -0
- package/dist/file/file.interface.js +9 -0
- package/dist/file/file.interface.js.map +1 -0
- package/dist/file-list/file-list.api.d.ts +15 -0
- package/dist/file-list/file-list.api.d.ts.map +1 -0
- package/dist/file-list/file-list.api.js +54 -0
- package/dist/file-list/file-list.api.js.map +1 -0
- package/dist/file-list/file-list.interface.d.ts +22 -0
- package/dist/file-list/file-list.interface.d.ts.map +1 -0
- package/dist/file-list/file-list.interface.js +5 -0
- package/dist/file-list/file-list.interface.js.map +1 -0
- package/dist/idea/idea.interface.d.ts +25 -0
- package/dist/idea/idea.interface.d.ts.map +1 -0
- package/dist/idea/idea.interface.js +15 -0
- package/dist/idea/idea.interface.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/link/link.interface.d.ts +18 -0
- package/dist/link/link.interface.d.ts.map +1 -0
- package/dist/link/link.interface.js +6 -0
- package/dist/link/link.interface.js.map +1 -0
- package/dist/parcel/parcel-manager.d.ts +39 -0
- package/dist/parcel/parcel-manager.d.ts.map +1 -0
- package/dist/parcel/parcel-manager.js +156 -0
- package/dist/parcel/parcel-manager.js.map +1 -0
- package/dist/parcel/parcel.interface.d.ts +34 -0
- package/dist/parcel/parcel.interface.d.ts.map +1 -0
- package/dist/parcel/parcel.interface.js +11 -0
- package/dist/parcel/parcel.interface.js.map +1 -0
- package/dist/question-list/question-list.api.d.ts +13 -0
- package/dist/question-list/question-list.api.d.ts.map +1 -0
- package/dist/question-list/question-list.api.js +35 -0
- package/dist/question-list/question-list.api.js.map +1 -0
- package/dist/question-list/question-list.interface.d.ts +18 -0
- package/dist/question-list/question-list.interface.d.ts.map +1 -0
- package/dist/question-list/question-list.interface.js +5 -0
- package/dist/question-list/question-list.interface.js.map +1 -0
- package/dist/review/review.api.d.ts +14 -0
- package/dist/review/review.api.d.ts.map +1 -0
- package/dist/review/review.api.js +133 -0
- package/dist/review/review.api.js.map +1 -0
- package/dist/review/review.interface.d.ts +24 -0
- package/dist/review/review.interface.d.ts.map +1 -0
- package/dist/review/review.interface.js +10 -0
- package/dist/review/review.interface.js.map +1 -0
- package/dist/review-v2/apis/post-review-setting.api.d.ts +12 -0
- package/dist/review-v2/apis/post-review-setting.api.d.ts.map +1 -0
- package/dist/review-v2/apis/post-review-setting.api.js +27 -0
- package/dist/review-v2/apis/post-review-setting.api.js.map +1 -0
- package/dist/review-v2/apis/post-review.api.d.ts +13 -0
- package/dist/review-v2/apis/post-review.api.d.ts.map +1 -0
- package/dist/review-v2/apis/post-review.api.js +46 -0
- package/dist/review-v2/apis/post-review.api.js.map +1 -0
- package/dist/review-v2/review-setting.interface.d.ts +23 -0
- package/dist/review-v2/review-setting.interface.d.ts.map +1 -0
- package/dist/review-v2/review-setting.interface.js +5 -0
- package/dist/review-v2/review-setting.interface.js.map +1 -0
- package/dist/review-v2/review.interface.d.ts +20 -0
- package/dist/review-v2/review.interface.d.ts.map +1 -0
- package/dist/review-v2/review.interface.js +5 -0
- package/dist/review-v2/review.interface.js.map +1 -0
- package/dist/service/api.interface.d.ts +35 -0
- package/dist/service/api.interface.d.ts.map +1 -0
- package/dist/service/api.interface.js +12 -0
- package/dist/service/api.interface.js.map +1 -0
- package/dist/service/base.interface.d.ts +83 -0
- package/dist/service/base.interface.d.ts.map +1 -0
- package/dist/service/base.interface.js +7 -0
- package/dist/service/base.interface.js.map +1 -0
- package/dist/service/service.interface.d.ts +24 -0
- package/dist/service/service.interface.d.ts.map +1 -0
- package/dist/service/service.interface.js +11 -0
- package/dist/service/service.interface.js.map +1 -0
- package/dist/supabase.d.ts +32 -0
- package/dist/supabase.d.ts.map +1 -0
- package/dist/supabase.js +100 -0
- package/dist/supabase.js.map +1 -0
- package/package.json +45 -0
- package/src/auth/auth.api.ts +85 -0
- package/src/core.api.ts +338 -0
- package/src/core.interface.ts +74 -0
- package/src/core.utils.ts +98 -0
- package/src/feed/feed.interface.ts +33 -0
- package/src/file/file-manager.ts +145 -0
- package/src/file/file.api.ts +43 -0
- package/src/file/file.interface.ts +30 -0
- package/src/file-list/file-list.api.ts +64 -0
- package/src/file-list/file-list.interface.ts +24 -0
- package/src/idea/idea.interface.ts +28 -0
- package/src/index.ts +50 -0
- package/src/link/link.interface.ts +21 -0
- package/src/parcel/parcel-manager.ts +209 -0
- package/src/parcel/parcel.interface.ts +40 -0
- package/src/question-list/question-list.api.ts +42 -0
- package/src/question-list/question-list.interface.ts +21 -0
- package/src/review/review.api.ts +145 -0
- package/src/review/review.interface.ts +26 -0
- package/src/review-v2/apis/post-review-setting.api.ts +35 -0
- package/src/review-v2/apis/post-review.api.ts +59 -0
- package/src/review-v2/review-setting.interface.ts +26 -0
- package/src/review-v2/review.interface.ts +23 -0
- package/src/service/api.interface.ts +47 -0
- package/src/service/base.interface.ts +97 -0
- package/src/service/service.interface.ts +27 -0
- package/src/supabase.ts +143 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { supabase } from "..";
|
|
2
|
+
import { createResponse } from "../core.interface";
|
|
3
|
+
import { handleSupabaseError } from "../core.utils";
|
|
4
|
+
|
|
5
|
+
export interface LoginPayload {
|
|
6
|
+
email: string;
|
|
7
|
+
password: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface RegisterPayload {
|
|
11
|
+
email: string;
|
|
12
|
+
password: string;
|
|
13
|
+
confirmPassword?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface User {
|
|
17
|
+
id: string;
|
|
18
|
+
email: string;
|
|
19
|
+
created_at: string;
|
|
20
|
+
last_sign_in_at?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 用户注册
|
|
24
|
+
export const registerUser = async ({ email, password }: RegisterPayload) => {
|
|
25
|
+
const { data, error } = await supabase.authClient!.auth.signUp({
|
|
26
|
+
email,
|
|
27
|
+
password,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (error) {
|
|
31
|
+
handleSupabaseError("registerUser", error, "Registration failed. Please try again.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return createResponse.success(data);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// 用户登录
|
|
38
|
+
export const loginUser = async ({ email, password }: LoginPayload) => {
|
|
39
|
+
const { data, error } = await supabase.authClient!.auth.signInWithPassword({
|
|
40
|
+
email,
|
|
41
|
+
password,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (error) {
|
|
45
|
+
handleSupabaseError("loginUser", error, "Login failed. Please check your credentials.");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return createResponse.success(data);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// 用户登出
|
|
52
|
+
export const logoutUser = async () => {
|
|
53
|
+
const { error } = await supabase.authClient!.auth.signOut();
|
|
54
|
+
|
|
55
|
+
if (error) {
|
|
56
|
+
handleSupabaseError("logoutUser", error, "Logout failed.");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return createResponse.success();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// 获取当前用户
|
|
63
|
+
export const getCurrentUser = async () => {
|
|
64
|
+
const {
|
|
65
|
+
data: { user },
|
|
66
|
+
error,
|
|
67
|
+
} = await supabase.authClient!.auth.getUser();
|
|
68
|
+
|
|
69
|
+
if (error) {
|
|
70
|
+
handleSupabaseError("getCurrentUser", error, "Failed to get current user.");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return createResponse.success<User | null>(user as User | null);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// 重置密码
|
|
77
|
+
export const resetPassword = async (email: string) => {
|
|
78
|
+
const { error } = await supabase.authClient!.auth.resetPasswordForEmail(email);
|
|
79
|
+
|
|
80
|
+
if (error) {
|
|
81
|
+
handleSupabaseError("resetPassword", error, "Password reset request failed.");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return createResponse.success();
|
|
85
|
+
};
|
package/src/core.api.ts
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { pick } from "lodash-es";
|
|
2
|
+
import { createResponse, Target } from "./core.interface";
|
|
3
|
+
import { supabase } from ".";
|
|
4
|
+
import { BaseValidator, handleSupabaseError } from "./core.utils";
|
|
5
|
+
import { PostgrestFilterBuilder } from "@supabase/postgrest-js";
|
|
6
|
+
|
|
7
|
+
export interface QueryFilter {
|
|
8
|
+
field: string;
|
|
9
|
+
operator: "eq";
|
|
10
|
+
value: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface QueryOrderBy {
|
|
14
|
+
field: string;
|
|
15
|
+
ascending: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface BaseQueryParams {
|
|
19
|
+
filterList?: QueryFilter[];
|
|
20
|
+
orderBy?: QueryOrderBy;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const getTarget = async ({ id, filterList }: { id: string; filterList?: QueryFilter[] }) => {
|
|
24
|
+
const query =
|
|
25
|
+
filterList != null && filterList.length > 0
|
|
26
|
+
? filterList.reduce((query, filter) => {
|
|
27
|
+
switch (filter.operator) {
|
|
28
|
+
case "eq":
|
|
29
|
+
return query.eq(filter.field, filter.value);
|
|
30
|
+
default:
|
|
31
|
+
return query;
|
|
32
|
+
}
|
|
33
|
+
}, supabase.client.from("target").select().eq("id", id))
|
|
34
|
+
: supabase.client.from("target").select().eq("id", id);
|
|
35
|
+
|
|
36
|
+
const { data, error } = await query.single();
|
|
37
|
+
if (error) {
|
|
38
|
+
handleSupabaseError("getTarget", error, "Failed to fetch target.");
|
|
39
|
+
}
|
|
40
|
+
return createResponse.success<Target>(data as Target);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const getPossibleTarget = async ({ filterList }: { filterList: QueryFilter[] }) => {
|
|
44
|
+
const query = filterList.reduce((query, filter) => {
|
|
45
|
+
switch (filter.operator) {
|
|
46
|
+
case "eq":
|
|
47
|
+
return query.eq(filter.field, filter.value);
|
|
48
|
+
default:
|
|
49
|
+
return query;
|
|
50
|
+
}
|
|
51
|
+
}, supabase.client.from("target").select());
|
|
52
|
+
|
|
53
|
+
const { data, error } = await query.maybeSingle();
|
|
54
|
+
if (error) {
|
|
55
|
+
handleSupabaseError("getPossibleTarget", error, "Failed to fetch target.");
|
|
56
|
+
}
|
|
57
|
+
return createResponse.success<Target | null>(data);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export interface GetTargetListParams extends BaseQueryParams {
|
|
61
|
+
pageNum: number;
|
|
62
|
+
pageSize: number;
|
|
63
|
+
category: Target["category"];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const getTargetList = async ({
|
|
67
|
+
pageNum,
|
|
68
|
+
pageSize,
|
|
69
|
+
category,
|
|
70
|
+
filterList = [],
|
|
71
|
+
orderBy = { field: "created_at", ascending: false },
|
|
72
|
+
filterBuilder,
|
|
73
|
+
}: GetTargetListParams & {
|
|
74
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
75
|
+
filterBuilder?: PostgrestFilterBuilder<any, any, any[], "target", unknown>;
|
|
76
|
+
}) => {
|
|
77
|
+
const query =
|
|
78
|
+
filterBuilder ??
|
|
79
|
+
filterList.reduce((query, filter) => {
|
|
80
|
+
switch (filter.operator) {
|
|
81
|
+
case "eq":
|
|
82
|
+
return query.eq(filter.field, filter.value);
|
|
83
|
+
default:
|
|
84
|
+
return query;
|
|
85
|
+
}
|
|
86
|
+
}, supabase.client.from("target").select("*").eq("category", category));
|
|
87
|
+
|
|
88
|
+
const { data, error } = await query
|
|
89
|
+
.order(orderBy.field, { ascending: orderBy.ascending })
|
|
90
|
+
.range(pageNum * pageSize, (pageNum + 1) * pageSize - 1);
|
|
91
|
+
|
|
92
|
+
if (error) {
|
|
93
|
+
handleSupabaseError("getTargetList", error, "Failed to fetch target list.");
|
|
94
|
+
}
|
|
95
|
+
return createResponse.success<Target[]>(data);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export interface GetTargetTotalCountParams extends BaseQueryParams {
|
|
99
|
+
category: Target["category"];
|
|
100
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
101
|
+
filterBuilder?: PostgrestFilterBuilder<any, any, any[], "target", unknown>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const getTargetTotalCount = async ({
|
|
105
|
+
category,
|
|
106
|
+
filterList = [],
|
|
107
|
+
filterBuilder,
|
|
108
|
+
}: GetTargetTotalCountParams) => {
|
|
109
|
+
const query =
|
|
110
|
+
filterBuilder ??
|
|
111
|
+
filterList.reduce(
|
|
112
|
+
(query, filter) => {
|
|
113
|
+
switch (filter.operator) {
|
|
114
|
+
case "eq":
|
|
115
|
+
return query.eq(filter.field, filter.value);
|
|
116
|
+
default:
|
|
117
|
+
return query;
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
supabase.client
|
|
121
|
+
.from("target")
|
|
122
|
+
.select("id", { count: "exact", head: true }) // head: true means no row payload
|
|
123
|
+
.eq("category", category)
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const { count, error } = await query;
|
|
127
|
+
|
|
128
|
+
if (error) {
|
|
129
|
+
handleSupabaseError("getTargetTotalCount", error, "Failed to fetch target count.");
|
|
130
|
+
}
|
|
131
|
+
return createResponse.success<number>(count ?? 0);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const deleteTarget = async ({ id, filterList }: { id: string; filterList?: QueryFilter[] }) => {
|
|
135
|
+
// Check whether the target exists given filterList
|
|
136
|
+
if (filterList != null && filterList.length > 0) {
|
|
137
|
+
const query = filterList.reduce((query, filter) => {
|
|
138
|
+
switch (filter.operator) {
|
|
139
|
+
case "eq":
|
|
140
|
+
return query.eq(filter.field, filter.value);
|
|
141
|
+
default:
|
|
142
|
+
return query;
|
|
143
|
+
}
|
|
144
|
+
}, supabase.client.from("target").select().eq("id", id));
|
|
145
|
+
const { data: existingTarget, error: existingError } = await query.single();
|
|
146
|
+
if (existingError || existingTarget == null) {
|
|
147
|
+
const msg = `[deleteTarget] Cannot find the target ${id}`;
|
|
148
|
+
console.error(msg, { filterList }, existingError);
|
|
149
|
+
throw new Error(msg);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const { error } = await supabase.client.from("target").delete().eq("id", id);
|
|
153
|
+
if (error) {
|
|
154
|
+
handleSupabaseError("deleteTarget", error, `Failed to delete target ${id}.`);
|
|
155
|
+
}
|
|
156
|
+
return createResponse.success();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export interface PostTargetPayload {
|
|
160
|
+
name: Target["name"];
|
|
161
|
+
category: Target["category"];
|
|
162
|
+
value: Target["value"];
|
|
163
|
+
tagList: Target["tagList"];
|
|
164
|
+
extra?: Target["extra"];
|
|
165
|
+
details?: Target["details"];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
class PostTargetPayloadValidator extends BaseValidator<PostTargetPayload> {
|
|
169
|
+
protected requiredFields: (keyof PostTargetPayload)[] = ["category", "name", "value", "tagList"];
|
|
170
|
+
protected optionalFields: (keyof PostTargetPayload)[] = ["extra", "details"];
|
|
171
|
+
|
|
172
|
+
constructor() {
|
|
173
|
+
super();
|
|
174
|
+
// Add custom
|
|
175
|
+
this.addValidator((val) => {
|
|
176
|
+
return true;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export const postTarget = async (payload: PostTargetPayload) => {
|
|
182
|
+
const validPayload = new PostTargetPayloadValidator().validate(payload);
|
|
183
|
+
|
|
184
|
+
const { data, error } = await supabase.client
|
|
185
|
+
.from("target")
|
|
186
|
+
.insert([{ ...validPayload }])
|
|
187
|
+
.select()
|
|
188
|
+
.single();
|
|
189
|
+
|
|
190
|
+
if (error) {
|
|
191
|
+
handleSupabaseError("postTarget", error, "Failed to create target.");
|
|
192
|
+
}
|
|
193
|
+
return createResponse.success<Target>(data as Target);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export interface PatchTargetPayload extends PostTargetPayload {
|
|
197
|
+
id: string;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const patchTarget = async ({ id, ...restPayload }: PatchTargetPayload) => {
|
|
201
|
+
const { data: currentData, error: fetchError } = await supabase.client.from("target").select().eq("id", id).single();
|
|
202
|
+
if (!currentData) {
|
|
203
|
+
throw new Error("Target NOT exists");
|
|
204
|
+
}
|
|
205
|
+
if (fetchError) handleSupabaseError("patchTarget", fetchError, "Failed to fetch target.");
|
|
206
|
+
|
|
207
|
+
const updatedTarget = new PostTargetPayloadValidator().validate(restPayload);
|
|
208
|
+
|
|
209
|
+
const { data, error } = await supabase.client
|
|
210
|
+
.from("target")
|
|
211
|
+
.update({
|
|
212
|
+
...updatedTarget,
|
|
213
|
+
})
|
|
214
|
+
.eq("id", id)
|
|
215
|
+
.select()
|
|
216
|
+
.single();
|
|
217
|
+
|
|
218
|
+
if (error) {
|
|
219
|
+
handleSupabaseError("patchTarget", error, "Failed to update target.");
|
|
220
|
+
}
|
|
221
|
+
return createResponse.success<Target>(data as Target);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export const updateTargetDetails = async <T, D>({
|
|
225
|
+
id,
|
|
226
|
+
updateFn,
|
|
227
|
+
beforeUpdateValidator = () => true,
|
|
228
|
+
updateExtraFn,
|
|
229
|
+
}: {
|
|
230
|
+
id: string;
|
|
231
|
+
updateFn: (existing: D) => D;
|
|
232
|
+
beforeUpdateValidator?: (existing: D) => true | string;
|
|
233
|
+
updateExtraFn?: (existing: D) => string;
|
|
234
|
+
}) => {
|
|
235
|
+
const { data: currentData, error: fetchError } = await supabase.client
|
|
236
|
+
.from("target")
|
|
237
|
+
.select("details")
|
|
238
|
+
.eq("id", id)
|
|
239
|
+
.single();
|
|
240
|
+
if (!currentData) {
|
|
241
|
+
const msg = "[updateTargetDetails] Target NOT exists: " + id;
|
|
242
|
+
console.error(msg);
|
|
243
|
+
throw new Error(msg);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const currentDetails = currentData.details as D;
|
|
247
|
+
|
|
248
|
+
// Check before update
|
|
249
|
+
const validatorResult = beforeUpdateValidator(currentDetails);
|
|
250
|
+
if (validatorResult !== true) {
|
|
251
|
+
throw new Error("[updateTargetDetails] validation failed. " + validatorResult);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const updatedDetails: D = updateFn(currentDetails);
|
|
255
|
+
const updated =
|
|
256
|
+
updateExtraFn == null
|
|
257
|
+
? {
|
|
258
|
+
details: updatedDetails,
|
|
259
|
+
}
|
|
260
|
+
: {
|
|
261
|
+
details: updatedDetails,
|
|
262
|
+
extra: updateExtraFn(currentDetails),
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const { data, error } = await supabase.client.from("target").update(updated).eq("id", id).select().single();
|
|
266
|
+
|
|
267
|
+
if (error) handleSupabaseError("updateTargetDetails", error, "Failed to update target details.");
|
|
268
|
+
return data as T;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export const createTarget = async <T extends Target, P extends object>({
|
|
272
|
+
payload,
|
|
273
|
+
validator,
|
|
274
|
+
createFn,
|
|
275
|
+
checkRedundancyFilterList,
|
|
276
|
+
upsert = false,
|
|
277
|
+
}: {
|
|
278
|
+
payload: P;
|
|
279
|
+
validator?: new () => BaseValidator<P>;
|
|
280
|
+
createFn: (validPayload: P) => Omit<T, "id" | "created_at">;
|
|
281
|
+
checkRedundancyFilterList?: QueryFilter[];
|
|
282
|
+
upsert?: boolean;
|
|
283
|
+
}) => {
|
|
284
|
+
// (Optional)Step 1: validate payload
|
|
285
|
+
const validPayload = validator != null ? new validator().validate(payload) : payload;
|
|
286
|
+
|
|
287
|
+
// (Optional)Step 2: check redundancy
|
|
288
|
+
if (checkRedundancyFilterList != null && checkRedundancyFilterList.length > 0) {
|
|
289
|
+
const query = checkRedundancyFilterList.reduce((query, filter) => {
|
|
290
|
+
switch (filter.operator) {
|
|
291
|
+
case "eq":
|
|
292
|
+
return query.eq(filter.field, filter.value);
|
|
293
|
+
default:
|
|
294
|
+
return query;
|
|
295
|
+
}
|
|
296
|
+
}, supabase.client.from("target").select("id"));
|
|
297
|
+
const { data: existingTarget, error: existingError } = await query.maybeSingle();
|
|
298
|
+
if (existingError) {
|
|
299
|
+
handleSupabaseError("createTarget", existingError, "Failed to check target existence.");
|
|
300
|
+
}
|
|
301
|
+
if (existingTarget) {
|
|
302
|
+
if (upsert) {
|
|
303
|
+
const newTarget = createFn(validPayload);
|
|
304
|
+
const updated = pick(newTarget, ["category", "name", "value", "tagList", "extra", "details"]);
|
|
305
|
+
const { data, error } = await supabase.client
|
|
306
|
+
.from("target")
|
|
307
|
+
.update(updated)
|
|
308
|
+
.eq("id", existingTarget.id)
|
|
309
|
+
.select()
|
|
310
|
+
.single();
|
|
311
|
+
if (error) {
|
|
312
|
+
handleSupabaseError("createTarget", error, "Failed to update existing target.");
|
|
313
|
+
}
|
|
314
|
+
return createResponse.success<T>(data);
|
|
315
|
+
}
|
|
316
|
+
const msg = "[createTarget] Target already exists";
|
|
317
|
+
console.error(msg, checkRedundancyFilterList);
|
|
318
|
+
throw new Error(msg);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Step 3: create target
|
|
323
|
+
const newTarget = createFn(validPayload);
|
|
324
|
+
const { data, error } = await supabase.client.from("target").insert([newTarget]).select().single();
|
|
325
|
+
if (error) {
|
|
326
|
+
handleSupabaseError("createTarget", error, "Failed to create target.");
|
|
327
|
+
}
|
|
328
|
+
return createResponse.success<T>(data);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
export function validateWith<P extends object, V extends BaseValidator<P>>(ValidatorClass: new () => V) {
|
|
332
|
+
return <R>(fn: (validPayload: P) => R) => {
|
|
333
|
+
return (payload: P): R => {
|
|
334
|
+
const validPayload = new ValidatorClass().validate(payload);
|
|
335
|
+
return fn(validPayload);
|
|
336
|
+
};
|
|
337
|
+
};
|
|
338
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { PostgrestError } from "@supabase/supabase-js";
|
|
2
|
+
|
|
3
|
+
export interface Target {
|
|
4
|
+
id: string;
|
|
5
|
+
category: string;
|
|
6
|
+
name: string;
|
|
7
|
+
value: string;
|
|
8
|
+
tagList: string[];
|
|
9
|
+
extra?: string;
|
|
10
|
+
details?: unknown;
|
|
11
|
+
created_at: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export enum StatusCode {
|
|
15
|
+
SUCCESS = 200,
|
|
16
|
+
ERROR = 400,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SupabaseResponse<T = unknown> {
|
|
20
|
+
status_code: StatusCode;
|
|
21
|
+
success: boolean;
|
|
22
|
+
message?: string;
|
|
23
|
+
data?: T;
|
|
24
|
+
error?: {
|
|
25
|
+
message: string;
|
|
26
|
+
code?: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const createResponse = {
|
|
31
|
+
success<T>(data?: T, message?: string): SupabaseResponse<T> {
|
|
32
|
+
return {
|
|
33
|
+
status_code: StatusCode.SUCCESS,
|
|
34
|
+
success: true,
|
|
35
|
+
message,
|
|
36
|
+
data,
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
error(
|
|
41
|
+
message: string | { key: string; error: PostgrestError } | { key: string; error: string },
|
|
42
|
+
statusCode?: StatusCode,
|
|
43
|
+
code?: string
|
|
44
|
+
): SupabaseResponse {
|
|
45
|
+
if (typeof message === "string") {
|
|
46
|
+
return {
|
|
47
|
+
status_code: statusCode ?? StatusCode.ERROR,
|
|
48
|
+
success: false,
|
|
49
|
+
error: {
|
|
50
|
+
message,
|
|
51
|
+
code,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (typeof message?.error === "string") {
|
|
56
|
+
return {
|
|
57
|
+
status_code: statusCode ?? StatusCode.ERROR,
|
|
58
|
+
success: false,
|
|
59
|
+
error: {
|
|
60
|
+
message: `Error from ${message.key}: ${message.error}`,
|
|
61
|
+
code,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
status_code: statusCode ?? StatusCode.ERROR,
|
|
67
|
+
success: false,
|
|
68
|
+
error: {
|
|
69
|
+
message: `Error from ${message.key}: ${message?.error?.message}`,
|
|
70
|
+
code,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { pick, pickBy } from "lodash-es";
|
|
2
|
+
|
|
3
|
+
/** True when running in a development build (Vite or Node). Defaults to false for safety. */
|
|
4
|
+
export function isDevEnvironment(): boolean {
|
|
5
|
+
if (typeof import.meta !== "undefined" && "env" in import.meta) {
|
|
6
|
+
const env = (import.meta as ImportMeta & { env?: { DEV?: boolean; MODE?: string } }).env;
|
|
7
|
+
if (env?.DEV === true || env?.MODE === "development") return true;
|
|
8
|
+
if (env?.DEV === false || env?.MODE === "production") return false;
|
|
9
|
+
}
|
|
10
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV) {
|
|
11
|
+
return process.env.NODE_ENV !== "production";
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function logDev(...args: unknown[]): void {
|
|
17
|
+
if (isDevEnvironment()) {
|
|
18
|
+
console.log(...args);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function logDevGroup(label: string, fn: () => void): void {
|
|
23
|
+
if (!isDevEnvironment()) return;
|
|
24
|
+
console.group(label);
|
|
25
|
+
try {
|
|
26
|
+
fn();
|
|
27
|
+
} finally {
|
|
28
|
+
console.groupEnd();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Log internal details, then throw a sanitized error safe to expose to callers. */
|
|
33
|
+
export function handleSupabaseError(context: string, error: unknown, publicMessage?: string): never {
|
|
34
|
+
console.error(`[${context}] failed:`, error);
|
|
35
|
+
throw new Error(publicMessage ?? `[${context}] Operation failed`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Raw payload -> Available data */
|
|
39
|
+
export abstract class BaseValidator<T extends object> {
|
|
40
|
+
protected requiredFields: (keyof T)[] = [];
|
|
41
|
+
protected optionalFields: (keyof T)[] = [];
|
|
42
|
+
protected ignoredFields: (keyof T)[] = [];
|
|
43
|
+
protected customValidators: ((val: T) => true | string)[] = [];
|
|
44
|
+
|
|
45
|
+
/** @deprecated -> addCustomValidator */
|
|
46
|
+
addValidator(validator: (val: T) => true | string) {
|
|
47
|
+
this.customValidators.push(validator);
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
addCustomValidator(validator: (val: T) => true | string) {
|
|
52
|
+
this.customValidators.push(validator);
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
protected validateField(field: keyof T, value: unknown): boolean {
|
|
57
|
+
// TODO now only empty check, later should add type checks
|
|
58
|
+
return value != null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
validate = (val: T): T => {
|
|
62
|
+
const errors: string[] = [];
|
|
63
|
+
|
|
64
|
+
// Step 1: Check required fields
|
|
65
|
+
this.requiredFields.forEach((field) => {
|
|
66
|
+
if (!this.validateField(field, val[field])) {
|
|
67
|
+
errors.push(`Missing required field: ${String(field)}`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Step 2: Run custom validators
|
|
72
|
+
this.customValidators.forEach((validator) => {
|
|
73
|
+
const result = validator(val);
|
|
74
|
+
if (result !== true) {
|
|
75
|
+
errors.push(`Custom validation failed ${result}`);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (errors.length > 0) {
|
|
80
|
+
const errorCount = errors.length;
|
|
81
|
+
const errorList = errors.map((error, index) => ` ${index + 1}. ${error}`).join("\n");
|
|
82
|
+
|
|
83
|
+
throw new Error(
|
|
84
|
+
`[${this.constructor.name}] Validation failed (${errorCount} error${errorCount > 1 ? "s" : ""}):\n${errorList}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Step 3: Pick required and optional fields
|
|
89
|
+
const required = pick(val, this.requiredFields);
|
|
90
|
+
const optional = pickBy(val, (value, key) => {
|
|
91
|
+
const typedKey = key as keyof T;
|
|
92
|
+
return this.optionalFields.includes(typedKey) && value != null;
|
|
93
|
+
}) as Partial<T>;
|
|
94
|
+
// TODO ignoredFields
|
|
95
|
+
|
|
96
|
+
return { ...required, ...optional } as T;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Target } from "../core.interface";
|
|
2
|
+
|
|
3
|
+
export interface Feed extends Target {
|
|
4
|
+
category: CategoryFeed;
|
|
5
|
+
details: FeedDetails;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export enum CategoryFeed {
|
|
9
|
+
FEED = "feed",
|
|
10
|
+
DOC = "doc",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FeedDetails {
|
|
14
|
+
manifestVersion: number;
|
|
15
|
+
list: FeedItem[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export enum FeedItemType {
|
|
19
|
+
/** Plain Target */
|
|
20
|
+
TEXT = "TEXT",
|
|
21
|
+
/** Parcel */
|
|
22
|
+
IMAGE = "IMAGE",
|
|
23
|
+
/** Parcel */
|
|
24
|
+
VIDEO = "VIDEO",
|
|
25
|
+
/** TODO Review */
|
|
26
|
+
// REVIEW = "REVIEW",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface FeedItem {
|
|
30
|
+
index: number;
|
|
31
|
+
type: FeedItemType;
|
|
32
|
+
refTargetId: string;
|
|
33
|
+
}
|