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/codegen.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// ── Codegen ─────────────────────────────────────────────
|
|
2
|
+
// Generates a `lazypock.types.ts` module from the live API schema.
|
|
3
|
+
//
|
|
4
|
+
// The generated file exports:
|
|
5
|
+
// - One interface per collection (e.g. `PostsRecord`)
|
|
6
|
+
// - A `LazypockCollections` map: { posts: PostsRecord; users: UsersRecord; ... }
|
|
7
|
+
// - A `createClient()` factory pre-bound to those types, so
|
|
8
|
+
// `client.collection("posts").create({ title: "x" })` is fully type-checked.
|
|
9
|
+
//
|
|
10
|
+
// The CLI in `src/cli.ts` wires this to `GET /collections`.
|
|
11
|
+
|
|
12
|
+
import type { CollectionSchema, SchemaField } from "./schema";
|
|
13
|
+
import { fieldTypeScriptType } from "./typegen";
|
|
14
|
+
|
|
15
|
+
/** Format a raw collection name into a valid TS identifier (PascalCase). */
|
|
16
|
+
export function collectionTypeName(name: string): string {
|
|
17
|
+
return name
|
|
18
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
21
|
+
.join("")
|
|
22
|
+
.replace(/^[0-9]/, "_$&");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Interface member name — sanitize hyphens/spaces but keep readability. */
|
|
26
|
+
export function fieldKey(name: string): string {
|
|
27
|
+
return name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generate the full TypeScript source for the typed SDK module.
|
|
34
|
+
*
|
|
35
|
+
* @param collections Collections fetched from the API.
|
|
36
|
+
* @param options Generation options.
|
|
37
|
+
*/
|
|
38
|
+
export function generateTypes(
|
|
39
|
+
collections: CollectionSchema[],
|
|
40
|
+
options: {
|
|
41
|
+
/** Import specifier for the lazypock package (default `lazypock`). */
|
|
42
|
+
packageName?: string;
|
|
43
|
+
/** Emit base record fields (id, created, updated, …). Default true. */
|
|
44
|
+
includeBaseFields?: boolean;
|
|
45
|
+
/** Skip system collections (names starting with `_` or `users`). Default false. */
|
|
46
|
+
skipSystem?: boolean;
|
|
47
|
+
} = {},
|
|
48
|
+
): string {
|
|
49
|
+
const {
|
|
50
|
+
packageName = "lazypock",
|
|
51
|
+
includeBaseFields = true,
|
|
52
|
+
skipSystem = false,
|
|
53
|
+
} = options;
|
|
54
|
+
|
|
55
|
+
const filtered = skipSystem
|
|
56
|
+
? collections.filter(
|
|
57
|
+
(c) => !c.system && !c.name.startsWith("_") && c.name !== "users",
|
|
58
|
+
)
|
|
59
|
+
: collections;
|
|
60
|
+
|
|
61
|
+
const sections: string[] = [];
|
|
62
|
+
sections.push(`// ── Auto-generated by lazypock-ts ──────────────────────────
|
|
63
|
+
// Do not edit by hand. Regenerate with: npx lazypock-gen
|
|
64
|
+
// Schema snapshot: ${new Date().toISOString()}`);
|
|
65
|
+
|
|
66
|
+
if (includeBaseFields) {
|
|
67
|
+
sections.push(`export interface BaseRecord {
|
|
68
|
+
id: string;
|
|
69
|
+
collectionId: string;
|
|
70
|
+
collectionName: string;
|
|
71
|
+
created: string;
|
|
72
|
+
updated: string;
|
|
73
|
+
}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// One interface per collection
|
|
77
|
+
for (const coll of filtered) {
|
|
78
|
+
const typeName = collectionTypeName(coll.name);
|
|
79
|
+
const fields = coll.fields ?? [];
|
|
80
|
+
const lines = fields.map((f) => memberLine(f)).filter((l) => l !== "");
|
|
81
|
+
const body = lines.join("\n");
|
|
82
|
+
sections.push(
|
|
83
|
+
`export interface ${typeName}Record${renderInterface({
|
|
84
|
+
extends: includeBaseFields ? "BaseRecord" : undefined,
|
|
85
|
+
body,
|
|
86
|
+
})}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Auth collection type
|
|
91
|
+
sections.push(`export interface AuthRecord extends BaseRecord {
|
|
92
|
+
email: string;
|
|
93
|
+
verified: boolean;
|
|
94
|
+
}`);
|
|
95
|
+
|
|
96
|
+
// Collections map
|
|
97
|
+
const mapEntries = filtered
|
|
98
|
+
.map(
|
|
99
|
+
(c) =>
|
|
100
|
+
` "${c.name}": ${collectionTypeName(c.name)}Record${
|
|
101
|
+
c.type === "auth" ? " & AuthRecord" : ""
|
|
102
|
+
};`,
|
|
103
|
+
)
|
|
104
|
+
.join("\n");
|
|
105
|
+
sections.push(`export interface LazypockCollections {
|
|
106
|
+
${mapEntries}
|
|
107
|
+
}`);
|
|
108
|
+
|
|
109
|
+
// createClient factory
|
|
110
|
+
sections.push(`import { LazypockClient, type LazypockClientOptions, type CollectionService } from "${packageName}";
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create a Lazypock client typed against this schema snapshot.
|
|
114
|
+
* Collection access is fully type-checked:
|
|
115
|
+
* client.collection("posts").create({ title: "x" }) // title must exist
|
|
116
|
+
*/
|
|
117
|
+
export function createClient(options: LazypockClientOptions): TypedClient {
|
|
118
|
+
return new TypedClient(options);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export class TypedClient extends LazypockClient {
|
|
122
|
+
override collection<K extends keyof LazypockCollections>(name: K): CollectionService<LazypockCollections[K]>;
|
|
123
|
+
override collection(name: string): CollectionService<unknown> {
|
|
124
|
+
return super.collection(name) as unknown as CollectionService<unknown>;
|
|
125
|
+
}
|
|
126
|
+
}`);
|
|
127
|
+
|
|
128
|
+
return sections.join("\n\n") + "\n";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Render the interface body including the extends clause.
|
|
133
|
+
* Empty body → ` extends BaseRecord {}` (valid TS).
|
|
134
|
+
*/
|
|
135
|
+
function renderInterface(opts: { extends?: string; body: string }): string {
|
|
136
|
+
const ext = opts.extends ? ` extends ${opts.extends}` : "";
|
|
137
|
+
if (!opts.body) return `${ext} {}`;
|
|
138
|
+
return `${ext} {\n${opts.body}\n}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Render a single interface member line for a field. */
|
|
142
|
+
function memberLine(f: SchemaField): string {
|
|
143
|
+
const key = fieldKey(f.name);
|
|
144
|
+
const req = f.required || f.type === "password" ? "" : "?";
|
|
145
|
+
const type = fieldTypeScriptType(f);
|
|
146
|
+
// `never` members (passwords) are omitted from read models.
|
|
147
|
+
if (type === "never") return "";
|
|
148
|
+
return ` ${JSON.stringify(key)}${req}: ${type};`;
|
|
149
|
+
}
|
package/src/collection.ts
CHANGED
|
@@ -3,22 +3,75 @@
|
|
|
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.
|
|
10
54
|
* Get an instance via {@link LazypockClient.collection}.
|
|
55
|
+
*
|
|
56
|
+
* @typeParam T — The record shape for this collection. Defaults to {@link ApiRecord}.
|
|
11
57
|
*/
|
|
12
|
-
export class CollectionService {
|
|
58
|
+
export class CollectionService<T = ApiRecord> {
|
|
13
59
|
private http: HttpClient;
|
|
14
60
|
private collectionName: string;
|
|
15
61
|
private authStore?: AuthStore;
|
|
62
|
+
private realtime?: RealtimeService;
|
|
16
63
|
|
|
17
64
|
/** @internal */
|
|
18
|
-
constructor(
|
|
65
|
+
constructor(
|
|
66
|
+
http: HttpClient,
|
|
67
|
+
collectionName: string,
|
|
68
|
+
authStore?: AuthStore,
|
|
69
|
+
realtime?: RealtimeService,
|
|
70
|
+
) {
|
|
19
71
|
this.http = http;
|
|
20
72
|
this.collectionName = collectionName;
|
|
21
73
|
this.authStore = authStore;
|
|
74
|
+
this.realtime = realtime;
|
|
22
75
|
}
|
|
23
76
|
|
|
24
77
|
private encodeId(id: string): string {
|
|
@@ -30,7 +83,7 @@ export class CollectionService {
|
|
|
30
83
|
* @param params Query parameters including `filter`, `sort`, `page`, `perPage`, `expand`.
|
|
31
84
|
* @param options Optional request options.
|
|
32
85
|
*/
|
|
33
|
-
list
|
|
86
|
+
list(
|
|
34
87
|
params?: Record<string, string>,
|
|
35
88
|
options?: RequestOptions,
|
|
36
89
|
): Promise<ListResult<T> | null> {
|
|
@@ -46,10 +99,7 @@ export class CollectionService {
|
|
|
46
99
|
* @param id Record ID.
|
|
47
100
|
* @param options Optional request options.
|
|
48
101
|
*/
|
|
49
|
-
getOne<T
|
|
50
|
-
id: string,
|
|
51
|
-
options?: RequestOptions,
|
|
52
|
-
): Promise<T | null> {
|
|
102
|
+
getOne(id: string, options?: RequestOptions): Promise<T | null> {
|
|
53
103
|
return this.http.get<T>(
|
|
54
104
|
"/" + this.encodeId(this.collectionName) + "/" + this.encodeId(id),
|
|
55
105
|
options,
|
|
@@ -58,11 +108,12 @@ export class CollectionService {
|
|
|
58
108
|
|
|
59
109
|
/**
|
|
60
110
|
* Create a new record.
|
|
61
|
-
* @param data Record fields.
|
|
111
|
+
* @param data Record fields. When `T` is a concrete shape (e.g. a generated
|
|
112
|
+
* record type), excess/unknown fields are rejected at compile time.
|
|
62
113
|
* @param options Optional request options.
|
|
63
114
|
*/
|
|
64
|
-
create
|
|
65
|
-
data: Record<string, unknown>,
|
|
115
|
+
create(
|
|
116
|
+
data: T extends ApiRecord ? Record<string, unknown> : CreateData<T>,
|
|
66
117
|
options?: RequestOptions,
|
|
67
118
|
): Promise<T | null> {
|
|
68
119
|
return this.http.post<T>(
|
|
@@ -75,12 +126,13 @@ export class CollectionService {
|
|
|
75
126
|
/**
|
|
76
127
|
* Update a record by ID.
|
|
77
128
|
* @param id Record ID.
|
|
78
|
-
* @param data Updated record fields.
|
|
129
|
+
* @param data Updated record fields. When `T` is a concrete shape, `data`
|
|
130
|
+
* must be a partial of `T` — unknown fields are rejected.
|
|
79
131
|
* @param options Optional request options.
|
|
80
132
|
*/
|
|
81
|
-
update
|
|
133
|
+
update(
|
|
82
134
|
id: string,
|
|
83
|
-
data: Record<string, unknown>,
|
|
135
|
+
data: T extends ApiRecord ? Record<string, unknown> : UpdateData<T>,
|
|
84
136
|
options?: RequestOptions,
|
|
85
137
|
): Promise<T | null> {
|
|
86
138
|
return this.http.patch<T>(
|
|
@@ -111,11 +163,12 @@ export class CollectionService {
|
|
|
111
163
|
options?: RequestOptions,
|
|
112
164
|
): Promise<{ field: string; targetCollection: string }[] | null> {
|
|
113
165
|
const data = await this.http.get<{
|
|
114
|
-
fields?: {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
166
|
+
fields?: {
|
|
167
|
+
name: string;
|
|
168
|
+
type: string;
|
|
169
|
+
options?: Record<string, string>;
|
|
170
|
+
}[];
|
|
171
|
+
}>("/collections/" + this.encodeId(this.collectionName), options);
|
|
119
172
|
if (!data?.fields) return null;
|
|
120
173
|
return data.fields
|
|
121
174
|
.filter((f) => f.type === "relation" && f.options?.collection)
|
|
@@ -125,6 +178,71 @@ export class CollectionService {
|
|
|
125
178
|
}));
|
|
126
179
|
}
|
|
127
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Cast this service to a specific record shape.
|
|
183
|
+
* Use when you have a hand-written or generated interface for the
|
|
184
|
+
* collection and want compile-time checking of create/update/list.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```ts
|
|
188
|
+
* interface Post {
|
|
189
|
+
* id: string;
|
|
190
|
+
* title: string;
|
|
191
|
+
* published: boolean;
|
|
192
|
+
* }
|
|
193
|
+
* const posts = client.collection("posts").typed<Post>();
|
|
194
|
+
* await posts.create({ title: "Hi", published: true }); // ✓
|
|
195
|
+
* await posts.create({ nope: 1 }); // ✗ compile error
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
typed<TRecord = ApiRecord>(): CollectionService<TRecord> {
|
|
199
|
+
return this as unknown as CollectionService<TRecord>;
|
|
200
|
+
}
|
|
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
|
+
|
|
128
246
|
// ── Auth Collection Methods ──
|
|
129
247
|
|
|
130
248
|
/**
|
|
@@ -135,7 +253,9 @@ export class CollectionService {
|
|
|
135
253
|
identity: string,
|
|
136
254
|
password: string,
|
|
137
255
|
options?: RequestOptions,
|
|
138
|
-
): Promise<
|
|
256
|
+
): Promise<
|
|
257
|
+
({ token: string; record: ApiRecord } & Record<string, unknown>) | null
|
|
258
|
+
> {
|
|
139
259
|
const data = await this.http.post<
|
|
140
260
|
{ token: string; record: ApiRecord } & Record<string, unknown>
|
|
141
261
|
>(
|
|
@@ -156,7 +276,9 @@ export class CollectionService {
|
|
|
156
276
|
*/
|
|
157
277
|
async authRefresh(
|
|
158
278
|
options?: RequestOptions,
|
|
159
|
-
): Promise<
|
|
279
|
+
): Promise<
|
|
280
|
+
({ token: string; record: ApiRecord } & Record<string, unknown>) | null
|
|
281
|
+
> {
|
|
160
282
|
const data = await this.http.post<
|
|
161
283
|
{ token: string; record: ApiRecord } & Record<string, unknown>
|
|
162
284
|
>(
|
|
@@ -174,6 +296,8 @@ export class CollectionService {
|
|
|
174
296
|
/**
|
|
175
297
|
* Get available auth methods for this collection.
|
|
176
298
|
*/
|
|
299
|
+
// ── end Realtime ──
|
|
300
|
+
|
|
177
301
|
async authMethods(
|
|
178
302
|
options?: RequestOptions,
|
|
179
303
|
): Promise<Record<string, unknown> | null> {
|