@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 +60 -0
- package/package.json +1 -1
- package/src/field-max-length.test.ts +39 -0
- package/src/index.ts +90 -0
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
|
@@ -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
|
},
|