@pylonsync/sdk 0.4.19 → 0.4.20

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
@@ -1,5 +1,5 @@
1
1
  export type RouteMode = "static" | "server" | "live" | "ssr";
2
- export type FieldType = "string" | "int" | "float" | "bool" | "datetime" | "richtext" | "json" | `id(${string})`;
2
+ export type FieldType = "string" | "int" | "float" | "bool" | "datetime" | "richtext" | "json" | `id(${string})` | `vector(${number})`;
3
3
  /**
4
4
  * CRDT container override for a field. Wire format is the kebab-case
5
5
  * string each variant maps to (`"text"`, `"counter"`, `"movable-list"`,
@@ -243,6 +243,19 @@ export declare const field: {
243
243
  */
244
244
  json: () => FieldBuilder;
245
245
  id: (target: string) => FieldBuilder;
246
+ /**
247
+ * `field.vector(1536)` — a fixed-dimension embedding, written as
248
+ * `number[]` and stored as a packed float array (BLOB in SQLite,
249
+ * BYTEA in Postgres). Query it with `ctx.db.vectorSearch(entity,
250
+ * { field, vector, limit })` — exact nearest-neighbor, cosine by
251
+ * default.
252
+ *
253
+ * Vector fields are always server-only: a 1536-dim embedding is
254
+ * 6KB+ per row and belongs to server-side retrieval, not client
255
+ * replicas or HTTP responses. Inserts/updates validate the exact
256
+ * dimension count and reject NaN/Infinity elements.
257
+ */
258
+ vector: (dims: number) => FieldBuilder;
246
259
  /**
247
260
  * `field.enum(["pending", "paid", "failed"])` — stored as a string with
248
261
  * allowed-values metadata so codegen emits a precise literal union
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.4.19",
6
+ "version": "0.4.20",
7
7
  "type": "module",
8
8
  "main": "./src/index.ts",
9
9
  "types": "./dist/index.d.ts",
package/src/index.ts CHANGED
@@ -16,7 +16,8 @@ export type FieldType =
16
16
  | "datetime"
17
17
  | "richtext"
18
18
  | "json"
19
- | `id(${string})`;
19
+ | `id(${string})`
20
+ | `vector(${number})`;
20
21
 
21
22
  // ---------------------------------------------------------------------------
22
23
  // Field builder
@@ -318,6 +319,26 @@ export const field = {
318
319
  */
319
320
  json: () => createFieldBuilder("json"),
320
321
  id: (target: string) => createFieldBuilder(`id(${target})`),
322
+ /**
323
+ * `field.vector(1536)` — a fixed-dimension embedding, written as
324
+ * `number[]` and stored as a packed float array (BLOB in SQLite,
325
+ * BYTEA in Postgres). Query it with `ctx.db.vectorSearch(entity,
326
+ * { field, vector, limit })` — exact nearest-neighbor, cosine by
327
+ * default.
328
+ *
329
+ * Vector fields are always server-only: a 1536-dim embedding is
330
+ * 6KB+ per row and belongs to server-side retrieval, not client
331
+ * replicas or HTTP responses. Inserts/updates validate the exact
332
+ * dimension count and reject NaN/Infinity elements.
333
+ */
334
+ vector: (dims: number) => {
335
+ if (!Number.isInteger(dims) || dims < 1 || dims > 8192) {
336
+ throw new Error(
337
+ `field.vector(dims): dims must be an integer in 1..8192, got ${dims}`
338
+ );
339
+ }
340
+ return createFieldBuilder(`vector(${dims})`);
341
+ },
321
342
  /**
322
343
  * `field.enum(["pending", "paid", "failed"])` — stored as a string with
323
344
  * allowed-values metadata so codegen emits a precise literal union
@@ -970,7 +991,9 @@ export function entitiesToManifest(
970
991
  if (fb._def.crdt !== undefined) {
971
992
  f.crdt = fb._def.crdt;
972
993
  }
973
- if (fb._def.serverOnly) {
994
+ if (fb._def.serverOnly || fb._def.type.startsWith("vector(")) {
995
+ // Vector fields are unconditionally server-only: multi-KB
996
+ // embeddings must never ride the sync stream or HTTP reads.
974
997
  f.serverOnly = true;
975
998
  }
976
999
  if (fb._def.syncOmit) {
@@ -0,0 +1,36 @@
1
+ /**
2
+ * `field.vector(dims)` manifest contract:
3
+ * - the wire type string is `vector(<dims>)`
4
+ * - vector fields serialize with `serverOnly: true` unconditionally
5
+ * (multi-KB embeddings must never enter sync or HTTP reads)
6
+ * - dims outside 1..8192 throw at schema-definition time
7
+ */
8
+ import { expect, test } from "bun:test";
9
+ import { entitiesToManifest, field } from "./index";
10
+
11
+ test("field.vector serializes as vector(dims) with forced serverOnly", () => {
12
+ const [ent] = entitiesToManifest([
13
+ {
14
+ name: "Doc",
15
+ fields: {
16
+ title: field.string(),
17
+ embedding: field.vector(1536).optional(),
18
+ },
19
+ },
20
+ ]);
21
+ const emb = ent.fields.find((f) => f.name === "embedding")!;
22
+ expect(emb.type).toBe("vector(1536)");
23
+ expect(emb.optional).toBe(true);
24
+ expect(emb.serverOnly).toBe(true);
25
+ // Non-vector fields don't inherit the forcing.
26
+ const title = ent.fields.find((f) => f.name === "title")!;
27
+ expect(title.serverOnly).toBeUndefined();
28
+ });
29
+
30
+ test("field.vector rejects out-of-range dims", () => {
31
+ expect(() => field.vector(0)).toThrow();
32
+ expect(() => field.vector(1.5)).toThrow();
33
+ expect(() => field.vector(8193)).toThrow();
34
+ expect(() => field.vector(8192)).not.toThrow();
35
+ expect(() => field.vector(1)).not.toThrow();
36
+ });