@pylonsync/sdk 0.20.0 → 0.22.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/dist/index.d.ts CHANGED
@@ -74,6 +74,14 @@ export interface FieldDefinition {
74
74
  * `serverOnly`, which hides a field from ALL client surfaces.
75
75
  */
76
76
  syncOmit?: boolean;
77
+ /**
78
+ * Longest value the field accepts, in characters (Unicode code
79
+ * points). Set with `field.string().max(n)` or
80
+ * `field.richtext().max(n)`. The server rejects a longer value with
81
+ * `FIELD_TOO_LONG` on every write path: the entity API, sync push,
82
+ * `ctx.db` in functions, and collaborative (CRDT) edits.
83
+ */
84
+ maxLength?: number;
77
85
  /**
78
86
  * When true, the field is **set on insert but cannot be changed
79
87
  * by client updates**. The framework rejects any `PATCH`/`PUT`
@@ -162,6 +170,15 @@ interface FieldBuilder {
162
170
  * editor still gets it via `db.get("Variant", id)`.
163
171
  */
164
172
  syncOmit(): FieldBuilder;
173
+ /**
174
+ * Cap the value at `n` characters (Unicode code points). String and
175
+ * richtext fields only. See [`FieldDefinition.maxLength`].
176
+ *
177
+ * Example: `bio: field.string().max(280)` stops any client, guests
178
+ * included, from storing a longer bio that every other client would
179
+ * then sync.
180
+ */
181
+ max(n: number): FieldBuilder;
165
182
  /**
166
183
  * Mark the field as set-on-insert-only. See [`FieldDefinition.readonly`]
167
184
  * for the full semantics.
@@ -334,6 +351,18 @@ export interface EntityDefinition {
334
351
  * can read" and "nothing".
335
352
  */
336
353
  sync?: boolean | SyncScope;
354
+ /**
355
+ * Record every insert, update, and delete in the audit log: actor,
356
+ * tenant, row id, and the names of changed fields (not their values).
357
+ * Read it with `ctx.audit.list({ entity })` or `GET /api/admin/audit`.
358
+ */
359
+ audit?: boolean;
360
+ /**
361
+ * Delete rows once they pass a retention period. A system job runs
362
+ * hourly; each deletion goes through the entity pipeline and is recorded
363
+ * in the audit log as `retention.delete`. See {@link RetentionRule}.
364
+ */
365
+ retention?: RetentionRule;
337
366
  /**
338
367
  * Per-row CRDT (Loro) documents. Default `true`: every write merges through
339
368
  * a document, and offline edits from several devices converge. Set `false`
@@ -376,6 +405,24 @@ export interface SyncScope {
376
405
  */
377
406
  limit?: number;
378
407
  }
