lazypock 0.8.1 → 0.8.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/src/codegen.ts CHANGED
@@ -183,8 +183,19 @@ export class TypedClient extends LazypockClient {
183
183
  //
184
184
  // create()/update() take the collection's *CreateData — the read model
185
185
  // omits password/hidden fields, but the write model carries them.
186
- override collection<K extends keyof LazypockCollections>(name: K): CollectionService<LazypockCollections[K], LazypockCreateData[K]> {
187
- return super.collection(name) as CollectionService<LazypockCollections[K], LazypockCreateData[K]>;
186
+ // T extends string (rather than keyof LazypockCollections) with a conditional
187
+ // return type, so the IDE suggests collection names AND unknown/dynamic names
188
+ // still resolve to the untyped service — a studio that manages
189
+ // user-created collections must be able to call collection(someString).
190
+ //
191
+ // create()/update() take the collection's *CreateData — the read model
192
+ // omits password/hidden fields, but the write model carries them.
193
+ override collection<T extends string>(name: T): T extends keyof LazypockCollections
194
+ ? CollectionService<LazypockCollections[T], LazypockCreateData[T]>
195
+ : CollectionService<unknown> {
196
+ return super.collection(name) as T extends keyof LazypockCollections
197
+ ? CollectionService<LazypockCollections[T], LazypockCreateData[T]>
198
+ : CollectionService<unknown>;
188
199
  }
189
200
  }
190
201
  `);
@@ -221,12 +232,22 @@ function memberLine(f: SchemaField): string {
221
232
  * Unlike the read model, create/update data includes password fields (sent as
222
233
  * plain strings — the server hashes them) and hidden fields: the server
223
234
  * accepts them on write and never returns them in responses.
235
+ *
236
+ * A field is only marked required when it actually needs a client value:
237
+ * fields with a server-side default (`options.defaultValue`) and the auth
238
+ * system fields `verified` / `emailVisibility` (the server defaults them to
239
+ * false/true even though the snapshot doesn't carry `defaultValue`) stay
240
+ * optional, so `create({ email, password_hash })` typechecks without forcing
241
+ * callers to send values the server would fill in anyway.
224
242
  */
225
243
  function createDataMemberLine(f: SchemaField): string {
226
244
  // Autodate columns are server-managed; never writable.
227
245
  if (f.type === "autodate") return "";
228
246
  const key = fieldKey(f.name);
229
- const req = f.required ? "" : "?";
247
+ const serverDefaulted =
248
+ f.options?.defaultValue !== undefined ||
249
+ (f.system && (f.name === "verified" || f.name === "emailVisibility"));
250
+ const req = f.required && !serverDefaulted ? "" : "?";
230
251
  const type = f.type === "password" ? "string" : fieldTypeScriptType(f);
231
252
  if (type === "never") return "";
232
253
  return ` ${JSON.stringify(key)}${req}: ${type};`;
package/src/http.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Only relies on globalThis.fetch — works in browser, React Native, and Node 18+
3
3
 
4
4
  import { ApiError, type Method, type RequestOptions } from "./types";
5
- import type { AuthStore } from "./auth";
5
+ import type { AuthModel, AuthStore } from "./auth";
6
6
 
7
7
  /**
8
8
  * Low-level HTTP client wrapping `fetch` with automatic auth token injection.
@@ -74,10 +74,10 @@ export class HttpClient {
74
74
  }
75
75
  const data = (await res.json()) as Record<string, unknown>;
76
76
  if (data && typeof data.token === "string") {
77
- this.authStore.set(
78
- data.token,
79
- (data.record as Record<string, unknown> as any) ?? null,
80
- );
77
+ const record = data["record"];
78
+ const model: AuthModel | null =
79
+ record && typeof record === "object" ? (record as AuthModel) : null;
80
+ this.authStore.set(data.token, model);
81
81
  return data as { token: string; record: Record<string, unknown> };
82
82
  }
83
83
  return null;
@@ -300,8 +300,22 @@ export class HttpClient {
300
300
  if (bodyText) {
301
301
  data = JSON.parse(bodyText) as Record<string, unknown>;
302
302
  }
303
- } catch {
304
- // Not JSON — keep data as empty object
303
+ } catch (err) {
304
+ // An abort that lands while the body is streaming (auto-cancelled
305
+ // duplicate, manual cancel, or external signal) means the whole
306
+ // request was cancelled — surface it as an abort error instead of
307
+ // silently returning an empty object, which callers would treat as
308
+ // a successful-but-empty response (e.g. a list request would clear
309
+ // the current rows).
310
+ if (isAbortError(err)) {
311
+ throw new ApiError(
312
+ "The request was aborted (most likely auto-cancelled by a newer request with the same requestKey)",
313
+ {},
314
+ 0,
315
+ true,
316
+ );
317
+ }
318
+ // Not JSON / read failure — keep data as empty object
305
319
  }
306
320
 
307
321
  if (!res!.ok) {