lazypock 0.4.0 → 0.6.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 +57 -108
- 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 +155 -402
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +162 -299
- package/dist/index.d.ts +162 -299
- package/dist/index.global.js +155 -400
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +130 -400
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/codegen.ts +34 -1
- package/src/collection.ts +158 -13
- package/src/collections.ts +0 -6
- package/src/http.ts +1 -131
- package/src/index.ts +8 -5
- package/src/lazypock.ts +19 -146
- package/src/types.ts +104 -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,8 +63,6 @@ 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
|
|
@@ -281,18 +82,6 @@ var HttpClient = class {
|
|
|
281
82
|
this.authStore = authStore;
|
|
282
83
|
this.defaultFetch = globalThis.fetch.bind(globalThis);
|
|
283
84
|
}
|
|
284
|
-
/**
|
|
285
|
-
* Attach a cache store + master switch.
|
|
286
|
-
* Called by the client constructor when cache config is present.
|
|
287
|
-
*/
|
|
288
|
-
setCache(cache, enabled) {
|
|
289
|
-
this.cache = cache;
|
|
290
|
-
this.cacheEnabled = enabled;
|
|
291
|
-
}
|
|
292
|
-
/** Whether the global cache flag is on (requests opt in/out individually too). */
|
|
293
|
-
get cacheIsEnabled() {
|
|
294
|
-
return this.cacheEnabled;
|
|
295
|
-
}
|
|
296
85
|
async refreshAuth() {
|
|
297
86
|
const collection = this.authStore.collectionName;
|
|
298
87
|
if (!collection) return null;
|
|
@@ -369,25 +158,10 @@ var HttpClient = class {
|
|
|
369
158
|
* @throws {ApiError} On non-2xx responses or when the request is aborted
|
|
370
159
|
* (aborted requests throw an `ApiError` with `isAbort === true`).
|
|
371
160
|
*/
|
|
372
|
-
/** Invalidate a namespace (collection name). No-op when cache is off. */
|
|
373
|
-
invalidateCache(namespace) {
|
|
374
|
-
this.cache?.invalidate(namespace);
|
|
375
|
-
}
|
|
376
|
-
/** Current cache statistics (hits/misses/entries), or null when disabled. */
|
|
377
|
-
cacheStats() {
|
|
378
|
-
return this.cache ? this.cache.stats() : null;
|
|
379
|
-
}
|
|
380
161
|
async request(method, path, body, options) {
|
|
381
162
|
if (this.authStore.isExpired && this.authStore.collectionName) {
|
|
382
163
|
await this.refreshAuth();
|
|
383
164
|
}
|
|
384
|
-
const cacheDirective = resolveCacheDirective(options);
|
|
385
|
-
const wantCache = cacheDirective !== null ? cacheDirective.enabled : this.cacheEnabled;
|
|
386
|
-
const cacheKey = method === "GET" && this.cache && wantCache ? this.cacheKeyFor(method, path, options?.params) : null;
|
|
387
|
-
if (cacheKey !== null) {
|
|
388
|
-
const hit = await this.cache?.get(cacheKey);
|
|
389
|
-
if (hit !== void 0) return hit;
|
|
390
|
-
}
|
|
391
165
|
let requestKey = options?.requestKey === void 0 ? options?.cancelKey ?? `${method} ${path}` : options.requestKey;
|
|
392
166
|
if (options?.autoCancel === false) requestKey = null;
|
|
393
167
|
if (options?.singleFlight && requestKey !== null) {
|
|
@@ -422,9 +196,7 @@ var HttpClient = class {
|
|
|
422
196
|
options,
|
|
423
197
|
signal,
|
|
424
198
|
requestKey,
|
|
425
|
-
controller
|
|
426
|
-
cacheKey,
|
|
427
|
-
cacheDirective
|
|
199
|
+
controller
|
|
428
200
|
);
|
|
429
201
|
} finally {
|
|
430
202
|
if (requestKey !== null) {
|
|
@@ -441,10 +213,10 @@ var HttpClient = class {
|
|
|
441
213
|
return promise;
|
|
442
214
|
}
|
|
443
215
|
/**
|
|
444
|
-
* Execute the actual HTTP request (fetch + parse
|
|
216
|
+
* Execute the actual HTTP request (fetch + parse). Called by {@link request}
|
|
445
217
|
* as the inner in-flight unit so single-flight callers can reuse the promise.
|
|
446
218
|
*/
|
|
447
|
-
async doRequest(method, path, body, options, signal, requestKey, controller
|
|
219
|
+
async doRequest(method, path, body, options, signal, requestKey, controller) {
|
|
448
220
|
let url = this.baseUrl + path;
|
|
449
221
|
if (options?.params) {
|
|
450
222
|
const qs = new URLSearchParams(options.params).toString();
|
|
@@ -509,63 +281,8 @@ var HttpClient = class {
|
|
|
509
281
|
res.status
|
|
510
282
|
);
|
|
511
283
|
}
|
|
512
|
-
if (cacheKey !== null && this.cache) {
|
|
513
|
-
const namespace = this.namespaceFromPath(path);
|
|
514
|
-
const ttl = cacheDirective?.ttl;
|
|
515
|
-
const tags = this.cacheTagsFor(path, namespace);
|
|
516
|
-
this.cache.set(
|
|
517
|
-
cacheKey,
|
|
518
|
-
data,
|
|
519
|
-
ttl,
|
|
520
|
-
namespace ?? void 0,
|
|
521
|
-
tags
|
|
522
|
-
);
|
|
523
|
-
}
|
|
524
|
-
if (method !== "GET" && this.cache) {
|
|
525
|
-
const namespaces = /* @__PURE__ */ new Set();
|
|
526
|
-
const ns = this.namespaceFromPath(path);
|
|
527
|
-
if (ns) namespaces.add(ns);
|
|
528
|
-
for (const extra of options?.invalidate ?? []) {
|
|
529
|
-
if (extra) namespaces.add(extra);
|
|
530
|
-
}
|
|
531
|
-
for (const nsName of namespaces) this.cache.invalidate(nsName);
|
|
532
|
-
}
|
|
533
284
|
return data;
|
|
534
285
|
}
|
|
535
|
-
// ── Cache key/namespace helpers ──
|
|
536
|
-
/** Build a token-scoped cache key: `METHOD path|token-hash|params`. */
|
|
537
|
-
cacheKeyFor(method, path, params) {
|
|
538
|
-
const token = this.authStore.token || "anon";
|
|
539
|
-
const qs = params ? "?" + new URLSearchParams(params).toString() : "";
|
|
540
|
-
return `${method} ${path}${qs}|${token}`;
|
|
541
|
-
}
|
|
542
|
-
/** Best-effort namespace (collection name) from a REST path. */
|
|
543
|
-
namespaceFromPath(path) {
|
|
544
|
-
const clean = path.split("?")[0];
|
|
545
|
-
const parts = clean.split("/").filter(Boolean);
|
|
546
|
-
if (parts.length === 0) return void 0;
|
|
547
|
-
if (parts[0] === "collections" || parts[0] === "_superusers") {
|
|
548
|
-
return parts[0];
|
|
549
|
-
}
|
|
550
|
-
return parts[0];
|
|
551
|
-
}
|
|
552
|
-
/**
|
|
553
|
-
* Semantic prefix tags for `deleteByPrefix`, derived from the REST shape:
|
|
554
|
-
* - `/{collection}?...` → `getList:{collection}`
|
|
555
|
-
* - `/{collection}/{id}` → `getOne:{collection}`
|
|
556
|
-
* - `/collections?...` / `/collections/{id}` → `collections:getList` / `collections:getOne`
|
|
557
|
-
*/
|
|
558
|
-
cacheTagsFor(path, namespace) {
|
|
559
|
-
if (!namespace) return [];
|
|
560
|
-
const clean = path.split("?")[0];
|
|
561
|
-
const parts = clean.split("/").filter(Boolean);
|
|
562
|
-
if (parts[0] === "collections" || parts[0] === "_superusers") {
|
|
563
|
-
const op2 = parts.length >= 2 ? "getOne" : "getList";
|
|
564
|
-
return [`${namespace}:${op2}`];
|
|
565
|
-
}
|
|
566
|
-
const op = parts.length >= 2 ? "getOne" : "getList";
|
|
567
|
-
return [`${op}:${namespace}`];
|
|
568
|
-
}
|
|
569
286
|
/**
|
|
570
287
|
* HTTP GET.
|
|
571
288
|
* @param path URL path.
|
|
@@ -777,17 +494,104 @@ function normalizeAction(event, rawAction) {
|
|
|
777
494
|
if (event === "delete" || event === "record_delete") return "delete";
|
|
778
495
|
return "update";
|
|
779
496
|
}
|
|
780
|
-
var CollectionService = class {
|
|
497
|
+
var CollectionService = class _CollectionService {
|
|
781
498
|
/** @internal */
|
|
782
|
-
constructor(http, collectionName, authStore, realtime) {
|
|
499
|
+
constructor(http, collectionName, authStore, realtime, schema) {
|
|
783
500
|
this.http = http;
|
|
784
501
|
this.collectionName = collectionName;
|
|
785
502
|
this.authStore = authStore;
|
|
786
503
|
this.realtime = realtime;
|
|
504
|
+
this.schema = schema;
|
|
787
505
|
}
|
|
788
506
|
encodeId(id) {
|
|
789
507
|
return encodeURIComponent(id);
|
|
790
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
|
+
}
|
|
791
595
|
/**
|
|
792
596
|
* Fetch a paginated list of records (PocketBase `getList`).
|
|
793
597
|
*
|
|
@@ -803,13 +607,17 @@ var CollectionService = class {
|
|
|
803
607
|
fetch,
|
|
804
608
|
headers,
|
|
805
609
|
signal,
|
|
806
|
-
cache,
|
|
807
|
-
ttl,
|
|
808
|
-
invalidate,
|
|
809
610
|
singleFlight,
|
|
810
611
|
params,
|
|
811
612
|
...queryParams
|
|
812
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
|
+
}
|
|
813
621
|
const qs = new URLSearchParams(
|
|
814
622
|
Object.fromEntries(
|
|
815
623
|
Object.entries({
|
|
@@ -828,9 +636,6 @@ var CollectionService = class {
|
|
|
828
636
|
fetch,
|
|
829
637
|
headers,
|
|
830
638
|
signal,
|
|
831
|
-
cache,
|
|
832
|
-
ttl,
|
|
833
|
-
invalidate,
|
|
834
639
|
singleFlight,
|
|
835
640
|
params
|
|
836
641
|
}
|
|
@@ -883,8 +688,16 @@ var CollectionService = class {
|
|
|
883
688
|
* @param options Optional request options.
|
|
884
689
|
*/
|
|
885
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();
|
|
886
699
|
return this.http.get(
|
|
887
|
-
"/" + this.encodeId(this.collectionName) + "/" + this.encodeId(id),
|
|
700
|
+
"/" + this.encodeId(this.collectionName) + "/" + this.encodeId(id) + (qsStr ? "?" + qsStr : ""),
|
|
888
701
|
options
|
|
889
702
|
);
|
|
890
703
|
}
|
|
@@ -959,6 +772,22 @@ var CollectionService = class {
|
|
|
959
772
|
typed() {
|
|
960
773
|
return this;
|
|
961
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
|
+
}
|
|
962
791
|
// ── Realtime Subscriptions (PocketBase-style) ──
|
|
963
792
|
/**
|
|
964
793
|
* Subscribe to realtime changes for this collection.
|
|
@@ -1383,9 +1212,6 @@ var CollectionsService = class {
|
|
|
1383
1212
|
fetch: fetchFn,
|
|
1384
1213
|
headers: hdrs,
|
|
1385
1214
|
signal: sig,
|
|
1386
|
-
cache: cacheOpt,
|
|
1387
|
-
ttl: ttlOpt,
|
|
1388
|
-
invalidate: inval,
|
|
1389
1215
|
params: passthroughParams,
|
|
1390
1216
|
...queryParams
|
|
1391
1217
|
} = rest;
|
|
@@ -1405,10 +1231,7 @@ var CollectionsService = class {
|
|
|
1405
1231
|
singleFlight: true,
|
|
1406
1232
|
...fetchFn ? { fetch: fetchFn } : {},
|
|
1407
1233
|
...hdrs ? { headers: hdrs } : {},
|
|
1408
|
-
...sig ? { signal: sig } : {}
|
|
1409
|
-
...cacheOpt !== void 0 ? { cache: cacheOpt } : {},
|
|
1410
|
-
...ttlOpt !== void 0 ? { ttl: ttlOpt } : {},
|
|
1411
|
-
...inval ? { invalidate: inval } : {}
|
|
1234
|
+
...sig ? { signal: sig } : {}
|
|
1412
1235
|
}
|
|
1413
1236
|
);
|
|
1414
1237
|
if (!res || !res.items || res.items.length === 0) break;
|
|
@@ -1649,15 +1472,39 @@ function generateTypes(collections, options = {}) {
|
|
|
1649
1472
|
sections.push(`export interface LazypockCollections {
|
|
1650
1473
|
${mapEntries}
|
|
1651
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
|
+
];`);
|
|
1652
1490
|
sections.push(`import { LazypockClient, type LazypockClientOptions, type CollectionService } from "${packageName}";
|
|
1653
1491
|
|
|
1654
1492
|
/**
|
|
1655
1493
|
* Create a Lazypock client typed against this schema snapshot.
|
|
1656
1494
|
* Collection access is fully type-checked:
|
|
1657
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.
|
|
1658
1499
|
*/
|
|
1659
1500
|
export function createClient(options: LazypockClientOptions): TypedClient {
|
|
1660
|
-
return new TypedClient(
|
|
1501
|
+
return new TypedClient({
|
|
1502
|
+
...options,
|
|
1503
|
+
types: {
|
|
1504
|
+
...options.types,
|
|
1505
|
+
schemas: options.types?.schemas ?? lazypockSchema,
|
|
1506
|
+
},
|
|
1507
|
+
});
|
|
1661
1508
|
}
|
|
1662
1509
|
|
|
1663
1510
|
export class TypedClient extends LazypockClient {
|
|
@@ -1678,6 +1525,7 @@ ${opts.body}
|
|
|
1678
1525
|
}`;
|
|
1679
1526
|
}
|
|
1680
1527
|
function memberLine(f) {
|
|
1528
|
+
if (f.hidden) return "";
|
|
1681
1529
|
const key = fieldKey(f.name);
|
|
1682
1530
|
const req = f.required || f.type === "password" ? "" : "?";
|
|
1683
1531
|
const type = fieldTypeScriptType(f);
|
|
@@ -1693,50 +1541,6 @@ var LazypockClient = class {
|
|
|
1693
1541
|
*/
|
|
1694
1542
|
constructor(options) {
|
|
1695
1543
|
this.collectionCache = /* @__PURE__ */ new Map();
|
|
1696
|
-
/** Namespace → realtime unsubscribe; used for realtime-driven invalidation. */
|
|
1697
|
-
this.realtimeInvalidators = /* @__PURE__ */ new Map();
|
|
1698
|
-
// ── Query cache (opt-in by default; opt-out per request) ──
|
|
1699
|
-
/**
|
|
1700
|
-
* Configure the query cache at runtime (also a namespace for cache
|
|
1701
|
-
* management methods).
|
|
1702
|
-
*
|
|
1703
|
-
* ```ts
|
|
1704
|
-
* client.cache({ enabled: true, defaultTTL: 30_000 });
|
|
1705
|
-
* client.cache.deleteByPrefix('getList:posts'); // all list caches for posts
|
|
1706
|
-
* client.cache.deleteByPrefix('getOne:posts'); // all one-record caches
|
|
1707
|
-
* ```
|
|
1708
|
-
*
|
|
1709
|
-
* When enabled, GET requests cache their payload; mutations invalidate the
|
|
1710
|
-
* affected collection automatically. Individual requests can opt out with
|
|
1711
|
-
* `{ cache: false }` or override the TTL with `{ ttl: ms }`.
|
|
1712
|
-
*/
|
|
1713
|
-
this.cache = Object.assign(
|
|
1714
|
-
((config) => {
|
|
1715
|
-
if (!this.cacheStore) {
|
|
1716
|
-
this.cacheStore = new CacheStore({
|
|
1717
|
-
defaultTTL: config?.defaultTTL,
|
|
1718
|
-
store: config?.store,
|
|
1719
|
-
maxEntries: config?.maxEntries
|
|
1720
|
-
});
|
|
1721
|
-
this.http.setCache(this.cacheStore, config?.enabled ?? true);
|
|
1722
|
-
} else {
|
|
1723
|
-
if (config?.enabled !== void 0) {
|
|
1724
|
-
this.http.setCache(this.cacheStore, config.enabled);
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
|
-
return this;
|
|
1728
|
-
}),
|
|
1729
|
-
{
|
|
1730
|
-
deleteByPrefix: (prefix) => this.cacheStore?.deleteByPrefix(prefix),
|
|
1731
|
-
invalidate: (namespace) => this.cacheStore?.invalidate(namespace),
|
|
1732
|
-
clear: () => {
|
|
1733
|
-
this.cacheStore?.clear();
|
|
1734
|
-
for (const unsub of this.realtimeInvalidators.values()) unsub();
|
|
1735
|
-
this.realtimeInvalidators.clear();
|
|
1736
|
-
},
|
|
1737
|
-
stats: () => this.cacheStore ? this.cacheStore.stats() : null
|
|
1738
|
-
}
|
|
1739
|
-
);
|
|
1740
1544
|
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
1741
1545
|
this.authStore = options.authStore ?? new AuthStore(options.storage ?? memoryStorage);
|
|
1742
1546
|
this.http = new HttpClient(baseUrl, this.authStore);
|
|
@@ -1746,14 +1550,6 @@ var LazypockClient = class {
|
|
|
1746
1550
|
}
|
|
1747
1551
|
this.files = new FilesService(this.http);
|
|
1748
1552
|
this.collections = new CollectionsService(this.http, this.realtime);
|
|
1749
|
-
if (options.cache) {
|
|
1750
|
-
this.cacheStore = new CacheStore({
|
|
1751
|
-
defaultTTL: options.cache.defaultTTL,
|
|
1752
|
-
store: options.cache.store,
|
|
1753
|
-
maxEntries: options.cache.maxEntries
|
|
1754
|
-
});
|
|
1755
|
-
this.http.setCache(this.cacheStore, options.cache.enabled ?? false);
|
|
1756
|
-
}
|
|
1757
1553
|
if (options.types?.schemas) {
|
|
1758
1554
|
this.schemaByName = new Map(
|
|
1759
1555
|
options.types.schemas.map((s) => [s.name, s])
|
|
@@ -1771,14 +1567,16 @@ var LazypockClient = class {
|
|
|
1771
1567
|
* @param name The collection name.
|
|
1772
1568
|
* @returns A {@link CollectionService} instance.
|
|
1773
1569
|
*/
|
|
1774
|
-
collection(name) {
|
|
1570
|
+
collection(name, schema) {
|
|
1775
1571
|
let svc = this.collectionCache.get(name);
|
|
1776
1572
|
if (!svc) {
|
|
1777
1573
|
svc = new CollectionService(
|
|
1778
1574
|
this.http,
|
|
1779
1575
|
name,
|
|
1780
1576
|
this.authStore,
|
|
1781
|
-
this.realtime
|
|
1577
|
+
this.realtime,
|
|
1578
|
+
// Prefer the explicit schema, then the client-level `types.schemas`.
|
|
1579
|
+
schema ?? this.schemaByName?.get(name)
|
|
1782
1580
|
);
|
|
1783
1581
|
this.collectionCache.set(name, svc);
|
|
1784
1582
|
}
|
|
@@ -1834,49 +1632,6 @@ var LazypockClient = class {
|
|
|
1834
1632
|
this.http.cancelAllRequests();
|
|
1835
1633
|
return this;
|
|
1836
1634
|
}
|
|
1837
|
-
/**
|
|
1838
|
-
* Drop every cached entry (all collections / namespaces).
|
|
1839
|
-
* Also disables realtime-driven invalidation subscriptions.
|
|
1840
|
-
*/
|
|
1841
|
-
clearCache() {
|
|
1842
|
-
this.cacheStore?.clear();
|
|
1843
|
-
for (const unsub of this.realtimeInvalidators.values()) unsub();
|
|
1844
|
-
this.realtimeInvalidators.clear();
|
|
1845
|
-
return this;
|
|
1846
|
-
}
|
|
1847
|
-
/**
|
|
1848
|
-
* Invalidate cached entries for a collection (or custom namespace).
|
|
1849
|
-
* Runs automatically on mutations — call explicitly when data changed
|
|
1850
|
-
* out-of-band (e.g. another client wrote to the same collection).
|
|
1851
|
-
*/
|
|
1852
|
-
invalidateCache(namespace) {
|
|
1853
|
-
this.cacheStore?.invalidate(namespace);
|
|
1854
|
-
return this;
|
|
1855
|
-
}
|
|
1856
|
-
/**
|
|
1857
|
-
* Cache hit/miss/entry statistics.
|
|
1858
|
-
* Returns null when caching was never configured.
|
|
1859
|
-
*/
|
|
1860
|
-
cacheStats() {
|
|
1861
|
-
return this.cacheStore ? this.cacheStore.stats() : null;
|
|
1862
|
-
}
|
|
1863
|
-
/**
|
|
1864
|
-
* Subscribe a collection's cache to realtime invalidation: any inbound
|
|
1865
|
-
* create/update/delete event for the collection clears its cached entries.
|
|
1866
|
-
* Returns an unsubscribe function.
|
|
1867
|
-
*/
|
|
1868
|
-
invalidateCacheOnRealtime(collectionName) {
|
|
1869
|
-
if (!this.cacheStore) {
|
|
1870
|
-
this.cache({ enabled: false });
|
|
1871
|
-
}
|
|
1872
|
-
const existing = this.realtimeInvalidators.get(collectionName);
|
|
1873
|
-
if (existing) return existing;
|
|
1874
|
-
const unsub = this.collection(collectionName).subscribe(() => {
|
|
1875
|
-
this.cacheStore?.invalidate(collectionName);
|
|
1876
|
-
});
|
|
1877
|
-
this.realtimeInvalidators.set(collectionName, unsub);
|
|
1878
|
-
return unsub;
|
|
1879
|
-
}
|
|
1880
1635
|
// ── Auth ──
|
|
1881
1636
|
/** Check whether any superuser exists (for login vs setup screen routing). */
|
|
1882
1637
|
async checkSuperuser() {
|
|
@@ -2014,7 +1769,6 @@ function createClient(options) {
|
|
|
2014
1769
|
0 && (module.exports = {
|
|
2015
1770
|
ApiError,
|
|
2016
1771
|
AuthStore,
|
|
2017
|
-
CacheStore,
|
|
2018
1772
|
CollectionService,
|
|
2019
1773
|
CollectionsService,
|
|
2020
1774
|
FilesService,
|
|
@@ -2030,7 +1784,6 @@ function createClient(options) {
|
|
|
2030
1784
|
getFileUrl,
|
|
2031
1785
|
getScaleUrl,
|
|
2032
1786
|
getThumbUrl,
|
|
2033
|
-
resolveCacheDirective,
|
|
2034
1787
|
schemaFieldType,
|
|
2035
1788
|
wsUrlFromBaseUrl
|
|
2036
1789
|
});
|