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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/dist/auth/auth.api.d.ts +28 -0
  4. package/dist/auth/auth.api.d.ts.map +1 -0
  5. package/dist/auth/auth.api.js +50 -0
  6. package/dist/auth/auth.api.js.map +1 -0
  7. package/dist/core.api.d.ts +68 -0
  8. package/dist/core.api.d.ts.map +1 -0
  9. package/dist/core.api.js +226 -0
  10. package/dist/core.api.js.map +1 -0
  11. package/dist/core.interface.d.ts +36 -0
  12. package/dist/core.interface.d.ts.map +1 -0
  13. package/dist/core.interface.js +46 -0
  14. package/dist/core.interface.js.map +1 -0
  15. package/dist/core.utils.d.ts +19 -0
  16. package/dist/core.utils.d.ts.map +1 -0
  17. package/dist/core.utils.js +86 -0
  18. package/dist/core.utils.js.map +1 -0
  19. package/dist/feed/feed.interface.d.ts +27 -0
  20. package/dist/feed/feed.interface.d.ts.map +1 -0
  21. package/dist/feed/feed.interface.js +17 -0
  22. package/dist/feed/feed.interface.js.map +1 -0
  23. package/dist/file/file-manager.d.ts +24 -0
  24. package/dist/file/file-manager.d.ts.map +1 -0
  25. package/dist/file/file-manager.js +109 -0
  26. package/dist/file/file-manager.js.map +1 -0
  27. package/dist/file/file.api.d.ts +15 -0
  28. package/dist/file/file.api.d.ts.map +1 -0
  29. package/dist/file/file.api.js +30 -0
  30. package/dist/file/file.api.js.map +1 -0
  31. package/dist/file/file.interface.d.ts +25 -0
  32. package/dist/file/file.interface.d.ts.map +1 -0
  33. package/dist/file/file.interface.js +9 -0
  34. package/dist/file/file.interface.js.map +1 -0
  35. package/dist/file-list/file-list.api.d.ts +15 -0
  36. package/dist/file-list/file-list.api.d.ts.map +1 -0
  37. package/dist/file-list/file-list.api.js +54 -0
  38. package/dist/file-list/file-list.api.js.map +1 -0
  39. package/dist/file-list/file-list.interface.d.ts +22 -0
  40. package/dist/file-list/file-list.interface.d.ts.map +1 -0
  41. package/dist/file-list/file-list.interface.js +5 -0
  42. package/dist/file-list/file-list.interface.js.map +1 -0
  43. package/dist/idea/idea.interface.d.ts +25 -0
  44. package/dist/idea/idea.interface.d.ts.map +1 -0
  45. package/dist/idea/idea.interface.js +15 -0
  46. package/dist/idea/idea.interface.js.map +1 -0
  47. package/dist/index.d.ts +33 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +28 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/link/link.interface.d.ts +18 -0
  52. package/dist/link/link.interface.d.ts.map +1 -0
  53. package/dist/link/link.interface.js +6 -0
  54. package/dist/link/link.interface.js.map +1 -0
  55. package/dist/parcel/parcel-manager.d.ts +39 -0
  56. package/dist/parcel/parcel-manager.d.ts.map +1 -0
  57. package/dist/parcel/parcel-manager.js +156 -0
  58. package/dist/parcel/parcel-manager.js.map +1 -0
  59. package/dist/parcel/parcel.interface.d.ts +34 -0
  60. package/dist/parcel/parcel.interface.d.ts.map +1 -0
  61. package/dist/parcel/parcel.interface.js +11 -0
  62. package/dist/parcel/parcel.interface.js.map +1 -0
  63. package/dist/question-list/question-list.api.d.ts +13 -0
  64. package/dist/question-list/question-list.api.d.ts.map +1 -0
  65. package/dist/question-list/question-list.api.js +35 -0
  66. package/dist/question-list/question-list.api.js.map +1 -0
  67. package/dist/question-list/question-list.interface.d.ts +18 -0
  68. package/dist/question-list/question-list.interface.d.ts.map +1 -0
  69. package/dist/question-list/question-list.interface.js +5 -0
  70. package/dist/question-list/question-list.interface.js.map +1 -0
  71. package/dist/review/review.api.d.ts +14 -0
  72. package/dist/review/review.api.d.ts.map +1 -0
  73. package/dist/review/review.api.js +133 -0
  74. package/dist/review/review.api.js.map +1 -0
  75. package/dist/review/review.interface.d.ts +24 -0
  76. package/dist/review/review.interface.d.ts.map +1 -0
  77. package/dist/review/review.interface.js +10 -0
  78. package/dist/review/review.interface.js.map +1 -0
  79. package/dist/review-v2/apis/post-review-setting.api.d.ts +12 -0
  80. package/dist/review-v2/apis/post-review-setting.api.d.ts.map +1 -0
  81. package/dist/review-v2/apis/post-review-setting.api.js +27 -0
  82. package/dist/review-v2/apis/post-review-setting.api.js.map +1 -0
  83. package/dist/review-v2/apis/post-review.api.d.ts +13 -0
  84. package/dist/review-v2/apis/post-review.api.d.ts.map +1 -0
  85. package/dist/review-v2/apis/post-review.api.js +46 -0
  86. package/dist/review-v2/apis/post-review.api.js.map +1 -0
  87. package/dist/review-v2/review-setting.interface.d.ts +23 -0
  88. package/dist/review-v2/review-setting.interface.d.ts.map +1 -0
  89. package/dist/review-v2/review-setting.interface.js +5 -0
  90. package/dist/review-v2/review-setting.interface.js.map +1 -0
  91. package/dist/review-v2/review.interface.d.ts +20 -0
  92. package/dist/review-v2/review.interface.d.ts.map +1 -0
  93. package/dist/review-v2/review.interface.js +5 -0
  94. package/dist/review-v2/review.interface.js.map +1 -0
  95. package/dist/service/api.interface.d.ts +35 -0
  96. package/dist/service/api.interface.d.ts.map +1 -0
  97. package/dist/service/api.interface.js +12 -0
  98. package/dist/service/api.interface.js.map +1 -0
  99. package/dist/service/base.interface.d.ts +83 -0
  100. package/dist/service/base.interface.d.ts.map +1 -0
  101. package/dist/service/base.interface.js +7 -0
  102. package/dist/service/base.interface.js.map +1 -0
  103. package/dist/service/service.interface.d.ts +24 -0
  104. package/dist/service/service.interface.d.ts.map +1 -0
  105. package/dist/service/service.interface.js +11 -0
  106. package/dist/service/service.interface.js.map +1 -0
  107. package/dist/supabase.d.ts +32 -0
  108. package/dist/supabase.d.ts.map +1 -0
  109. package/dist/supabase.js +100 -0
  110. package/dist/supabase.js.map +1 -0
  111. package/package.json +45 -0
  112. package/src/auth/auth.api.ts +85 -0
  113. package/src/core.api.ts +338 -0
  114. package/src/core.interface.ts +74 -0
  115. package/src/core.utils.ts +98 -0
  116. package/src/feed/feed.interface.ts +33 -0
  117. package/src/file/file-manager.ts +145 -0
  118. package/src/file/file.api.ts +43 -0
  119. package/src/file/file.interface.ts +30 -0
  120. package/src/file-list/file-list.api.ts +64 -0
  121. package/src/file-list/file-list.interface.ts +24 -0
  122. package/src/idea/idea.interface.ts +28 -0
  123. package/src/index.ts +50 -0
  124. package/src/link/link.interface.ts +21 -0
  125. package/src/parcel/parcel-manager.ts +209 -0
  126. package/src/parcel/parcel.interface.ts +40 -0
  127. package/src/question-list/question-list.api.ts +42 -0
  128. package/src/question-list/question-list.interface.ts +21 -0
  129. package/src/review/review.api.ts +145 -0
  130. package/src/review/review.interface.ts +26 -0
  131. package/src/review-v2/apis/post-review-setting.api.ts +35 -0
  132. package/src/review-v2/apis/post-review.api.ts +59 -0
  133. package/src/review-v2/review-setting.interface.ts +26 -0
  134. package/src/review-v2/review.interface.ts +23 -0
  135. package/src/service/api.interface.ts +47 -0
  136. package/src/service/base.interface.ts +97 -0
  137. package/src/service/service.interface.ts +27 -0
  138. 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
+ };
@@ -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
+ }