@weirdscience/based-client 0.4.1 → 0.6.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/README.md +267 -49
- package/dist/client.d.ts.map +1 -1
- package/dist/core.cjs +376 -0
- package/dist/core.d.ts +4 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +334 -0
- package/dist/hooks/use-mutation.d.ts +1 -3
- package/dist/hooks/use-mutation.d.ts.map +1 -1
- package/dist/hooks/use-query.d.ts.map +1 -1
- package/dist/hooks/use-record.d.ts.map +1 -1
- package/dist/hooks/use-user.d.ts.map +1 -1
- package/dist/index.cjs +593 -0
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +132 -38
- package/dist/provider.d.ts.map +1 -1
- package/dist/types.d.ts +44 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +25 -6
package/dist/core.cjs
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
function __accessProp(key) {
|
|
6
|
+
return this[key];
|
|
7
|
+
}
|
|
8
|
+
var __toCommonJS = (from) => {
|
|
9
|
+
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
|
10
|
+
if (entry)
|
|
11
|
+
return entry;
|
|
12
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (var key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(entry, key))
|
|
16
|
+
__defProp(entry, key, {
|
|
17
|
+
get: __accessProp.bind(from, key),
|
|
18
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
__moduleCache.set(from, entry);
|
|
22
|
+
return entry;
|
|
23
|
+
};
|
|
24
|
+
var __moduleCache;
|
|
25
|
+
var __returnValue = (v) => v;
|
|
26
|
+
function __exportSetter(name, newValue) {
|
|
27
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
28
|
+
}
|
|
29
|
+
var __export = (target, all) => {
|
|
30
|
+
for (var name in all)
|
|
31
|
+
__defProp(target, name, {
|
|
32
|
+
get: all[name],
|
|
33
|
+
enumerable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
set: __exportSetter.bind(all, name)
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/core.ts
|
|
40
|
+
var exports_core = {};
|
|
41
|
+
__export(exports_core, {
|
|
42
|
+
createClient: () => createClient,
|
|
43
|
+
BasedError: () => BasedError
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(exports_core);
|
|
46
|
+
|
|
47
|
+
// src/types.ts
|
|
48
|
+
class BasedError extends Error {
|
|
49
|
+
code;
|
|
50
|
+
status;
|
|
51
|
+
details;
|
|
52
|
+
constructor(code, message, status, details) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "BasedError";
|
|
55
|
+
this.code = code;
|
|
56
|
+
this.status = status;
|
|
57
|
+
this.details = details;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/client.ts
|
|
62
|
+
var DEFAULT_STORAGE_KEY = "based.session";
|
|
63
|
+
function defaultStorage() {
|
|
64
|
+
if (typeof globalThis === "undefined")
|
|
65
|
+
return null;
|
|
66
|
+
const ls = globalThis.localStorage;
|
|
67
|
+
if (!ls || typeof ls.getItem !== "function")
|
|
68
|
+
return null;
|
|
69
|
+
return {
|
|
70
|
+
getItem: (k) => ls.getItem(k),
|
|
71
|
+
setItem: (k, v) => ls.setItem(k, v),
|
|
72
|
+
removeItem: (k) => ls.removeItem(k)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function createClient(options) {
|
|
76
|
+
const { url, anonKey } = options;
|
|
77
|
+
const baseUrl = url.replace(/\/$/, "");
|
|
78
|
+
const storage = options.storage === false ? null : options.storage ?? defaultStorage();
|
|
79
|
+
const storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY;
|
|
80
|
+
let state = {
|
|
81
|
+
user: null,
|
|
82
|
+
accessToken: null,
|
|
83
|
+
refreshToken: null,
|
|
84
|
+
isLoading: !!storage
|
|
85
|
+
};
|
|
86
|
+
const listeners = new Set;
|
|
87
|
+
function notify() {
|
|
88
|
+
for (const listener of listeners)
|
|
89
|
+
listener();
|
|
90
|
+
}
|
|
91
|
+
function subscribe(listener) {
|
|
92
|
+
listeners.add(listener);
|
|
93
|
+
return () => listeners.delete(listener);
|
|
94
|
+
}
|
|
95
|
+
function getState() {
|
|
96
|
+
return state;
|
|
97
|
+
}
|
|
98
|
+
function onAuthStateChange(callback) {
|
|
99
|
+
return subscribe(() => callback(state));
|
|
100
|
+
}
|
|
101
|
+
async function persist() {
|
|
102
|
+
if (!storage)
|
|
103
|
+
return;
|
|
104
|
+
const snapshot = {
|
|
105
|
+
user: state.user,
|
|
106
|
+
accessToken: state.accessToken,
|
|
107
|
+
refreshToken: state.refreshToken
|
|
108
|
+
};
|
|
109
|
+
if (!snapshot.refreshToken) {
|
|
110
|
+
await storage.removeItem(storageKey);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
await storage.setItem(storageKey, JSON.stringify(snapshot));
|
|
114
|
+
}
|
|
115
|
+
function setState(next) {
|
|
116
|
+
state = { ...state, ...next };
|
|
117
|
+
notify();
|
|
118
|
+
persist().catch(() => {});
|
|
119
|
+
}
|
|
120
|
+
const readyPromise = (async () => {
|
|
121
|
+
if (!storage) {
|
|
122
|
+
setState({ isLoading: false });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const raw = await storage.getItem(storageKey);
|
|
127
|
+
if (!raw) {
|
|
128
|
+
setState({ isLoading: false });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const stored = JSON.parse(raw);
|
|
132
|
+
if (!stored.refreshToken) {
|
|
133
|
+
setState({ isLoading: false });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
state = {
|
|
137
|
+
user: stored.user,
|
|
138
|
+
accessToken: stored.accessToken,
|
|
139
|
+
refreshToken: stored.refreshToken,
|
|
140
|
+
isLoading: true
|
|
141
|
+
};
|
|
142
|
+
notify();
|
|
143
|
+
const user = await getUser();
|
|
144
|
+
if (!user) {
|
|
145
|
+
const refreshed = await refreshSession();
|
|
146
|
+
if (refreshed) {
|
|
147
|
+
await getUser();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
try {
|
|
152
|
+
await storage.removeItem(storageKey);
|
|
153
|
+
} catch {}
|
|
154
|
+
} finally {
|
|
155
|
+
setState({ isLoading: false });
|
|
156
|
+
}
|
|
157
|
+
})();
|
|
158
|
+
function ready() {
|
|
159
|
+
return readyPromise;
|
|
160
|
+
}
|
|
161
|
+
async function fetchWithAuth(path, init = {}) {
|
|
162
|
+
const headers = new Headers(init.headers);
|
|
163
|
+
if (state.accessToken) {
|
|
164
|
+
headers.set("Authorization", `Bearer ${state.accessToken}`);
|
|
165
|
+
} else {
|
|
166
|
+
headers.set("apikey", anonKey);
|
|
167
|
+
}
|
|
168
|
+
if (!headers.has("Content-Type") && init.body) {
|
|
169
|
+
headers.set("Content-Type", "application/json");
|
|
170
|
+
}
|
|
171
|
+
const tokenAtRequest = state.accessToken;
|
|
172
|
+
let res = await fetch(`${baseUrl}${path}`, { ...init, headers });
|
|
173
|
+
if (res.status === 401 && state.refreshToken) {
|
|
174
|
+
const alreadyRefreshed = state.accessToken !== tokenAtRequest;
|
|
175
|
+
const refreshed = alreadyRefreshed ? true : await refreshSession();
|
|
176
|
+
if (refreshed && state.accessToken) {
|
|
177
|
+
headers.set("Authorization", `Bearer ${state.accessToken}`);
|
|
178
|
+
res = await fetch(`${baseUrl}${path}`, { ...init, headers });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return res;
|
|
182
|
+
}
|
|
183
|
+
async function authError(res, fallback) {
|
|
184
|
+
const json = await res.json().catch(() => null);
|
|
185
|
+
return new BasedError(json?.error?.code ?? "AUTH_ERROR", json?.error?.message || fallback, res.status, json?.error?.details);
|
|
186
|
+
}
|
|
187
|
+
async function signUp(email, password) {
|
|
188
|
+
const res = await fetch(`${baseUrl}/auth/signup`, {
|
|
189
|
+
method: "POST",
|
|
190
|
+
headers: { "Content-Type": "application/json" },
|
|
191
|
+
body: JSON.stringify({ email, password })
|
|
192
|
+
});
|
|
193
|
+
if (!res.ok) {
|
|
194
|
+
throw await authError(res, "Sign up failed");
|
|
195
|
+
}
|
|
196
|
+
const { data } = await res.json();
|
|
197
|
+
setState({
|
|
198
|
+
user: data.user,
|
|
199
|
+
accessToken: data.accessToken,
|
|
200
|
+
refreshToken: data.refreshToken,
|
|
201
|
+
isLoading: false
|
|
202
|
+
});
|
|
203
|
+
return data.user;
|
|
204
|
+
}
|
|
205
|
+
async function signIn(email, password) {
|
|
206
|
+
const res = await fetch(`${baseUrl}/auth/signin`, {
|
|
207
|
+
method: "POST",
|
|
208
|
+
headers: { "Content-Type": "application/json" },
|
|
209
|
+
body: JSON.stringify({ email, password })
|
|
210
|
+
});
|
|
211
|
+
if (!res.ok) {
|
|
212
|
+
throw await authError(res, "Sign in failed");
|
|
213
|
+
}
|
|
214
|
+
const { data } = await res.json();
|
|
215
|
+
setState({
|
|
216
|
+
user: data.user,
|
|
217
|
+
accessToken: data.accessToken,
|
|
218
|
+
refreshToken: data.refreshToken,
|
|
219
|
+
isLoading: false
|
|
220
|
+
});
|
|
221
|
+
return data.user;
|
|
222
|
+
}
|
|
223
|
+
async function signOut() {
|
|
224
|
+
if (state.accessToken) {
|
|
225
|
+
try {
|
|
226
|
+
await fetch(`${baseUrl}/auth/signout`, {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers: { Authorization: `Bearer ${state.accessToken}` }
|
|
229
|
+
});
|
|
230
|
+
} catch {}
|
|
231
|
+
}
|
|
232
|
+
setState({ user: null, accessToken: null, refreshToken: null, isLoading: false });
|
|
233
|
+
}
|
|
234
|
+
let refreshInFlight = null;
|
|
235
|
+
function refreshSession() {
|
|
236
|
+
if (!refreshInFlight) {
|
|
237
|
+
refreshInFlight = doRefresh().finally(() => {
|
|
238
|
+
refreshInFlight = null;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return refreshInFlight;
|
|
242
|
+
}
|
|
243
|
+
async function doRefresh() {
|
|
244
|
+
if (!state.refreshToken)
|
|
245
|
+
return false;
|
|
246
|
+
try {
|
|
247
|
+
const res = await fetch(`${baseUrl}/auth/refresh`, {
|
|
248
|
+
method: "POST",
|
|
249
|
+
headers: { "Content-Type": "application/json" },
|
|
250
|
+
body: JSON.stringify({ refreshToken: state.refreshToken })
|
|
251
|
+
});
|
|
252
|
+
if (!res.ok) {
|
|
253
|
+
setState({ user: null, accessToken: null, refreshToken: null, isLoading: false });
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
const { data } = await res.json();
|
|
257
|
+
setState({
|
|
258
|
+
accessToken: data.accessToken,
|
|
259
|
+
refreshToken: data.refreshToken
|
|
260
|
+
});
|
|
261
|
+
return true;
|
|
262
|
+
} catch {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async function getUser() {
|
|
267
|
+
if (!state.accessToken)
|
|
268
|
+
return null;
|
|
269
|
+
try {
|
|
270
|
+
const res = await fetch(`${baseUrl}/auth/me`, {
|
|
271
|
+
headers: { Authorization: `Bearer ${state.accessToken}` }
|
|
272
|
+
});
|
|
273
|
+
if (!res.ok) {
|
|
274
|
+
if (res.status === 401) {
|
|
275
|
+
setState({ user: null });
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
const { data } = await res.json();
|
|
280
|
+
const user = { id: data.id, email: data.email, role: data.role };
|
|
281
|
+
setState({ user });
|
|
282
|
+
return user;
|
|
283
|
+
} catch {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async function parseOrThrow(res, fallbackCode) {
|
|
288
|
+
let json = {};
|
|
289
|
+
try {
|
|
290
|
+
json = await res.json();
|
|
291
|
+
} catch {}
|
|
292
|
+
if (!res.ok) {
|
|
293
|
+
const err = json.error;
|
|
294
|
+
throw new BasedError(err?.code || fallbackCode, err?.message || `Request failed with status ${res.status}`, res.status, err?.details);
|
|
295
|
+
}
|
|
296
|
+
return json;
|
|
297
|
+
}
|
|
298
|
+
function from(table) {
|
|
299
|
+
const basePath = `/api/${table}`;
|
|
300
|
+
return {
|
|
301
|
+
async select(opts) {
|
|
302
|
+
const params = new URLSearchParams;
|
|
303
|
+
if (opts?.limit !== undefined)
|
|
304
|
+
params.set("limit", String(opts.limit));
|
|
305
|
+
if (opts?.offset !== undefined)
|
|
306
|
+
params.set("offset", String(opts.offset));
|
|
307
|
+
if (opts?.order !== undefined)
|
|
308
|
+
params.set("order", opts.order);
|
|
309
|
+
if (opts?.filter) {
|
|
310
|
+
for (const [key, value] of Object.entries(opts.filter)) {
|
|
311
|
+
if (value !== undefined)
|
|
312
|
+
params.set(key, String(value));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const query = params.toString();
|
|
316
|
+
const res = await fetchWithAuth(`${basePath}${query ? `?${query}` : ""}`);
|
|
317
|
+
const json = await parseOrThrow(res, "SELECT_FAILED");
|
|
318
|
+
return { data: json.data, total: json.total ?? json.data.length };
|
|
319
|
+
},
|
|
320
|
+
async get(id) {
|
|
321
|
+
const res = await fetchWithAuth(`${basePath}/${encodeURIComponent(id)}`);
|
|
322
|
+
if (res.status === 404)
|
|
323
|
+
return null;
|
|
324
|
+
const json = await parseOrThrow(res, "GET_FAILED");
|
|
325
|
+
return json.data;
|
|
326
|
+
},
|
|
327
|
+
async insert(data) {
|
|
328
|
+
const res = await fetchWithAuth(basePath, {
|
|
329
|
+
method: "POST",
|
|
330
|
+
body: JSON.stringify(data)
|
|
331
|
+
});
|
|
332
|
+
const json = await parseOrThrow(res, "INSERT_FAILED");
|
|
333
|
+
return json.data;
|
|
334
|
+
},
|
|
335
|
+
async update(id, data) {
|
|
336
|
+
const body = { ...data };
|
|
337
|
+
delete body.id;
|
|
338
|
+
const res = await fetchWithAuth(`${basePath}/${encodeURIComponent(id)}`, {
|
|
339
|
+
method: "PUT",
|
|
340
|
+
body: JSON.stringify(body)
|
|
341
|
+
});
|
|
342
|
+
const json = await parseOrThrow(res, "UPDATE_FAILED");
|
|
343
|
+
return json.data;
|
|
344
|
+
},
|
|
345
|
+
async upsert(data) {
|
|
346
|
+
const body = { ...data };
|
|
347
|
+
const id = body.id;
|
|
348
|
+
if (!id) {
|
|
349
|
+
throw new BasedError("MISSING_ID", "upsert() requires `data.id`. Use insert() to let the server generate one.", 400);
|
|
350
|
+
}
|
|
351
|
+
delete body.id;
|
|
352
|
+
const res = await fetchWithAuth(`${basePath}/${encodeURIComponent(id)}`, {
|
|
353
|
+
method: "PUT",
|
|
354
|
+
body: JSON.stringify(body)
|
|
355
|
+
});
|
|
356
|
+
const json = await parseOrThrow(res, "UPSERT_FAILED");
|
|
357
|
+
return json.data;
|
|
358
|
+
},
|
|
359
|
+
async delete(id) {
|
|
360
|
+
const res = await fetchWithAuth(`${basePath}/${encodeURIComponent(id)}`, {
|
|
361
|
+
method: "DELETE"
|
|
362
|
+
});
|
|
363
|
+
await parseOrThrow(res, "DELETE_FAILED");
|
|
364
|
+
return { deleted: true };
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
auth: { signUp, signIn, signOut, refreshSession, getUser, onAuthStateChange },
|
|
370
|
+
fetch: fetchWithAuth,
|
|
371
|
+
from,
|
|
372
|
+
subscribe,
|
|
373
|
+
getState,
|
|
374
|
+
ready
|
|
375
|
+
};
|
|
376
|
+
}
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createClient } from "./client";
|
|
2
|
+
export { BasedError } from "./types";
|
|
3
|
+
export type { BasedClient, BasedClientOptions, BasedTable, ListResult, StorageAdapter, AuthUser, AuthState, QueryOptions, DefaultTables, } from "./types";
|
|
4
|
+
//# sourceMappingURL=core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErC,YAAY,EACV,WAAW,EACX,kBAAkB,EAClB,UAAU,EACV,UAAU,EACV,cAAc,EACd,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,aAAa,GACd,MAAM,SAAS,CAAC"}
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
class BasedError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
status;
|
|
5
|
+
details;
|
|
6
|
+
constructor(code, message, status, details) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "BasedError";
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.details = details;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/client.ts
|
|
16
|
+
var DEFAULT_STORAGE_KEY = "based.session";
|
|
17
|
+
function defaultStorage() {
|
|
18
|
+
if (typeof globalThis === "undefined")
|
|
19
|
+
return null;
|
|
20
|
+
const ls = globalThis.localStorage;
|
|
21
|
+
if (!ls || typeof ls.getItem !== "function")
|
|
22
|
+
return null;
|
|
23
|
+
return {
|
|
24
|
+
getItem: (k) => ls.getItem(k),
|
|
25
|
+
setItem: (k, v) => ls.setItem(k, v),
|
|
26
|
+
removeItem: (k) => ls.removeItem(k)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function createClient(options) {
|
|
30
|
+
const { url, anonKey } = options;
|
|
31
|
+
const baseUrl = url.replace(/\/$/, "");
|
|
32
|
+
const storage = options.storage === false ? null : options.storage ?? defaultStorage();
|
|
33
|
+
const storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY;
|
|
34
|
+
let state = {
|
|
35
|
+
user: null,
|
|
36
|
+
accessToken: null,
|
|
37
|
+
refreshToken: null,
|
|
38
|
+
isLoading: !!storage
|
|
39
|
+
};
|
|
40
|
+
const listeners = new Set;
|
|
41
|
+
function notify() {
|
|
42
|
+
for (const listener of listeners)
|
|
43
|
+
listener();
|
|
44
|
+
}
|
|
45
|
+
function subscribe(listener) {
|
|
46
|
+
listeners.add(listener);
|
|
47
|
+
return () => listeners.delete(listener);
|
|
48
|
+
}
|
|
49
|
+
function getState() {
|
|
50
|
+
return state;
|
|
51
|
+
}
|
|
52
|
+
function onAuthStateChange(callback) {
|
|
53
|
+
return subscribe(() => callback(state));
|
|
54
|
+
}
|
|
55
|
+
async function persist() {
|
|
56
|
+
if (!storage)
|
|
57
|
+
return;
|
|
58
|
+
const snapshot = {
|
|
59
|
+
user: state.user,
|
|
60
|
+
accessToken: state.accessToken,
|
|
61
|
+
refreshToken: state.refreshToken
|
|
62
|
+
};
|
|
63
|
+
if (!snapshot.refreshToken) {
|
|
64
|
+
await storage.removeItem(storageKey);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
await storage.setItem(storageKey, JSON.stringify(snapshot));
|
|
68
|
+
}
|
|
69
|
+
function setState(next) {
|
|
70
|
+
state = { ...state, ...next };
|
|
71
|
+
notify();
|
|
72
|
+
persist().catch(() => {});
|
|
73
|
+
}
|
|
74
|
+
const readyPromise = (async () => {
|
|
75
|
+
if (!storage) {
|
|
76
|
+
setState({ isLoading: false });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const raw = await storage.getItem(storageKey);
|
|
81
|
+
if (!raw) {
|
|
82
|
+
setState({ isLoading: false });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const stored = JSON.parse(raw);
|
|
86
|
+
if (!stored.refreshToken) {
|
|
87
|
+
setState({ isLoading: false });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
state = {
|
|
91
|
+
user: stored.user,
|
|
92
|
+
accessToken: stored.accessToken,
|
|
93
|
+
refreshToken: stored.refreshToken,
|
|
94
|
+
isLoading: true
|
|
95
|
+
};
|
|
96
|
+
notify();
|
|
97
|
+
const user = await getUser();
|
|
98
|
+
if (!user) {
|
|
99
|
+
const refreshed = await refreshSession();
|
|
100
|
+
if (refreshed) {
|
|
101
|
+
await getUser();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
try {
|
|
106
|
+
await storage.removeItem(storageKey);
|
|
107
|
+
} catch {}
|
|
108
|
+
} finally {
|
|
109
|
+
setState({ isLoading: false });
|
|
110
|
+
}
|
|
111
|
+
})();
|
|
112
|
+
function ready() {
|
|
113
|
+
return readyPromise;
|
|
114
|
+
}
|
|
115
|
+
async function fetchWithAuth(path, init = {}) {
|
|
116
|
+
const headers = new Headers(init.headers);
|
|
117
|
+
if (state.accessToken) {
|
|
118
|
+
headers.set("Authorization", `Bearer ${state.accessToken}`);
|
|
119
|
+
} else {
|
|
120
|
+
headers.set("apikey", anonKey);
|
|
121
|
+
}
|
|
122
|
+
if (!headers.has("Content-Type") && init.body) {
|
|
123
|
+
headers.set("Content-Type", "application/json");
|
|
124
|
+
}
|
|
125
|
+
const tokenAtRequest = state.accessToken;
|
|
126
|
+
let res = await fetch(`${baseUrl}${path}`, { ...init, headers });
|
|
127
|
+
if (res.status === 401 && state.refreshToken) {
|
|
128
|
+
const alreadyRefreshed = state.accessToken !== tokenAtRequest;
|
|
129
|
+
const refreshed = alreadyRefreshed ? true : await refreshSession();
|
|
130
|
+
if (refreshed && state.accessToken) {
|
|
131
|
+
headers.set("Authorization", `Bearer ${state.accessToken}`);
|
|
132
|
+
res = await fetch(`${baseUrl}${path}`, { ...init, headers });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return res;
|
|
136
|
+
}
|
|
137
|
+
async function authError(res, fallback) {
|
|
138
|
+
const json = await res.json().catch(() => null);
|
|
139
|
+
return new BasedError(json?.error?.code ?? "AUTH_ERROR", json?.error?.message || fallback, res.status, json?.error?.details);
|
|
140
|
+
}
|
|
141
|
+
async function signUp(email, password) {
|
|
142
|
+
const res = await fetch(`${baseUrl}/auth/signup`, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers: { "Content-Type": "application/json" },
|
|
145
|
+
body: JSON.stringify({ email, password })
|
|
146
|
+
});
|
|
147
|
+
if (!res.ok) {
|
|
148
|
+
throw await authError(res, "Sign up failed");
|
|
149
|
+
}
|
|
150
|
+
const { data } = await res.json();
|
|
151
|
+
setState({
|
|
152
|
+
user: data.user,
|
|
153
|
+
accessToken: data.accessToken,
|
|
154
|
+
refreshToken: data.refreshToken,
|
|
155
|
+
isLoading: false
|
|
156
|
+
});
|
|
157
|
+
return data.user;
|
|
158
|
+
}
|
|
159
|
+
async function signIn(email, password) {
|
|
160
|
+
const res = await fetch(`${baseUrl}/auth/signin`, {
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: { "Content-Type": "application/json" },
|
|
163
|
+
body: JSON.stringify({ email, password })
|
|
164
|
+
});
|
|
165
|
+
if (!res.ok) {
|
|
166
|
+
throw await authError(res, "Sign in failed");
|
|
167
|
+
}
|
|
168
|
+
const { data } = await res.json();
|
|
169
|
+
setState({
|
|
170
|
+
user: data.user,
|
|
171
|
+
accessToken: data.accessToken,
|
|
172
|
+
refreshToken: data.refreshToken,
|
|
173
|
+
isLoading: false
|
|
174
|
+
});
|
|
175
|
+
return data.user;
|
|
176
|
+
}
|
|
177
|
+
async function signOut() {
|
|
178
|
+
if (state.accessToken) {
|
|
179
|
+
try {
|
|
180
|
+
await fetch(`${baseUrl}/auth/signout`, {
|
|
181
|
+
method: "POST",
|
|
182
|
+
headers: { Authorization: `Bearer ${state.accessToken}` }
|
|
183
|
+
});
|
|
184
|
+
} catch {}
|
|
185
|
+
}
|
|
186
|
+
setState({ user: null, accessToken: null, refreshToken: null, isLoading: false });
|
|
187
|
+
}
|
|
188
|
+
let refreshInFlight = null;
|
|
189
|
+
function refreshSession() {
|
|
190
|
+
if (!refreshInFlight) {
|
|
191
|
+
refreshInFlight = doRefresh().finally(() => {
|
|
192
|
+
refreshInFlight = null;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return refreshInFlight;
|
|
196
|
+
}
|
|
197
|
+
async function doRefresh() {
|
|
198
|
+
if (!state.refreshToken)
|
|
199
|
+
return false;
|
|
200
|
+
try {
|
|
201
|
+
const res = await fetch(`${baseUrl}/auth/refresh`, {
|
|
202
|
+
method: "POST",
|
|
203
|
+
headers: { "Content-Type": "application/json" },
|
|
204
|
+
body: JSON.stringify({ refreshToken: state.refreshToken })
|
|
205
|
+
});
|
|
206
|
+
if (!res.ok) {
|
|
207
|
+
setState({ user: null, accessToken: null, refreshToken: null, isLoading: false });
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
const { data } = await res.json();
|
|
211
|
+
setState({
|
|
212
|
+
accessToken: data.accessToken,
|
|
213
|
+
refreshToken: data.refreshToken
|
|
214
|
+
});
|
|
215
|
+
return true;
|
|
216
|
+
} catch {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function getUser() {
|
|
221
|
+
if (!state.accessToken)
|
|
222
|
+
return null;
|
|
223
|
+
try {
|
|
224
|
+
const res = await fetch(`${baseUrl}/auth/me`, {
|
|
225
|
+
headers: { Authorization: `Bearer ${state.accessToken}` }
|
|
226
|
+
});
|
|
227
|
+
if (!res.ok) {
|
|
228
|
+
if (res.status === 401) {
|
|
229
|
+
setState({ user: null });
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
const { data } = await res.json();
|
|
234
|
+
const user = { id: data.id, email: data.email, role: data.role };
|
|
235
|
+
setState({ user });
|
|
236
|
+
return user;
|
|
237
|
+
} catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function parseOrThrow(res, fallbackCode) {
|
|
242
|
+
let json = {};
|
|
243
|
+
try {
|
|
244
|
+
json = await res.json();
|
|
245
|
+
} catch {}
|
|
246
|
+
if (!res.ok) {
|
|
247
|
+
const err = json.error;
|
|
248
|
+
throw new BasedError(err?.code || fallbackCode, err?.message || `Request failed with status ${res.status}`, res.status, err?.details);
|
|
249
|
+
}
|
|
250
|
+
return json;
|
|
251
|
+
}
|
|
252
|
+
function from(table) {
|
|
253
|
+
const basePath = `/api/${table}`;
|
|
254
|
+
return {
|
|
255
|
+
async select(opts) {
|
|
256
|
+
const params = new URLSearchParams;
|
|
257
|
+
if (opts?.limit !== undefined)
|
|
258
|
+
params.set("limit", String(opts.limit));
|
|
259
|
+
if (opts?.offset !== undefined)
|
|
260
|
+
params.set("offset", String(opts.offset));
|
|
261
|
+
if (opts?.order !== undefined)
|
|
262
|
+
params.set("order", opts.order);
|
|
263
|
+
if (opts?.filter) {
|
|
264
|
+
for (const [key, value] of Object.entries(opts.filter)) {
|
|
265
|
+
if (value !== undefined)
|
|
266
|
+
params.set(key, String(value));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const query = params.toString();
|
|
270
|
+
const res = await fetchWithAuth(`${basePath}${query ? `?${query}` : ""}`);
|
|
271
|
+
const json = await parseOrThrow(res, "SELECT_FAILED");
|
|
272
|
+
return { data: json.data, total: json.total ?? json.data.length };
|
|
273
|
+
},
|
|
274
|
+
async get(id) {
|
|
275
|
+
const res = await fetchWithAuth(`${basePath}/${encodeURIComponent(id)}`);
|
|
276
|
+
if (res.status === 404)
|
|
277
|
+
return null;
|
|
278
|
+
const json = await parseOrThrow(res, "GET_FAILED");
|
|
279
|
+
return json.data;
|
|
280
|
+
},
|
|
281
|
+
async insert(data) {
|
|
282
|
+
const res = await fetchWithAuth(basePath, {
|
|
283
|
+
method: "POST",
|
|
284
|
+
body: JSON.stringify(data)
|
|
285
|
+
});
|
|
286
|
+
const json = await parseOrThrow(res, "INSERT_FAILED");
|
|
287
|
+
return json.data;
|
|
288
|
+
},
|
|
289
|
+
async update(id, data) {
|
|
290
|
+
const body = { ...data };
|
|
291
|
+
delete body.id;
|
|
292
|
+
const res = await fetchWithAuth(`${basePath}/${encodeURIComponent(id)}`, {
|
|
293
|
+
method: "PUT",
|
|
294
|
+
body: JSON.stringify(body)
|
|
295
|
+
});
|
|
296
|
+
const json = await parseOrThrow(res, "UPDATE_FAILED");
|
|
297
|
+
return json.data;
|
|
298
|
+
},
|
|
299
|
+
async upsert(data) {
|
|
300
|
+
const body = { ...data };
|
|
301
|
+
const id = body.id;
|
|
302
|
+
if (!id) {
|
|
303
|
+
throw new BasedError("MISSING_ID", "upsert() requires `data.id`. Use insert() to let the server generate one.", 400);
|
|
304
|
+
}
|
|
305
|
+
delete body.id;
|
|
306
|
+
const res = await fetchWithAuth(`${basePath}/${encodeURIComponent(id)}`, {
|
|
307
|
+
method: "PUT",
|
|
308
|
+
body: JSON.stringify(body)
|
|
309
|
+
});
|
|
310
|
+
const json = await parseOrThrow(res, "UPSERT_FAILED");
|
|
311
|
+
return json.data;
|
|
312
|
+
},
|
|
313
|
+
async delete(id) {
|
|
314
|
+
const res = await fetchWithAuth(`${basePath}/${encodeURIComponent(id)}`, {
|
|
315
|
+
method: "DELETE"
|
|
316
|
+
});
|
|
317
|
+
await parseOrThrow(res, "DELETE_FAILED");
|
|
318
|
+
return { deleted: true };
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
auth: { signUp, signIn, signOut, refreshSession, getUser, onAuthStateChange },
|
|
324
|
+
fetch: fetchWithAuth,
|
|
325
|
+
from,
|
|
326
|
+
subscribe,
|
|
327
|
+
getState,
|
|
328
|
+
ready
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
export {
|
|
332
|
+
createClient,
|
|
333
|
+
BasedError
|
|
334
|
+
};
|