lazypock 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/README.md +226 -0
- package/dist/index.cjs +971 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +520 -0
- package/dist/index.d.ts +520 -0
- package/dist/index.global.js +963 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +938 -0
- package/dist/index.js.map +1 -0
- package/package.json +32 -0
- package/src/auth.ts +185 -0
- package/src/collection.ts +185 -0
- package/src/files.ts +96 -0
- package/src/http.ts +195 -0
- package/src/index.ts +423 -0
- package/src/realtime.ts +264 -0
- package/src/types.ts +46 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ApiError: () => ApiError,
|
|
24
|
+
AuthStore: () => AuthStore,
|
|
25
|
+
FilesService: () => FilesService,
|
|
26
|
+
LazypockClient: () => LazypockClient,
|
|
27
|
+
RealtimeService: () => RealtimeService,
|
|
28
|
+
getFileUrl: () => getFileUrl,
|
|
29
|
+
wsUrlFromBaseUrl: () => wsUrlFromBaseUrl
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(index_exports);
|
|
32
|
+
|
|
33
|
+
// src/types.ts
|
|
34
|
+
var ApiError = class extends Error {
|
|
35
|
+
constructor(message, data, status) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "ApiError";
|
|
38
|
+
this.data = data;
|
|
39
|
+
this.status = status;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/http.ts
|
|
44
|
+
var HttpClient = class {
|
|
45
|
+
/**
|
|
46
|
+
* @param baseUrl The API base URL (e.g. `http://localhost:4000/api`). Trailing slash stripped.
|
|
47
|
+
* @param authStore The auth store providing the token for Authorization headers.
|
|
48
|
+
*/
|
|
49
|
+
constructor(baseUrl, authStore) {
|
|
50
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
51
|
+
this.authStore = authStore;
|
|
52
|
+
this.defaultFetch = globalThis.fetch.bind(globalThis);
|
|
53
|
+
}
|
|
54
|
+
async refreshAuth() {
|
|
55
|
+
const collection = this.authStore.collectionName;
|
|
56
|
+
if (!collection) return null;
|
|
57
|
+
try {
|
|
58
|
+
const url = this.baseUrl + "/" + encodeURIComponent(collection) + "/auth-refresh";
|
|
59
|
+
const headers = {
|
|
60
|
+
"Content-Type": "application/json"
|
|
61
|
+
};
|
|
62
|
+
if (this.authStore.token) {
|
|
63
|
+
headers["Authorization"] = "Bearer " + this.authStore.token;
|
|
64
|
+
}
|
|
65
|
+
const res = await this.defaultFetch(url, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
this.authStore.clear();
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const data = await res.json();
|
|
74
|
+
if (data && typeof data.token === "string") {
|
|
75
|
+
this.authStore.set(
|
|
76
|
+
data.token,
|
|
77
|
+
data.record ?? null
|
|
78
|
+
);
|
|
79
|
+
return data;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Make an HTTP request with automatic auth token injection and optional auto-refresh.
|
|
88
|
+
*
|
|
89
|
+
* @param method HTTP method.
|
|
90
|
+
* @param path URL path (appended to baseUrl).
|
|
91
|
+
* @param body JSON-serializable body, or FormData for file uploads.
|
|
92
|
+
* @param options Optional request options.
|
|
93
|
+
* @returns Parsed JSON response, or null for 204 No Content.
|
|
94
|
+
* @throws {ApiError} On non-2xx responses.
|
|
95
|
+
*/
|
|
96
|
+
async request(method, path, body, options) {
|
|
97
|
+
if (this.authStore.isExpired && this.authStore.collectionName) {
|
|
98
|
+
await this.refreshAuth();
|
|
99
|
+
}
|
|
100
|
+
let url = this.baseUrl + path;
|
|
101
|
+
if (options?.params) {
|
|
102
|
+
const qs = new URLSearchParams(options.params).toString();
|
|
103
|
+
if (qs) {
|
|
104
|
+
url += (path.includes("?") ? "&" : "?") + qs;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const headers = {
|
|
108
|
+
...options?.headers
|
|
109
|
+
};
|
|
110
|
+
if (!(body instanceof FormData)) {
|
|
111
|
+
headers["Content-Type"] = "application/json";
|
|
112
|
+
}
|
|
113
|
+
if (this.authStore.token) {
|
|
114
|
+
headers["Authorization"] = "Bearer " + this.authStore.token;
|
|
115
|
+
}
|
|
116
|
+
const init = {
|
|
117
|
+
method,
|
|
118
|
+
headers,
|
|
119
|
+
signal: options?.signal
|
|
120
|
+
};
|
|
121
|
+
if (body != null && method !== "GET" && method !== "DELETE") {
|
|
122
|
+
if (body instanceof FormData) {
|
|
123
|
+
init.body = body;
|
|
124
|
+
} else {
|
|
125
|
+
init.body = JSON.stringify(body);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const fetcher = options?.fetch ?? this.defaultFetch;
|
|
129
|
+
const res = await fetcher(url, init);
|
|
130
|
+
if (res.status === 204) return null;
|
|
131
|
+
let bodyText = "";
|
|
132
|
+
let data = {};
|
|
133
|
+
try {
|
|
134
|
+
bodyText = await res.text();
|
|
135
|
+
if (bodyText) {
|
|
136
|
+
data = JSON.parse(bodyText);
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
if (!res.ok) {
|
|
141
|
+
throw new ApiError(
|
|
142
|
+
(typeof data.message === "string" ? data.message : res.statusText) || `Request failed with status ${res.status}`,
|
|
143
|
+
data,
|
|
144
|
+
res.status
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return data;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* HTTP GET.
|
|
151
|
+
* @param path URL path.
|
|
152
|
+
* @param options Optional request options.
|
|
153
|
+
*/
|
|
154
|
+
get(path, options) {
|
|
155
|
+
return this.request("GET", path, void 0, options);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* HTTP POST.
|
|
159
|
+
* @param path URL path.
|
|
160
|
+
* @param body Optional request body.
|
|
161
|
+
* @param options Optional request options.
|
|
162
|
+
*/
|
|
163
|
+
post(path, body, options) {
|
|
164
|
+
return this.request("POST", path, body, options);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* HTTP PATCH.
|
|
168
|
+
* @param path URL path.
|
|
169
|
+
* @param body Optional request body.
|
|
170
|
+
* @param options Optional request options.
|
|
171
|
+
*/
|
|
172
|
+
patch(path, body, options) {
|
|
173
|
+
return this.request("PATCH", path, body, options);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* HTTP DELETE.
|
|
177
|
+
* @param path URL path.
|
|
178
|
+
* @param options Optional request options.
|
|
179
|
+
*/
|
|
180
|
+
delete(path, options) {
|
|
181
|
+
return this.request("DELETE", path, void 0, options);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// src/auth.ts
|
|
186
|
+
var TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
187
|
+
var AuthStore = class {
|
|
188
|
+
/**
|
|
189
|
+
* Create an AuthStore with optional custom storage adapter.
|
|
190
|
+
* @param storage Persistence backend. Defaults to `memoryStorage` (localStorage fallback).
|
|
191
|
+
*/
|
|
192
|
+
constructor(storage) {
|
|
193
|
+
this._token = "";
|
|
194
|
+
this._model = null;
|
|
195
|
+
this._tokenExpiresAt = null;
|
|
196
|
+
this._collectionName = null;
|
|
197
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
198
|
+
this.storage = storage ?? {
|
|
199
|
+
get: (_key) => null,
|
|
200
|
+
set: () => {
|
|
201
|
+
},
|
|
202
|
+
remove: () => {
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/** The current JWT token string, or empty string if not authenticated. */
|
|
207
|
+
get token() {
|
|
208
|
+
return this._token;
|
|
209
|
+
}
|
|
210
|
+
/** The current authenticated user record, or null. */
|
|
211
|
+
get model() {
|
|
212
|
+
return this._model;
|
|
213
|
+
}
|
|
214
|
+
/** Whether a token exists (does not check expiry). */
|
|
215
|
+
get isValid() {
|
|
216
|
+
return !!this._token;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Whether the current token has expired (with a 30-second buffer).
|
|
220
|
+
* Returns false when no expiry has been recorded (e.g. superuser tokens).
|
|
221
|
+
*/
|
|
222
|
+
get isExpired() {
|
|
223
|
+
return this._tokenExpiresAt !== null && Date.now() >= this._tokenExpiresAt - 3e4;
|
|
224
|
+
}
|
|
225
|
+
/** The auth collection name used for automatic token refresh. */
|
|
226
|
+
get collectionName() {
|
|
227
|
+
return this._collectionName;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Set the auth collection name (used internally by auto-refresh).
|
|
231
|
+
* @param name The collection name, or null for superuser tokens.
|
|
232
|
+
*/
|
|
233
|
+
setCollectionName(name) {
|
|
234
|
+
this._collectionName = name;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Load persisted auth state from storage.
|
|
238
|
+
* Should be called once at application startup.
|
|
239
|
+
*/
|
|
240
|
+
async init() {
|
|
241
|
+
const [token, model, expiresAt] = await Promise.all([
|
|
242
|
+
this.storage.get("auth_token"),
|
|
243
|
+
this.storage.get("auth_model"),
|
|
244
|
+
this.storage.get("auth_expires_at")
|
|
245
|
+
]);
|
|
246
|
+
if (token) this._token = token;
|
|
247
|
+
if (expiresAt) this._tokenExpiresAt = parseInt(expiresAt, 10) || null;
|
|
248
|
+
if (model) {
|
|
249
|
+
try {
|
|
250
|
+
this._model = JSON.parse(model);
|
|
251
|
+
} catch {
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Update the current auth token and model, persist to storage, and notify listeners.
|
|
257
|
+
* @param token The JWT token string.
|
|
258
|
+
* @param model The authenticated user record, or null for superusers.
|
|
259
|
+
*/
|
|
260
|
+
set(token, model) {
|
|
261
|
+
this._token = token;
|
|
262
|
+
this._model = model;
|
|
263
|
+
this._tokenExpiresAt = Date.now() + TOKEN_TTL_MS;
|
|
264
|
+
void Promise.all([
|
|
265
|
+
this.storage.set("auth_token", token),
|
|
266
|
+
this.storage.set("auth_expires_at", String(this._tokenExpiresAt)),
|
|
267
|
+
model ? this.storage.set("auth_model", JSON.stringify(model)) : this.storage.remove("auth_model")
|
|
268
|
+
]);
|
|
269
|
+
this.notify();
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Clear all auth state (token, model, expiry) and notify listeners.
|
|
273
|
+
*/
|
|
274
|
+
clear() {
|
|
275
|
+
this._token = "";
|
|
276
|
+
this._model = null;
|
|
277
|
+
this._tokenExpiresAt = null;
|
|
278
|
+
this._collectionName = null;
|
|
279
|
+
void Promise.all([
|
|
280
|
+
this.storage.remove("auth_token"),
|
|
281
|
+
this.storage.remove("auth_expires_at"),
|
|
282
|
+
this.storage.remove("auth_model")
|
|
283
|
+
]);
|
|
284
|
+
this.notify();
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Register a listener for auth state changes.
|
|
288
|
+
* @param fn Callback invoked with (model, token) on every change.
|
|
289
|
+
* @returns An unsubscribe function.
|
|
290
|
+
*/
|
|
291
|
+
onChange(fn) {
|
|
292
|
+
this.listeners.add(fn);
|
|
293
|
+
return () => this.listeners.delete(fn);
|
|
294
|
+
}
|
|
295
|
+
notify() {
|
|
296
|
+
for (const fn of this.listeners) {
|
|
297
|
+
fn(this._model, this._token);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
var memoryStorage = {
|
|
302
|
+
get(key) {
|
|
303
|
+
try {
|
|
304
|
+
return localStorage.getItem(key);
|
|
305
|
+
} catch {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
set(key, value) {
|
|
310
|
+
try {
|
|
311
|
+
localStorage.setItem(key, value);
|
|
312
|
+
} catch {
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
remove(key) {
|
|
316
|
+
try {
|
|
317
|
+
localStorage.removeItem(key);
|
|
318
|
+
} catch {
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// src/collection.ts
|
|
324
|
+
var CollectionService = class {
|
|
325
|
+
/** @internal */
|
|
326
|
+
constructor(http, collectionName, authStore) {
|
|
327
|
+
this.http = http;
|
|
328
|
+
this.collectionName = collectionName;
|
|
329
|
+
this.authStore = authStore;
|
|
330
|
+
}
|
|
331
|
+
encodeId(id) {
|
|
332
|
+
return encodeURIComponent(id);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* List records with optional filter/sort/pagination.
|
|
336
|
+
* @param params Query parameters including `filter`, `sort`, `page`, `perPage`, `expand`.
|
|
337
|
+
* @param options Optional request options.
|
|
338
|
+
*/
|
|
339
|
+
list(params, options) {
|
|
340
|
+
const qs = params ? "?" + new URLSearchParams(params).toString() : "";
|
|
341
|
+
return this.http.get(
|
|
342
|
+
"/" + this.encodeId(this.collectionName) + qs,
|
|
343
|
+
options
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Get a single record by ID.
|
|
348
|
+
* @param id Record ID.
|
|
349
|
+
* @param options Optional request options.
|
|
350
|
+
*/
|
|
351
|
+
getOne(id, options) {
|
|
352
|
+
return this.http.get(
|
|
353
|
+
"/" + this.encodeId(this.collectionName) + "/" + this.encodeId(id),
|
|
354
|
+
options
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Create a new record.
|
|
359
|
+
* @param data Record fields.
|
|
360
|
+
* @param options Optional request options.
|
|
361
|
+
*/
|
|
362
|
+
create(data, options) {
|
|
363
|
+
return this.http.post(
|
|
364
|
+
"/" + this.encodeId(this.collectionName),
|
|
365
|
+
data,
|
|
366
|
+
options
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Update a record by ID.
|
|
371
|
+
* @param id Record ID.
|
|
372
|
+
* @param data Updated record fields.
|
|
373
|
+
* @param options Optional request options.
|
|
374
|
+
*/
|
|
375
|
+
update(id, data, options) {
|
|
376
|
+
return this.http.patch(
|
|
377
|
+
"/" + this.encodeId(this.collectionName) + "/" + this.encodeId(id),
|
|
378
|
+
data,
|
|
379
|
+
options
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Delete a record by ID.
|
|
384
|
+
* @param id Record ID.
|
|
385
|
+
* @param options Optional request options.
|
|
386
|
+
*/
|
|
387
|
+
delete(id, options) {
|
|
388
|
+
return this.http.delete(
|
|
389
|
+
"/" + this.encodeId(this.collectionName) + "/" + this.encodeId(id),
|
|
390
|
+
options
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
// ── Expand / Relation Fields ──
|
|
394
|
+
/**
|
|
395
|
+
* Get a list of expandable (relation) fields for this collection.
|
|
396
|
+
* Useful for constructing `expand` query parameters.
|
|
397
|
+
*/
|
|
398
|
+
async expandFields(options) {
|
|
399
|
+
const data = await this.http.get(
|
|
400
|
+
"/collections/" + this.encodeId(this.collectionName),
|
|
401
|
+
options
|
|
402
|
+
);
|
|
403
|
+
if (!data?.fields) return null;
|
|
404
|
+
return data.fields.filter((f) => f.type === "relation" && f.options?.collection).map((f) => ({
|
|
405
|
+
field: f.name,
|
|
406
|
+
targetCollection: f.options.collection
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
// ── Auth Collection Methods ──
|
|
410
|
+
/**
|
|
411
|
+
* Authenticate with email/password against this auth collection.
|
|
412
|
+
* Stores the returned token and user model in the auth store.
|
|
413
|
+
*/
|
|
414
|
+
async authWithPassword(identity, password, options) {
|
|
415
|
+
const data = await this.http.post(
|
|
416
|
+
"/" + this.encodeId(this.collectionName) + "/auth-with-password",
|
|
417
|
+
{ identity, password },
|
|
418
|
+
options
|
|
419
|
+
);
|
|
420
|
+
if (data && this.authStore) {
|
|
421
|
+
this.authStore.setCollectionName(this.collectionName);
|
|
422
|
+
this.authStore.set(data.token, data.record);
|
|
423
|
+
}
|
|
424
|
+
return data;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Refresh the auth token for the currently authenticated user.
|
|
428
|
+
* Updates the stored token and user model.
|
|
429
|
+
*/
|
|
430
|
+
async authRefresh(options) {
|
|
431
|
+
const data = await this.http.post(
|
|
432
|
+
"/" + this.encodeId(this.collectionName) + "/auth-refresh",
|
|
433
|
+
void 0,
|
|
434
|
+
options
|
|
435
|
+
);
|
|
436
|
+
if (data && this.authStore) {
|
|
437
|
+
this.authStore.setCollectionName(this.collectionName);
|
|
438
|
+
this.authStore.set(data.token, data.record);
|
|
439
|
+
}
|
|
440
|
+
return data;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Get available auth methods for this collection.
|
|
444
|
+
*/
|
|
445
|
+
async authMethods(options) {
|
|
446
|
+
return this.http.get(
|
|
447
|
+
"/" + this.encodeId(this.collectionName) + "/auth-methods",
|
|
448
|
+
options
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// src/realtime.ts
|
|
454
|
+
function wsUrlFromBaseUrl(baseUrl) {
|
|
455
|
+
try {
|
|
456
|
+
const url = new URL(baseUrl);
|
|
457
|
+
const protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
458
|
+
return `${protocol}//${url.host}/socket/websocket`;
|
|
459
|
+
} catch {
|
|
460
|
+
return `${baseUrl.replace(/^http/, "ws").replace(/\/api$/, "")}/socket/websocket`;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
var RealtimeService = class {
|
|
464
|
+
constructor() {
|
|
465
|
+
this.ws = null;
|
|
466
|
+
this.refCounter = 0;
|
|
467
|
+
this.subscriptions = /* @__PURE__ */ new Map();
|
|
468
|
+
this.reconnectTimer = null;
|
|
469
|
+
this.reconnectAttempt = 0;
|
|
470
|
+
this.maxReconnectDelay = 5e3;
|
|
471
|
+
this.url = "";
|
|
472
|
+
// ── Heartbeat ──
|
|
473
|
+
this.heartbeatInterval = null;
|
|
474
|
+
}
|
|
475
|
+
connect(opts) {
|
|
476
|
+
this.url = opts.url;
|
|
477
|
+
this.token = opts.token;
|
|
478
|
+
this.reconnectAttempt = 0;
|
|
479
|
+
this.doConnect();
|
|
480
|
+
}
|
|
481
|
+
disconnect() {
|
|
482
|
+
this.clearReconnectTimer();
|
|
483
|
+
this.ws?.close();
|
|
484
|
+
this.ws = null;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Subscribe to a topic (e.g. "collection:posts" or "collection:posts:*").
|
|
488
|
+
* The backend Channel authorizes via listRule on join.
|
|
489
|
+
*/
|
|
490
|
+
subscribe(topic, callback) {
|
|
491
|
+
const subs = this.subscriptions.get(topic) || [];
|
|
492
|
+
subs.push({ topic, callback });
|
|
493
|
+
this.subscriptions.set(topic, subs);
|
|
494
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
495
|
+
this.joinTopic(topic);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Unsubscribe a specific callback from a topic.
|
|
500
|
+
*/
|
|
501
|
+
unsubscribe(topic, callback) {
|
|
502
|
+
if (!callback) {
|
|
503
|
+
this.subscriptions.delete(topic);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const subs = this.subscriptions.get(topic)?.filter((s) => s.callback !== callback);
|
|
507
|
+
if (subs && subs.length > 0) {
|
|
508
|
+
this.subscriptions.set(topic, subs);
|
|
509
|
+
} else {
|
|
510
|
+
this.subscriptions.delete(topic);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
resubscribeAll() {
|
|
514
|
+
for (const topic of this.subscriptions.keys()) {
|
|
515
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
516
|
+
this.joinTopic(topic);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
doConnect() {
|
|
521
|
+
if (typeof WebSocket === "undefined") {
|
|
522
|
+
console.warn(
|
|
523
|
+
"[lazypock] WebSocket not available \u2014 realtime subscriptions disabled"
|
|
524
|
+
);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
let url = this.url;
|
|
528
|
+
if (this.token) {
|
|
529
|
+
url += (url.includes("?") ? "&" : "?") + "token=" + encodeURIComponent(this.token);
|
|
530
|
+
}
|
|
531
|
+
this.ws = new WebSocket(url);
|
|
532
|
+
this.ws.onopen = () => {
|
|
533
|
+
this.reconnectAttempt = 0;
|
|
534
|
+
this.resubscribeAll();
|
|
535
|
+
this.startHeartbeat();
|
|
536
|
+
};
|
|
537
|
+
this.ws.onmessage = (msg) => {
|
|
538
|
+
this.handleMessage(msg.data);
|
|
539
|
+
};
|
|
540
|
+
this.ws.onclose = () => {
|
|
541
|
+
this.stopHeartbeat();
|
|
542
|
+
this.onDisconnect?.();
|
|
543
|
+
this.scheduleReconnect();
|
|
544
|
+
};
|
|
545
|
+
this.ws.onerror = (err) => {
|
|
546
|
+
this.onError?.(err);
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
handleMessage(data) {
|
|
550
|
+
let parsed;
|
|
551
|
+
try {
|
|
552
|
+
parsed = JSON.parse(data);
|
|
553
|
+
} catch {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (typeof parsed !== "object" || !parsed.topic || !parsed.event) return;
|
|
557
|
+
const topic = parsed.topic;
|
|
558
|
+
const event = parsed.event;
|
|
559
|
+
const payload = parsed.payload || {};
|
|
560
|
+
if (event === "phx_reply") return;
|
|
561
|
+
const subs = this.subscriptions.get(topic);
|
|
562
|
+
if (subs) {
|
|
563
|
+
const e = {
|
|
564
|
+
event,
|
|
565
|
+
topic,
|
|
566
|
+
payload
|
|
567
|
+
};
|
|
568
|
+
for (const s of subs) {
|
|
569
|
+
try {
|
|
570
|
+
s.callback(e);
|
|
571
|
+
} catch {
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
joinTopic(topic) {
|
|
577
|
+
const ref = this.nextRef();
|
|
578
|
+
const msg = JSON.stringify({
|
|
579
|
+
topic,
|
|
580
|
+
event: "phx_join",
|
|
581
|
+
payload: {},
|
|
582
|
+
ref
|
|
583
|
+
});
|
|
584
|
+
this.ws?.send(msg);
|
|
585
|
+
}
|
|
586
|
+
nextRef() {
|
|
587
|
+
this.refCounter++;
|
|
588
|
+
return this.refCounter.toString();
|
|
589
|
+
}
|
|
590
|
+
startHeartbeat() {
|
|
591
|
+
this.stopHeartbeat();
|
|
592
|
+
this.heartbeatInterval = setInterval(() => {
|
|
593
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
594
|
+
const ref = this.nextRef();
|
|
595
|
+
const msg = JSON.stringify({
|
|
596
|
+
topic: "phoenix",
|
|
597
|
+
event: "heartbeat",
|
|
598
|
+
payload: {},
|
|
599
|
+
ref
|
|
600
|
+
});
|
|
601
|
+
this.ws.send(msg);
|
|
602
|
+
}
|
|
603
|
+
}, 3e4);
|
|
604
|
+
}
|
|
605
|
+
stopHeartbeat() {
|
|
606
|
+
if (this.heartbeatInterval) {
|
|
607
|
+
clearInterval(this.heartbeatInterval);
|
|
608
|
+
this.heartbeatInterval = null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
// ── Reconnect ──
|
|
612
|
+
scheduleReconnect() {
|
|
613
|
+
this.clearReconnectTimer();
|
|
614
|
+
const delay = Math.min(
|
|
615
|
+
1e3 * 2 ** this.reconnectAttempt,
|
|
616
|
+
this.maxReconnectDelay
|
|
617
|
+
);
|
|
618
|
+
this.reconnectAttempt++;
|
|
619
|
+
this.reconnectTimer = setTimeout(() => {
|
|
620
|
+
this.reconnectTimer = null;
|
|
621
|
+
this.onReconnect?.();
|
|
622
|
+
this.doConnect();
|
|
623
|
+
}, delay);
|
|
624
|
+
}
|
|
625
|
+
clearReconnectTimer() {
|
|
626
|
+
if (this.reconnectTimer) {
|
|
627
|
+
clearTimeout(this.reconnectTimer);
|
|
628
|
+
this.reconnectTimer = null;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
// src/files.ts
|
|
634
|
+
function getFileUrl(baseUrl, fileId) {
|
|
635
|
+
return baseUrl.replace(/\/+$/, "") + "/files/" + encodeURIComponent(fileId);
|
|
636
|
+
}
|
|
637
|
+
var FilesService = class {
|
|
638
|
+
constructor(http) {
|
|
639
|
+
this.http = http;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Upload a file or blob.
|
|
643
|
+
*
|
|
644
|
+
* @param file The File or Blob to upload.
|
|
645
|
+
* @param filename Optional filename (required if `file` is a Blob without a name).
|
|
646
|
+
* @param options Optional request options (signal, custom fetch).
|
|
647
|
+
* @param meta Optional metadata: collectionName, recordId, fieldName for ownership tracking.
|
|
648
|
+
*/
|
|
649
|
+
async upload(file, filename, options, meta) {
|
|
650
|
+
if (typeof FormData === "undefined") {
|
|
651
|
+
throw new Error("FormData is not available in this environment");
|
|
652
|
+
}
|
|
653
|
+
const formData = new FormData();
|
|
654
|
+
const name = filename || (file instanceof File ? file.name : "file");
|
|
655
|
+
formData.append("file", file, name);
|
|
656
|
+
if (meta?.collectionName)
|
|
657
|
+
formData.append("collection_name", meta.collectionName);
|
|
658
|
+
if (meta?.recordId) formData.append("record_id", meta.recordId);
|
|
659
|
+
if (meta?.fieldName) formData.append("field_name", meta.fieldName);
|
|
660
|
+
const data = await this.http.request(
|
|
661
|
+
"POST",
|
|
662
|
+
"/files",
|
|
663
|
+
formData,
|
|
664
|
+
options
|
|
665
|
+
);
|
|
666
|
+
return data;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Fetch file metadata including URL.
|
|
670
|
+
* @param fileId The file ID.
|
|
671
|
+
*/
|
|
672
|
+
async getUrl(fileId) {
|
|
673
|
+
const data = await this.http.request(
|
|
674
|
+
"GET",
|
|
675
|
+
"/files/" + encodeURIComponent(fileId)
|
|
676
|
+
);
|
|
677
|
+
if (data && typeof data === "object" && "url" in data) {
|
|
678
|
+
return data.url;
|
|
679
|
+
}
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Delete a file by ID.
|
|
684
|
+
* @param fileId The file ID.
|
|
685
|
+
* @param options Optional request options.
|
|
686
|
+
*/
|
|
687
|
+
async delete(fileId, options) {
|
|
688
|
+
return this.http.request(
|
|
689
|
+
"DELETE",
|
|
690
|
+
"/files/" + encodeURIComponent(fileId),
|
|
691
|
+
void 0,
|
|
692
|
+
options
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
// src/index.ts
|
|
698
|
+
var LazypockClient = class {
|
|
699
|
+
/**
|
|
700
|
+
* Create a new Lazypock client.
|
|
701
|
+
* @param options Configuration options.
|
|
702
|
+
*/
|
|
703
|
+
constructor(options) {
|
|
704
|
+
this.collectionCache = /* @__PURE__ */ new Map();
|
|
705
|
+
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
706
|
+
this.authStore = options.authStore ?? new AuthStore(options.storage ?? memoryStorage);
|
|
707
|
+
this.http = new HttpClient(baseUrl, this.authStore);
|
|
708
|
+
this.realtime = options.realtime ?? new RealtimeService();
|
|
709
|
+
this.files = new FilesService(this.http);
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Get or create a typed service for the given collection.
|
|
713
|
+
* Services are cached after first access.
|
|
714
|
+
*
|
|
715
|
+
* @param name The collection name.
|
|
716
|
+
* @returns A {@link CollectionService} instance.
|
|
717
|
+
*/
|
|
718
|
+
collection(name) {
|
|
719
|
+
let svc = this.collectionCache.get(name);
|
|
720
|
+
if (!svc) {
|
|
721
|
+
svc = new CollectionService(this.http, name, this.authStore);
|
|
722
|
+
this.collectionCache.set(name, svc);
|
|
723
|
+
}
|
|
724
|
+
return svc;
|
|
725
|
+
}
|
|
726
|
+
// ── Auth ──
|
|
727
|
+
/** Check whether any superuser exists (for login vs setup screen routing). */
|
|
728
|
+
async checkSuperuser() {
|
|
729
|
+
return this.http.get("/superusers/check");
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Create the initial superuser account.
|
|
733
|
+
* Only works when no superuser exists yet.
|
|
734
|
+
* Stores the returned token in the auth store.
|
|
735
|
+
* @param email Superuser email.
|
|
736
|
+
* @param password Superuser password (min 8 chars).
|
|
737
|
+
*/
|
|
738
|
+
async setup(email, password) {
|
|
739
|
+
const data = await this.http.post("/superusers/setup", { email, password });
|
|
740
|
+
if (data) {
|
|
741
|
+
this.authStore.setCollectionName(null);
|
|
742
|
+
this.authStore.set(data.token, null);
|
|
743
|
+
}
|
|
744
|
+
return data;
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Authenticate as a superuser or auth collection user.
|
|
748
|
+
*
|
|
749
|
+
* When `collection` is provided, authenticates against
|
|
750
|
+
* `/{collection}/auth-with-password`. Otherwise logs in as superuser.
|
|
751
|
+
* Stores the returned token in the auth store.
|
|
752
|
+
*
|
|
753
|
+
* @param email User email or identity.
|
|
754
|
+
* @param password User password.
|
|
755
|
+
* @param collection Optional auth collection name.
|
|
756
|
+
*/
|
|
757
|
+
async login(email, password, collection) {
|
|
758
|
+
let data;
|
|
759
|
+
if (collection) {
|
|
760
|
+
data = await this.http.post("/" + encodeURIComponent(collection) + "/auth-with-password", {
|
|
761
|
+
identity: email,
|
|
762
|
+
password
|
|
763
|
+
});
|
|
764
|
+
if (data && data.record) {
|
|
765
|
+
this.authStore.setCollectionName(collection);
|
|
766
|
+
this.authStore.set(data.token, data.record);
|
|
767
|
+
}
|
|
768
|
+
} else {
|
|
769
|
+
data = await this.http.post(
|
|
770
|
+
"/superusers/login",
|
|
771
|
+
{ email, password }
|
|
772
|
+
);
|
|
773
|
+
if (data) {
|
|
774
|
+
this.authStore.setCollectionName(null);
|
|
775
|
+
this.authStore.set(data.token, null);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return data;
|
|
779
|
+
}
|
|
780
|
+
/** Fetch the current superuser profile and refresh the auth model. */
|
|
781
|
+
async me(options) {
|
|
782
|
+
const data = await this.http.get("/superusers/me", options);
|
|
783
|
+
if (data) {
|
|
784
|
+
this.authStore.set(this.authStore.token, data);
|
|
785
|
+
}
|
|
786
|
+
return data;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Authenticate against an auth collection with email/password.
|
|
790
|
+
* Stores the returned token and user record in the auth store.
|
|
791
|
+
*
|
|
792
|
+
* @param collection The auth collection name.
|
|
793
|
+
* @param identity Email or username.
|
|
794
|
+
* @param password Password.
|
|
795
|
+
* @param options Optional request options.
|
|
796
|
+
*/
|
|
797
|
+
async authWithPassword(collection, identity, password, options) {
|
|
798
|
+
const data = await this.http.post(
|
|
799
|
+
"/" + encodeURIComponent(collection) + "/auth-with-password",
|
|
800
|
+
{ identity, password },
|
|
801
|
+
options
|
|
802
|
+
);
|
|
803
|
+
if (data) {
|
|
804
|
+
this.authStore.setCollectionName(collection);
|
|
805
|
+
this.authStore.set(data.token, data.record);
|
|
806
|
+
}
|
|
807
|
+
return data;
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Refresh an auth collection token.
|
|
811
|
+
* Uses the currently stored auth token.
|
|
812
|
+
*
|
|
813
|
+
* @param collection The auth collection name.
|
|
814
|
+
* @param options Optional request options.
|
|
815
|
+
*/
|
|
816
|
+
async authRefresh(collection, options) {
|
|
817
|
+
const data = await this.http.post(
|
|
818
|
+
"/" + encodeURIComponent(collection) + "/auth-refresh",
|
|
819
|
+
void 0,
|
|
820
|
+
options
|
|
821
|
+
);
|
|
822
|
+
if (data) {
|
|
823
|
+
this.authStore.setCollectionName(collection);
|
|
824
|
+
this.authStore.set(data.token, data.record);
|
|
825
|
+
}
|
|
826
|
+
return data;
|
|
827
|
+
}
|
|
828
|
+
/** Clear the current auth state and remove persisted tokens. */
|
|
829
|
+
logout() {
|
|
830
|
+
this.authStore.clear();
|
|
831
|
+
}
|
|
832
|
+
// ── Health ──
|
|
833
|
+
/** Ping the API health endpoint. */
|
|
834
|
+
health(options) {
|
|
835
|
+
return this.http.get("/health", options);
|
|
836
|
+
}
|
|
837
|
+
// ── Collection Management (admin) ──
|
|
838
|
+
/**
|
|
839
|
+
* List all collections (admin).
|
|
840
|
+
* @param q URL query string (e.g. `page=1&perPage=200`).
|
|
841
|
+
* @param options Optional request options.
|
|
842
|
+
*/
|
|
843
|
+
listCollections(q, options) {
|
|
844
|
+
return this.http.get(
|
|
845
|
+
"/collections" + (q ? "?" + q : ""),
|
|
846
|
+
options
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Get a single collection by ID or name.
|
|
851
|
+
* @param id Collection ID or name.
|
|
852
|
+
* @param options Optional request options.
|
|
853
|
+
*/
|
|
854
|
+
getCollection(id, options) {
|
|
855
|
+
return this.http.get(
|
|
856
|
+
"/collections/" + encodeURIComponent(id),
|
|
857
|
+
options
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Create a new collection (admin).
|
|
862
|
+
* @param data Collection definition (name, type, fields, options, rules, etc.).
|
|
863
|
+
* @param options Optional request options.
|
|
864
|
+
*/
|
|
865
|
+
createCollection(data, options) {
|
|
866
|
+
return this.http.post("/collections", data, options);
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Update an existing collection (admin).
|
|
870
|
+
* @param id Collection ID or name.
|
|
871
|
+
* @param data Updated collection fields.
|
|
872
|
+
* @param options Optional request options.
|
|
873
|
+
*/
|
|
874
|
+
updateCollection(id, data, options) {
|
|
875
|
+
return this.http.patch(
|
|
876
|
+
"/collections/" + encodeURIComponent(id),
|
|
877
|
+
data,
|
|
878
|
+
options
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Delete a collection (admin).
|
|
883
|
+
* @param id Collection ID or name.
|
|
884
|
+
* @param options Optional request options.
|
|
885
|
+
*/
|
|
886
|
+
deleteCollection(id, options) {
|
|
887
|
+
return this.http.delete("/collections/" + encodeURIComponent(id), options);
|
|
888
|
+
}
|
|
889
|
+
// ── Records (dynamic collection) ──
|
|
890
|
+
/**
|
|
891
|
+
* List records from a dynamic collection with optional filter/sort/pagination.
|
|
892
|
+
*
|
|
893
|
+
* @param coll Collection name.
|
|
894
|
+
* @param params Query parameters including:
|
|
895
|
+
* - `filter` — PocketBase filter syntax (e.g. `title~'hello' && published=true`)
|
|
896
|
+
* - `sort` — Comma-separated, `-` prefix for DESC (e.g. `-created,title`)
|
|
897
|
+
* - `page` — Page number (default: 1)
|
|
898
|
+
* - `perPage` — Items per page (default: 30, max: 200)
|
|
899
|
+
* - `expand` — Comma-separated relation fields (e.g. `author,category`)
|
|
900
|
+
* @param options Optional request options.
|
|
901
|
+
*/
|
|
902
|
+
listRecords(coll, params, options) {
|
|
903
|
+
const qs = params ? "?" + new URLSearchParams(params).toString() : "";
|
|
904
|
+
return this.http.get(
|
|
905
|
+
"/" + encodeURIComponent(coll) + qs,
|
|
906
|
+
options
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Get a single record by ID.
|
|
911
|
+
* @param coll Collection name.
|
|
912
|
+
* @param id Record ID.
|
|
913
|
+
* @param options Optional request options.
|
|
914
|
+
*/
|
|
915
|
+
getRecord(coll, id, options) {
|
|
916
|
+
return this.http.get(
|
|
917
|
+
"/" + encodeURIComponent(coll) + "/" + encodeURIComponent(id),
|
|
918
|
+
options
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Create a record in a dynamic collection.
|
|
923
|
+
* @param coll Collection name.
|
|
924
|
+
* @param data Record fields.
|
|
925
|
+
* @param options Optional request options.
|
|
926
|
+
*/
|
|
927
|
+
createRecord(coll, data, options) {
|
|
928
|
+
return this.http.post(
|
|
929
|
+
"/" + encodeURIComponent(coll),
|
|
930
|
+
data,
|
|
931
|
+
options
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Update a record in a dynamic collection.
|
|
936
|
+
* @param coll Collection name.
|
|
937
|
+
* @param id Record ID.
|
|
938
|
+
* @param data Updated record fields.
|
|
939
|
+
* @param options Optional request options.
|
|
940
|
+
*/
|
|
941
|
+
updateRecord(coll, id, data, options) {
|
|
942
|
+
return this.http.patch(
|
|
943
|
+
"/" + encodeURIComponent(coll) + "/" + encodeURIComponent(id),
|
|
944
|
+
data,
|
|
945
|
+
options
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Delete a record from a dynamic collection.
|
|
950
|
+
* @param coll Collection name.
|
|
951
|
+
* @param id Record ID.
|
|
952
|
+
* @param options Optional request options.
|
|
953
|
+
*/
|
|
954
|
+
deleteRecord(coll, id, options) {
|
|
955
|
+
return this.http.delete(
|
|
956
|
+
"/" + encodeURIComponent(coll) + "/" + encodeURIComponent(id),
|
|
957
|
+
options
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
962
|
+
0 && (module.exports = {
|
|
963
|
+
ApiError,
|
|
964
|
+
AuthStore,
|
|
965
|
+
FilesService,
|
|
966
|
+
LazypockClient,
|
|
967
|
+
RealtimeService,
|
|
968
|
+
getFileUrl,
|
|
969
|
+
wsUrlFromBaseUrl
|
|
970
|
+
});
|
|
971
|
+
//# sourceMappingURL=index.cjs.map
|