@appurist/offlinedb 1.0.1 → 1.0.2
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/package.json +40 -39
- package/src/client.js +826 -587
- package/src/neon.js +274 -209
package/src/neon.js
CHANGED
|
@@ -1,209 +1,274 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thin fetch-based wrappers for Neon Auth and the Neon Data API.
|
|
3
|
-
*
|
|
4
|
-
* The auth endpoint defaults below are inferred from Better Auth's
|
|
5
|
-
* documented email/password routes because Neon Auth is built on Better Auth.
|
|
6
|
-
* Consumers can override endpoint paths if their Neon project uses different
|
|
7
|
-
* URLs or proxy routing.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @param {{
|
|
12
|
-
* baseUrl: string,
|
|
13
|
-
* fetchImpl?: typeof fetch,
|
|
14
|
-
* endpoints?: Partial<{
|
|
15
|
-
* loginWithPassword: string,
|
|
16
|
-
* signUpWithPassword: string,
|
|
17
|
-
* logout: string
|
|
18
|
-
* }>
|
|
19
|
-
* }} options
|
|
20
|
-
*/
|
|
21
|
-
export function createOdbAuthClient(options) {
|
|
22
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
23
|
-
const endpoints = {
|
|
24
|
-
loginWithPassword: "sign-in/email",
|
|
25
|
-
signUpWithPassword: "sign-up/email",
|
|
26
|
-
logout: "sign-out",
|
|
27
|
-
...(options.endpoints ?? {})
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
/**
|
|
32
|
-
* @param {{email: string, password: string, rememberMe?: boolean, callbackURL?: string}} args
|
|
33
|
-
*/
|
|
34
|
-
async odbLoginWithPassword(args) {
|
|
35
|
-
return odbPostJson(fetchImpl, joinUrl(options.baseUrl, endpoints.loginWithPassword), args);
|
|
36
|
-
},
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* @param {{email: string, password: string, name?: string, callbackURL?: string, image?: string}} args
|
|
40
|
-
*/
|
|
41
|
-
async odbSignUpWithPassword(args) {
|
|
42
|
-
return odbPostJson(fetchImpl, joinUrl(options.baseUrl, endpoints.signUpWithPassword), args);
|
|
43
|
-
},
|
|
44
|
-
|
|
45
|
-
async odbLogout() {
|
|
46
|
-
return odbPostJson(fetchImpl, joinUrl(options.baseUrl, endpoints.logout), {});
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* @param {{
|
|
53
|
-
* baseUrl: string,
|
|
54
|
-
* fetchImpl?: typeof fetch,
|
|
55
|
-
* getAuthToken?: () => Promise<string | null> | string | null,
|
|
56
|
-
* headers?: HeadersInit
|
|
57
|
-
* }} options
|
|
58
|
-
*/
|
|
59
|
-
export function createOdbDataClient(options) {
|
|
60
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
/**
|
|
64
|
-
* @param {{path: string, method?: string, body?: unknown, headers?: HeadersInit}} args
|
|
65
|
-
*/
|
|
66
|
-
async odbRequest(args) {
|
|
67
|
-
const token = await options.getAuthToken?.();
|
|
68
|
-
const response = await fetchImpl(joinUrl(options.baseUrl, args.path), {
|
|
69
|
-
method: args.method ?? "GET",
|
|
70
|
-
headers: {
|
|
71
|
-
...(args.body != null ? { "content-type": "application/json" } : {}),
|
|
72
|
-
...(options.headers ?? {}),
|
|
73
|
-
...(args.headers ?? {}),
|
|
74
|
-
...(token ? { authorization: `Bearer ${token}` } : {})
|
|
75
|
-
},
|
|
76
|
-
body: args.body == null ? undefined : JSON.stringify(args.body)
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
if (!response.ok) {
|
|
80
|
-
throw new Error(`Neon Data API request failed with status ${response.status}`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return readJsonIfPossible(response);
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* PostgREST-style RPC call.
|
|
88
|
-
*
|
|
89
|
-
* @param {string} fn
|
|
90
|
-
* @param {Record<string, unknown>=} args
|
|
91
|
-
*/
|
|
92
|
-
async odbRpc(fn, args = {}) {
|
|
93
|
-
return this.odbRequest({
|
|
94
|
-
path: `rpc/${fn}`,
|
|
95
|
-
method: "POST",
|
|
96
|
-
body: args
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Direct Neon-native sync transport.
|
|
104
|
-
*
|
|
105
|
-
* It uses PostgREST-style RPC calls against database functions rather than a
|
|
106
|
-
* custom app-owned sync service.
|
|
107
|
-
*
|
|
108
|
-
* @param {{
|
|
109
|
-
* dataClient: ReturnType<typeof createOdbDataClient>,
|
|
110
|
-
* mutateFunctionName?: ((table: string) => string) | string,
|
|
111
|
-
* pullFunctionName?: string
|
|
112
|
-
*
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Thin fetch-based wrappers for Neon Auth and the Neon Data API.
|
|
3
|
+
*
|
|
4
|
+
* The auth endpoint defaults below are inferred from Better Auth's
|
|
5
|
+
* documented email/password routes because Neon Auth is built on Better Auth.
|
|
6
|
+
* Consumers can override endpoint paths if their Neon project uses different
|
|
7
|
+
* URLs or proxy routing.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {{
|
|
12
|
+
* baseUrl: string,
|
|
13
|
+
* fetchImpl?: typeof fetch,
|
|
14
|
+
* endpoints?: Partial<{
|
|
15
|
+
* loginWithPassword: string,
|
|
16
|
+
* signUpWithPassword: string,
|
|
17
|
+
* logout: string
|
|
18
|
+
* }>
|
|
19
|
+
* }} options
|
|
20
|
+
*/
|
|
21
|
+
export function createOdbAuthClient(options) {
|
|
22
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
23
|
+
const endpoints = {
|
|
24
|
+
loginWithPassword: "sign-in/email",
|
|
25
|
+
signUpWithPassword: "sign-up/email",
|
|
26
|
+
logout: "sign-out",
|
|
27
|
+
...(options.endpoints ?? {})
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
/**
|
|
32
|
+
* @param {{email: string, password: string, rememberMe?: boolean, callbackURL?: string}} args
|
|
33
|
+
*/
|
|
34
|
+
async odbLoginWithPassword(args) {
|
|
35
|
+
return odbPostJson(fetchImpl, joinUrl(options.baseUrl, endpoints.loginWithPassword), args);
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {{email: string, password: string, name?: string, callbackURL?: string, image?: string}} args
|
|
40
|
+
*/
|
|
41
|
+
async odbSignUpWithPassword(args) {
|
|
42
|
+
return odbPostJson(fetchImpl, joinUrl(options.baseUrl, endpoints.signUpWithPassword), args);
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
async odbLogout() {
|
|
46
|
+
return odbPostJson(fetchImpl, joinUrl(options.baseUrl, endpoints.logout), {});
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {{
|
|
53
|
+
* baseUrl: string,
|
|
54
|
+
* fetchImpl?: typeof fetch,
|
|
55
|
+
* getAuthToken?: () => Promise<string | null> | string | null,
|
|
56
|
+
* headers?: HeadersInit
|
|
57
|
+
* }} options
|
|
58
|
+
*/
|
|
59
|
+
export function createOdbDataClient(options) {
|
|
60
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
/**
|
|
64
|
+
* @param {{path: string, method?: string, body?: unknown, headers?: HeadersInit}} args
|
|
65
|
+
*/
|
|
66
|
+
async odbRequest(args) {
|
|
67
|
+
const token = await options.getAuthToken?.();
|
|
68
|
+
const response = await fetchImpl(joinUrl(options.baseUrl, args.path), {
|
|
69
|
+
method: args.method ?? "GET",
|
|
70
|
+
headers: {
|
|
71
|
+
...(args.body != null ? { "content-type": "application/json" } : {}),
|
|
72
|
+
...(options.headers ?? {}),
|
|
73
|
+
...(args.headers ?? {}),
|
|
74
|
+
...(token ? { authorization: `Bearer ${token}` } : {})
|
|
75
|
+
},
|
|
76
|
+
body: args.body == null ? undefined : JSON.stringify(args.body)
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
throw new Error(`Neon Data API request failed with status ${response.status}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return readJsonIfPossible(response);
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* PostgREST-style RPC call.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} fn
|
|
90
|
+
* @param {Record<string, unknown>=} args
|
|
91
|
+
*/
|
|
92
|
+
async odbRpc(fn, args = {}) {
|
|
93
|
+
return this.odbRequest({
|
|
94
|
+
path: `rpc/${fn}`,
|
|
95
|
+
method: "POST",
|
|
96
|
+
body: args
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Direct Neon-native sync transport.
|
|
104
|
+
*
|
|
105
|
+
* It uses PostgREST-style RPC calls against database functions rather than a
|
|
106
|
+
* custom app-owned sync service.
|
|
107
|
+
*
|
|
108
|
+
* @param {{
|
|
109
|
+
* dataClient: ReturnType<typeof createOdbDataClient>,
|
|
110
|
+
* mutateFunctionName?: ((table: string) => string) | string,
|
|
111
|
+
* pullFunctionName?: string,
|
|
112
|
+
* pullPageFunctionName?: string,
|
|
113
|
+
* pullPageSize?: number,
|
|
114
|
+
* pullPageMinSize?: number
|
|
115
|
+
* }} options
|
|
116
|
+
*/
|
|
117
|
+
export function createOdbSyncTransport(options) {
|
|
118
|
+
const mutateFunctionName =
|
|
119
|
+
typeof options.mutateFunctionName === "function"
|
|
120
|
+
? options.mutateFunctionName
|
|
121
|
+
: (table) => options.mutateFunctionName ?? `mutate_${table}`;
|
|
122
|
+
const pullFunctionName = options.pullFunctionName ?? "pull_changes";
|
|
123
|
+
const pullPageFunctionName = options.pullPageFunctionName ?? "pull_changes_page";
|
|
124
|
+
const pullPageSize = Number(options.pullPageSize ?? 500);
|
|
125
|
+
const pullPageMinSize = Number(options.pullPageMinSize ?? 25);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
async sync(request) {
|
|
129
|
+
const accepted = [];
|
|
130
|
+
const conflicts = [];
|
|
131
|
+
|
|
132
|
+
for (const mutation of request.mutations) {
|
|
133
|
+
const result = normalizeRpcResult(
|
|
134
|
+
await options.dataClient.odbRpc(mutateFunctionName(mutation.table), {
|
|
135
|
+
p_local_id: mutation.localId,
|
|
136
|
+
p_row_key: mutation.key,
|
|
137
|
+
p_base_revision: mutation.baseRevision,
|
|
138
|
+
p_values: mutation.values
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
if (result?.ok === false) {
|
|
143
|
+
conflicts.push({
|
|
144
|
+
localId: mutation.localId,
|
|
145
|
+
code: result.code ?? "revision_conflict",
|
|
146
|
+
serverRow: result.row ?? null
|
|
147
|
+
});
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
accepted.push({
|
|
152
|
+
localId: result?.localId ?? mutation.localId,
|
|
153
|
+
globalId: Number(result?.globalId ?? 0),
|
|
154
|
+
row: result?.row ?? null
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const pull = await pullChangesPaged({
|
|
159
|
+
dataClient: options.dataClient,
|
|
160
|
+
pullFunctionName,
|
|
161
|
+
pullPageFunctionName,
|
|
162
|
+
request,
|
|
163
|
+
initialPageSize: pullPageSize,
|
|
164
|
+
minPageSize: pullPageMinSize
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
accepted,
|
|
169
|
+
conflicts,
|
|
170
|
+
changes: (pull.changes ?? []).map((entry) => ({
|
|
171
|
+
table: entry.table,
|
|
172
|
+
key: entry.key,
|
|
173
|
+
row: entry.row ?? null
|
|
174
|
+
})),
|
|
175
|
+
lastGlobalId: Number(pull.lastGlobalId ?? request.lastGlobalId ?? 0)
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function pullChangesPaged(args) {
|
|
182
|
+
let pageSize = args.initialPageSize;
|
|
183
|
+
let lastGlobalId = Number(args.request.lastGlobalId ?? 0);
|
|
184
|
+
const changes = [];
|
|
185
|
+
|
|
186
|
+
while (true) {
|
|
187
|
+
let page;
|
|
188
|
+
try {
|
|
189
|
+
page = normalizeRpcResult(
|
|
190
|
+
await args.dataClient.odbRpc(args.pullPageFunctionName, {
|
|
191
|
+
p_last_global_id: lastGlobalId,
|
|
192
|
+
p_tables: args.request.tables?.length ? args.request.tables : null,
|
|
193
|
+
p_limit: pageSize
|
|
194
|
+
})
|
|
195
|
+
);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (isMissingPagedFunction(error)) {
|
|
198
|
+
return normalizeRpcResult(
|
|
199
|
+
await args.dataClient.odbRpc(args.pullFunctionName, {
|
|
200
|
+
p_last_global_id: lastGlobalId,
|
|
201
|
+
p_tables: args.request.tables?.length ? args.request.tables : null
|
|
202
|
+
})
|
|
203
|
+
) ?? { changes: [], lastGlobalId };
|
|
204
|
+
}
|
|
205
|
+
if (!isResponseTooLarge(error) || pageSize <= args.minPageSize) {
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
pageSize = Math.max(args.minPageSize, Math.floor(pageSize / 2));
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!page || !Array.isArray(page.changes)) {
|
|
213
|
+
page = normalizeRpcResult(
|
|
214
|
+
await args.dataClient.odbRpc(args.pullFunctionName, {
|
|
215
|
+
p_last_global_id: lastGlobalId,
|
|
216
|
+
p_tables: args.request.tables?.length ? args.request.tables : null
|
|
217
|
+
})
|
|
218
|
+
) ?? { changes: [], lastGlobalId };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
changes.push(...(page.changes ?? []));
|
|
222
|
+
const nextGlobalId = Number(page.lastGlobalId ?? lastGlobalId);
|
|
223
|
+
if (nextGlobalId <= lastGlobalId || page.hasMore !== true) {
|
|
224
|
+
return { changes, lastGlobalId: nextGlobalId };
|
|
225
|
+
}
|
|
226
|
+
lastGlobalId = nextGlobalId;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function isResponseTooLarge(error) {
|
|
231
|
+
return /response is too large|max is 10485760/i.test(error?.message ?? "");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function isMissingPagedFunction(error) {
|
|
235
|
+
return /status 404|function .*pull_changes_page|could not find.*pull_changes_page|schema cache/i.test(error?.message ?? "");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function odbPostJson(fetchImpl, url, body) {
|
|
239
|
+
const response = await fetchImpl(url, {
|
|
240
|
+
method: "POST",
|
|
241
|
+
headers: {
|
|
242
|
+
"content-type": "application/json"
|
|
243
|
+
},
|
|
244
|
+
body: JSON.stringify(body)
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
if (!response.ok) {
|
|
248
|
+
throw new Error(`Auth request failed with status ${response.status}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return readJsonIfPossible(response);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function readJsonIfPossible(response) {
|
|
255
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
256
|
+
if (contentType.includes("application/json")) {
|
|
257
|
+
return response.json();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return response.text();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function normalizeRpcResult(value) {
|
|
264
|
+
if (Array.isArray(value)) {
|
|
265
|
+
return value[0] ?? null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return value ?? null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function joinUrl(baseUrl, path) {
|
|
272
|
+
const normalizedBase = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
273
|
+
return new URL(path, normalizedBase);
|
|
274
|
+
}
|