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.cjs
CHANGED
|
@@ -22,7 +22,6 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
ApiError: () => ApiError,
|
|
24
24
|
AuthStore: () => AuthStore,
|
|
25
|
-
CacheStore: () => CacheStore,
|
|
26
25
|
CollectionService: () => CollectionService,
|
|
27
26
|
CollectionsService: () => CollectionsService,
|
|
28
27
|
FilesService: () => FilesService,
|
|
@@ -38,7 +37,6 @@ __export(index_exports, {
|
|
|
38
37
|
getFileUrl: () => getFileUrl,
|
|
39
38
|
getScaleUrl: () => getScaleUrl,
|
|
40
39
|
getThumbUrl: () => getThumbUrl,
|
|
41
|
-
resolveCacheDirective: () => resolveCacheDirective,
|
|
42
40
|
schemaFieldType: () => schemaFieldType,
|
|
43
41
|
wsUrlFromBaseUrl: () => wsUrlFromBaseUrl
|
|
44
42
|
});
|
|
@@ -55,201 +53,6 @@ var ApiError = class extends Error {
|
|
|
55
53
|
}
|
|
56
54
|
};
|
|
57
55
|
|
|
58
|
-
// src/cache.ts
|
|
59
|
-
var CacheStore = class {
|
|
60
|
-
constructor(config = {}) {
|
|
61
|
-
this.memory = /* @__PURE__ */ new Map();
|
|
62
|
-
this.hits = 0;
|
|
63
|
-
this.misses = 0;
|
|
64
|
-
this.namespaceEntries = /* @__PURE__ */ new Map();
|
|
65
|
-
/** Key → set of prefix tags registered for that key (e.g. `getList:posts`). */
|
|
66
|
-
this.prefixEntries = /* @__PURE__ */ new Map();
|
|
67
|
-
this.ttl = config.defaultTTL ?? 6e4;
|
|
68
|
-
this.persistence = config.store;
|
|
69
|
-
this.maxEntries = config.maxEntries ?? 500;
|
|
70
|
-
}
|
|
71
|
-
/** Resolve the effective TTL: request override → global default. */
|
|
72
|
-
resolveTTL(ttl) {
|
|
73
|
-
return ttl && ttl > 0 ? ttl : this.ttl;
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Read a cached value. Fast sync path (memory) with async persistence
|
|
77
|
-
* fallback for adapters whose `get` returns a Promise.
|
|
78
|
-
* @param key Cache key (e.g. `"GET /posts?page=1"`).
|
|
79
|
-
* @returns The cached value, or undefined when absent/expired (the hit is
|
|
80
|
-
* cleared on expiry so a stale value is never served).
|
|
81
|
-
*/
|
|
82
|
-
async get(key) {
|
|
83
|
-
const mem = this.memory.get(key);
|
|
84
|
-
if (mem !== void 0) {
|
|
85
|
-
if (Date.now() > mem.expiresAt) {
|
|
86
|
-
this.delete(key);
|
|
87
|
-
this.misses++;
|
|
88
|
-
return void 0;
|
|
89
|
-
}
|
|
90
|
-
this.memory.delete(key);
|
|
91
|
-
this.memory.set(key, mem);
|
|
92
|
-
this.hits++;
|
|
93
|
-
return mem.value;
|
|
94
|
-
}
|
|
95
|
-
if (this.persistence) {
|
|
96
|
-
const entry = await this.readPersisted(key);
|
|
97
|
-
if (entry) {
|
|
98
|
-
if (Date.now() > entry.expiresAt) {
|
|
99
|
-
this.delete(key);
|
|
100
|
-
this.misses++;
|
|
101
|
-
return void 0;
|
|
102
|
-
}
|
|
103
|
-
this.hits++;
|
|
104
|
-
return entry.value;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
this.misses++;
|
|
108
|
-
return void 0;
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Store a value.
|
|
112
|
-
* @param key Cache key.
|
|
113
|
-
* @param value The response payload.
|
|
114
|
-
* @param ttlOverride Optional TTL override (ms).
|
|
115
|
-
* @param namespace Optional namespace for group invalidation.
|
|
116
|
-
*/
|
|
117
|
-
set(key, value, ttlOverride, namespace, tags) {
|
|
118
|
-
const expiresAt = Date.now() + this.resolveTTL(ttlOverride);
|
|
119
|
-
const entry = { value, expiresAt, namespace, tags };
|
|
120
|
-
this.memory.set(key, entry);
|
|
121
|
-
if (this.memory.size > this.maxEntries) {
|
|
122
|
-
const oldest = this.memory.keys().next().value;
|
|
123
|
-
if (oldest !== void 0) this.delete(oldest);
|
|
124
|
-
}
|
|
125
|
-
if (namespace) {
|
|
126
|
-
let keys = this.namespaceEntries.get(namespace);
|
|
127
|
-
if (!keys) {
|
|
128
|
-
keys = /* @__PURE__ */ new Set();
|
|
129
|
-
this.namespaceEntries.set(namespace, keys);
|
|
130
|
-
}
|
|
131
|
-
keys.add(key);
|
|
132
|
-
}
|
|
133
|
-
for (const tag of tags ?? []) {
|
|
134
|
-
let keys = this.prefixEntries.get(tag);
|
|
135
|
-
if (!keys) {
|
|
136
|
-
keys = /* @__PURE__ */ new Set();
|
|
137
|
-
this.prefixEntries.set(tag, keys);
|
|
138
|
-
}
|
|
139
|
-
keys.add(key);
|
|
140
|
-
}
|
|
141
|
-
if (this.persistence) {
|
|
142
|
-
void this.persistence.set(this.persistKey(key), JSON.stringify(entry));
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Invalidate entries belonging to a namespace (e.g. a collection name).
|
|
147
|
-
* Also clears the namespace index entry.
|
|
148
|
-
*/
|
|
149
|
-
invalidate(namespace) {
|
|
150
|
-
const keys = Array.from(this.namespaceEntries.get(namespace) ?? []);
|
|
151
|
-
for (const key of keys) this.delete(key);
|
|
152
|
-
this.namespaceEntries.delete(namespace);
|
|
153
|
-
}
|
|
154
|
-
/** Remove a single key. */
|
|
155
|
-
delete(key) {
|
|
156
|
-
const entry = this.memory.get(key);
|
|
157
|
-
if (entry?.namespace) {
|
|
158
|
-
const set = this.namespaceEntries.get(entry.namespace);
|
|
159
|
-
if (set) {
|
|
160
|
-
set.delete(key);
|
|
161
|
-
if (set.size === 0) this.namespaceEntries.delete(entry.namespace);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
for (const tag of entry?.tags ?? []) {
|
|
165
|
-
const set = this.prefixEntries.get(tag);
|
|
166
|
-
if (set) {
|
|
167
|
-
set.delete(key);
|
|
168
|
-
if (set.size === 0) this.prefixEntries.delete(tag);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
this.memory.delete(key);
|
|
172
|
-
if (this.persistence) {
|
|
173
|
-
void this.persistence.remove(this.persistKey(key));
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Delete every entry whose key starts with `prefix`.
|
|
178
|
-
*
|
|
179
|
-
* Useful for fine-grained invalidation, e.g.:
|
|
180
|
-
* ```ts
|
|
181
|
-
* client.cache.deleteByPrefix('getList:posts'); // delete all getList cache
|
|
182
|
-
* client.cache.deleteByPrefix('getOne:posts'); // delete all getOne cache
|
|
183
|
-
* ```
|
|
184
|
-
*/
|
|
185
|
-
deleteByPrefix(prefix) {
|
|
186
|
-
if (!prefix) return;
|
|
187
|
-
const tagged = this.prefixEntries.get(prefix);
|
|
188
|
-
if (tagged) {
|
|
189
|
-
for (const key of Array.from(tagged)) this.delete(key);
|
|
190
|
-
this.prefixEntries.delete(prefix);
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
for (const key of Array.from(this.memory.keys())) {
|
|
194
|
-
if (key.startsWith(prefix)) this.delete(key);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
/** Drop every cached entry (memory + persistence). */
|
|
198
|
-
clear() {
|
|
199
|
-
this.memory.clear();
|
|
200
|
-
this.namespaceEntries.clear();
|
|
201
|
-
this.prefixEntries.clear();
|
|
202
|
-
}
|
|
203
|
-
/** Cache hit/miss/entry statistics. */
|
|
204
|
-
stats() {
|
|
205
|
-
return { hits: this.hits, misses: this.misses, entries: this.memory.size };
|
|
206
|
-
}
|
|
207
|
-
persistKey(key) {
|
|
208
|
-
return "lazypock:cache:" + key;
|
|
209
|
-
}
|
|
210
|
-
async readPersisted(key) {
|
|
211
|
-
if (!this.persistence) return void 0;
|
|
212
|
-
const raw = await this.persistence.get(this.persistKey(key));
|
|
213
|
-
if (raw == null) return void 0;
|
|
214
|
-
try {
|
|
215
|
-
const entry = JSON.parse(raw);
|
|
216
|
-
this.memory.set(key, entry);
|
|
217
|
-
if (entry.namespace) {
|
|
218
|
-
let keys = this.namespaceEntries.get(entry.namespace);
|
|
219
|
-
if (!keys) {
|
|
220
|
-
keys = /* @__PURE__ */ new Set();
|
|
221
|
-
this.namespaceEntries.set(entry.namespace, keys);
|
|
222
|
-
}
|
|
223
|
-
keys.add(key);
|
|
224
|
-
}
|
|
225
|
-
for (const tag of entry.tags ?? []) {
|
|
226
|
-
let keys = this.prefixEntries.get(tag);
|
|
227
|
-
if (!keys) {
|
|
228
|
-
keys = /* @__PURE__ */ new Set();
|
|
229
|
-
this.prefixEntries.set(tag, keys);
|
|
230
|
-
}
|
|
231
|
-
keys.add(key);
|
|
232
|
-
}
|
|
233
|
-
return entry;
|
|
234
|
-
} catch {
|
|
235
|
-
void this.persistence.remove(this.persistKey(key));
|
|
236
|
-
return void 0;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
function resolveCacheDirective(opts) {
|
|
241
|
-
if (!opts) return null;
|
|
242
|
-
if (typeof opts.ttl === "number" && opts.ttl > 0) {
|
|
243
|
-
return { enabled: true, ttl: opts.ttl };
|
|
244
|
-
}
|
|
245
|
-
const c = opts.cache;
|
|
246
|
-
if (c === void 0) return null;
|
|
247
|
-
if (c === true) return { enabled: true };
|
|
248
|
-
if (c === false) return { enabled: false };
|
|
249
|
-
if (typeof c === "number") return { enabled: true, ttl: c > 0 ? c : void 0 };
|
|
250
|
-
return { enabled: true, ttl: c.ttl, key: c.key };
|
|
251
|
-
}
|
|
252
|
-
|
|
253
56
|
// src/http.ts
|
|
254
57
|
function isAbortError(err) {
|
|
255
58
|
return err instanceof Error && (err.name === "AbortError" || err.message === "Aborted");
|
|
@@ -260,32 +63,25 @@ var HttpClient = class {
|
|
|
260
63
|
* @param authStore The auth store providing the token for Authorization headers.
|
|
261
64
|
*/
|
|
262
65
|
constructor(baseUrl, authStore) {
|
|
263
|
-
/** Master switch resolved from CacheConfig.enabled. */
|
|
264
|
-
this.cacheEnabled = false;
|
|
265
66
|
/**
|
|
266
67
|
* Abort controllers for in-flight requests, keyed by their cancellation key
|
|
267
68
|
* (default `METHOD path`). A new request with the same key aborts the
|
|
268
69
|
* previous one — PocketBase-style auto-cancellation of duplicated requests.
|
|
269
70
|
*/
|
|
270
71
|
this.cancelControllers = {};
|
|
72
|
+
/**
|
|
73
|
+
* In-flight request promises, keyed by cancellation key. When auto-cancellation
|
|
74
|
+
* would abort a pending duplicate, the newer request instead awaits the same
|
|
75
|
+
* promise — single-flight coalescing (no duplicate network request, no
|
|
76
|
+
* spurious abort rejection for the caller).
|
|
77
|
+
*/
|
|
78
|
+
this.inflight = {};
|
|
271
79
|
/** Global toggle for the auto-cancellation behaviour (default: on). */
|
|
272
80
|
this.enableAutoCancellation = true;
|
|
273
81
|
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
274
82
|
this.authStore = authStore;
|
|
275
83
|
this.defaultFetch = globalThis.fetch.bind(globalThis);
|
|
276
84
|
}
|
|
277
|
-
/**
|
|
278
|
-
* Attach a cache store + master switch.
|
|
279
|
-
* Called by the client constructor when cache config is present.
|
|
280
|
-
*/
|
|
281
|
-
setCache(cache, enabled) {
|
|
282
|
-
this.cache = cache;
|
|
283
|
-
this.cacheEnabled = enabled;
|
|
284
|
-
}
|
|
285
|
-
/** Whether the global cache flag is on (requests opt in/out individually too). */
|
|
286
|
-
get cacheIsEnabled() {
|
|
287
|
-
return this.cacheEnabled;
|
|
288
|
-
}
|
|
289
85
|
async refreshAuth() {
|
|
290
86
|
const collection = this.authStore.collectionName;
|
|
291
87
|
if (!collection) return null;
|
|
@@ -362,27 +158,18 @@ var HttpClient = class {
|
|
|
362
158
|
* @throws {ApiError} On non-2xx responses or when the request is aborted
|
|
363
159
|
* (aborted requests throw an `ApiError` with `isAbort === true`).
|
|
364
160
|
*/
|
|
365
|
-
/** Invalidate a namespace (collection name). No-op when cache is off. */
|
|
366
|
-
invalidateCache(namespace) {
|
|
367
|
-
this.cache?.invalidate(namespace);
|
|
368
|
-
}
|
|
369
|
-
/** Current cache statistics (hits/misses/entries), or null when disabled. */
|
|
370
|
-
cacheStats() {
|
|
371
|
-
return this.cache ? this.cache.stats() : null;
|
|
372
|
-
}
|
|
373
161
|
async request(method, path, body, options) {
|
|
374
162
|
if (this.authStore.isExpired && this.authStore.collectionName) {
|
|
375
163
|
await this.refreshAuth();
|
|
376
164
|
}
|
|
377
|
-
const cacheDirective = resolveCacheDirective(options);
|
|
378
|
-
const wantCache = cacheDirective !== null ? cacheDirective.enabled : this.cacheEnabled;
|
|
379
|
-
const cacheKey = method === "GET" && this.cache && wantCache ? this.cacheKeyFor(method, path, options?.params) : null;
|
|
380
|
-
if (cacheKey !== null) {
|
|
381
|
-
const hit = await this.cache?.get(cacheKey);
|
|
382
|
-
if (hit !== void 0) return hit;
|
|
383
|
-
}
|
|
384
165
|
let requestKey = options?.requestKey === void 0 ? options?.cancelKey ?? `${method} ${path}` : options.requestKey;
|
|
385
166
|
if (options?.autoCancel === false) requestKey = null;
|
|
167
|
+
if (options?.singleFlight && requestKey !== null) {
|
|
168
|
+
const pending = this.inflight[requestKey];
|
|
169
|
+
if (pending !== void 0) {
|
|
170
|
+
return pending;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
386
173
|
let controller = null;
|
|
387
174
|
const externalSignal = options?.signal;
|
|
388
175
|
if (requestKey !== null) {
|
|
@@ -400,6 +187,36 @@ var HttpClient = class {
|
|
|
400
187
|
}
|
|
401
188
|
}
|
|
402
189
|
const signal = controller?.signal ?? externalSignal;
|
|
190
|
+
const perform = async () => {
|
|
191
|
+
try {
|
|
192
|
+
return await this.doRequest(
|
|
193
|
+
method,
|
|
194
|
+
path,
|
|
195
|
+
body,
|
|
196
|
+
options,
|
|
197
|
+
signal,
|
|
198
|
+
requestKey,
|
|
199
|
+
controller
|
|
200
|
+
);
|
|
201
|
+
} finally {
|
|
202
|
+
if (requestKey !== null) {
|
|
203
|
+
if (this.inflight[requestKey] === promise) {
|
|
204
|
+
delete this.inflight[requestKey];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
const promise = perform();
|
|
210
|
+
if (requestKey !== null) {
|
|
211
|
+
this.inflight[requestKey] = promise;
|
|
212
|
+
}
|
|
213
|
+
return promise;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Execute the actual HTTP request (fetch + parse). Called by {@link request}
|
|
217
|
+
* as the inner in-flight unit so single-flight callers can reuse the promise.
|
|
218
|
+
*/
|
|
219
|
+
async doRequest(method, path, body, options, signal, requestKey, controller) {
|
|
403
220
|
let url = this.baseUrl + path;
|
|
404
221
|
if (options?.params) {
|
|
405
222
|
const qs = new URLSearchParams(options.params).toString();
|
|
@@ -464,63 +281,8 @@ var HttpClient = class {
|
|
|
464
281
|
res.status
|
|
465
282
|
);
|
|
466
283
|
}
|
|
467
|
-
if (cacheKey !== null && this.cache) {
|
|
468
|
-
const namespace = this.namespaceFromPath(path);
|
|
469
|
-
const ttl = cacheDirective?.ttl;
|
|
470
|
-
const tags = this.cacheTagsFor(path, namespace);
|
|
471
|
-
this.cache.set(
|
|
472
|
-
cacheKey,
|
|
473
|
-
data,
|
|
474
|
-
ttl,
|
|
475
|
-
namespace ?? void 0,
|
|
476
|
-
tags
|
|
477
|
-
);
|
|
478
|
-
}
|
|
479
|
-
if (method !== "GET" && this.cache) {
|
|
480
|
-
const namespaces = /* @__PURE__ */ new Set();
|
|
481
|
-
const ns = this.namespaceFromPath(path);
|
|
482
|
-
if (ns) namespaces.add(ns);
|
|
483
|
-
for (const extra of options?.invalidate ?? []) {
|
|
484
|
-
if (extra) namespaces.add(extra);
|
|
485
|
-
}
|
|
486
|
-
for (const nsName of namespaces) this.cache.invalidate(nsName);
|
|
487
|
-
}
|
|
488
284
|
return data;
|
|
489
285
|
}
|
|
490
|
-
// ── Cache key/namespace helpers ──
|
|
491
|
-
/** Build a token-scoped cache key: `METHOD path|token-hash|params`. */
|
|
492
|
-
cacheKeyFor(method, path, params) {
|
|
493
|
-
const token = this.authStore.token || "anon";
|
|
494
|
-
const qs = params ? "?" + new URLSearchParams(params).toString() : "";
|
|
495
|
-
return `${method} ${path}${qs}|${token}`;
|
|
496
|
-
}
|
|
497
|
-
/** Best-effort namespace (collection name) from a REST path. */
|
|
498
|
-
namespaceFromPath(path) {
|
|
499
|
-
const clean = path.split("?")[0];
|
|
500
|
-
const parts = clean.split("/").filter(Boolean);
|
|
501
|
-
if (parts.length === 0) return void 0;
|
|
502
|
-
if (parts[0] === "collections" || parts[0] === "_superusers") {
|
|
503
|
-
return parts[0];
|
|
504
|
-
}
|
|
505
|
-
return parts[0];
|
|
506
|
-
}
|
|
507
|
-
/**
|
|
508
|
-
* Semantic prefix tags for `deleteByPrefix`, derived from the REST shape:
|
|
509
|
-
* - `/{collection}?...` → `getList:{collection}`
|
|
510
|
-
* - `/{collection}/{id}` → `getOne:{collection}`
|
|
511
|
-
* - `/collections?...` / `/collections/{id}` → `collections:getList` / `collections:getOne`
|
|
512
|
-
*/
|
|
513
|
-
cacheTagsFor(path, namespace) {
|
|
514
|
-
if (!namespace) return [];
|
|
515
|
-
const clean = path.split("?")[0];
|
|
516
|
-
const parts = clean.split("/").filter(Boolean);
|
|
517
|
-
if (parts[0] === "collections" || parts[0] === "_superusers") {
|
|
518
|
-
const op2 = parts.length >= 2 ? "getOne" : "getList";
|
|
519
|
-
return [`${namespace}:${op2}`];
|
|
520
|
-
}
|
|
521
|
-
const op = parts.length >= 2 ? "getOne" : "getList";
|
|
522
|
-
return [`${op}:${namespace}`];
|
|
523
|
-
}
|
|
524
286
|
/**
|
|
525
287
|
* HTTP GET.
|
|
526
288
|
* @param path URL path.
|
|
@@ -696,6 +458,30 @@ var memoryStorage = {
|
|
|
696
458
|
};
|
|
697
459
|
|
|
698
460
|
// src/collection.ts
|
|
461
|
+
function stableStringify(value) {
|
|
462
|
+
const seen = /* @__PURE__ */ new Set();
|
|
463
|
+
const sort = (v) => {
|
|
464
|
+
if (Array.isArray(v)) return v.map(sort);
|
|
465
|
+
if (v && typeof v === "object") {
|
|
466
|
+
if (seen.has(v)) return "[Circular]";
|
|
467
|
+
seen.add(v);
|
|
468
|
+
const out = {};
|
|
469
|
+
for (const k of Object.keys(v).sort()) {
|
|
470
|
+
if (k === "requestKey" || k === "singleFlight" || k === "fetch" || k === "signal") {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
out[k] = sort(v[k]);
|
|
474
|
+
}
|
|
475
|
+
return out;
|
|
476
|
+
}
|
|
477
|
+
return v;
|
|
478
|
+
};
|
|
479
|
+
try {
|
|
480
|
+
return JSON.stringify(sort(value));
|
|
481
|
+
} catch {
|
|
482
|
+
return String(value);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
699
485
|
function normalizeAction(event, rawAction) {
|
|
700
486
|
if (typeof rawAction === "string") {
|
|
701
487
|
const a = rawAction.toLowerCase();
|
|
@@ -708,17 +494,104 @@ function normalizeAction(event, rawAction) {
|
|
|
708
494
|
if (event === "delete" || event === "record_delete") return "delete";
|
|
709
495
|
return "update";
|
|
710
496
|
}
|
|
711
|
-
var CollectionService = class {
|
|
497
|
+
var CollectionService = class _CollectionService {
|
|
712
498
|
/** @internal */
|
|
713
|
-
constructor(http, collectionName, authStore, realtime) {
|
|
499
|
+
constructor(http, collectionName, authStore, realtime, schema) {
|
|
714
500
|
this.http = http;
|
|
715
501
|
this.collectionName = collectionName;
|
|
716
502
|
this.authStore = authStore;
|
|
717
503
|
this.realtime = realtime;
|
|
504
|
+
this.schema = schema;
|
|
718
505
|
}
|
|
719
506
|
encodeId(id) {
|
|
720
507
|
return encodeURIComponent(id);
|
|
721
508
|
}
|
|
509
|
+
// ── Field projection (`.select`) ──
|
|
510
|
+
/**
|
|
511
|
+
* Project list/read responses to the given fields (PocketBase `fields`
|
|
512
|
+
* param). Field names are type-checked when this service is typed
|
|
513
|
+
* (codegen / `typed<T>()`).
|
|
514
|
+
*
|
|
515
|
+
* Returns a derived service — the original is untouched.
|
|
516
|
+
*
|
|
517
|
+
* @example
|
|
518
|
+
* ```ts
|
|
519
|
+
* const t = await client.collection("posts").select("id", "title").getList();
|
|
520
|
+
* // GET /api/posts?fields=id,title
|
|
521
|
+
*
|
|
522
|
+
* // All visible fields (hidden fields are excluded server-side)
|
|
523
|
+
* const all = await client.collection("posts").select("*").getList();
|
|
524
|
+
* // GET /api/posts?fields=id,title,published,… (schema known)
|
|
525
|
+
* ```
|
|
526
|
+
*
|
|
527
|
+
* When no schema is known, the default (no `select()` call) sends no
|
|
528
|
+
* `fields` param — the server returns every non-password field.
|
|
529
|
+
* Pass `select("*")` to restore the default after a projection.
|
|
530
|
+
*/
|
|
531
|
+
select(...fields) {
|
|
532
|
+
const preset = fields.length === 0 ? "*" : fields.join(",");
|
|
533
|
+
const derived = new _CollectionService(
|
|
534
|
+
this.http,
|
|
535
|
+
this.collectionName,
|
|
536
|
+
this.authStore,
|
|
537
|
+
this.realtime,
|
|
538
|
+
this.schema
|
|
539
|
+
);
|
|
540
|
+
derived.fieldsPreset = preset;
|
|
541
|
+
if (this.schema && preset !== "*") {
|
|
542
|
+
const known = new Set(this.schema.fields?.map((f) => f.name) ?? []);
|
|
543
|
+
for (const f of fields) {
|
|
544
|
+
if (!known.has(f)) {
|
|
545
|
+
console.warn(
|
|
546
|
+
`[lazypock] select("${f}"): unknown field on collection "${this.collectionName}"`
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return derived;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Fields to send with reads:
|
|
555
|
+
* 1. explicit `options.fields` (caller wins)
|
|
556
|
+
* 2. `select()` preset
|
|
557
|
+
* 3. schema default — all visible (non-hidden, non-system) fields
|
|
558
|
+
*/
|
|
559
|
+
effectiveFields(optionsFields) {
|
|
560
|
+
if (optionsFields !== void 0) return optionsFields;
|
|
561
|
+
if (this.fieldsPreset !== void 0) {
|
|
562
|
+
if (this.fieldsPreset === "*") {
|
|
563
|
+
return this.visibleFields();
|
|
564
|
+
}
|
|
565
|
+
return this.fieldsPreset;
|
|
566
|
+
}
|
|
567
|
+
return this.visibleFields();
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* All non-hidden, non-system, non-password field names from the schema.
|
|
571
|
+
* `undefined` when no schema is available (server decides).
|
|
572
|
+
*/
|
|
573
|
+
visibleFields() {
|
|
574
|
+
if (!this.schema?.fields) return void 0;
|
|
575
|
+
const visible = this.schema.fields.filter(
|
|
576
|
+
(f) => !f.hidden && f.type !== "password" && f.type !== "autodate"
|
|
577
|
+
).map((f) => f.name);
|
|
578
|
+
return visible.length > 0 ? visible.join(",") : void 0;
|
|
579
|
+
}
|
|
580
|
+
/** Warn once when an expand field is not a relation (schema known). */
|
|
581
|
+
validateExpand(expand) {
|
|
582
|
+
if (!expand || !this.schema?.fields) return;
|
|
583
|
+
const relations = new Set(
|
|
584
|
+
this.schema.fields.filter((f) => f.type === "relation").map((f) => f.name)
|
|
585
|
+
);
|
|
586
|
+
for (const f of expand.split(",")) {
|
|
587
|
+
const name = f.trim();
|
|
588
|
+
if (name && !relations.has(name)) {
|
|
589
|
+
console.warn(
|
|
590
|
+
`[lazypock] expand("${name}"): field is not a relation on collection "${this.collectionName}"`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
722
595
|
/**
|
|
723
596
|
* Fetch a paginated list of records (PocketBase `getList`).
|
|
724
597
|
*
|
|
@@ -734,12 +607,17 @@ var CollectionService = class {
|
|
|
734
607
|
fetch,
|
|
735
608
|
headers,
|
|
736
609
|
signal,
|
|
737
|
-
|
|
738
|
-
ttl,
|
|
739
|
-
invalidate,
|
|
610
|
+
singleFlight,
|
|
740
611
|
params,
|
|
741
612
|
...queryParams
|
|
742
613
|
} = options ?? {};
|
|
614
|
+
const fields = this.effectiveFields(queryParams.fields);
|
|
615
|
+
if (fields !== void 0 && queryParams.fields === void 0) {
|
|
616
|
+
queryParams.fields = fields;
|
|
617
|
+
}
|
|
618
|
+
if (typeof queryParams.expand === "string") {
|
|
619
|
+
this.validateExpand(queryParams.expand);
|
|
620
|
+
}
|
|
743
621
|
const qs = new URLSearchParams(
|
|
744
622
|
Object.fromEntries(
|
|
745
623
|
Object.entries({
|
|
@@ -758,9 +636,7 @@ var CollectionService = class {
|
|
|
758
636
|
fetch,
|
|
759
637
|
headers,
|
|
760
638
|
signal,
|
|
761
|
-
|
|
762
|
-
ttl,
|
|
763
|
-
invalidate,
|
|
639
|
+
singleFlight,
|
|
764
640
|
params
|
|
765
641
|
}
|
|
766
642
|
);
|
|
@@ -773,6 +649,7 @@ var CollectionService = class {
|
|
|
773
649
|
*/
|
|
774
650
|
async getFullList(options) {
|
|
775
651
|
const { batch = 1e3, ...rest } = options ?? {};
|
|
652
|
+
const effectiveKey = typeof rest.requestKey === "string" ? rest.requestKey : `getFullList:${this.collectionName}:${stableStringify(rest)}`;
|
|
776
653
|
const items = [];
|
|
777
654
|
let page = 1;
|
|
778
655
|
for (; ; ) {
|
|
@@ -780,9 +657,9 @@ var CollectionService = class {
|
|
|
780
657
|
page,
|
|
781
658
|
batch,
|
|
782
659
|
{
|
|
783
|
-
// disable auto-cancellation across pages — each page request is unique
|
|
784
660
|
...rest,
|
|
785
|
-
requestKey:
|
|
661
|
+
requestKey: effectiveKey,
|
|
662
|
+
singleFlight: true
|
|
786
663
|
}
|
|
787
664
|
);
|
|
788
665
|
if (!res || !res.items || res.items.length === 0) break;
|
|
@@ -811,8 +688,16 @@ var CollectionService = class {
|
|
|
811
688
|
* @param options Optional request options.
|
|
812
689
|
*/
|
|
813
690
|
getOne(id, options) {
|
|
691
|
+
const fields = this.effectiveFields(options?.fields);
|
|
692
|
+
if (typeof options?.expand === "string") {
|
|
693
|
+
this.validateExpand(options.expand);
|
|
694
|
+
}
|
|
695
|
+
const qs = new URLSearchParams();
|
|
696
|
+
if (fields !== void 0) qs.set("fields", fields);
|
|
697
|
+
if (typeof options?.expand === "string") qs.set("expand", options.expand);
|
|
698
|
+
const qsStr = qs.toString();
|
|
814
699
|
return this.http.get(
|
|
815
|
-
"/" + this.encodeId(this.collectionName) + "/" + this.encodeId(id),
|
|
700
|
+
"/" + this.encodeId(this.collectionName) + "/" + this.encodeId(id) + (qsStr ? "?" + qsStr : ""),
|
|
816
701
|
options
|
|
817
702
|
);
|
|
818
703
|
}
|
|
@@ -887,6 +772,22 @@ var CollectionService = class {
|
|
|
887
772
|
typed() {
|
|
888
773
|
return this;
|
|
889
774
|
}
|
|
775
|
+
/**
|
|
776
|
+
* Explicitly bind a schema for this service (overrides the client-level
|
|
777
|
+
* `types.schemas`). Enables schema-aware defaults: hidden fields are
|
|
778
|
+
* excluded from responses, select/expand are validated.
|
|
779
|
+
*/
|
|
780
|
+
withSchema(schema) {
|
|
781
|
+
const derived = new _CollectionService(
|
|
782
|
+
this.http,
|
|
783
|
+
this.collectionName,
|
|
784
|
+
this.authStore,
|
|
785
|
+
this.realtime,
|
|
786
|
+
schema
|
|
787
|
+
);
|
|
788
|
+
derived.fieldsPreset = this.fieldsPreset;
|
|
789
|
+
return derived;
|
|
790
|
+
}
|
|
890
791
|
// ── Realtime Subscriptions (PocketBase-style) ──
|
|
891
792
|
/**
|
|
892
793
|
* Subscribe to realtime changes for this collection.
|
|
@@ -1305,14 +1206,34 @@ var CollectionsService = class {
|
|
|
1305
1206
|
async getFullList(options) {
|
|
1306
1207
|
if (!this.http) return [];
|
|
1307
1208
|
const { batch = 1e3, ...rest } = options ?? {};
|
|
1209
|
+
const {
|
|
1210
|
+
requestKey: reqKey,
|
|
1211
|
+
singleFlight: _singleFlight,
|
|
1212
|
+
fetch: fetchFn,
|
|
1213
|
+
headers: hdrs,
|
|
1214
|
+
signal: sig,
|
|
1215
|
+
params: passthroughParams,
|
|
1216
|
+
...queryParams
|
|
1217
|
+
} = rest;
|
|
1218
|
+
const effectiveKey = typeof reqKey === "string" ? reqKey : `getFullList:collections:${stableStringify(rest)}`;
|
|
1308
1219
|
const items = [];
|
|
1309
1220
|
let page = 1;
|
|
1310
1221
|
for (; ; ) {
|
|
1311
|
-
const res = await this.getList(
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1222
|
+
const res = await this.getList(
|
|
1223
|
+
{
|
|
1224
|
+
...queryParams,
|
|
1225
|
+
...passthroughParams ?? {},
|
|
1226
|
+
page,
|
|
1227
|
+
perPage: batch
|
|
1228
|
+
},
|
|
1229
|
+
{
|
|
1230
|
+
requestKey: effectiveKey,
|
|
1231
|
+
singleFlight: true,
|
|
1232
|
+
...fetchFn ? { fetch: fetchFn } : {},
|
|
1233
|
+
...hdrs ? { headers: hdrs } : {},
|
|
1234
|
+
...sig ? { signal: sig } : {}
|
|
1235
|
+
}
|
|
1236
|
+
);
|
|
1316
1237
|
if (!res || !res.items || res.items.length === 0) break;
|
|
1317
1238
|
items.push(...res.items);
|
|
1318
1239
|
if (page >= (res.totalPages ?? page)) break;
|
|
@@ -1551,15 +1472,39 @@ function generateTypes(collections, options = {}) {
|
|
|
1551
1472
|
sections.push(`export interface LazypockCollections {
|
|
1552
1473
|
${mapEntries}
|
|
1553
1474
|
}`);
|
|
1475
|
+
const schemaEntries = collections.map(
|
|
1476
|
+
(c) => ` {
|
|
1477
|
+
id: ${JSON.stringify(c.id ?? null)},
|
|
1478
|
+
name: ${JSON.stringify(c.name)},
|
|
1479
|
+
type: ${JSON.stringify(c.type)},
|
|
1480
|
+
system: ${JSON.stringify(c.system ?? false)},
|
|
1481
|
+
fields: ${JSON.stringify(c.fields ?? [], null, 2)},
|
|
1482
|
+
rules: ${JSON.stringify(c.rules ?? {})},
|
|
1483
|
+
options: ${JSON.stringify(c.options ?? {})},
|
|
1484
|
+
}`
|
|
1485
|
+
).join(",\n");
|
|
1486
|
+
sections.push(`// Schema snapshot (runtime) \u2014 hidden-field exclusion + query validation.
|
|
1487
|
+
export const lazypockSchema: import("${packageName}").CollectionSchema[] = [
|
|
1488
|
+
${schemaEntries}
|
|
1489
|
+
];`);
|
|
1554
1490
|
sections.push(`import { LazypockClient, type LazypockClientOptions, type CollectionService } from "${packageName}";
|
|
1555
1491
|
|
|
1556
1492
|
/**
|
|
1557
1493
|
* Create a Lazypock client typed against this schema snapshot.
|
|
1558
1494
|
* Collection access is fully type-checked:
|
|
1559
1495
|
* client.collection("posts").create({ title: "x" }) // title must exist
|
|
1496
|
+
*
|
|
1497
|
+
* The schema snapshot is wired into the client automatically, so hidden
|
|
1498
|
+
* fields are excluded from responses and select/expand are validated.
|
|
1560
1499
|
*/
|
|
1561
1500
|
export function createClient(options: LazypockClientOptions): TypedClient {
|
|
1562
|
-
return new TypedClient(
|
|
1501
|
+
return new TypedClient({
|
|
1502
|
+
...options,
|
|
1503
|
+
types: {
|
|
1504
|
+
...options.types,
|
|
1505
|
+
schemas: options.types?.schemas ?? lazypockSchema,
|
|
1506
|
+
},
|
|
1507
|
+
});
|
|
1563
1508
|
}
|
|
1564
1509
|
|
|
1565
1510
|
export class TypedClient extends LazypockClient {
|
|
@@ -1580,6 +1525,7 @@ ${opts.body}
|
|
|
1580
1525
|
}`;
|
|
1581
1526
|
}
|
|
1582
1527
|
function memberLine(f) {
|
|
1528
|
+
if (f.hidden) return "";
|
|
1583
1529
|
const key = fieldKey(f.name);
|
|
1584
1530
|
const req = f.required || f.type === "password" ? "" : "?";
|
|
1585
1531
|
const type = fieldTypeScriptType(f);
|
|
@@ -1595,50 +1541,6 @@ var LazypockClient = class {
|
|
|
1595
1541
|
*/
|
|
1596
1542
|
constructor(options) {
|
|
1597
1543
|
this.collectionCache = /* @__PURE__ */ new Map();
|
|
1598
|
-
/** Namespace → realtime unsubscribe; used for realtime-driven invalidation. */
|
|
1599
|
-
this.realtimeInvalidators = /* @__PURE__ */ new Map();
|
|
1600
|
-
// ── Query cache (opt-in by default; opt-out per request) ──
|
|
1601
|
-
/**
|
|
1602
|
-
* Configure the query cache at runtime (also a namespace for cache
|
|
1603
|
-
* management methods).
|
|
1604
|
-
*
|
|
1605
|
-
* ```ts
|
|
1606
|
-
* client.cache({ enabled: true, defaultTTL: 30_000 });
|
|
1607
|
-
* client.cache.deleteByPrefix('getList:posts'); // all list caches for posts
|
|
1608
|
-
* client.cache.deleteByPrefix('getOne:posts'); // all one-record caches
|
|
1609
|
-
* ```
|
|
1610
|
-
*
|
|
1611
|
-
* When enabled, GET requests cache their payload; mutations invalidate the
|
|
1612
|
-
* affected collection automatically. Individual requests can opt out with
|
|
1613
|
-
* `{ cache: false }` or override the TTL with `{ ttl: ms }`.
|
|
1614
|
-
*/
|
|
1615
|
-
this.cache = Object.assign(
|
|
1616
|
-
((config) => {
|
|
1617
|
-
if (!this.cacheStore) {
|
|
1618
|
-
this.cacheStore = new CacheStore({
|
|
1619
|
-
defaultTTL: config?.defaultTTL,
|
|
1620
|
-
store: config?.store,
|
|
1621
|
-
maxEntries: config?.maxEntries
|
|
1622
|
-
});
|
|
1623
|
-
this.http.setCache(this.cacheStore, config?.enabled ?? true);
|
|
1624
|
-
} else {
|
|
1625
|
-
if (config?.enabled !== void 0) {
|
|
1626
|
-
this.http.setCache(this.cacheStore, config.enabled);
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
return this;
|
|
1630
|
-
}),
|
|
1631
|
-
{
|
|
1632
|
-
deleteByPrefix: (prefix) => this.cacheStore?.deleteByPrefix(prefix),
|
|
1633
|
-
invalidate: (namespace) => this.cacheStore?.invalidate(namespace),
|
|
1634
|
-
clear: () => {
|
|
1635
|
-
this.cacheStore?.clear();
|
|
1636
|
-
for (const unsub of this.realtimeInvalidators.values()) unsub();
|
|
1637
|
-
this.realtimeInvalidators.clear();
|
|
1638
|
-
},
|
|
1639
|
-
stats: () => this.cacheStore ? this.cacheStore.stats() : null
|
|
1640
|
-
}
|
|
1641
|
-
);
|
|
1642
1544
|
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
1643
1545
|
this.authStore = options.authStore ?? new AuthStore(options.storage ?? memoryStorage);
|
|
1644
1546
|
this.http = new HttpClient(baseUrl, this.authStore);
|
|
@@ -1648,14 +1550,6 @@ var LazypockClient = class {
|
|
|
1648
1550
|
}
|
|
1649
1551
|
this.files = new FilesService(this.http);
|
|
1650
1552
|
this.collections = new CollectionsService(this.http, this.realtime);
|
|
1651
|
-
if (options.cache) {
|
|
1652
|
-
this.cacheStore = new CacheStore({
|
|
1653
|
-
defaultTTL: options.cache.defaultTTL,
|
|
1654
|
-
store: options.cache.store,
|
|
1655
|
-
maxEntries: options.cache.maxEntries
|
|
1656
|
-
});
|
|
1657
|
-
this.http.setCache(this.cacheStore, options.cache.enabled ?? false);
|
|
1658
|
-
}
|
|
1659
1553
|
if (options.types?.schemas) {
|
|
1660
1554
|
this.schemaByName = new Map(
|
|
1661
1555
|
options.types.schemas.map((s) => [s.name, s])
|
|
@@ -1673,14 +1567,16 @@ var LazypockClient = class {
|
|
|
1673
1567
|
* @param name The collection name.
|
|
1674
1568
|
* @returns A {@link CollectionService} instance.
|
|
1675
1569
|
*/
|
|
1676
|
-
collection(name) {
|
|
1570
|
+
collection(name, schema) {
|
|
1677
1571
|
let svc = this.collectionCache.get(name);
|
|
1678
1572
|
if (!svc) {
|
|
1679
1573
|
svc = new CollectionService(
|
|
1680
1574
|
this.http,
|
|
1681
1575
|
name,
|
|
1682
1576
|
this.authStore,
|
|
1683
|
-
this.realtime
|
|
1577
|
+
this.realtime,
|
|
1578
|
+
// Prefer the explicit schema, then the client-level `types.schemas`.
|
|
1579
|
+
schema ?? this.schemaByName?.get(name)
|
|
1684
1580
|
);
|
|
1685
1581
|
this.collectionCache.set(name, svc);
|
|
1686
1582
|
}
|
|
@@ -1736,49 +1632,6 @@ var LazypockClient = class {
|
|
|
1736
1632
|
this.http.cancelAllRequests();
|
|
1737
1633
|
return this;
|
|
1738
1634
|
}
|
|
1739
|
-
/**
|
|
1740
|
-
* Drop every cached entry (all collections / namespaces).
|
|
1741
|
-
* Also disables realtime-driven invalidation subscriptions.
|
|
1742
|
-
*/
|
|
1743
|
-
clearCache() {
|
|
1744
|
-
this.cacheStore?.clear();
|
|
1745
|
-
for (const unsub of this.realtimeInvalidators.values()) unsub();
|
|
1746
|
-
this.realtimeInvalidators.clear();
|
|
1747
|
-
return this;
|
|
1748
|
-
}
|
|
1749
|
-
/**
|
|
1750
|
-
* Invalidate cached entries for a collection (or custom namespace).
|
|
1751
|
-
* Runs automatically on mutations — call explicitly when data changed
|
|
1752
|
-
* out-of-band (e.g. another client wrote to the same collection).
|
|
1753
|
-
*/
|
|
1754
|
-
invalidateCache(namespace) {
|
|
1755
|
-
this.cacheStore?.invalidate(namespace);
|
|
1756
|
-
return this;
|
|
1757
|
-
}
|
|
1758
|
-
/**
|
|
1759
|
-
* Cache hit/miss/entry statistics.
|
|
1760
|
-
* Returns null when caching was never configured.
|
|
1761
|
-
*/
|
|
1762
|
-
cacheStats() {
|
|
1763
|
-
return this.cacheStore ? this.cacheStore.stats() : null;
|
|
1764
|
-
}
|
|
1765
|
-
/**
|
|
1766
|
-
* Subscribe a collection's cache to realtime invalidation: any inbound
|
|
1767
|
-
* create/update/delete event for the collection clears its cached entries.
|
|
1768
|
-
* Returns an unsubscribe function.
|
|
1769
|
-
*/
|
|
1770
|
-
invalidateCacheOnRealtime(collectionName) {
|
|
1771
|
-
if (!this.cacheStore) {
|
|
1772
|
-
this.cache({ enabled: false });
|
|
1773
|
-
}
|
|
1774
|
-
const existing = this.realtimeInvalidators.get(collectionName);
|
|
1775
|
-
if (existing) return existing;
|
|
1776
|
-
const unsub = this.collection(collectionName).subscribe(() => {
|
|
1777
|
-
this.cacheStore?.invalidate(collectionName);
|
|
1778
|
-
});
|
|
1779
|
-
this.realtimeInvalidators.set(collectionName, unsub);
|
|
1780
|
-
return unsub;
|
|
1781
|
-
}
|
|
1782
1635
|
// ── Auth ──
|
|
1783
1636
|
/** Check whether any superuser exists (for login vs setup screen routing). */
|
|
1784
1637
|
async checkSuperuser() {
|
|
@@ -1916,7 +1769,6 @@ function createClient(options) {
|
|
|
1916
1769
|
0 && (module.exports = {
|
|
1917
1770
|
ApiError,
|
|
1918
1771
|
AuthStore,
|
|
1919
|
-
CacheStore,
|
|
1920
1772
|
CollectionService,
|
|
1921
1773
|
CollectionsService,
|
|
1922
1774
|
FilesService,
|
|
@@ -1932,7 +1784,6 @@ function createClient(options) {
|
|
|
1932
1784
|
getFileUrl,
|
|
1933
1785
|
getScaleUrl,
|
|
1934
1786
|
getThumbUrl,
|
|
1935
|
-
resolveCacheDirective,
|
|
1936
1787
|
schemaFieldType,
|
|
1937
1788
|
wsUrlFromBaseUrl
|
|
1938
1789
|
});
|