lazypock 0.3.0 → 0.5.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 +75 -98
- package/dist/{chunk-Y6PMBL2S.js → chunk-HOBNUW5Z.js} +27 -2
- package/dist/{chunk-Y6PMBL2S.js.map → chunk-HOBNUW5Z.js.map} +1 -1
- package/dist/cli.cjs +26 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.global.js +26 -1
- package/dist/cli.global.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +248 -397
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +182 -296
- package/dist/index.d.ts +182 -296
- package/dist/index.global.js +248 -395
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +223 -395
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/codegen.ts +34 -1
- package/src/collection.ts +203 -15
- package/src/collections.ts +35 -5
- package/src/http.ts +60 -126
- package/src/index.ts +8 -5
- package/src/lazypock.ts +19 -146
- package/src/types.ts +115 -12
- package/src/cache.ts +0 -314
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
fieldTypeScriptType,
|
|
5
5
|
generateTypes,
|
|
6
6
|
schemaFieldType
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-HOBNUW5Z.js";
|
|
8
8
|
|
|
9
9
|
// src/types.ts
|
|
10
10
|
var ApiError = class extends Error {
|
|
@@ -17,201 +17,6 @@ var ApiError = class extends Error {
|
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
// src/cache.ts
|
|
21
|
-
var CacheStore = class {
|
|
22
|
-
constructor(config = {}) {
|
|
23
|
-
this.memory = /* @__PURE__ */ new Map();
|
|
24
|
-
this.hits = 0;
|
|
25
|
-
this.misses = 0;
|
|
26
|
-
this.namespaceEntries = /* @__PURE__ */ new Map();
|
|
27
|
-
/** Key → set of prefix tags registered for that key (e.g. `getList:posts`). */
|
|
28
|
-
this.prefixEntries = /* @__PURE__ */ new Map();
|
|
29
|
-
this.ttl = config.defaultTTL ?? 6e4;
|
|
30
|
-
this.persistence = config.store;
|
|
31
|
-
this.maxEntries = config.maxEntries ?? 500;
|
|
32
|
-
}
|
|
33
|
-
/** Resolve the effective TTL: request override → global default. */
|
|
34
|
-
resolveTTL(ttl) {
|
|
35
|
-
return ttl && ttl > 0 ? ttl : this.ttl;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Read a cached value. Fast sync path (memory) with async persistence
|
|
39
|
-
* fallback for adapters whose `get` returns a Promise.
|
|
40
|
-
* @param key Cache key (e.g. `"GET /posts?page=1"`).
|
|
41
|
-
* @returns The cached value, or undefined when absent/expired (the hit is
|
|
42
|
-
* cleared on expiry so a stale value is never served).
|
|
43
|
-
*/
|
|
44
|
-
async get(key) {
|
|
45
|
-
const mem = this.memory.get(key);
|
|
46
|
-
if (mem !== void 0) {
|
|
47
|
-
if (Date.now() > mem.expiresAt) {
|
|
48
|
-
this.delete(key);
|
|
49
|
-
this.misses++;
|
|
50
|
-
return void 0;
|
|
51
|
-
}
|
|
52
|
-
this.memory.delete(key);
|
|
53
|
-
this.memory.set(key, mem);
|
|
54
|
-
this.hits++;
|
|
55
|
-
return mem.value;
|
|
56
|
-
}
|
|
57
|
-
if (this.persistence) {
|
|
58
|
-
const entry = await this.readPersisted(key);
|
|
59
|
-
if (entry) {
|
|
60
|
-
if (Date.now() > entry.expiresAt) {
|
|
61
|
-
this.delete(key);
|
|
62
|
-
this.misses++;
|
|
63
|
-
return void 0;
|
|
64
|
-
}
|
|
65
|
-
this.hits++;
|
|
66
|
-
return entry.value;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
this.misses++;
|
|
70
|
-
return void 0;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Store a value.
|
|
74
|
-
* @param key Cache key.
|
|
75
|
-
* @param value The response payload.
|
|
76
|
-
* @param ttlOverride Optional TTL override (ms).
|
|
77
|
-
* @param namespace Optional namespace for group invalidation.
|
|
78
|
-
*/
|
|
79
|
-
set(key, value, ttlOverride, namespace, tags) {
|
|
80
|
-
const expiresAt = Date.now() + this.resolveTTL(ttlOverride);
|
|
81
|
-
const entry = { value, expiresAt, namespace, tags };
|
|
82
|
-
this.memory.set(key, entry);
|
|
83
|
-
if (this.memory.size > this.maxEntries) {
|
|
84
|
-
const oldest = this.memory.keys().next().value;
|
|
85
|
-
if (oldest !== void 0) this.delete(oldest);
|
|
86
|
-
}
|
|
87
|
-
if (namespace) {
|
|
88
|
-
let keys = this.namespaceEntries.get(namespace);
|
|
89
|
-
if (!keys) {
|
|
90
|
-
keys = /* @__PURE__ */ new Set();
|
|
91
|
-
this.namespaceEntries.set(namespace, keys);
|
|
92
|
-
}
|
|
93
|
-
keys.add(key);
|
|
94
|
-
}
|
|
95
|
-
for (const tag of tags ?? []) {
|
|
96
|
-
let keys = this.prefixEntries.get(tag);
|
|
97
|
-
if (!keys) {
|
|
98
|
-
keys = /* @__PURE__ */ new Set();
|
|
99
|
-
this.prefixEntries.set(tag, keys);
|
|
100
|
-
}
|
|
101
|
-
keys.add(key);
|
|
102
|
-
}
|
|
103
|
-
if (this.persistence) {
|
|
104
|
-
void this.persistence.set(this.persistKey(key), JSON.stringify(entry));
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Invalidate entries belonging to a namespace (e.g. a collection name).
|
|
109
|
-
* Also clears the namespace index entry.
|
|
110
|
-
*/
|
|
111
|
-
invalidate(namespace) {
|
|
112
|
-
const keys = Array.from(this.namespaceEntries.get(namespace) ?? []);
|
|
113
|
-
for (const key of keys) this.delete(key);
|
|
114
|
-
this.namespaceEntries.delete(namespace);
|
|
115
|
-
}
|
|
116
|
-
/** Remove a single key. */
|
|
117
|
-
delete(key) {
|
|
118
|
-
const entry = this.memory.get(key);
|
|
119
|
-
if (entry?.namespace) {
|
|
120
|
-
const set = this.namespaceEntries.get(entry.namespace);
|
|
121
|
-
if (set) {
|
|
122
|
-
set.delete(key);
|
|
123
|
-
if (set.size === 0) this.namespaceEntries.delete(entry.namespace);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
for (const tag of entry?.tags ?? []) {
|
|
127
|
-
const set = this.prefixEntries.get(tag);
|
|
128
|
-
if (set) {
|
|
129
|
-
set.delete(key);
|
|
130
|
-
if (set.size === 0) this.prefixEntries.delete(tag);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
this.memory.delete(key);
|
|
134
|
-
if (this.persistence) {
|
|
135
|
-
void this.persistence.remove(this.persistKey(key));
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Delete every entry whose key starts with `prefix`.
|
|
140
|
-
*
|
|
141
|
-
* Useful for fine-grained invalidation, e.g.:
|
|
142
|
-
* ```ts
|
|
143
|
-
* client.cache.deleteByPrefix('getList:posts'); // delete all getList cache
|
|
144
|
-
* client.cache.deleteByPrefix('getOne:posts'); // delete all getOne cache
|
|
145
|
-
* ```
|
|
146
|
-
*/
|
|
147
|
-
deleteByPrefix(prefix) {
|
|
148
|
-
if (!prefix) return;
|
|
149
|
-
const tagged = this.prefixEntries.get(prefix);
|
|
150
|
-
if (tagged) {
|
|
151
|
-
for (const key of Array.from(tagged)) this.delete(key);
|
|
152
|
-
this.prefixEntries.delete(prefix);
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
for (const key of Array.from(this.memory.keys())) {
|
|
156
|
-
if (key.startsWith(prefix)) this.delete(key);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
/** Drop every cached entry (memory + persistence). */
|
|
160
|
-
clear() {
|
|
161
|
-
this.memory.clear();
|
|
162
|
-
this.namespaceEntries.clear();
|
|
163
|
-
this.prefixEntries.clear();
|
|
164
|
-
}
|
|
165
|
-
/** Cache hit/miss/entry statistics. */
|
|
166
|
-
stats() {
|
|
167
|
-
return { hits: this.hits, misses: this.misses, entries: this.memory.size };
|
|
168
|
-
}
|
|
169
|
-
persistKey(key) {
|
|
170
|
-
return "lazypock:cache:" + key;
|
|
171
|
-
}
|
|
172
|
-
async readPersisted(key) {
|
|
173
|
-
if (!this.persistence) return void 0;
|
|
174
|
-
const raw = await this.persistence.get(this.persistKey(key));
|
|
175
|
-
if (raw == null) return void 0;
|
|
176
|
-
try {
|
|
177
|
-
const entry = JSON.parse(raw);
|
|
178
|
-
this.memory.set(key, entry);
|
|
179
|
-
if (entry.namespace) {
|
|
180
|
-
let keys = this.namespaceEntries.get(entry.namespace);
|
|
181
|
-
if (!keys) {
|
|
182
|
-
keys = /* @__PURE__ */ new Set();
|
|
183
|
-
this.namespaceEntries.set(entry.namespace, keys);
|
|
184
|
-
}
|
|
185
|
-
keys.add(key);
|
|
186
|
-
}
|
|
187
|
-
for (const tag of entry.tags ?? []) {
|
|
188
|
-
let keys = this.prefixEntries.get(tag);
|
|
189
|
-
if (!keys) {
|
|
190
|
-
keys = /* @__PURE__ */ new Set();
|
|
191
|
-
this.prefixEntries.set(tag, keys);
|
|
192
|
-
}
|
|
193
|
-
keys.add(key);
|
|
194
|
-
}
|
|
195
|
-
return entry;
|
|
196
|
-
} catch {
|
|
197
|
-
void this.persistence.remove(this.persistKey(key));
|
|
198
|
-
return void 0;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
function resolveCacheDirective(opts) {
|
|
203
|
-
if (!opts) return null;
|
|
204
|
-
if (typeof opts.ttl === "number" && opts.ttl > 0) {
|
|
205
|
-
return { enabled: true, ttl: opts.ttl };
|
|
206
|
-
}
|
|
207
|
-
const c = opts.cache;
|
|
208
|
-
if (c === void 0) return null;
|
|
209
|
-
if (c === true) return { enabled: true };
|
|
210
|
-
if (c === false) return { enabled: false };
|
|
211
|
-
if (typeof c === "number") return { enabled: true, ttl: c > 0 ? c : void 0 };
|
|
212
|
-
return { enabled: true, ttl: c.ttl, key: c.key };
|
|
213
|
-
}
|
|
214
|
-
|
|
215
20
|
// src/http.ts
|
|
216
21
|
function isAbortError(err) {
|
|
217
22
|
return err instanceof Error && (err.name === "AbortError" || err.message === "Aborted");
|
|
@@ -222,32 +27,25 @@ var HttpClient = class {
|
|
|
222
27
|
* @param authStore The auth store providing the token for Authorization headers.
|
|
223
28
|
*/
|
|
224
29
|
constructor(baseUrl, authStore) {
|
|
225
|
-
/** Master switch resolved from CacheConfig.enabled. */
|
|
226
|
-
this.cacheEnabled = false;
|
|
227
30
|
/**
|
|
228
31
|
* Abort controllers for in-flight requests, keyed by their cancellation key
|
|
229
32
|
* (default `METHOD path`). A new request with the same key aborts the
|
|
230
33
|
* previous one — PocketBase-style auto-cancellation of duplicated requests.
|
|
231
34
|
*/
|
|
232
35
|
this.cancelControllers = {};
|
|
36
|
+
/**
|
|
37
|
+
* In-flight request promises, keyed by cancellation key. When auto-cancellation
|
|
38
|
+
* would abort a pending duplicate, the newer request instead awaits the same
|
|
39
|
+
* promise — single-flight coalescing (no duplicate network request, no
|
|
40
|
+
* spurious abort rejection for the caller).
|
|
41
|
+
*/
|
|
42
|
+
this.inflight = {};
|
|
233
43
|
/** Global toggle for the auto-cancellation behaviour (default: on). */
|
|
234
44
|
this.enableAutoCancellation = true;
|
|
235
45
|
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
236
46
|
this.authStore = authStore;
|
|
237
47
|
this.defaultFetch = globalThis.fetch.bind(globalThis);
|
|
238
48
|
}
|
|
239
|
-
/**
|
|
240
|
-
* Attach a cache store + master switch.
|
|
241
|
-
* Called by the client constructor when cache config is present.
|
|
242
|
-
*/
|
|
243
|
-
setCache(cache, enabled) {
|
|
244
|
-
this.cache = cache;
|
|
245
|
-
this.cacheEnabled = enabled;
|
|
246
|
-
}
|
|
247
|
-
/** Whether the global cache flag is on (requests opt in/out individually too). */
|
|
248
|
-
get cacheIsEnabled() {
|
|
249
|
-
return this.cacheEnabled;
|
|
250
|
-
}
|
|
251
49
|
async refreshAuth() {
|
|
252
50
|
const collection = this.authStore.collectionName;
|
|
253
51
|
if (!collection) return null;
|
|
@@ -324,27 +122,18 @@ var HttpClient = class {
|
|
|
324
122
|
* @throws {ApiError} On non-2xx responses or when the request is aborted
|
|
325
123
|
* (aborted requests throw an `ApiError` with `isAbort === true`).
|
|
326
124
|
*/
|
|
327
|
-
/** Invalidate a namespace (collection name). No-op when cache is off. */
|
|
328
|
-
invalidateCache(namespace) {
|
|
329
|
-
this.cache?.invalidate(namespace);
|
|
330
|
-
}
|
|
331
|
-
/** Current cache statistics (hits/misses/entries), or null when disabled. */
|
|
332
|
-
cacheStats() {
|
|
333
|
-
return this.cache ? this.cache.stats() : null;
|
|
334
|
-
}
|
|
335
125
|
async request(method, path, body, options) {
|
|
336
126
|
if (this.authStore.isExpired && this.authStore.collectionName) {
|
|
337
127
|
await this.refreshAuth();
|
|
338
128
|
}
|
|
339
|
-
const cacheDirective = resolveCacheDirective(options);
|
|
340
|
-
const wantCache = cacheDirective !== null ? cacheDirective.enabled : this.cacheEnabled;
|
|
341
|
-
const cacheKey = method === "GET" && this.cache && wantCache ? this.cacheKeyFor(method, path, options?.params) : null;
|
|
342
|
-
if (cacheKey !== null) {
|
|
343
|
-
const hit = await this.cache?.get(cacheKey);
|
|
344
|
-
if (hit !== void 0) return hit;
|
|
345
|
-
}
|
|
346
129
|
let requestKey = options?.requestKey === void 0 ? options?.cancelKey ?? `${method} ${path}` : options.requestKey;
|
|
347
130
|
if (options?.autoCancel === false) requestKey = null;
|
|
131
|
+
if (options?.singleFlight && requestKey !== null) {
|
|
132
|
+
const pending = this.inflight[requestKey];
|
|
133
|
+
if (pending !== void 0) {
|
|
134
|
+
return pending;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
348
137
|
let controller = null;
|
|
349
138
|
const externalSignal = options?.signal;
|
|
350
139
|
if (requestKey !== null) {
|
|
@@ -362,6 +151,36 @@ var HttpClient = class {
|
|
|
362
151
|
}
|
|
363
152
|
}
|
|
364
153
|
const signal = controller?.signal ?? externalSignal;
|
|
154
|
+
const perform = async () => {
|
|
155
|
+
try {
|
|
156
|
+
return await this.doRequest(
|
|
157
|
+
method,
|
|
158
|
+
path,
|
|
159
|
+
body,
|
|
160
|
+
options,
|
|
161
|
+
signal,
|
|
162
|
+
requestKey,
|
|
163
|
+
controller
|
|
164
|
+
);
|
|
165
|
+
} finally {
|
|
166
|
+
if (requestKey !== null) {
|
|
167
|
+
if (this.inflight[requestKey] === promise) {
|
|
168
|
+
delete this.inflight[requestKey];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const promise = perform();
|
|
174
|
+
if (requestKey !== null) {
|
|
175
|
+
this.inflight[requestKey] = promise;
|
|
176
|
+
}
|
|
177
|
+
return promise;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Execute the actual HTTP request (fetch + parse). Called by {@link request}
|
|
181
|
+
* as the inner in-flight unit so single-flight callers can reuse the promise.
|
|
182
|
+
*/
|
|
183
|
+
async doRequest(method, path, body, options, signal, requestKey, controller) {
|
|
365
184
|
let url = this.baseUrl + path;
|
|
366
185
|
if (options?.params) {
|
|
367
186
|
const qs = new URLSearchParams(options.params).toString();
|
|
@@ -426,63 +245,8 @@ var HttpClient = class {
|
|
|
426
245
|
res.status
|
|
427
246
|
);
|
|
428
247
|
}
|
|
429
|
-
if (cacheKey !== null && this.cache) {
|
|
430
|
-
const namespace = this.namespaceFromPath(path);
|
|
431
|
-
const ttl = cacheDirective?.ttl;
|
|
432
|
-
const tags = this.cacheTagsFor(path, namespace);
|
|
433
|
-
this.cache.set(
|
|
434
|
-
cacheKey,
|
|
435
|
-
data,
|
|
436
|
-
ttl,
|
|
437
|
-
namespace ?? void 0,
|
|
438
|
-
tags
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
if (method !== "GET" && this.cache) {
|
|
442
|
-
const namespaces = /* @__PURE__ */ new Set();
|
|
443
|
-
const ns = this.namespaceFromPath(path);
|
|
444
|
-
if (ns) namespaces.add(ns);
|
|
445
|
-
for (const extra of options?.invalidate ?? []) {
|
|
446
|
-
if (extra) namespaces.add(extra);
|
|
447
|
-
}
|
|
448
|
-
for (const nsName of namespaces) this.cache.invalidate(nsName);
|
|
449
|
-
}
|
|
450
248
|
return data;
|
|
451
249
|
}
|
|
452
|
-
// ── Cache key/namespace helpers ──
|
|
453
|
-
/** Build a token-scoped cache key: `METHOD path|token-hash|params`. */
|
|
454
|
-
cacheKeyFor(method, path, params) {
|
|
455
|
-
const token = this.authStore.token || "anon";
|
|
456
|
-
const qs = params ? "?" + new URLSearchParams(params).toString() : "";
|
|
457
|
-
return `${method} ${path}${qs}|${token}`;
|
|
458
|
-
}
|
|
459
|
-
/** Best-effort namespace (collection name) from a REST path. */
|
|
460
|
-
namespaceFromPath(path) {
|
|
461
|
-
const clean = path.split("?")[0];
|
|
462
|
-
const parts = clean.split("/").filter(Boolean);
|
|
463
|
-
if (parts.length === 0) return void 0;
|
|
464
|
-
if (parts[0] === "collections" || parts[0] === "_superusers") {
|
|
465
|
-
return parts[0];
|
|
466
|
-
}
|
|
467
|
-
return parts[0];
|
|
468
|
-
}
|
|
469
|
-
/**
|
|
470
|
-
* Semantic prefix tags for `deleteByPrefix`, derived from the REST shape:
|
|
471
|
-
* - `/{collection}?...` → `getList:{collection}`
|
|
472
|
-
* - `/{collection}/{id}` → `getOne:{collection}`
|
|
473
|
-
* - `/collections?...` / `/collections/{id}` → `collections:getList` / `collections:getOne`
|
|
474
|
-
*/
|
|
475
|
-
cacheTagsFor(path, namespace) {
|
|
476
|
-
if (!namespace) return [];
|
|
477
|
-
const clean = path.split("?")[0];
|
|
478
|
-
const parts = clean.split("/").filter(Boolean);
|
|
479
|
-
if (parts[0] === "collections" || parts[0] === "_superusers") {
|
|
480
|
-
const op2 = parts.length >= 2 ? "getOne" : "getList";
|
|
481
|
-
return [`${namespace}:${op2}`];
|
|
482
|
-
}
|
|
483
|
-
const op = parts.length >= 2 ? "getOne" : "getList";
|
|
484
|
-
return [`${op}:${namespace}`];
|
|
485
|
-
}
|
|
486
250
|
/**
|
|
487
251
|
* HTTP GET.
|
|
488
252
|
* @param path URL path.
|
|
@@ -658,6 +422,30 @@ var memoryStorage = {
|
|
|
658
422
|
};
|
|
659
423
|
|
|
660
424
|
// src/collection.ts
|
|
425
|
+
function stableStringify(value) {
|
|
426
|
+
const seen = /* @__PURE__ */ new Set();
|
|
427
|
+
const sort = (v) => {
|
|
428
|
+
if (Array.isArray(v)) return v.map(sort);
|
|
429
|
+
if (v && typeof v === "object") {
|
|
430
|
+
if (seen.has(v)) return "[Circular]";
|
|
431
|
+
seen.add(v);
|
|
432
|
+
const out = {};
|
|
433
|
+
for (const k of Object.keys(v).sort()) {
|
|
434
|
+
if (k === "requestKey" || k === "singleFlight" || k === "fetch" || k === "signal") {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
out[k] = sort(v[k]);
|
|
438
|
+
}
|
|
439
|
+
return out;
|
|
440
|
+
}
|
|
441
|
+
return v;
|
|
442
|
+
};
|
|
443
|
+
try {
|
|
444
|
+
return JSON.stringify(sort(value));
|
|
445
|
+
} catch {
|
|
446
|
+
return String(value);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
661
449
|
function normalizeAction(event, rawAction) {
|
|
662
450
|
if (typeof rawAction === "string") {
|
|
663
451
|
const a = rawAction.toLowerCase();
|
|
@@ -670,17 +458,104 @@ function normalizeAction(event, rawAction) {
|
|
|
670
458
|
if (event === "delete" || event === "record_delete") return "delete";
|
|
671
459
|
return "update";
|
|
672
460
|
}
|
|
673
|
-
var CollectionService = class {
|
|
461
|
+
var CollectionService = class _CollectionService {
|
|
674
462
|
/** @internal */
|
|
675
|
-
constructor(http, collectionName, authStore, realtime) {
|
|
463
|
+
constructor(http, collectionName, authStore, realtime, schema) {
|
|
676
464
|
this.http = http;
|
|
677
465
|
this.collectionName = collectionName;
|
|
678
466
|
this.authStore = authStore;
|
|
679
467
|
this.realtime = realtime;
|
|
468
|
+
this.schema = schema;
|
|
680
469
|
}
|
|
681
470
|
encodeId(id) {
|
|
682
471
|
return encodeURIComponent(id);
|
|
683
472
|
}
|
|
473
|
+
// ── Field projection (`.select`) ──
|
|
474
|
+
/**
|
|
475
|
+
* Project list/read responses to the given fields (PocketBase `fields`
|
|
476
|
+
* param). Field names are type-checked when this service is typed
|
|
477
|
+
* (codegen / `typed<T>()`).
|
|
478
|
+
*
|
|
479
|
+
* Returns a derived service — the original is untouched.
|
|
480
|
+
*
|
|
481
|
+
* @example
|
|
482
|
+
* ```ts
|
|
483
|
+
* const t = await client.collection("posts").select("id", "title").getList();
|
|
484
|
+
* // GET /api/posts?fields=id,title
|
|
485
|
+
*
|
|
486
|
+
* // All visible fields (hidden fields are excluded server-side)
|
|
487
|
+
* const all = await client.collection("posts").select("*").getList();
|
|
488
|
+
* // GET /api/posts?fields=id,title,published,… (schema known)
|
|
489
|
+
* ```
|
|
490
|
+
*
|
|
491
|
+
* When no schema is known, the default (no `select()` call) sends no
|
|
492
|
+
* `fields` param — the server returns every non-password field.
|
|
493
|
+
* Pass `select("*")` to restore the default after a projection.
|
|
494
|
+
*/
|
|
495
|
+
select(...fields) {
|
|
496
|
+
const preset = fields.length === 0 ? "*" : fields.join(",");
|
|
497
|
+
const derived = new _CollectionService(
|
|
498
|
+
this.http,
|
|
499
|
+
this.collectionName,
|
|
500
|
+
this.authStore,
|
|
501
|
+
this.realtime,
|
|
502
|
+
this.schema
|
|
503
|
+
);
|
|
504
|
+
derived.fieldsPreset = preset;
|
|
505
|
+
if (this.schema && preset !== "*") {
|
|
506
|
+
const known = new Set(this.schema.fields?.map((f) => f.name) ?? []);
|
|
507
|
+
for (const f of fields) {
|
|
508
|
+
if (!known.has(f)) {
|
|
509
|
+
console.warn(
|
|
510
|
+
`[lazypock] select("${f}"): unknown field on collection "${this.collectionName}"`
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return derived;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Fields to send with reads:
|
|
519
|
+
* 1. explicit `options.fields` (caller wins)
|
|
520
|
+
* 2. `select()` preset
|
|
521
|
+
* 3. schema default — all visible (non-hidden, non-system) fields
|
|
522
|
+
*/
|
|
523
|
+
effectiveFields(optionsFields) {
|
|
524
|
+
if (optionsFields !== void 0) return optionsFields;
|
|
525
|
+
if (this.fieldsPreset !== void 0) {
|
|
526
|
+
if (this.fieldsPreset === "*") {
|
|
527
|
+
return this.visibleFields();
|
|
528
|
+
}
|
|
529
|
+
return this.fieldsPreset;
|
|
530
|
+
}
|
|
531
|
+
return this.visibleFields();
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* All non-hidden, non-system, non-password field names from the schema.
|
|
535
|
+
* `undefined` when no schema is available (server decides).
|
|
536
|
+
*/
|
|
537
|
+
visibleFields() {
|
|
538
|
+
if (!this.schema?.fields) return void 0;
|
|
539
|
+
const visible = this.schema.fields.filter(
|
|
540
|
+
(f) => !f.hidden && f.type !== "password" && f.type !== "autodate"
|
|
541
|
+
).map((f) => f.name);
|
|
542
|
+
return visible.length > 0 ? visible.join(",") : void 0;
|
|
543
|
+
}
|
|
544
|
+
/** Warn once when an expand field is not a relation (schema known). */
|
|
545
|
+
validateExpand(expand) {
|
|
546
|
+
if (!expand || !this.schema?.fields) return;
|
|
547
|
+
const relations = new Set(
|
|
548
|
+
this.schema.fields.filter((f) => f.type === "relation").map((f) => f.name)
|
|
549
|
+
);
|
|
550
|
+
for (const f of expand.split(",")) {
|
|
551
|
+
const name = f.trim();
|
|
552
|
+
if (name && !relations.has(name)) {
|
|
553
|
+
console.warn(
|
|
554
|
+
`[lazypock] expand("${name}"): field is not a relation on collection "${this.collectionName}"`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
684
559
|
/**
|
|
685
560
|
* Fetch a paginated list of records (PocketBase `getList`).
|
|
686
561
|
*
|
|
@@ -696,12 +571,17 @@ var CollectionService = class {
|
|
|
696
571
|
fetch,
|
|
697
572
|
headers,
|
|
698
573
|
signal,
|
|
699
|
-
|
|
700
|
-
ttl,
|
|
701
|
-
invalidate,
|
|
574
|
+
singleFlight,
|
|
702
575
|
params,
|
|
703
576
|
...queryParams
|
|
704
577
|
} = options ?? {};
|
|
578
|
+
const fields = this.effectiveFields(queryParams.fields);
|
|
579
|
+
if (fields !== void 0 && queryParams.fields === void 0) {
|
|
580
|
+
queryParams.fields = fields;
|
|
581
|
+
}
|
|
582
|
+
if (typeof queryParams.expand === "string") {
|
|
583
|
+
this.validateExpand(queryParams.expand);
|
|
584
|
+
}
|
|
705
585
|
const qs = new URLSearchParams(
|
|
706
586
|
Object.fromEntries(
|
|
707
587
|
Object.entries({
|
|
@@ -720,9 +600,7 @@ var CollectionService = class {
|
|
|
720
600
|
fetch,
|
|
721
601
|
headers,
|
|
722
602
|
signal,
|
|
723
|
-
|
|
724
|
-
ttl,
|
|
725
|
-
invalidate,
|
|
603
|
+
singleFlight,
|
|
726
604
|
params
|
|
727
605
|
}
|
|
728
606
|
);
|
|
@@ -735,6 +613,7 @@ var CollectionService = class {
|
|
|
735
613
|
*/
|
|
736
614
|
async getFullList(options) {
|
|
737
615
|
const { batch = 1e3, ...rest } = options ?? {};
|
|
616
|
+
const effectiveKey = typeof rest.requestKey === "string" ? rest.requestKey : `getFullList:${this.collectionName}:${stableStringify(rest)}`;
|
|
738
617
|
const items = [];
|
|
739
618
|
let page = 1;
|
|
740
619
|
for (; ; ) {
|
|
@@ -742,9 +621,9 @@ var CollectionService = class {
|
|
|
742
621
|
page,
|
|
743
622
|
batch,
|
|
744
623
|
{
|
|
745
|
-
// disable auto-cancellation across pages — each page request is unique
|
|
746
624
|
...rest,
|
|
747
|
-
requestKey:
|
|
625
|
+
requestKey: effectiveKey,
|
|
626
|
+
singleFlight: true
|
|
748
627
|
}
|
|
749
628
|
);
|
|
750
629
|
if (!res || !res.items || res.items.length === 0) break;
|
|
@@ -773,8 +652,16 @@ var CollectionService = class {
|
|
|
773
652
|
* @param options Optional request options.
|
|
774
653
|
*/
|
|
775
654
|
getOne(id, options) {
|
|
655
|
+
const fields = this.effectiveFields(options?.fields);
|
|
656
|
+
if (typeof options?.expand === "string") {
|
|
657
|
+
this.validateExpand(options.expand);
|
|
658
|
+
}
|
|
659
|
+
const qs = new URLSearchParams();
|
|
660
|
+
if (fields !== void 0) qs.set("fields", fields);
|
|
661
|
+
if (typeof options?.expand === "string") qs.set("expand", options.expand);
|
|
662
|
+
const qsStr = qs.toString();
|
|
776
663
|
return this.http.get(
|
|
777
|
-
"/" + this.encodeId(this.collectionName) + "/" + this.encodeId(id),
|
|
664
|
+
"/" + this.encodeId(this.collectionName) + "/" + this.encodeId(id) + (qsStr ? "?" + qsStr : ""),
|
|
778
665
|
options
|
|
779
666
|
);
|
|
780
667
|
}
|
|
@@ -849,6 +736,22 @@ var CollectionService = class {
|
|
|
849
736
|
typed() {
|
|
850
737
|
return this;
|
|
851
738
|
}
|
|
739
|
+
/**
|
|
740
|
+
* Explicitly bind a schema for this service (overrides the client-level
|
|
741
|
+
* `types.schemas`). Enables schema-aware defaults: hidden fields are
|
|
742
|
+
* excluded from responses, select/expand are validated.
|
|
743
|
+
*/
|
|
744
|
+
withSchema(schema) {
|
|
745
|
+
const derived = new _CollectionService(
|
|
746
|
+
this.http,
|
|
747
|
+
this.collectionName,
|
|
748
|
+
this.authStore,
|
|
749
|
+
this.realtime,
|
|
750
|
+
schema
|
|
751
|
+
);
|
|
752
|
+
derived.fieldsPreset = this.fieldsPreset;
|
|
753
|
+
return derived;
|
|
754
|
+
}
|
|
852
755
|
// ── Realtime Subscriptions (PocketBase-style) ──
|
|
853
756
|
/**
|
|
854
757
|
* Subscribe to realtime changes for this collection.
|
|
@@ -1267,14 +1170,34 @@ var CollectionsService = class {
|
|
|
1267
1170
|
async getFullList(options) {
|
|
1268
1171
|
if (!this.http) return [];
|
|
1269
1172
|
const { batch = 1e3, ...rest } = options ?? {};
|
|
1173
|
+
const {
|
|
1174
|
+
requestKey: reqKey,
|
|
1175
|
+
singleFlight: _singleFlight,
|
|
1176
|
+
fetch: fetchFn,
|
|
1177
|
+
headers: hdrs,
|
|
1178
|
+
signal: sig,
|
|
1179
|
+
params: passthroughParams,
|
|
1180
|
+
...queryParams
|
|
1181
|
+
} = rest;
|
|
1182
|
+
const effectiveKey = typeof reqKey === "string" ? reqKey : `getFullList:collections:${stableStringify(rest)}`;
|
|
1270
1183
|
const items = [];
|
|
1271
1184
|
let page = 1;
|
|
1272
1185
|
for (; ; ) {
|
|
1273
|
-
const res = await this.getList(
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1186
|
+
const res = await this.getList(
|
|
1187
|
+
{
|
|
1188
|
+
...queryParams,
|
|
1189
|
+
...passthroughParams ?? {},
|
|
1190
|
+
page,
|
|
1191
|
+
perPage: batch
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
requestKey: effectiveKey,
|
|
1195
|
+
singleFlight: true,
|
|
1196
|
+
...fetchFn ? { fetch: fetchFn } : {},
|
|
1197
|
+
...hdrs ? { headers: hdrs } : {},
|
|
1198
|
+
...sig ? { signal: sig } : {}
|
|
1199
|
+
}
|
|
1200
|
+
);
|
|
1278
1201
|
if (!res || !res.items || res.items.length === 0) break;
|
|
1279
1202
|
items.push(...res.items);
|
|
1280
1203
|
if (page >= (res.totalPages ?? page)) break;
|
|
@@ -1373,50 +1296,6 @@ var LazypockClient = class {
|
|
|
1373
1296
|
*/
|
|
1374
1297
|
constructor(options) {
|
|
1375
1298
|
this.collectionCache = /* @__PURE__ */ new Map();
|
|
1376
|
-
/** Namespace → realtime unsubscribe; used for realtime-driven invalidation. */
|
|
1377
|
-
this.realtimeInvalidators = /* @__PURE__ */ new Map();
|
|
1378
|
-
// ── Query cache (opt-in by default; opt-out per request) ──
|
|
1379
|
-
/**
|
|
1380
|
-
* Configure the query cache at runtime (also a namespace for cache
|
|
1381
|
-
* management methods).
|
|
1382
|
-
*
|
|
1383
|
-
* ```ts
|
|
1384
|
-
* client.cache({ enabled: true, defaultTTL: 30_000 });
|
|
1385
|
-
* client.cache.deleteByPrefix('getList:posts'); // all list caches for posts
|
|
1386
|
-
* client.cache.deleteByPrefix('getOne:posts'); // all one-record caches
|
|
1387
|
-
* ```
|
|
1388
|
-
*
|
|
1389
|
-
* When enabled, GET requests cache their payload; mutations invalidate the
|
|
1390
|
-
* affected collection automatically. Individual requests can opt out with
|
|
1391
|
-
* `{ cache: false }` or override the TTL with `{ ttl: ms }`.
|
|
1392
|
-
*/
|
|
1393
|
-
this.cache = Object.assign(
|
|
1394
|
-
((config) => {
|
|
1395
|
-
if (!this.cacheStore) {
|
|
1396
|
-
this.cacheStore = new CacheStore({
|
|
1397
|
-
defaultTTL: config?.defaultTTL,
|
|
1398
|
-
store: config?.store,
|
|
1399
|
-
maxEntries: config?.maxEntries
|
|
1400
|
-
});
|
|
1401
|
-
this.http.setCache(this.cacheStore, config?.enabled ?? true);
|
|
1402
|
-
} else {
|
|
1403
|
-
if (config?.enabled !== void 0) {
|
|
1404
|
-
this.http.setCache(this.cacheStore, config.enabled);
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
return this;
|
|
1408
|
-
}),
|
|
1409
|
-
{
|
|
1410
|
-
deleteByPrefix: (prefix) => this.cacheStore?.deleteByPrefix(prefix),
|
|
1411
|
-
invalidate: (namespace) => this.cacheStore?.invalidate(namespace),
|
|
1412
|
-
clear: () => {
|
|
1413
|
-
this.cacheStore?.clear();
|
|
1414
|
-
for (const unsub of this.realtimeInvalidators.values()) unsub();
|
|
1415
|
-
this.realtimeInvalidators.clear();
|
|
1416
|
-
},
|
|
1417
|
-
stats: () => this.cacheStore ? this.cacheStore.stats() : null
|
|
1418
|
-
}
|
|
1419
|
-
);
|
|
1420
1299
|
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
1421
1300
|
this.authStore = options.authStore ?? new AuthStore(options.storage ?? memoryStorage);
|
|
1422
1301
|
this.http = new HttpClient(baseUrl, this.authStore);
|
|
@@ -1426,14 +1305,6 @@ var LazypockClient = class {
|
|
|
1426
1305
|
}
|
|
1427
1306
|
this.files = new FilesService(this.http);
|
|
1428
1307
|
this.collections = new CollectionsService(this.http, this.realtime);
|
|
1429
|
-
if (options.cache) {
|
|
1430
|
-
this.cacheStore = new CacheStore({
|
|
1431
|
-
defaultTTL: options.cache.defaultTTL,
|
|
1432
|
-
store: options.cache.store,
|
|
1433
|
-
maxEntries: options.cache.maxEntries
|
|
1434
|
-
});
|
|
1435
|
-
this.http.setCache(this.cacheStore, options.cache.enabled ?? false);
|
|
1436
|
-
}
|
|
1437
1308
|
if (options.types?.schemas) {
|
|
1438
1309
|
this.schemaByName = new Map(
|
|
1439
1310
|
options.types.schemas.map((s) => [s.name, s])
|
|
@@ -1451,14 +1322,16 @@ var LazypockClient = class {
|
|
|
1451
1322
|
* @param name The collection name.
|
|
1452
1323
|
* @returns A {@link CollectionService} instance.
|
|
1453
1324
|
*/
|
|
1454
|
-
collection(name) {
|
|
1325
|
+
collection(name, schema) {
|
|
1455
1326
|
let svc = this.collectionCache.get(name);
|
|
1456
1327
|
if (!svc) {
|
|
1457
1328
|
svc = new CollectionService(
|
|
1458
1329
|
this.http,
|
|
1459
1330
|
name,
|
|
1460
1331
|
this.authStore,
|
|
1461
|
-
this.realtime
|
|
1332
|
+
this.realtime,
|
|
1333
|
+
// Prefer the explicit schema, then the client-level `types.schemas`.
|
|
1334
|
+
schema ?? this.schemaByName?.get(name)
|
|
1462
1335
|
);
|
|
1463
1336
|
this.collectionCache.set(name, svc);
|
|
1464
1337
|
}
|
|
@@ -1514,49 +1387,6 @@ var LazypockClient = class {
|
|
|
1514
1387
|
this.http.cancelAllRequests();
|
|
1515
1388
|
return this;
|
|
1516
1389
|
}
|
|
1517
|
-
/**
|
|
1518
|
-
* Drop every cached entry (all collections / namespaces).
|
|
1519
|
-
* Also disables realtime-driven invalidation subscriptions.
|
|
1520
|
-
*/
|
|
1521
|
-
clearCache() {
|
|
1522
|
-
this.cacheStore?.clear();
|
|
1523
|
-
for (const unsub of this.realtimeInvalidators.values()) unsub();
|
|
1524
|
-
this.realtimeInvalidators.clear();
|
|
1525
|
-
return this;
|
|
1526
|
-
}
|
|
1527
|
-
/**
|
|
1528
|
-
* Invalidate cached entries for a collection (or custom namespace).
|
|
1529
|
-
* Runs automatically on mutations — call explicitly when data changed
|
|
1530
|
-
* out-of-band (e.g. another client wrote to the same collection).
|
|
1531
|
-
*/
|
|
1532
|
-
invalidateCache(namespace) {
|
|
1533
|
-
this.cacheStore?.invalidate(namespace);
|
|
1534
|
-
return this;
|
|
1535
|
-
}
|
|
1536
|
-
/**
|
|
1537
|
-
* Cache hit/miss/entry statistics.
|
|
1538
|
-
* Returns null when caching was never configured.
|
|
1539
|
-
*/
|
|
1540
|
-
cacheStats() {
|
|
1541
|
-
return this.cacheStore ? this.cacheStore.stats() : null;
|
|
1542
|
-
}
|
|
1543
|
-
/**
|
|
1544
|
-
* Subscribe a collection's cache to realtime invalidation: any inbound
|
|
1545
|
-
* create/update/delete event for the collection clears its cached entries.
|
|
1546
|
-
* Returns an unsubscribe function.
|
|
1547
|
-
*/
|
|
1548
|
-
invalidateCacheOnRealtime(collectionName) {
|
|
1549
|
-
if (!this.cacheStore) {
|
|
1550
|
-
this.cache({ enabled: false });
|
|
1551
|
-
}
|
|
1552
|
-
const existing = this.realtimeInvalidators.get(collectionName);
|
|
1553
|
-
if (existing) return existing;
|
|
1554
|
-
const unsub = this.collection(collectionName).subscribe(() => {
|
|
1555
|
-
this.cacheStore?.invalidate(collectionName);
|
|
1556
|
-
});
|
|
1557
|
-
this.realtimeInvalidators.set(collectionName, unsub);
|
|
1558
|
-
return unsub;
|
|
1559
|
-
}
|
|
1560
1390
|
// ── Auth ──
|
|
1561
1391
|
/** Check whether any superuser exists (for login vs setup screen routing). */
|
|
1562
1392
|
async checkSuperuser() {
|
|
@@ -1693,7 +1523,6 @@ function createClient(options) {
|
|
|
1693
1523
|
export {
|
|
1694
1524
|
ApiError,
|
|
1695
1525
|
AuthStore,
|
|
1696
|
-
CacheStore,
|
|
1697
1526
|
CollectionService,
|
|
1698
1527
|
CollectionsService,
|
|
1699
1528
|
FilesService,
|
|
@@ -1709,7 +1538,6 @@ export {
|
|
|
1709
1538
|
getFileUrl,
|
|
1710
1539
|
getScaleUrl,
|
|
1711
1540
|
getThumbUrl,
|
|
1712
|
-
resolveCacheDirective,
|
|
1713
1541
|
schemaFieldType,
|
|
1714
1542
|
wsUrlFromBaseUrl
|
|
1715
1543
|
};
|