lazypock 0.1.0 → 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 +145 -0
- package/dist/chunk-3MZJ5HCD.js +190 -0
- package/dist/chunk-3MZJ5HCD.js.map +1 -0
- package/dist/cli.cjs +235 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.global.js +243 -0
- package/dist/cli.global.js.map +1 -0
- package/dist/cli.js +105 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +360 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +298 -77
- package/dist/index.d.ts +298 -77
- package/dist/index.global.js +352 -10
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +178 -10
- package/dist/index.js.map +1 -1
- package/package.json +8 -1
- package/src/cli.ts +145 -0
- package/src/client.ts +48 -0
- package/src/codegen.ts +149 -0
- package/src/collection.ts +145 -21
- package/src/index.ts +29 -408
- package/src/lazypock.ts +490 -0
- package/src/realtime.ts +38 -0
- package/src/schema.ts +39 -0
- package/src/typegen.ts +137 -0
- package/src/types.ts +36 -2
package/src/lazypock.ts
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
// ── LazypockClient — Root Client ───────────────────────
|
|
2
|
+
// Extracted into its own module to avoid a circular import with
|
|
3
|
+
// the typed-client factory (src/client.ts).
|
|
4
|
+
|
|
5
|
+
import { HttpClient } from "./http";
|
|
6
|
+
import {
|
|
7
|
+
AuthStore,
|
|
8
|
+
memoryStorage,
|
|
9
|
+
type StorageAdapter,
|
|
10
|
+
type AuthModel,
|
|
11
|
+
} from "./auth";
|
|
12
|
+
import { CollectionService } from "./collection";
|
|
13
|
+
import {
|
|
14
|
+
ApiError,
|
|
15
|
+
type ApiRecord,
|
|
16
|
+
type ListResult,
|
|
17
|
+
type RecordShape,
|
|
18
|
+
type CreateData,
|
|
19
|
+
type UpdateData,
|
|
20
|
+
type SystemFields,
|
|
21
|
+
type RequestOptions,
|
|
22
|
+
} from "./types";
|
|
23
|
+
import { RealtimeService, wsUrlFromBaseUrl } from "./realtime";
|
|
24
|
+
import { FilesService, getFileUrl, type FileRecord } from "./files";
|
|
25
|
+
import type { CollectionSchema, SchemaField } from "./schema";
|
|
26
|
+
import { generateTypes, collectionTypeName } from "./codegen";
|
|
27
|
+
import { fieldTypeScriptType, fieldTypeKind, schemaFieldType } from "./typegen";
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
AuthStore,
|
|
31
|
+
ApiError,
|
|
32
|
+
RealtimeService,
|
|
33
|
+
wsUrlFromBaseUrl,
|
|
34
|
+
FilesService,
|
|
35
|
+
getFileUrl,
|
|
36
|
+
CollectionService,
|
|
37
|
+
generateTypes,
|
|
38
|
+
collectionTypeName,
|
|
39
|
+
fieldTypeScriptType,
|
|
40
|
+
fieldTypeKind,
|
|
41
|
+
schemaFieldType,
|
|
42
|
+
};
|
|
43
|
+
export type {
|
|
44
|
+
StorageAdapter,
|
|
45
|
+
AuthModel,
|
|
46
|
+
ApiRecord,
|
|
47
|
+
ListResult,
|
|
48
|
+
RecordShape,
|
|
49
|
+
CreateData,
|
|
50
|
+
UpdateData,
|
|
51
|
+
SystemFields,
|
|
52
|
+
RequestOptions,
|
|
53
|
+
FileRecord,
|
|
54
|
+
CollectionSchema,
|
|
55
|
+
SchemaField,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Options for constructing a {@link LazypockClient}. */
|
|
59
|
+
export interface LazypockClientOptions {
|
|
60
|
+
/** API base URL (e.g. 'http://localhost:4000/api') */
|
|
61
|
+
baseUrl: string;
|
|
62
|
+
/** Custom storage adapter (default: localStorage fallback) */
|
|
63
|
+
storage?: StorageAdapter;
|
|
64
|
+
/** Explicit auth store instance (for sharing across modules) */
|
|
65
|
+
authStore?: AuthStore;
|
|
66
|
+
/** Real-time service for Phoenix Channel WebSocket subscriptions */
|
|
67
|
+
realtime?: RealtimeService;
|
|
68
|
+
/**
|
|
69
|
+
* Optional schema types for generating typed services at runtime.
|
|
70
|
+
* When provided, `collection()` returns a service whose create/update
|
|
71
|
+
* inputs are validated against the mapped field types.
|
|
72
|
+
*
|
|
73
|
+
* @experimental
|
|
74
|
+
*/
|
|
75
|
+
types?: {
|
|
76
|
+
/** Collection schemas fetched from the API (e.g. via `GET /collections`). */
|
|
77
|
+
schemas?: CollectionSchema[];
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Lazypock API client.
|
|
83
|
+
*
|
|
84
|
+
* Provides methods for authentication, CRUD operations on dynamic collections,
|
|
85
|
+
* file management, and real-time subscriptions.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* const client = new LazypockClient({ baseUrl: 'http://localhost:4000/api' });
|
|
90
|
+
* await client.login('admin@example.com', 'password');
|
|
91
|
+
* const posts = await client.collection('posts').list();
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export class LazypockClient {
|
|
95
|
+
readonly http: HttpClient;
|
|
96
|
+
readonly authStore: AuthStore;
|
|
97
|
+
readonly realtime: RealtimeService;
|
|
98
|
+
readonly files: FilesService;
|
|
99
|
+
private collectionCache = new Map<string, CollectionService>();
|
|
100
|
+
private schemaByName?: Map<string, CollectionSchema>;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Create a new Lazypock client.
|
|
104
|
+
* @param options Configuration options.
|
|
105
|
+
*/
|
|
106
|
+
constructor(options: LazypockClientOptions) {
|
|
107
|
+
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
108
|
+
this.authStore =
|
|
109
|
+
options.authStore ?? new AuthStore(options.storage ?? memoryStorage);
|
|
110
|
+
this.http = new HttpClient(baseUrl, this.authStore);
|
|
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
|
+
}
|
|
116
|
+
this.files = new FilesService(this.http);
|
|
117
|
+
if (options.types?.schemas) {
|
|
118
|
+
this.schemaByName = new Map(
|
|
119
|
+
options.types.schemas.map((s) => [s.name, s]),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get or create a service for the given collection.
|
|
126
|
+
* Services are cached after first access.
|
|
127
|
+
*
|
|
128
|
+
* For typed CRUD, either:
|
|
129
|
+
* - cast at the call site: `client.collection("posts").typed<Post>()`
|
|
130
|
+
* - or use the typed factory: `createClient<{ posts: Post }>()`
|
|
131
|
+
*
|
|
132
|
+
* @param name The collection name.
|
|
133
|
+
* @returns A {@link CollectionService} instance.
|
|
134
|
+
*/
|
|
135
|
+
collection(name: string): CollectionService<unknown> {
|
|
136
|
+
let svc = this.collectionCache.get(name);
|
|
137
|
+
if (!svc) {
|
|
138
|
+
svc = new CollectionService(
|
|
139
|
+
this.http,
|
|
140
|
+
name,
|
|
141
|
+
this.authStore,
|
|
142
|
+
this.realtime,
|
|
143
|
+
);
|
|
144
|
+
this.collectionCache.set(name, svc);
|
|
145
|
+
}
|
|
146
|
+
return svc;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get a typed service whose record shape is derived from the schema
|
|
151
|
+
* passed via `options.types.schemas` (if available), or fall back to
|
|
152
|
+
* the untyped service otherwise.
|
|
153
|
+
*
|
|
154
|
+
* @experimental
|
|
155
|
+
*/
|
|
156
|
+
collectionFor<TRecord = ApiRecord>(name: string): CollectionService<TRecord> {
|
|
157
|
+
return this.collection(name) as unknown as CollectionService<TRecord>;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Generate TypeScript types from the schemas provided to this client
|
|
162
|
+
* (via `options.types.schemas`). Returns a string ready to write to a
|
|
163
|
+
* `lazypock.types.ts` file.
|
|
164
|
+
*/
|
|
165
|
+
generateTypes(options?: { packageName?: string }): string {
|
|
166
|
+
const schemas = this.schemaByName ? [...this.schemaByName.values()] : [];
|
|
167
|
+
return generateTypes(schemas, options);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── Auth ──
|
|
171
|
+
|
|
172
|
+
/** Check whether any superuser exists (for login vs setup screen routing). */
|
|
173
|
+
async checkSuperuser(): Promise<{ has_superuser: boolean } | null> {
|
|
174
|
+
return this.http.get<{ has_superuser: boolean }>("/superusers/check");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create the initial superuser account.
|
|
179
|
+
* Only works when no superuser exists yet.
|
|
180
|
+
* Stores the returned token in the auth store.
|
|
181
|
+
* @param email Superuser email.
|
|
182
|
+
* @param password Superuser password (min 8 chars).
|
|
183
|
+
*/
|
|
184
|
+
async setup(
|
|
185
|
+
email: string,
|
|
186
|
+
password: string,
|
|
187
|
+
): Promise<({ token: string } & Record<string, unknown>) | null> {
|
|
188
|
+
const data = await this.http.post<
|
|
189
|
+
{ token: string } & Record<string, unknown>
|
|
190
|
+
>("/superusers/setup", { email, password });
|
|
191
|
+
if (data) {
|
|
192
|
+
this.authStore.setCollectionName(null);
|
|
193
|
+
this.authStore.set(data.token, null);
|
|
194
|
+
}
|
|
195
|
+
return data;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Authenticate as a superuser or auth collection user.
|
|
200
|
+
*
|
|
201
|
+
* When `collection` is provided, authenticates against
|
|
202
|
+
* `/{collection}/auth-with-password`. Otherwise logs in as superuser.
|
|
203
|
+
* Stores the returned token in the auth store.
|
|
204
|
+
*
|
|
205
|
+
* @param email User email or identity.
|
|
206
|
+
* @param password User password.
|
|
207
|
+
* @param collection Optional auth collection name.
|
|
208
|
+
*/
|
|
209
|
+
async login(
|
|
210
|
+
email: string,
|
|
211
|
+
password: string,
|
|
212
|
+
collection?: string,
|
|
213
|
+
): Promise<({ token: string } & Record<string, unknown>) | null> {
|
|
214
|
+
let data;
|
|
215
|
+
if (collection) {
|
|
216
|
+
data = await this.http.post<
|
|
217
|
+
{ token: string; record: Record<string, unknown> } & Record<
|
|
218
|
+
string,
|
|
219
|
+
unknown
|
|
220
|
+
>
|
|
221
|
+
>("/" + encodeURIComponent(collection) + "/auth-with-password", {
|
|
222
|
+
identity: email,
|
|
223
|
+
password,
|
|
224
|
+
});
|
|
225
|
+
if (data && data.record) {
|
|
226
|
+
this.authStore.setCollectionName(collection);
|
|
227
|
+
this.authStore.set(data.token, data.record as unknown as AuthModel);
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
data = await this.http.post<{ token: string } & Record<string, unknown>>(
|
|
231
|
+
"/superusers/login",
|
|
232
|
+
{ email, password },
|
|
233
|
+
);
|
|
234
|
+
if (data) {
|
|
235
|
+
this.authStore.setCollectionName(null);
|
|
236
|
+
this.authStore.set(data.token, null);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return data;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Fetch the current superuser profile and refresh the auth model. */
|
|
243
|
+
async me<T = ApiRecord>(options?: RequestOptions): Promise<T | null> {
|
|
244
|
+
const data = await this.http.get<T>("/superusers/me", options);
|
|
245
|
+
if (data) {
|
|
246
|
+
// Update the auth model with fresh data
|
|
247
|
+
this.authStore.set(this.authStore.token, data as unknown as AuthModel);
|
|
248
|
+
}
|
|
249
|
+
return data;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Authenticate against an auth collection with email/password.
|
|
254
|
+
* Stores the returned token and user record in the auth store.
|
|
255
|
+
*
|
|
256
|
+
* @param collection The auth collection name.
|
|
257
|
+
* @param identity Email or username.
|
|
258
|
+
* @param password Password.
|
|
259
|
+
* @param options Optional request options.
|
|
260
|
+
*/
|
|
261
|
+
async authWithPassword(
|
|
262
|
+
collection: string,
|
|
263
|
+
identity: string,
|
|
264
|
+
password: string,
|
|
265
|
+
options?: RequestOptions,
|
|
266
|
+
): Promise<
|
|
267
|
+
({ token: string; record: ApiRecord } & Record<string, unknown>) | null
|
|
268
|
+
> {
|
|
269
|
+
const data = await this.http.post<
|
|
270
|
+
{ token: string; record: ApiRecord } & Record<string, unknown>
|
|
271
|
+
>(
|
|
272
|
+
"/" + encodeURIComponent(collection) + "/auth-with-password",
|
|
273
|
+
{ identity, password },
|
|
274
|
+
options,
|
|
275
|
+
);
|
|
276
|
+
if (data) {
|
|
277
|
+
this.authStore.setCollectionName(collection);
|
|
278
|
+
this.authStore.set(data.token, data.record as unknown as AuthModel);
|
|
279
|
+
}
|
|
280
|
+
return data;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Refresh an auth collection token.
|
|
285
|
+
* Uses the currently stored auth token.
|
|
286
|
+
*
|
|
287
|
+
* @param collection The auth collection name.
|
|
288
|
+
* @param options Optional request options.
|
|
289
|
+
*/
|
|
290
|
+
async authRefresh(
|
|
291
|
+
collection: string,
|
|
292
|
+
options?: RequestOptions,
|
|
293
|
+
): Promise<
|
|
294
|
+
({ token: string; record: ApiRecord } & Record<string, unknown>) | null
|
|
295
|
+
> {
|
|
296
|
+
const data = await this.http.post<
|
|
297
|
+
{ token: string; record: ApiRecord } & Record<string, unknown>
|
|
298
|
+
>(
|
|
299
|
+
"/" + encodeURIComponent(collection) + "/auth-refresh",
|
|
300
|
+
undefined,
|
|
301
|
+
options,
|
|
302
|
+
);
|
|
303
|
+
if (data) {
|
|
304
|
+
this.authStore.setCollectionName(collection);
|
|
305
|
+
this.authStore.set(data.token, data.record as unknown as AuthModel);
|
|
306
|
+
}
|
|
307
|
+
return data;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Clear the current auth state and remove persisted tokens. */
|
|
311
|
+
logout(): void {
|
|
312
|
+
this.authStore.clear();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── Health ──
|
|
316
|
+
|
|
317
|
+
/** Ping the API health endpoint. */
|
|
318
|
+
health(options?: RequestOptions): Promise<Record<string, unknown> | null> {
|
|
319
|
+
return this.http.get<Record<string, unknown>>("/health", options);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ── Collection Management (admin) ──
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* List all collections (admin).
|
|
326
|
+
* @param q URL query string (e.g. `page=1&perPage=200`).
|
|
327
|
+
* @param options Optional request options.
|
|
328
|
+
*/
|
|
329
|
+
listCollections(
|
|
330
|
+
q?: string,
|
|
331
|
+
options?: RequestOptions,
|
|
332
|
+
): Promise<ListResult<ApiRecord> | null> {
|
|
333
|
+
return this.http.get<ListResult<ApiRecord>>(
|
|
334
|
+
"/collections" + (q ? "?" + q : ""),
|
|
335
|
+
options,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Get a single collection by ID or name.
|
|
341
|
+
* @param id Collection ID or name.
|
|
342
|
+
* @param options Optional request options.
|
|
343
|
+
*/
|
|
344
|
+
getCollection(
|
|
345
|
+
id: string,
|
|
346
|
+
options?: RequestOptions,
|
|
347
|
+
): Promise<ApiRecord | null> {
|
|
348
|
+
return this.http.get<ApiRecord>(
|
|
349
|
+
"/collections/" + encodeURIComponent(id),
|
|
350
|
+
options,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Create a new collection (admin).
|
|
356
|
+
* @param data Collection definition (name, type, fields, options, rules, etc.).
|
|
357
|
+
* @param options Optional request options.
|
|
358
|
+
*/
|
|
359
|
+
createCollection(
|
|
360
|
+
data: Record<string, unknown>,
|
|
361
|
+
options?: RequestOptions,
|
|
362
|
+
): Promise<ApiRecord | null> {
|
|
363
|
+
return this.http.post<ApiRecord>("/collections", data, options);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Update an existing collection (admin).
|
|
368
|
+
* @param id Collection ID or name.
|
|
369
|
+
* @param data Updated collection fields.
|
|
370
|
+
* @param options Optional request options.
|
|
371
|
+
*/
|
|
372
|
+
updateCollection(
|
|
373
|
+
id: string,
|
|
374
|
+
data: Record<string, unknown>,
|
|
375
|
+
options?: RequestOptions,
|
|
376
|
+
): Promise<ApiRecord | null> {
|
|
377
|
+
return this.http.patch<ApiRecord>(
|
|
378
|
+
"/collections/" + encodeURIComponent(id),
|
|
379
|
+
data,
|
|
380
|
+
options,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Delete a collection (admin).
|
|
386
|
+
* @param id Collection ID or name.
|
|
387
|
+
* @param options Optional request options.
|
|
388
|
+
*/
|
|
389
|
+
deleteCollection(id: string, options?: RequestOptions): Promise<null> {
|
|
390
|
+
return this.http.delete("/collections/" + encodeURIComponent(id), options);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ── Records (dynamic collection) ──
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* List records from a dynamic collection with optional filter/sort/pagination.
|
|
397
|
+
*
|
|
398
|
+
* @param coll Collection name.
|
|
399
|
+
* @param params Query parameters including:
|
|
400
|
+
* - `filter` — PocketBase filter syntax (e.g. `title~'hello' && published=true`)
|
|
401
|
+
* - `sort` — Comma-separated, `-` prefix for DESC (e.g. `-created,title`)
|
|
402
|
+
* - `page` — Page number (default: 1)
|
|
403
|
+
* - `perPage` — Items per page (default: 30, max: 200)
|
|
404
|
+
* - `expand` — Comma-separated relation fields (e.g. `author,category`)
|
|
405
|
+
* @param options Optional request options.
|
|
406
|
+
*/
|
|
407
|
+
listRecords(
|
|
408
|
+
coll: string,
|
|
409
|
+
params?: Record<string, string>,
|
|
410
|
+
options?: RequestOptions,
|
|
411
|
+
): Promise<ListResult<ApiRecord> | null> {
|
|
412
|
+
const qs = params ? "?" + new URLSearchParams(params).toString() : "";
|
|
413
|
+
return this.http.get<ListResult<ApiRecord>>(
|
|
414
|
+
"/" + encodeURIComponent(coll) + qs,
|
|
415
|
+
options,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Get a single record by ID.
|
|
421
|
+
* @param coll Collection name.
|
|
422
|
+
* @param id Record ID.
|
|
423
|
+
* @param options Optional request options.
|
|
424
|
+
*/
|
|
425
|
+
getRecord(
|
|
426
|
+
coll: string,
|
|
427
|
+
id: string,
|
|
428
|
+
options?: RequestOptions,
|
|
429
|
+
): Promise<ApiRecord | null> {
|
|
430
|
+
return this.http.get<ApiRecord>(
|
|
431
|
+
"/" + encodeURIComponent(coll) + "/" + encodeURIComponent(id),
|
|
432
|
+
options,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Create a record in a dynamic collection.
|
|
438
|
+
* @param coll Collection name.
|
|
439
|
+
* @param data Record fields.
|
|
440
|
+
* @param options Optional request options.
|
|
441
|
+
*/
|
|
442
|
+
createRecord(
|
|
443
|
+
coll: string,
|
|
444
|
+
data: Record<string, unknown>,
|
|
445
|
+
options?: RequestOptions,
|
|
446
|
+
): Promise<ApiRecord | null> {
|
|
447
|
+
return this.http.post<ApiRecord>(
|
|
448
|
+
"/" + encodeURIComponent(coll),
|
|
449
|
+
data,
|
|
450
|
+
options,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Update a record in a dynamic collection.
|
|
456
|
+
* @param coll Collection name.
|
|
457
|
+
* @param id Record ID.
|
|
458
|
+
* @param data Updated record fields.
|
|
459
|
+
* @param options Optional request options.
|
|
460
|
+
*/
|
|
461
|
+
updateRecord(
|
|
462
|
+
coll: string,
|
|
463
|
+
id: string,
|
|
464
|
+
data: Record<string, unknown>,
|
|
465
|
+
options?: RequestOptions,
|
|
466
|
+
): Promise<ApiRecord | null> {
|
|
467
|
+
return this.http.patch<ApiRecord>(
|
|
468
|
+
"/" + encodeURIComponent(coll) + "/" + encodeURIComponent(id),
|
|
469
|
+
data,
|
|
470
|
+
options,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Delete a record from a dynamic collection.
|
|
476
|
+
* @param coll Collection name.
|
|
477
|
+
* @param id Record ID.
|
|
478
|
+
* @param options Optional request options.
|
|
479
|
+
*/
|
|
480
|
+
deleteRecord(
|
|
481
|
+
coll: string,
|
|
482
|
+
id: string,
|
|
483
|
+
options?: RequestOptions,
|
|
484
|
+
): Promise<null> {
|
|
485
|
+
return this.http.delete(
|
|
486
|
+
"/" + encodeURIComponent(coll) + "/" + encodeURIComponent(id),
|
|
487
|
+
options,
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
}
|
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;
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// ── Schema types ─────────────────────────────────────────
|
|
2
|
+
// Shared runtime + codegen types for describing collections/fields.
|
|
3
|
+
|
|
4
|
+
/** A single field definition as returned by `GET /collections`. */
|
|
5
|
+
export interface SchemaField {
|
|
6
|
+
id?: string;
|
|
7
|
+
name: string;
|
|
8
|
+
type: string;
|
|
9
|
+
required?: boolean;
|
|
10
|
+
unique?: boolean;
|
|
11
|
+
options?: {
|
|
12
|
+
/** For relation fields: the target collection name. */
|
|
13
|
+
collection?: string;
|
|
14
|
+
/** Max selectable/related items (multi when > 1). */
|
|
15
|
+
maxSelect?: number;
|
|
16
|
+
/** Allowed values for select / multi_select fields. */
|
|
17
|
+
values?: string[];
|
|
18
|
+
} & Record<string, unknown>;
|
|
19
|
+
indexed?: boolean;
|
|
20
|
+
hidden?: boolean;
|
|
21
|
+
system?: boolean;
|
|
22
|
+
sort_order?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A collection definition as returned by `GET /collections`. */
|
|
26
|
+
export interface CollectionSchema {
|
|
27
|
+
id?: string;
|
|
28
|
+
name: string;
|
|
29
|
+
type: "base" | "auth";
|
|
30
|
+
system?: boolean;
|
|
31
|
+
fields?: SchemaField[];
|
|
32
|
+
rules?: Record<string, unknown>;
|
|
33
|
+
options?: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Shape of the `GET /collections` response. */
|
|
37
|
+
export interface CollectionsResponse {
|
|
38
|
+
items: CollectionSchema[];
|
|
39
|
+
}
|
package/src/typegen.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// ── Type mapping (runtime + codegen) ────────────────────
|
|
2
|
+
// Maps server field types to TypeScript types.
|
|
3
|
+
// Shared by the runtime `SchemaTypes` helper and the codegen CLI.
|
|
4
|
+
|
|
5
|
+
import type { SchemaField } from "./schema";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Map a single server field to its TypeScript type string.
|
|
9
|
+
* Used by the codegen CLI to emit interface members.
|
|
10
|
+
*
|
|
11
|
+
* @param field The field definition.
|
|
12
|
+
* @param fallback Fallback type for unknown field types (default `unknown`).
|
|
13
|
+
*/
|
|
14
|
+
export function fieldTypeScriptType(
|
|
15
|
+
field: SchemaField,
|
|
16
|
+
fallback = "unknown",
|
|
17
|
+
): string {
|
|
18
|
+
const opts = field.options ?? {};
|
|
19
|
+
switch (field.type) {
|
|
20
|
+
case "text":
|
|
21
|
+
case "email":
|
|
22
|
+
case "url":
|
|
23
|
+
case "editor":
|
|
24
|
+
case "date":
|
|
25
|
+
case "datetime":
|
|
26
|
+
return "string";
|
|
27
|
+
case "number":
|
|
28
|
+
return "number";
|
|
29
|
+
case "bool":
|
|
30
|
+
return "boolean";
|
|
31
|
+
case "select": {
|
|
32
|
+
const values = Array.isArray(opts.values) ? opts.values : [];
|
|
33
|
+
if (values.length > 0) {
|
|
34
|
+
return values.map((v) => JSON.stringify(String(v))).join(" | ");
|
|
35
|
+
}
|
|
36
|
+
return "string";
|
|
37
|
+
}
|
|
38
|
+
case "multi_select": {
|
|
39
|
+
const values = Array.isArray(opts.values) ? opts.values : [];
|
|
40
|
+
if (values.length > 0) {
|
|
41
|
+
return `(${values.map((v) => JSON.stringify(String(v))).join(" | ")})[]`;
|
|
42
|
+
}
|
|
43
|
+
return "string[]";
|
|
44
|
+
}
|
|
45
|
+
case "file":
|
|
46
|
+
return "string";
|
|
47
|
+
case "multi_file":
|
|
48
|
+
return "string[]";
|
|
49
|
+
case "json":
|
|
50
|
+
case "geo":
|
|
51
|
+
return "Record<string, unknown>";
|
|
52
|
+
case "relation":
|
|
53
|
+
// Relations store the target record's ID (string) — or an array
|
|
54
|
+
// of IDs when multi-relation (maxSelect > 1).
|
|
55
|
+
return (opts.maxSelect ?? 1) > 1 ? "string[]" : "string";
|
|
56
|
+
case "password":
|
|
57
|
+
// Passwords are write-only; never expose on read models.
|
|
58
|
+
return "never";
|
|
59
|
+
default:
|
|
60
|
+
return fallback;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Returns the runtime type kind for a field — used by {@link schemaFieldType}
|
|
66
|
+
* to build structural types at runtime.
|
|
67
|
+
*/
|
|
68
|
+
export type FieldTypeKind =
|
|
69
|
+
| "string"
|
|
70
|
+
| "number"
|
|
71
|
+
| "boolean"
|
|
72
|
+
| "string-array"
|
|
73
|
+
| "json"
|
|
74
|
+
| "relation"
|
|
75
|
+
| "relation-many"
|
|
76
|
+
| "password"
|
|
77
|
+
| "unknown";
|
|
78
|
+
|
|
79
|
+
/** Map a server field to its runtime type kind. */
|
|
80
|
+
export function fieldTypeKind(field: SchemaField): FieldTypeKind {
|
|
81
|
+
const opts = field.options ?? {};
|
|
82
|
+
switch (field.type) {
|
|
83
|
+
case "text":
|
|
84
|
+
case "email":
|
|
85
|
+
case "url":
|
|
86
|
+
case "editor":
|
|
87
|
+
case "date":
|
|
88
|
+
case "datetime":
|
|
89
|
+
case "select":
|
|
90
|
+
case "file":
|
|
91
|
+
return "string";
|
|
92
|
+
case "number":
|
|
93
|
+
return "number";
|
|
94
|
+
case "bool":
|
|
95
|
+
return "boolean";
|
|
96
|
+
case "multi_select":
|
|
97
|
+
case "multi_file":
|
|
98
|
+
return "string-array";
|
|
99
|
+
case "json":
|
|
100
|
+
case "geo":
|
|
101
|
+
return "json";
|
|
102
|
+
case "relation":
|
|
103
|
+
return (opts.maxSelect ?? 1) > 1 ? "relation-many" : "relation";
|
|
104
|
+
case "password":
|
|
105
|
+
return "password";
|
|
106
|
+
default:
|
|
107
|
+
return "unknown";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Derive a TypeScript field type from a {@link SchemaField} — the runtime
|
|
113
|
+
* counterpart to the codegen mapper. Lets consumers build typed clients
|
|
114
|
+
* from a fetched schema without running the CLI.
|
|
115
|
+
*/
|
|
116
|
+
export function schemaFieldType(field: SchemaField): unknown {
|
|
117
|
+
switch (fieldTypeKind(field)) {
|
|
118
|
+
case "string":
|
|
119
|
+
return String;
|
|
120
|
+
case "number":
|
|
121
|
+
return Number;
|
|
122
|
+
case "boolean":
|
|
123
|
+
return Boolean;
|
|
124
|
+
case "string-array":
|
|
125
|
+
return [String] as const;
|
|
126
|
+
case "relation":
|
|
127
|
+
return String;
|
|
128
|
+
case "relation-many":
|
|
129
|
+
return [String] as const;
|
|
130
|
+
case "json":
|
|
131
|
+
return Object;
|
|
132
|
+
case "password":
|
|
133
|
+
return undefined;
|
|
134
|
+
case "unknown":
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|