408
+ /**
409
+ * Retention rule for an entity.
410
+ *
411
+ * - `{ field: "createdAt", after: "365d" }` deletes a row once `createdAt`
412
+ * is older than 365 days. Durations: `s`, `m`, `h`, `d`, `w`, `y`.
413
+ * - `{ field: "deleteAfter" }` (no `after`) deletes a row once
414
+ * `deleteAfter` is in the past. Use it for a retention period per tenant:
415
+ * set `deleteAfter` on each row at insert.
416
+ *
417
+ * `field` is a `datetime` field, or an `int`/`float` field holding unix
418
+ * milliseconds. Rows whose `hold` field (a `bool`) is true are kept, for
419
+ * example under a legal hold.
420
+ */
421
+ export interface RetentionRule {
422
+ field: string;
423
+ after?: string;
424
+ hold?: string;
425
+ }
379
426
  export declare function entity(name: string, fields: Record<string, FieldBuilder>, options?: {
380
427
  indexes?: IndexDefinition[];
381
428
  relations?: RelationDefinition[];
@@ -387,6 +434,12 @@ export declare function entity(name: string, fields: Record<string, FieldBuilder
387
434
  /** `false` for server-written rows that need no CRDT document. Mirrors
388
435
  * {@link EntityDefinition.crdt}. */
389
436
  crdt?: boolean;
437
+ /** Record every write in the audit log. Mirrors
438
+ * {@link EntityDefinition.audit}. */
439
+ audit?: boolean;
440
+ /** Delete rows past a retention period. Mirrors
441
+ * {@link EntityDefinition.retention}. */
442
+ retention?: RetentionRule;
390
443
  }): EntityDefinition;
391
444
  export declare function relation(def: RelationDefinition): RelationDefinition;
392
445
  export { defineRoute } from "./route";
@@ -507,6 +560,9 @@ export interface ManifestField {
507
560
  /** Set when the field is `field.X().syncOmit()` — see
508
561
  * [`FieldDefinition.syncOmit`]. Omitted by default. */
509
562
  syncOmit?: boolean;
563
+ /** Set by `.max(n)` — see [`FieldDefinition.maxLength`]. Omitted when
564
+ * the field has no limit. */
565
+ maxLength?: number;
510
566
  /** Set when the field is `field.X().readonly()` — see
511
567
  * [`FieldDefinition.readonly`]. Omitted by default. */
512
568
  readonly?: boolean;
@@ -571,6 +627,10 @@ export interface ManifestEntity {
571
627
  sync_limit?: number;
572
628
  /** CRDT mode; omitted when true (the runtime default). */
573
629
  crdt?: boolean;
630
+ /** Audit every write; omitted when false. */
631
+ audit?: boolean;
632
+ /** Retention rule; omitted when unset. */
633
+ retention?: RetentionRule;
574
634
  }
575
635
  export interface ManifestRoute {
576
636
  path: string;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.20.0",
6
+ "version": "0.22.0",
7
7
  "type": "module",
8
8
  "main": "./src/index.ts",
9
9
  "types": "./dist/index.d.ts",
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `field.string().max(n)` manifest contract:
3
+ * - the limit serializes as `maxLength` (the Rust `ManifestField`
4
+ * reads that key), and only when set
5
+ * - it composes with the other modifiers in any order
6
+ * - it is refused on non-text fields and for non-positive limits
7
+ */
8
+ import { expect, test } from "bun:test";
9
+ import { entitiesToManifest, field } from "./index";
10
+
11
+ test(".max(n) serializes as maxLength on string and richtext fields", () => {
12
+ const [ent] = entitiesToManifest([
13
+ {
14
+ name: "Post",
15
+ fields: {
16
+ title: field.string().max(120),
17
+ body: field.richtext().optional().max(20_000),
18
+ status: field.enum(["draft", "live"]).max(5),
19
+ slug: field.string(),
20
+ },
21
+ },
22
+ ]);
23
+ const byName = (n: string) => ent.fields.find((f) => f.name === n)!;
24
+ expect(byName("title").maxLength).toBe(120);
25
+ expect(byName("body").maxLength).toBe(20_000);
26
+ expect(byName("body").optional).toBe(true);
27
+ expect(byName("status").maxLength).toBe(5);
28
+ // No limit, no key: unannotated manifests are unchanged.
29
+ expect("maxLength" in byName("slug")).toBe(false);
30
+ });
31
+
32
+ test(".max(n) is refused on other field types and for bad limits", () => {
33
+ expect(() => (field.int() as { max(n: number): unknown }).max(5)).toThrow(/field.string/);
34
+ expect(() => field.json().max(5)).toThrow(/field.string/);
35
+ expect(() => field.string().max(0)).toThrow(/positive integer/);
36
+ expect(() => field.string().max(1.5)).toThrow(/positive integer/);
37
+ expect(() => field.string().max(-1)).toThrow(/positive integer/);
38
+ expect(() => field.string().max(1)).not.toThrow();
39
+ });
package/src/index.ts CHANGED
@@ -101,6 +101,14 @@ export interface FieldDefinition {
101
101
  * `serverOnly`, which hides a field from ALL client surfaces.
102
102
  */
103
103
  syncOmit?: boolean;
104
+ /**
105
+ * Longest value the field accepts, in characters (Unicode code
106
+ * points). Set with `field.string().max(n)` or
107
+ * `field.richtext().max(n)`. The server rejects a longer value with
108
+ * `FIELD_TOO_LONG` on every write path: the entity API, sync push,
109
+ * `ctx.db` in functions, and collaborative (CRDT) edits.
110
+ */
111
+ maxLength?: number;
104
112
  /**
105
113
  * When true, the field is **set on insert but cannot be changed
106
114
  * by client updates**. The framework rejects any `PATCH`/`PUT`
@@ -190,6 +198,15 @@ interface FieldBuilder {
190
198
  * editor still gets it via `db.get("Variant", id)`.
191
199
  */
192
200
  syncOmit(): FieldBuilder;
201
+ /**
202
+ * Cap the value at `n` characters (Unicode code points). String and
203
+ * richtext fields only. See [`FieldDefinition.maxLength`].
204
+ *
205
+ * Example: `bio: field.string().max(280)` stops any client, guests
206
+ * included, from storing a longer bio that every other client would
207
+ * then sync.
208
+ */
209
+ max(n: number): FieldBuilder;
193
210
  /**
194
211
  * Mark the field as set-on-insert-only. See [`FieldDefinition.readonly`]
195
212
  * for the full semantics.
@@ -273,6 +290,17 @@ function buildField(def: FieldDefinition): FieldBuilder {
273
290
  syncOmit() {
274
291
  return buildField({ ...def, syncOmit: true });
275
292
  },
293
+ max(n: number) {
294
+ if (def.type !== "string" && def.type !== "richtext") {
295
+ throw new Error(
296
+ `.max(n) applies to field.string() and field.richtext(), not ${def.type}`,
297
+ );
298
+ }
299
+ if (!Number.isInteger(n) || n < 1 || n > 0xffffffff) {
300
+ throw new Error(`.max(n): n must be a positive integer, got ${n}`);
301
+ }
302
+ return buildField({ ...def, maxLength: n });
303
+ },
276
304
  readonly() {
277
305
  return buildField({ ...def, readonly: true });
278
306
  },
@@ -431,6 +459,18 @@ export interface EntityDefinition {
431
459
  * can read" and "nothing".
432
460
  */
433
461
  sync?: boolean | SyncScope;
462
+ /**
463
+ * Record every insert, update, and delete in the audit log: actor,
464
+ * tenant, row id, and the names of changed fields (not their values).
465
+ * Read it with `ctx.audit.list({ entity })` or `GET /api/admin/audit`.
466
+ */
467
+ audit?: boolean;
468
+ /**
469
+ * Delete rows once they pass a retention period. A system job runs
470
+ * hourly; each deletion goes through the entity pipeline and is recorded
471
+ * in the audit log as `retention.delete`. See {@link RetentionRule}.
472
+ */
473
+ retention?: RetentionRule;
434
474
  /**
435
475
  * Per-row CRDT (Loro) documents. Default `true`: every write merges through
436
476
  * a document, and offline edits from several devices converge. Set `false`
@@ -475,6 +515,25 @@ export interface SyncScope {
475
515
  limit?: number;
476
516
  }
477
517
 
518
+ /**
519
+ * Retention rule for an entity.
520
+ *
521
+ * - `{ field: "createdAt", after: "365d" }` deletes a row once `createdAt`
522
+ * is older than 365 days. Durations: `s`, `m`, `h`, `d`, `w`, `y`.
523
+ * - `{ field: "deleteAfter" }` (no `after`) deletes a row once
524
+ * `deleteAfter` is in the past. Use it for a retention period per tenant:
525
+ * set `deleteAfter` on each row at insert.
526
+ *
527
+ * `field` is a `datetime` field, or an `int`/`float` field holding unix
528
+ * milliseconds. Rows whose `hold` field (a `bool`) is true are kept, for
529
+ * example under a legal hold.
530
+ */
531
+ export interface RetentionRule {
532
+ field: string;
533
+ after?: string;
534
+ hold?: string;
535
+ }
536
+
478
537
  export function entity(
479
538
  name: string,
480
539
  fields: Record<string, FieldBuilder>,
@@ -489,6 +548,12 @@ export function entity(
489
548
  /** `false` for server-written rows that need no CRDT document. Mirrors
490
549
  * {@link EntityDefinition.crdt}. */
491
550
  crdt?: boolean;
551
+ /** Record every write in the audit log. Mirrors
552
+ * {@link EntityDefinition.audit}. */
553
+ audit?: boolean;
554
+ /** Delete rows past a retention period. Mirrors
555
+ * {@link EntityDefinition.retention}. */
556
+ retention?: RetentionRule;
492
557
  },
493
558
  ): EntityDefinition {
494
559
  return {
@@ -499,6 +564,8 @@ export function entity(
499
564
  search: options?.search,
500
565
  sync: options?.sync,
501
566
  crdt: options?.crdt,
567
+ audit: options?.audit,
568
+ retention: options?.retention,
502
569
  };
503
570
  }
504
571
 
@@ -676,6 +743,9 @@ export interface ManifestField {
676
743
  /** Set when the field is `field.X().syncOmit()` — see
677
744
  * [`FieldDefinition.syncOmit`]. Omitted by default. */
678
745
  syncOmit?: boolean;
746
+ /** Set by `.max(n)` — see [`FieldDefinition.maxLength`]. Omitted when
747
+ * the field has no limit. */
748
+ maxLength?: number;
679
749
  /** Set when the field is `field.X().readonly()` — see
680
750
  * [`FieldDefinition.readonly`]. Omitted by default. */
681
751
  readonly?: boolean;
@@ -743,6 +813,10 @@ export interface ManifestEntity {
743
813
  sync_limit?: number;
744
814
  /** CRDT mode; omitted when true (the runtime default). */
745
815
  crdt?: boolean;
816
+ /** Audit every write; omitted when false. */
817
+ audit?: boolean;
818
+ /** Retention rule; omitted when unset. */
819
+ retention?: RetentionRule;
746
820
  }
747
821
 
748
822
  export interface ManifestRoute {
@@ -1197,6 +1271,9 @@ export function entitiesToManifest(
1197
1271
  if (fb._def.syncOmit) {
1198
1272
  f.syncOmit = true;
1199
1273
  }
1274
+ if (fb._def.maxLength !== undefined) {
1275
+ f.maxLength = fb._def.maxLength;
1276
+ }
1200
1277
  if (fb._def.readonly) {
1201
1278
  f.readonly = true;
1202
1279
  }
@@ -1274,6 +1351,16 @@ export function entitiesToManifest(
1274
1351
  if (e.crdt === false) {
1275
1352
  result.crdt = false;
1276
1353
  }
1354
+ if (e.audit === true) {
1355
+ result.audit = true;
1356
+ }
1357
+ if (e.retention) {
1358
+ result.retention = {
1359
+ field: e.retention.field,
1360
+ ...(e.retention.after ? { after: e.retention.after } : {}),
1361
+ ...(e.retention.hold ? { hold: e.retention.hold } : {}),
1362
+ };
1363
+ }
1277
1364
  return result;
1278
1365
  });
1279
1366
  }
@@ -2274,6 +2361,9 @@ function agentEntities(): EntityDefinition[] {
2274
2361
  // Cumulative model↔tool round-trips across every invocation of
2275
2362
  // this run. `maxSteps` bounds one invocation, not this.
2276
2363
  steps: field.int().optional(),
2364
+ // What server code passed as `context` to ctx.agents.run; the
2365
+ // tools receive it as `run.context`. Written by server code only.
2366
+ context: field.json().optional(),
2277
2367
  createdAt: field.datetime(),
2278
2368
  updatedAt: field.datetime(),
2279
2369
  },