@pylonsync/sdk 0.22.43 → 0.22.44

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
@@ -367,6 +367,11 @@ export interface EntityDefinition {
367
367
  * in the audit log as `retention.delete`. See {@link RetentionRule}.
368
368
  */
369
369
  retention?: RetentionRule;
370
+ /**
371
+ * Run an internal function after edits to a row land. See
372
+ * {@link AfterEditHook}.
373
+ */
374
+ afterEdit?: AfterEditHook;
370
375
  /**
371
376
  * Per-row CRDT (Loro) documents. Default `true`: every write merges through
372
377
  * a document, and offline edits from several devices converge. Set `false`
@@ -427,6 +432,27 @@ export interface RetentionRule {
427
432
  after?: string;
428
433
  hold?: string;
429
434
  }
435
+ /**
436
+ * An internal function that runs after edits to a row land: a search index,
437
+ * a preview, a review queue.
438
+ *
439
+ * ```ts
440
+ * entity("Document", { … }, { afterEdit: { fn: "indexDocument", delaySeconds: 5 } })
441
+ * ```
442
+ *
443
+ * Any committed write to a row (an insert, an update, a live edit, a
444
+ * delete) schedules `fn` with `{ id }`, to run at the end of the row's
445
+ * current `delaySeconds` window; every edit in that window shares the run.
446
+ * A row edited without pause gets one run per window. `fn` must be
447
+ * `internal: true`; it runs without a user, like a cron target, and reads
448
+ * the row itself (it may have been deleted). Runs are jobs: they survive a
449
+ * restart, retry on failure, and show in `pylon jobs`.
450
+ */
451
+ export interface AfterEditHook {
452
+ fn: string;
453
+ /** The window, in seconds. Default 5. */
454
+ delaySeconds?: number;
455
+ }
430
456
  export declare function entity(name: string, fields: Record<string, FieldBuilder>, options?: {
431
457
  indexes?: IndexDefinition[];
432
458
  relations?: RelationDefinition[];
@@ -444,6 +470,9 @@ export declare function entity(name: string, fields: Record<string, FieldBuilder
444
470
  /** Delete rows past a retention period. Mirrors
445
471
  * {@link EntityDefinition.retention}. */
446
472
  retention?: RetentionRule;
473
+ /** Run an internal function after edits land. Mirrors
474
+ * {@link EntityDefinition.afterEdit}. */
475
+ afterEdit?: AfterEditHook;
447
476
  }): EntityDefinition;
448
477
  export declare function relation(def: RelationDefinition): RelationDefinition;
449
478
  export { defineRoute } from "./route";
@@ -635,6 +664,11 @@ export interface ManifestEntity {
635
664
  audit?: boolean;
636
665
  /** Retention rule; omitted when unset. */
637
666
  retention?: RetentionRule;
667
+ /** After-edit hook; omitted when unset. */
668
+ after_edit?: {
669
+ function: string;
670
+ delay_secs: number;
671
+ };
638
672
  }
639
673
  export interface ManifestRoute {
640
674
  path: string;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.22.43",
6
+ "version": "0.22.44",
7
7
  "type": "module",
8
8
  "main": "./src/index.ts",
9
9
  "types": "./dist/index.d.ts",
@@ -0,0 +1,29 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { entitiesToManifest, entity, field } from "./index";
3
+
4
+ describe("entity afterEdit option", () => {
5
+ test("reaches the manifest as after_edit, with the default window", () => {
6
+ const [a, b] = entitiesToManifest([
7
+ entity("Doc", { title: field.string() }, { afterEdit: { fn: "indexDoc" } }),
8
+ entity("Note", { title: field.string() }, { afterEdit: { fn: "indexNote", delaySeconds: 30 } }),
9
+ ]);
10
+ expect(a.after_edit).toEqual({ function: "indexDoc", delay_secs: 5 });
11
+ expect(b.after_edit).toEqual({ function: "indexNote", delay_secs: 30 });
12
+ });
13
+
14
+ test("is omitted when unset", () => {
15
+ const [m] = entitiesToManifest([entity("Doc", { title: field.string() })]);
16
+ expect("after_edit" in m).toBe(false);
17
+ });
18
+
19
+ test("refuses a window that is not a whole number of seconds, and a missing function", () => {
20
+ for (const delaySeconds of [0, 1.5, -1]) {
21
+ expect(() =>
22
+ entitiesToManifest([entity("Doc", { t: field.string() }, { afterEdit: { fn: "f", delaySeconds } })]),
23
+ ).toThrow("whole number of seconds");
24
+ }
25
+ expect(() => entitiesToManifest([entity("Doc", { t: field.string() }, { afterEdit: { fn: "" } })])).toThrow(
26
+ "afterEdit needs fn",
27
+ );
28
+ });
29
+ });
package/src/index.ts CHANGED
@@ -476,6 +476,11 @@ export interface EntityDefinition {
476
476
  * in the audit log as `retention.delete`. See {@link RetentionRule}.
477
477
  */
478
478
  retention?: RetentionRule;
479
+ /**
480
+ * Run an internal function after edits to a row land. See
481
+ * {@link AfterEditHook}.
482
+ */
483
+ afterEdit?: AfterEditHook;
479
484
  /**
480
485
  * Per-row CRDT (Loro) documents. Default `true`: every write merges through
481
486
  * a document, and offline edits from several devices converge. Set `false`
@@ -539,6 +544,28 @@ export interface RetentionRule {
539
544
  hold?: string;
540
545
  }
541
546
 
547
+ /**
548
+ * An internal function that runs after edits to a row land: a search index,
549
+ * a preview, a review queue.
550
+ *
551
+ * ```ts
552
+ * entity("Document", { … }, { afterEdit: { fn: "indexDocument", delaySeconds: 5 } })
553
+ * ```
554
+ *
555
+ * Any committed write to a row (an insert, an update, a live edit, a
556
+ * delete) schedules `fn` with `{ id }`, to run at the end of the row's
557
+ * current `delaySeconds` window; every edit in that window shares the run.
558
+ * A row edited without pause gets one run per window. `fn` must be
559
+ * `internal: true`; it runs without a user, like a cron target, and reads
560
+ * the row itself (it may have been deleted). Runs are jobs: they survive a
561
+ * restart, retry on failure, and show in `pylon jobs`.
562
+ */
563
+ export interface AfterEditHook {
564
+ fn: string;
565
+ /** The window, in seconds. Default 5. */
566
+ delaySeconds?: number;
567
+ }
568
+
542
569
  export function entity(
543
570
  name: string,
544
571
  fields: Record<string, FieldBuilder>,
@@ -559,6 +586,9 @@ export function entity(
559
586
  /** Delete rows past a retention period. Mirrors
560
587
  * {@link EntityDefinition.retention}. */
561
588
  retention?: RetentionRule;
589
+ /** Run an internal function after edits land. Mirrors
590
+ * {@link EntityDefinition.afterEdit}. */
591
+ afterEdit?: AfterEditHook;
562
592
  },
563
593
  ): EntityDefinition {
564
594
  return {
@@ -571,6 +601,7 @@ export function entity(
571
601
  crdt: options?.crdt,
572
602
  audit: options?.audit,
573
603
  retention: options?.retention,
604
+ afterEdit: options?.afterEdit,
574
605
  };
575
606
  }
576
607
 
@@ -822,6 +853,8 @@ export interface ManifestEntity {
822
853
  audit?: boolean;
823
854
  /** Retention rule; omitted when unset. */
824
855
  retention?: RetentionRule;
856
+ /** After-edit hook; omitted when unset. */
857
+ after_edit?: { function: string; delay_secs: number };
825
858
  }
826
859
 
827
860
  export interface ManifestRoute {
@@ -1376,6 +1409,14 @@ export function entitiesToManifest(
1376
1409
  ...(e.retention.hold ? { hold: e.retention.hold } : {}),
1377
1410
  };
1378
1411
  }
1412
+ if (e.afterEdit) {
1413
+ const delay = e.afterEdit.delaySeconds ?? 5;
1414
+ if (!e.afterEdit.fn) throw new Error(`${e.name}: afterEdit needs fn`);
1415
+ if (!Number.isInteger(delay) || delay < 1) {
1416
+ throw new Error(`${e.name}: afterEdit.delaySeconds must be a whole number of seconds, at least 1`);
1417
+ }
1418
+ result.after_edit = { function: e.afterEdit.fn, delay_secs: delay };
1419
+ }
1379
1420
  return result;
1380
1421
  });
1381
1422
  }