@pylonsync/sdk 0.3.167 → 0.3.169

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +112 -14
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.167",
6
+ "version": "0.3.169",
7
7
  "type": "module",
8
8
  "main": "src/index.ts",
9
9
  "types": "src/index.ts",
package/src/index.ts CHANGED
@@ -325,7 +325,12 @@ export function action(
325
325
  // ---------------------------------------------------------------------------
326
326
 
327
327
  export interface PolicyDefinition {
328
- name: string;
328
+ /** Optional — `buildManifest` auto-generates a name from the entity
329
+ * + a counter when the fluent `.policies(policy({...}))` chain
330
+ * omits one. Explicit names are still recommended for the
331
+ * procedural API since they appear in policy-denied error
332
+ * messages. */
333
+ name?: string;
329
334
  entity?: string;
330
335
  action?: string;
331
336
  /**
@@ -390,6 +395,16 @@ export interface ManifestField {
390
395
  /** Set when the field is `field.X().readonly()` — see
391
396
  * [`FieldDefinition.readonly`]. Omitted by default. */
392
397
  readonly?: boolean;
398
+ /** Default value to fill on insert when the row omits this field.
399
+ * - `"now"` → runtime stamps the current UTC time
400
+ * - any literal → runtime stamps that exact value
401
+ * Maps to `field.X().defaultNow()` / `.default(value)`. */
402
+ default?: "now" | string | number | boolean | null;
403
+ /** Allowed values for `field.enum([...])` — recorded so codegen
404
+ * can emit a literal-union type and runtime validation can
405
+ * reject out-of-set inserts. Plain `field.string()` doesn't
406
+ * carry this; only `field.enum()`. */
407
+ enumValues?: readonly string[];
393
408
  }
394
409
 
395
410
  export interface ManifestIndex {
@@ -499,6 +514,24 @@ export function entitiesToManifest(
499
514
  if (fb._def.readonly) {
500
515
  f.readonly = true;
501
516
  }
517
+ // `default` + `enumValues` are surfaced on the fluent
518
+ // FieldBuilder via the v0.4 SDK. Read off the private
519
+ // backing slot so both APIs serialize identically — apps
520
+ // using the procedural `field` exports get the same
521
+ // ManifestField shape as fluent apps.
522
+ const extra = fb._def as FieldDefinition & {
523
+ default?: { kind: "value"; value: unknown } | { kind: "now" };
524
+ enumValues?: readonly string[];
525
+ };
526
+ if (extra.default) {
527
+ f.default =
528
+ extra.default.kind === "now"
529
+ ? "now"
530
+ : (extra.default.value as ManifestField["default"]);
531
+ }
532
+ if (extra.enumValues && extra.enumValues.length > 0) {
533
+ f.enumValues = extra.enumValues;
534
+ }
502
535
  return f;
503
536
  }),
504
537
  indexes: (e.indexes ?? []).map((idx) => ({
@@ -580,8 +613,18 @@ export function actionsToManifest(
580
613
  export function policiesToManifest(
581
614
  policies: PolicyDefinition[]
582
615
  ): ManifestPolicy[] {
583
- return policies.map((p) => {
584
- const result: ManifestPolicy = { name: p.name };
616
+ return policies.map((p, i) => {
617
+ const result: ManifestPolicy = {
618
+ // Final-resort name autogen. `buildManifest` upstream already
619
+ // names every attached-via-fluent policy, but a caller passing
620
+ // `policies: [policy({ allowRead: "..." })]` to a custom
621
+ // manifest builder would slip through with `name: undefined`.
622
+ // Stamp a unique fallback so the runtime never sees a blank.
623
+ name:
624
+ p.name && p.name.length > 0
625
+ ? p.name
626
+ : `${(p.entity ?? p.action ?? "unnamed").toLowerCase()}_p${i}`,
627
+ };
585
628
  if (p.allow) result.allow = p.allow;
586
629
  if (p.allowRead) result.allowRead = p.allowRead;
587
630
  if (p.allowInsert) result.allowInsert = p.allowInsert;
@@ -759,6 +802,33 @@ export function buildManifest(options: {
759
802
  policies?: PolicyDefinition[];
760
803
  auth?: ManifestAuthConfig;
761
804
  }): AppManifest {
805
+ // Pull policies attached via the fluent `e.entity().policies(...)`
806
+ // chain onto the top-level policies list. Without this, fluent
807
+ // apps would register entities without policies and every read
808
+ // would default-deny. Existing apps using the procedural API
809
+ // (`entity()` + separate `policy({...})` exports) are unaffected
810
+ // because `extractAttachedPolicies` returns an empty array for
811
+ // them. Concat order: top-level policies first (explicit beats
812
+ // attached), then anything pulled off entities.
813
+ //
814
+ // Stamp a name if the fluent caller omitted it — `name` is
815
+ // technically required on PolicyDefinition but the docs imply you
816
+ // can `.policies(policy({ allowRead: "..." }))` without one. Auto-
817
+ // derive from the entity + a counter so two attached policies
818
+ // don't collide.
819
+ const attached: PolicyDefinition[] = [];
820
+ for (const ent of options.entities) {
821
+ const extracted = extractAttachedPolicies(ent);
822
+ extracted.forEach((p, i) => {
823
+ attached.push({
824
+ ...p,
825
+ name: p.name && p.name.length > 0
826
+ ? p.name
827
+ : `${(p.entity ?? ent.name).toLowerCase()}_attached_${i}`,
828
+ });
829
+ });
830
+ }
831
+ const allPolicies = [...(options.policies ?? []), ...attached];
762
832
  return {
763
833
  manifest_version: MANIFEST_VERSION,
764
834
  name: options.name,
@@ -767,7 +837,7 @@ export function buildManifest(options: {
767
837
  routes: routesToManifest(options.routes),
768
838
  queries: queriesToManifest(options.queries ?? []),
769
839
  actions: actionsToManifest(options.actions ?? []),
770
- policies: policiesToManifest(options.policies ?? []),
840
+ policies: policiesToManifest(allPolicies),
771
841
  auth: options.auth ?? auth(),
772
842
  };
773
843
  }
@@ -943,19 +1013,47 @@ declare module "./index" {
943
1013
  // surface declared above.)
944
1014
  }
945
1015
 
946
- // Patch the existing buildField to add default markers. Apps reach
947
- // the chainables through any returned FieldBuilder — `.default(val)`
948
- // records a literal, `.defaultNow()` records the `now` marker.
949
- const __originalBuildField = buildField;
950
- function buildFieldWithDefaults(def: FieldDefinition & { default?: DefaultMarker }): FieldBuilder & {
951
- default(value: unknown): FieldBuilder;
952
- defaultNow(): FieldBuilder;
1016
+ // Field builder with chainable `.default()` / `.defaultNow()`. All
1017
+ // other chainables (`optional`, `unique`, `crdt`, `serverOnly`,
1018
+ // `readonly`) are reimplemented here to return another
1019
+ // `buildFieldWithDefaults` without this, calling `.optional()`
1020
+ // after `.default()` would drop the default markers off the chain
1021
+ // (codex Wave-3 review: the previous `{ ...base, default, defaultNow }`
1022
+ // pattern delegated optional/unique to the original buildField which
1023
+ // returned a builder lacking `.default()`). Recursion through the
1024
+ // same constructor keeps the surface stable regardless of chain
1025
+ // order.
1026
+ function buildFieldWithDefaults(
1027
+ def: FieldDefinition & {
1028
+ default?: DefaultMarker;
1029
+ enumValues?: readonly string[];
1030
+ },
1031
+ ): FieldBuilder & {
1032
+ default(value: unknown): ReturnType<typeof buildFieldWithDefaults>;
1033
+ defaultNow(): ReturnType<typeof buildFieldWithDefaults>;
953
1034
  } {
954
- const base = __originalBuildField(def);
955
1035
  return {
956
- ...base,
1036
+ _def: def,
1037
+ optional() {
1038
+ return buildFieldWithDefaults({ ...def, optional: true });
1039
+ },
1040
+ unique() {
1041
+ return buildFieldWithDefaults({ ...def, unique: true });
1042
+ },
1043
+ crdt(annotation) {
1044
+ return buildFieldWithDefaults({ ...def, crdt: annotation });
1045
+ },
1046
+ serverOnly() {
1047
+ return buildFieldWithDefaults({ ...def, serverOnly: true });
1048
+ },
1049
+ readonly() {
1050
+ return buildFieldWithDefaults({ ...def, readonly: true });
1051
+ },
957
1052
  default(value: unknown) {
958
- return buildFieldWithDefaults({ ...def, default: { kind: "value", value } });
1053
+ return buildFieldWithDefaults({
1054
+ ...def,
1055
+ default: { kind: "value", value },
1056
+ });
959
1057
  },
960
1058
  defaultNow() {
961
1059
  return buildFieldWithDefaults({ ...def, default: { kind: "now" } });