lazypock 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -1
- package/dist/cli.cjs +33 -19
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.global.js +33 -19
- package/dist/cli.global.js.map +1 -1
- package/dist/cli.js +33 -19
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +98 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +111 -66
- package/dist/index.d.ts +111 -66
- package/dist/index.global.js +98 -6
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +98 -6
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/cli.ts +52 -24
- package/src/collection.ts +124 -12
- package/src/index.ts +1 -0
- package/src/lazypock.ts +10 -1
- package/src/realtime.ts +38 -0
package/src/collection.ts
CHANGED
|
@@ -3,7 +3,51 @@
|
|
|
3
3
|
|
|
4
4
|
import type { HttpClient } from "./http";
|
|
5
5
|
import type { AuthStore, AuthModel } from "./auth";
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
ApiRecord,
|
|
8
|
+
ListResult,
|
|
9
|
+
RequestOptions,
|
|
10
|
+
CreateData,
|
|
11
|
+
UpdateData,
|
|
12
|
+
} from "./types";
|
|
13
|
+
import type { RealtimeService } from "./realtime";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A realtime record-change event delivered to subscription callbacks.
|
|
17
|
+
* Mirrors PocketBase's RealtimeService result shape (`action` + `record`).
|
|
18
|
+
*/
|
|
19
|
+
export interface RealtimeMessage {
|
|
20
|
+
action: "create" | "update" | "delete";
|
|
21
|
+
record: Record<string, unknown>;
|
|
22
|
+
topic?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Subscription callback for a collection's realtime events. */
|
|
26
|
+
export type RealtimeCallback = (e: RealtimeMessage) => void;
|
|
27
|
+
|
|
28
|
+
/** Raw event passed by the low-level realtime service. */
|
|
29
|
+
interface RealtimeEventLike {
|
|
30
|
+
event: string;
|
|
31
|
+
topic: string;
|
|
32
|
+
payload?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Map a raw event/action to a normalised create/update/delete action. */
|
|
36
|
+
function normalizeAction(
|
|
37
|
+
event: string,
|
|
38
|
+
rawAction?: unknown,
|
|
39
|
+
): RealtimeMessage["action"] {
|
|
40
|
+
if (typeof rawAction === "string") {
|
|
41
|
+
const a = rawAction.toLowerCase();
|
|
42
|
+
if (a === "create" || a === "update" || a === "delete") {
|
|
43
|
+
return a;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (event === "record_change") return "update";
|
|
47
|
+
if (event === "create" || event === "record_create") return "create";
|
|
48
|
+
if (event === "delete" || event === "record_delete") return "delete";
|
|
49
|
+
return "update";
|
|
50
|
+
}
|
|
7
51
|
|
|
8
52
|
/**
|
|
9
53
|
* Typed CRUD service for a single dynamic collection.
|
|
@@ -15,12 +59,19 @@ export class CollectionService<T = ApiRecord> {
|
|
|
15
59
|
private http: HttpClient;
|
|
16
60
|
private collectionName: string;
|
|
17
61
|
private authStore?: AuthStore;
|
|
62
|
+
private realtime?: RealtimeService;
|
|
18
63
|
|
|
19
64
|
/** @internal */
|
|
20
|
-
constructor(
|
|
65
|
+
constructor(
|
|
66
|
+
http: HttpClient,
|
|
67
|
+
collectionName: string,
|
|
68
|
+
authStore?: AuthStore,
|
|
69
|
+
realtime?: RealtimeService,
|
|
70
|
+
) {
|
|
21
71
|
this.http = http;
|
|
22
72
|
this.collectionName = collectionName;
|
|
23
73
|
this.authStore = authStore;
|
|
74
|
+
this.realtime = realtime;
|
|
24
75
|
}
|
|
25
76
|
|
|
26
77
|
private encodeId(id: string): string {
|
|
@@ -32,7 +83,10 @@ export class CollectionService<T = ApiRecord> {
|
|
|
32
83
|
* @param params Query parameters including `filter`, `sort`, `page`, `perPage`, `expand`.
|
|
33
84
|
* @param options Optional request options.
|
|
34
85
|
*/
|
|
35
|
-
list(
|
|
86
|
+
list(
|
|
87
|
+
params?: Record<string, string>,
|
|
88
|
+
options?: RequestOptions,
|
|
89
|
+
): Promise<ListResult<T> | null> {
|
|
36
90
|
const qs = params ? "?" + new URLSearchParams(params).toString() : "";
|
|
37
91
|
return this.http.get<ListResult<T>>(
|
|
38
92
|
"/" + this.encodeId(this.collectionName) + qs,
|
|
@@ -58,7 +112,10 @@ export class CollectionService<T = ApiRecord> {
|
|
|
58
112
|
* record type), excess/unknown fields are rejected at compile time.
|
|
59
113
|
* @param options Optional request options.
|
|
60
114
|
*/
|
|
61
|
-
create(
|
|
115
|
+
create(
|
|
116
|
+
data: T extends ApiRecord ? Record<string, unknown> : CreateData<T>,
|
|
117
|
+
options?: RequestOptions,
|
|
118
|
+
): Promise<T | null> {
|
|
62
119
|
return this.http.post<T>(
|
|
63
120
|
"/" + this.encodeId(this.collectionName),
|
|
64
121
|
data,
|
|
@@ -73,7 +130,11 @@ export class CollectionService<T = ApiRecord> {
|
|
|
73
130
|
* must be a partial of `T` — unknown fields are rejected.
|
|
74
131
|
* @param options Optional request options.
|
|
75
132
|
*/
|
|
76
|
-
update(
|
|
133
|
+
update(
|
|
134
|
+
id: string,
|
|
135
|
+
data: T extends ApiRecord ? Record<string, unknown> : UpdateData<T>,
|
|
136
|
+
options?: RequestOptions,
|
|
137
|
+
): Promise<T | null> {
|
|
77
138
|
return this.http.patch<T>(
|
|
78
139
|
"/" + this.encodeId(this.collectionName) + "/" + this.encodeId(id),
|
|
79
140
|
data,
|
|
@@ -102,11 +163,12 @@ export class CollectionService<T = ApiRecord> {
|
|
|
102
163
|
options?: RequestOptions,
|
|
103
164
|
): Promise<{ field: string; targetCollection: string }[] | null> {
|
|
104
165
|
const data = await this.http.get<{
|
|
105
|
-
fields?: {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
166
|
+
fields?: {
|
|
167
|
+
name: string;
|
|
168
|
+
type: string;
|
|
169
|
+
options?: Record<string, string>;
|
|
170
|
+
}[];
|
|
171
|
+
}>("/collections/" + this.encodeId(this.collectionName), options);
|
|
110
172
|
if (!data?.fields) return null;
|
|
111
173
|
return data.fields
|
|
112
174
|
.filter((f) => f.type === "relation" && f.options?.collection)
|
|
@@ -137,6 +199,50 @@ export class CollectionService<T = ApiRecord> {
|
|
|
137
199
|
return this as unknown as CollectionService<TRecord>;
|
|
138
200
|
}
|
|
139
201
|
|
|
202
|
+
// ── Realtime Subscriptions (PocketBase-style) ──
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Subscribe to realtime changes for this collection.
|
|
206
|
+
* The event's `action` is one of `"create" | "update" | "delete"`.
|
|
207
|
+
*
|
|
208
|
+
* Access is governed by the collection's `listRule` (PocketBase semantics):
|
|
209
|
+
* public collections allow anonymous subscriptions; other collections
|
|
210
|
+
* require a matching logged-in user or superuser.
|
|
211
|
+
*
|
|
212
|
+
* @param callback Received on every record change.
|
|
213
|
+
* @param recordId Optional — subscribe to a single record instead of `*`.
|
|
214
|
+
* @returns A function that unsubscribes this callback.
|
|
215
|
+
*/
|
|
216
|
+
subscribe(callback: RealtimeCallback, recordId?: string): () => void {
|
|
217
|
+
if (!this.realtime) {
|
|
218
|
+
console.warn("[lazypock] No realtime service configured.");
|
|
219
|
+
return () => {};
|
|
220
|
+
}
|
|
221
|
+
const topic =
|
|
222
|
+
"collection:" + this.collectionName + (recordId ? ":" + recordId : "");
|
|
223
|
+
const handler = (raw: RealtimeEventLike) => {
|
|
224
|
+
const record = (raw.payload?.["record"] ?? {}) as Record<string, unknown>;
|
|
225
|
+
callback({
|
|
226
|
+
action: normalizeAction(raw.event, raw.payload?.["action"]),
|
|
227
|
+
record,
|
|
228
|
+
topic: raw.topic,
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
this.realtime.ensureConnected();
|
|
232
|
+
this.realtime.subscribe(topic, handler as never);
|
|
233
|
+
return () => this.realtime?.unsubscribe(topic, handler as never);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Unsubscribe all callbacks from this collection (or a specific record).
|
|
238
|
+
* @param recordId Optional record id; omitting it unsubs everything.
|
|
239
|
+
*/
|
|
240
|
+
unsubscribe(recordId?: string): void {
|
|
241
|
+
const topic =
|
|
242
|
+
"collection:" + this.collectionName + (recordId ? ":" + recordId : "");
|
|
243
|
+
this.realtime?.unsubscribe(topic);
|
|
244
|
+
}
|
|
245
|
+
|
|
140
246
|
// ── Auth Collection Methods ──
|
|
141
247
|
|
|
142
248
|
/**
|
|
@@ -147,7 +253,9 @@ export class CollectionService<T = ApiRecord> {
|
|
|
147
253
|
identity: string,
|
|
148
254
|
password: string,
|
|
149
255
|
options?: RequestOptions,
|
|
150
|
-
): Promise<
|
|
256
|
+
): Promise<
|
|
257
|
+
({ token: string; record: ApiRecord } & Record<string, unknown>) | null
|
|
258
|
+
> {
|
|
151
259
|
const data = await this.http.post<
|
|
152
260
|
{ token: string; record: ApiRecord } & Record<string, unknown>
|
|
153
261
|
>(
|
|
@@ -168,7 +276,9 @@ export class CollectionService<T = ApiRecord> {
|
|
|
168
276
|
*/
|
|
169
277
|
async authRefresh(
|
|
170
278
|
options?: RequestOptions,
|
|
171
|
-
): Promise<
|
|
279
|
+
): Promise<
|
|
280
|
+
({ token: string; record: ApiRecord } & Record<string, unknown>) | null
|
|
281
|
+
> {
|
|
172
282
|
const data = await this.http.post<
|
|
173
283
|
{ token: string; record: ApiRecord } & Record<string, unknown>
|
|
174
284
|
>(
|
|
@@ -186,6 +296,8 @@ export class CollectionService<T = ApiRecord> {
|
|
|
186
296
|
/**
|
|
187
297
|
* Get available auth methods for this collection.
|
|
188
298
|
*/
|
|
299
|
+
// ── end Realtime ──
|
|
300
|
+
|
|
189
301
|
async authMethods(
|
|
190
302
|
options?: RequestOptions,
|
|
191
303
|
): Promise<Record<string, unknown> | null> {
|
package/src/index.ts
CHANGED
package/src/lazypock.ts
CHANGED
|
@@ -109,6 +109,10 @@ export class LazypockClient {
|
|
|
109
109
|
options.authStore ?? new AuthStore(options.storage ?? memoryStorage);
|
|
110
110
|
this.http = new HttpClient(baseUrl, this.authStore);
|
|
111
111
|
this.realtime = options.realtime ?? new RealtimeService();
|
|
112
|
+
// Cache the socket URL so collection-level subscribe() can auto-connect.
|
|
113
|
+
if (!options.realtime) {
|
|
114
|
+
this.realtime.setUrl(wsUrlFromBaseUrl(baseUrl));
|
|
115
|
+
}
|
|
112
116
|
this.files = new FilesService(this.http);
|
|
113
117
|
if (options.types?.schemas) {
|
|
114
118
|
this.schemaByName = new Map(
|
|
@@ -131,7 +135,12 @@ export class LazypockClient {
|
|
|
131
135
|
collection(name: string): CollectionService<unknown> {
|
|
132
136
|
let svc = this.collectionCache.get(name);
|
|
133
137
|
if (!svc) {
|
|
134
|
-
svc = new CollectionService(
|
|
138
|
+
svc = new CollectionService(
|
|
139
|
+
this.http,
|
|
140
|
+
name,
|
|
141
|
+
this.authStore,
|
|
142
|
+
this.realtime,
|
|
143
|
+
);
|
|
135
144
|
this.collectionCache.set(name, svc);
|
|
136
145
|
}
|
|
137
146
|
return svc;
|
package/src/realtime.ts
CHANGED
|
@@ -68,6 +68,44 @@ export class RealtimeService {
|
|
|
68
68
|
private url: string = "";
|
|
69
69
|
private token: string | undefined;
|
|
70
70
|
|
|
71
|
+
/** Whether the WebSocket is currently open. */
|
|
72
|
+
get isOpen(): boolean {
|
|
73
|
+
return this.ws?.readyState === WebSocket.OPEN;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The most recently used WebSocket URL (set on {@link connect}).
|
|
78
|
+
* Useful for SDK convenience methods that auto-connect before subscribing.
|
|
79
|
+
*/
|
|
80
|
+
get lastUrl(): string {
|
|
81
|
+
return this.url;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The auth token configured for this connection (set on connect). */
|
|
85
|
+
get lastToken(): string | undefined {
|
|
86
|
+
return this.token;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Set the socket URL. Useful before subscribing so the SDK can
|
|
91
|
+
* auto-connect on the first {@link subscribe}.
|
|
92
|
+
*/
|
|
93
|
+
setUrl(url: string): void {
|
|
94
|
+
this.url = url;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/*
|
|
98
|
+
* Ensure the socket is connected, then subscribe.
|
|
99
|
+
* Used by collection-level convenience wrappers so a connection is opened
|
|
100
|
+
* automatically on the first subscribe (matching PocketBase behaviour —
|
|
101
|
+
* works for anonymous/public collections too).
|
|
102
|
+
*/
|
|
103
|
+
ensureConnected(): void {
|
|
104
|
+
if (this.isOpen || !this.url) return;
|
|
105
|
+
if (typeof WebSocket === "undefined") return;
|
|
106
|
+
this.doConnect();
|
|
107
|
+
}
|
|
108
|
+
|
|
71
109
|
connect(opts: RealtimeConnectOpts): void {
|
|
72
110
|
this.url = opts.url;
|
|
73
111
|
this.token = opts.token;
|