@pylonsync/sdk 0.21.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.
@@ -543,6 +560,9 @@ export interface ManifestField {
543
560
  /** Set when the field is `field.X().syncOmit()` — see
544
561
  * [`FieldDefinition.syncOmit`]. Omitted by default. */
545
562
  syncOmit?: boolean;
563
+ /** Set by `.max(n)` — see [`FieldDefinition.maxLength`]. Omitted when
564
+ * the field has no limit. */
565
+ maxLength?: number;
546
566
  /** Set when the field is `field.X().readonly()` — see
547
567
  * [`FieldDefinition.readonly`]. Omitted by default. */
548
568
  readonly?: boolean;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.21.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
  },
@@ -715,6 +743,9 @@ export interface ManifestField {
715
743
  /** Set when the field is `field.X().syncOmit()` — see
716
744
  * [`FieldDefinition.syncOmit`]. Omitted by default. */
717
745
  syncOmit?: boolean;
746
+ /** Set by `.max(n)` — see [`FieldDefinition.maxLength`]. Omitted when
747
+ * the field has no limit. */
748
+ maxLength?: number;
718
749
  /** Set when the field is `field.X().readonly()` — see
719
750
  * [`FieldDefinition.readonly`]. Omitted by default. */
720
751
  readonly?: boolean;
@@ -1240,6 +1271,9 @@ export function entitiesToManifest(
1240
1271
  if (fb._def.syncOmit) {
1241
1272
  f.syncOmit = true;
1242
1273
  }
1274
+ if (fb._def.maxLength !== undefined) {
1275
+ f.maxLength = fb._def.maxLength;
1276
+ }
1243
1277
  if (fb._def.readonly) {
1244
1278
  f.readonly = true;
1245
1279
  }
@@ -2327,6 +2361,9 @@ function agentEntities(): EntityDefinition[] {
2327
2361
  // Cumulative model↔tool round-trips across every invocation of
2328
2362
  // this run. `maxSteps` bounds one invocation, not this.
2329
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(),
2330
2367
  createdAt: field.datetime(),
2331
2368
  updatedAt: field.datetime(),
2332
2369
  },