@pylonsync/sdk 0.3.166 → 0.3.167
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/package.json +1 -1
- package/src/index.ts +301 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -816,3 +816,304 @@ export {
|
|
|
816
816
|
type StudioPageProps,
|
|
817
817
|
type StudioExtensions,
|
|
818
818
|
} from "./studio";
|
|
819
|
+
|
|
820
|
+
// ---------------------------------------------------------------------------
|
|
821
|
+
// Fluent schema API (`e` namespace)
|
|
822
|
+
//
|
|
823
|
+
// `entity(name, fields, options)` + `field.*` + `policy({...})` are the
|
|
824
|
+
// stable foundation — every dependent app uses them today. The fluent
|
|
825
|
+
// API below sits on top and compiles down to the same EntityDefinition
|
|
826
|
+
// + PolicyDefinition shapes the runtime already understands, so both
|
|
827
|
+
// styles can coexist forever. The fluent shape just reads better in
|
|
828
|
+
// docs + marketing snippets:
|
|
829
|
+
//
|
|
830
|
+
// ```ts
|
|
831
|
+
// export const Order = e.entity("Order", {
|
|
832
|
+
// customer: field.id("Customer"),
|
|
833
|
+
// total: field.int(),
|
|
834
|
+
// status: field.enum(["pending", "paid", "failed"]),
|
|
835
|
+
// createdAt: field.datetime().defaultNow(),
|
|
836
|
+
// })
|
|
837
|
+
// .indexes(e.idx("customer", "createdAt"), e.idx("status"))
|
|
838
|
+
// .policies(policy({
|
|
839
|
+
// allowRead: "auth.userId == data.customer || auth.hasRole('admin')",
|
|
840
|
+
// allowUpdate: "auth.hasRole('admin')",
|
|
841
|
+
// }))
|
|
842
|
+
// .behaviors([timestamps, softDelete]);
|
|
843
|
+
// ```
|
|
844
|
+
//
|
|
845
|
+
// Behaviors are field-injection helpers. `timestamps` adds
|
|
846
|
+
// `createdAt` + `updatedAt` to the entity fields; `softDelete` adds
|
|
847
|
+
// `deletedAt`. They mutate the EntityDefinition's fields before the
|
|
848
|
+
// runtime sees it, so the rest of the framework (storage, sync,
|
|
849
|
+
// policy gates) treats them as ordinary columns. Auto-stamping
|
|
850
|
+
// (filling `now()` on insert/update) needs runtime support — landing
|
|
851
|
+
// in a follow-up patch via the `defaultExpr: "now"` marker the
|
|
852
|
+
// builder records.
|
|
853
|
+
// ---------------------------------------------------------------------------
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Behavior — a function that mutates the entity definition before it's
|
|
857
|
+
* registered. Implementations should be idempotent (the user can list
|
|
858
|
+
* the same behavior twice without breaking the schema).
|
|
859
|
+
*/
|
|
860
|
+
export interface Behavior {
|
|
861
|
+
/** Stable identifier — surfaced in the manifest for tooling, lets a
|
|
862
|
+
* pass-through inspector see which behaviors are active. */
|
|
863
|
+
readonly id: string;
|
|
864
|
+
apply(def: EntityDefinition): EntityDefinition;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* `timestamps` — auto-add `createdAt` + `updatedAt` datetime fields.
|
|
869
|
+
* The `defaultNow()` marker on each tells the runtime to fill `now()`
|
|
870
|
+
* on insert (and on update for `updatedAt`). Wiring lands with the
|
|
871
|
+
* runtime patch — until then, app code can still set the values
|
|
872
|
+
* manually and the fields exist on the row.
|
|
873
|
+
*/
|
|
874
|
+
export const timestamps: Behavior = {
|
|
875
|
+
id: "timestamps",
|
|
876
|
+
apply(def) {
|
|
877
|
+
const fields = { ...def.fields };
|
|
878
|
+
if (!fields.createdAt) {
|
|
879
|
+
fields.createdAt = (field.datetime() as FieldBuilder & {
|
|
880
|
+
defaultNow?: () => FieldBuilder;
|
|
881
|
+
}).defaultNow?.() ?? field.datetime();
|
|
882
|
+
}
|
|
883
|
+
if (!fields.updatedAt) {
|
|
884
|
+
fields.updatedAt = (field.datetime() as FieldBuilder & {
|
|
885
|
+
defaultNow?: () => FieldBuilder;
|
|
886
|
+
updateOnWrite?: () => FieldBuilder;
|
|
887
|
+
}).defaultNow?.() ?? field.datetime();
|
|
888
|
+
}
|
|
889
|
+
return { ...def, fields };
|
|
890
|
+
},
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* `softDelete` — auto-add a nullable `deletedAt` datetime field.
|
|
895
|
+
* Rows with `deletedAt != null` are filtered from default reads
|
|
896
|
+
* (TS-side filtering today; runtime filter lands in the follow-up).
|
|
897
|
+
*/
|
|
898
|
+
export const softDelete: Behavior = {
|
|
899
|
+
id: "softDelete",
|
|
900
|
+
apply(def) {
|
|
901
|
+
const fields = { ...def.fields };
|
|
902
|
+
if (!fields.deletedAt) {
|
|
903
|
+
fields.deletedAt = field.datetime().optional();
|
|
904
|
+
}
|
|
905
|
+
return { ...def, fields };
|
|
906
|
+
},
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* `audit` — marker behavior. Tags the entity for the framework's
|
|
911
|
+
* audit pipeline (writes an `AuditEvent` row per mutation, recording
|
|
912
|
+
* the actor + diff). Runtime hook lands in a follow-up patch — for
|
|
913
|
+
* now the marker is preserved on the manifest so apps can opt in
|
|
914
|
+
* early without breaking later.
|
|
915
|
+
*/
|
|
916
|
+
export const audit: Behavior = {
|
|
917
|
+
id: "audit",
|
|
918
|
+
apply(def) {
|
|
919
|
+
// No field injection today. The behavior flag is recorded via
|
|
920
|
+
// the EntityBuilder's `_behaviors` list (read off on serialize)
|
|
921
|
+
// so the runtime can pick it up once the audit pipeline lands.
|
|
922
|
+
return def;
|
|
923
|
+
},
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Internal sentinel — apps don't construct these directly. The
|
|
928
|
+
* `field.X` builders gain `default(val)` / `defaultNow()` chainables
|
|
929
|
+
* below; this is just the shape stored on the field definition for
|
|
930
|
+
* the runtime to read.
|
|
931
|
+
*/
|
|
932
|
+
type DefaultMarker = { kind: "value"; value: unknown } | { kind: "now" };
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* Augment FieldBuilder with the new `default()` / `defaultNow()`
|
|
936
|
+
* chainables. Runtime support for actually filling these values on
|
|
937
|
+
* insert lands as part of v0.4.1; until then the markers are
|
|
938
|
+
* recorded in the manifest for tooling + the codegen layer.
|
|
939
|
+
*/
|
|
940
|
+
declare module "./index" {
|
|
941
|
+
// (Empty — the `default*` methods are added at runtime via the
|
|
942
|
+
// patched buildField below. Apps see them via the FieldBuilder
|
|
943
|
+
// surface declared above.)
|
|
944
|
+
}
|
|
945
|
+
|
|
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;
|
|
953
|
+
} {
|
|
954
|
+
const base = __originalBuildField(def);
|
|
955
|
+
return {
|
|
956
|
+
...base,
|
|
957
|
+
default(value: unknown) {
|
|
958
|
+
return buildFieldWithDefaults({ ...def, default: { kind: "value", value } });
|
|
959
|
+
},
|
|
960
|
+
defaultNow() {
|
|
961
|
+
return buildFieldWithDefaults({ ...def, default: { kind: "now" } });
|
|
962
|
+
},
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
// Re-export `field` with the patched builder so callers picking up
|
|
966
|
+
// the new SDK get the chainables transparently. The old `field`
|
|
967
|
+
// surface still works — `.default()` / `.defaultNow()` are additive.
|
|
968
|
+
// We intentionally re-export from the same name so existing imports
|
|
969
|
+
// (`import { field } from "@pylonsync/sdk"`) keep working AND gain
|
|
970
|
+
// the new methods without a code change.
|
|
971
|
+
Object.assign(field, {
|
|
972
|
+
string: () => buildFieldWithDefaults({ type: "string", optional: false, unique: false }),
|
|
973
|
+
int: () => buildFieldWithDefaults({ type: "int", optional: false, unique: false }),
|
|
974
|
+
float: () => buildFieldWithDefaults({ type: "float", optional: false, unique: false }),
|
|
975
|
+
number: () => buildFieldWithDefaults({ type: "float", optional: false, unique: false }),
|
|
976
|
+
bool: () => buildFieldWithDefaults({ type: "bool", optional: false, unique: false }),
|
|
977
|
+
boolean: () => buildFieldWithDefaults({ type: "bool", optional: false, unique: false }),
|
|
978
|
+
datetime: () => buildFieldWithDefaults({ type: "datetime", optional: false, unique: false }),
|
|
979
|
+
richtext: () => buildFieldWithDefaults({ type: "richtext", optional: false, unique: false }),
|
|
980
|
+
id: (target: string) => buildFieldWithDefaults({ type: `id(${target})` as FieldType, optional: false, unique: false }),
|
|
981
|
+
/**
|
|
982
|
+
* `field.enum(["pending", "paid", "failed"])` — stored as a string
|
|
983
|
+
* with allowed-values metadata. Runtime enforcement (CHECK
|
|
984
|
+
* constraint or insert-time validation) lands in a follow-up
|
|
985
|
+
* patch; for now the values flow through to codegen so the
|
|
986
|
+
* generated client gets a precise `"pending" | "paid" | "failed"`
|
|
987
|
+
* literal-union type instead of a wide `string`.
|
|
988
|
+
*/
|
|
989
|
+
enum(values: readonly string[]) {
|
|
990
|
+
const def: FieldDefinition & { enumValues?: readonly string[] } = {
|
|
991
|
+
type: "string",
|
|
992
|
+
optional: false,
|
|
993
|
+
unique: false,
|
|
994
|
+
enumValues: values,
|
|
995
|
+
};
|
|
996
|
+
return buildFieldWithDefaults(def);
|
|
997
|
+
},
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
/** Variadic index helper — `e.idx("customer", "createdAt")` reads
|
|
1001
|
+
* better than the options-object form for the common case. */
|
|
1002
|
+
function idx(...fields: string[]): IndexDefinition {
|
|
1003
|
+
return {
|
|
1004
|
+
name: `by_${fields.join("_")}`,
|
|
1005
|
+
fields,
|
|
1006
|
+
unique: false,
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
interface EntityBuilder {
|
|
1011
|
+
readonly _def: EntityDefinition & { behaviors?: string[] };
|
|
1012
|
+
indexes(...idxs: IndexDefinition[]): EntityBuilder;
|
|
1013
|
+
policies(...policies: PolicyDefinition[]): EntityBuilder;
|
|
1014
|
+
behaviors(list: readonly Behavior[]): EntityBuilder;
|
|
1015
|
+
relations(...rels: RelationDefinition[]): EntityBuilder;
|
|
1016
|
+
search(cfg: SearchConfig): EntityBuilder;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Internal — `e.entity()` is the public surface. Wraps an
|
|
1021
|
+
* EntityDefinition with chainable builders that all return another
|
|
1022
|
+
* EntityBuilder so the fluent calls compose freely. The terminal
|
|
1023
|
+
* call is implicit: any place the framework expects an
|
|
1024
|
+
* EntityDefinition (e.g. `entities: [...]` on the manifest), the
|
|
1025
|
+
* builder unwraps via the `_def` getter on access.
|
|
1026
|
+
*/
|
|
1027
|
+
function buildEntity(def: EntityDefinition & { behaviors?: string[] }): EntityBuilder {
|
|
1028
|
+
const self: EntityBuilder = {
|
|
1029
|
+
get _def() {
|
|
1030
|
+
return def;
|
|
1031
|
+
},
|
|
1032
|
+
indexes(...idxs) {
|
|
1033
|
+
return buildEntity({
|
|
1034
|
+
...def,
|
|
1035
|
+
indexes: [...(def.indexes ?? []), ...idxs],
|
|
1036
|
+
});
|
|
1037
|
+
},
|
|
1038
|
+
policies(..._policies) {
|
|
1039
|
+
// Policies aren't carried on the EntityDefinition itself —
|
|
1040
|
+
// they live in the manifest's top-level `policies` list. We
|
|
1041
|
+
// store them under a non-standard key here so the manifest
|
|
1042
|
+
// builder can pluck them off; the export shape stays
|
|
1043
|
+
// EntityDefinition-compatible.
|
|
1044
|
+
const carried = { ...(def as EntityDefinition & { _attachedPolicies?: PolicyDefinition[] }) };
|
|
1045
|
+
const existing = carried._attachedPolicies ?? [];
|
|
1046
|
+
const stamped = _policies.map((p) => ({
|
|
1047
|
+
...p,
|
|
1048
|
+
// Auto-bind the entity if the policy didn't specify one —
|
|
1049
|
+
// this is the whole point of attaching policies via the
|
|
1050
|
+
// builder: don't repeat the entity name.
|
|
1051
|
+
entity: p.entity ?? def.name,
|
|
1052
|
+
}));
|
|
1053
|
+
carried._attachedPolicies = [...existing, ...stamped];
|
|
1054
|
+
return buildEntity(carried);
|
|
1055
|
+
},
|
|
1056
|
+
behaviors(list) {
|
|
1057
|
+
let next = def;
|
|
1058
|
+
for (const b of list) {
|
|
1059
|
+
next = b.apply(next);
|
|
1060
|
+
}
|
|
1061
|
+
return buildEntity({
|
|
1062
|
+
...next,
|
|
1063
|
+
behaviors: [...(def.behaviors ?? []), ...list.map((b) => b.id)],
|
|
1064
|
+
});
|
|
1065
|
+
},
|
|
1066
|
+
relations(...rels) {
|
|
1067
|
+
return buildEntity({
|
|
1068
|
+
...def,
|
|
1069
|
+
relations: [...(def.relations ?? []), ...rels],
|
|
1070
|
+
});
|
|
1071
|
+
},
|
|
1072
|
+
search(cfg) {
|
|
1073
|
+
return buildEntity({
|
|
1074
|
+
...def,
|
|
1075
|
+
search: cfg,
|
|
1076
|
+
});
|
|
1077
|
+
},
|
|
1078
|
+
};
|
|
1079
|
+
|
|
1080
|
+
// Spread `def` keys onto the builder so anywhere the framework
|
|
1081
|
+
// expects an EntityDefinition shape (name, fields, indexes, etc.)
|
|
1082
|
+
// sees them directly without unwrapping. Manifest builder paths
|
|
1083
|
+
// walk `.name` and `.fields` straight off the builder.
|
|
1084
|
+
Object.assign(self, def);
|
|
1085
|
+
return self;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* The fluent `e` namespace. Equivalent to the procedural `entity()`
|
|
1090
|
+
* function — both produce manifest-compatible definitions, both can
|
|
1091
|
+
* be mixed in the same app.
|
|
1092
|
+
*/
|
|
1093
|
+
export const e = {
|
|
1094
|
+
entity(
|
|
1095
|
+
name: string,
|
|
1096
|
+
fields: Record<string, FieldBuilder>,
|
|
1097
|
+
): EntityBuilder {
|
|
1098
|
+
return buildEntity({ name, fields });
|
|
1099
|
+
},
|
|
1100
|
+
idx,
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
/**
|
|
1104
|
+
* Extract attached policies from a fluent entity. The manifest
|
|
1105
|
+
* builder calls this when assembling the top-level `policies` list,
|
|
1106
|
+
* so apps using the fluent `.policies(...)` chain don't have to
|
|
1107
|
+
* register policies separately at the manifest root.
|
|
1108
|
+
*
|
|
1109
|
+
* Returns an empty array for entities produced by the procedural
|
|
1110
|
+
* `entity()` API — those apps register policies the old way.
|
|
1111
|
+
*/
|
|
1112
|
+
export function extractAttachedPolicies(
|
|
1113
|
+
e: EntityDefinition | EntityBuilder,
|
|
1114
|
+
): PolicyDefinition[] {
|
|
1115
|
+
const carried = (e as EntityDefinition & {
|
|
1116
|
+
_attachedPolicies?: PolicyDefinition[];
|
|
1117
|
+
})._attachedPolicies;
|
|
1118
|
+
return carried ?? [];
|
|
1119
|
+
}
|